Browse Source

Merge branch 'no-controls'

Davide Alberani 4 years ago
parent
commit
98b974621f
6 changed files with 265 additions and 63 deletions
  1. 1 1
      README.md
  2. 5 0
      static/css/iziToast.min.css
  3. 6 7
      static/index.html
  4. 5 0
      static/js/iziToast.min.js
  5. 156 52
      static/js/sb.js
  6. 92 3
      toot-my-t-shirt

+ 1 - 1
README.md

@@ -36,7 +36,7 @@ Some browsers require HTTPS, to allow access to the webcam.  In the *ssl/* direc
 
 # License and copyright
 
-Copyright 2018 Davide Alberani <da@mimante.net>
+Copyright 2018-2019 Davide Alberani <da@mimante.net>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

File diff suppressed because it is too large
+ 5 - 0
static/css/iziToast.min.css


+ 6 - 7
static/index.html

@@ -6,20 +6,22 @@
         <link rel="icon" type="image/png" href="/static/images/sb-favicon.png">
         <link rel="stylesheet" type="text/css" href="/static/css/materialize-icons.css">
         <link rel="stylesheet" type="text/css" href="/static/css/materialize.min.css">
+        <link rel="stylesheet" type="text/css" href="/static/css/iziToast.min.css">
         <link rel="stylesheet" type="text/css" href="/static/css/sb.css">
         <script type="text/javascript" src="/static/js/materialize.min.js"></script>
+        <script type="text/javascript" src="/static/js/iziToast.min.js"></script>
         <script type="text/javascript" src="/static/js/sb.js"></script>
     </head>
     <body>
         <div id="main" class="container">
-            <div id="header" class="row">
+            <div id="header" class="row center-align">
                 <h1>toot-my-t-shirt</h1>
                 Share the love for your geek t-shirt.
             </div>
 
-            <div id="button-containers" class="row">
+            <div id="button-containers" class="row center-align">
                 <button id="take-photo-btn" class="btn waves-effect waves-light disabled" name="take-photo-btn" onClick="takePhoto()">
-                    <i class="material-icons left">camera</i>Take photo <small>(2 secs delay)</small>
+                    <i class="material-icons left">camera</i>Take photo <small>(1 sec delay)</small>
                 </button>
                 <button id="send-photo-btn" class="btn waves-effect waves-light disabled" name="send-photo-btn" onClick="sendPhoto()">
                     <i class="material-icons left">share</i>Share photo
@@ -32,10 +34,7 @@
                 </button>
             </div>
 
-            <div class="row">
-                <div id="sb-message">
-                    <span id="sb-message-text">will be gone in few seconds! <span id="sb-countdown">&nbsp;</span></span>
-                </div>
+            <div class="row center-align">
                 <div id="canvas-container">
                     <video id="sb-video" autoplay="true" muted="muted"></video>
                     <canvas id="sb-canvas"></canvas>

File diff suppressed because it is too large
+ 5 - 0
static/js/iziToast.min.js


+ 156 - 52
static/js/sb.js

@@ -1,4 +1,4 @@
-var countdown = {
+var Countdown = {
     _timeout: null,
     _stepCb: null,
     _timeoutCb: null,
@@ -7,48 +7,89 @@ var countdown = {
     _initial_seconds: 5,
 
     start: function(seconds, timeoutCb, stepCb) {
-        countdown.stop();
-        countdown.seconds = countdown._initial_seconds = seconds || 5;
-        countdown._timeoutCb = timeoutCb || countdown._timeoutCb;
-        countdown._stepCb = stepCb || countdown._stepCb;
-        countdown.running = true;
-        countdown._step();
+        Countdown.stop();
+        Countdown.seconds = Countdown._initial_seconds = seconds || 5;
+        Countdown._timeoutCb = timeoutCb || Countdown._timeoutCb;
+        Countdown._stepCb = stepCb || Countdown._stepCb;
+        Countdown.running = true;
+        Countdown._step();
     },
 
     stop: function() {
-        if (countdown._timeout) {
-            window.clearTimeout(countdown._timeout);
+        if (Countdown._timeout) {
+            window.clearTimeout(Countdown._timeout);
         }
-        countdown.running = false;
+        Countdown.running = false;
     },
 
     restart: function() {
-        countdown.start(countdown._initial_seconds);
+        Countdown.start(Countdown._initial_seconds);
     },
 
     _step: function() {
-        if (countdown._stepCb) {
-            countdown._stepCb();
+        if (Countdown._stepCb) {
+            Countdown._stepCb();
         }
-        if (countdown.seconds === 0) {
-            if (countdown._timeoutCb) {
-                countdown._timeoutCb();
+        if (Countdown.seconds === 0) {
+            if (Countdown._timeoutCb) {
+                Countdown._timeoutCb();
             }
-            countdown.stop();
+            Countdown.stop();
         } else {
-            countdown._decrement();
+            Countdown._decrement();
         }
     },
 
     _decrement: function() {
-        countdown.seconds = countdown.seconds - 1;
-        countdown._timeout = window.setTimeout(function() {
-            countdown._step();
+        Countdown.seconds = Countdown.seconds - 1;
+        Countdown._timeout = window.setTimeout(function() {
+            Countdown._step();
         }, 1000);
     }
 };
 
 
+function uuidv4() {
+    return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
+        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
+    );
+}
+
+var UUID = uuidv4();
+
+function getWSPath() {
+    var loc = window.location, new_uri;
+    if (loc.protocol === "https:") {
+        new_uri = "wss:";
+    } else {
+        new_uri = "ws:";
+    }
+    new_uri += "//" + loc.host + "/ws?uuid=" + UUID;
+    return new_uri;
+}
+
+
+var WS = new WebSocket(getWSPath(), ['soap', 'xmpp']);
+
+WS.onerror = function(error) {
+    console.log('WebSocket Error ' + error);
+};
+
+
+WS.onmessage = function(e) {
+    console.log("received message on websocket: " + e.data);
+    var jdata = JSON.parse(e.data);
+    if (!(jdata && jdata.source == "button" && jdata.action == "clicked")) {
+        return;
+    }
+    if (!Countdown.running) {
+        takePhoto("press again to publish!");
+    } else {
+        sendPhoto();
+    }
+};
+
+
 function runCamera(stream) {
     console.log("initialize the camera");
     var video = document.querySelector("#sb-video");
@@ -62,8 +103,6 @@ function runCamera(stream) {
 
 
 function sendData(data) {
-    var xhr = new XMLHttpRequest();
-    var boundary = "youarenotsupposedtolookatthis";
     var formData = new FormData();
     var msg = "";
     formData.append("selfie", new Blob([data]), "selfie.jpeg");
@@ -72,9 +111,18 @@ function sendData(data) {
         body: formData
     }).then(function(response) {
         if (response.status !== 200) {
-            msg = "something went wrong sending the data: " + response.status;
+            msg = response.status;
             console.log(msg);
-            M.toast({"html": msg});
+            iziToast.error({
+                "title": "😭 something wrong sending the data 😭",
+                "message": msg,
+                "titleSize": "3em",
+                "messageSize": "2em",
+                "close": false,
+                "drag": false,
+                "pauseOnHover": false,
+                "position": "topCenter"
+            });
         }
         cancelPhoto();
         return response.json();
@@ -84,19 +132,42 @@ function sendData(data) {
         if (json && json.success) {
             msg = "❤ ❤ ❤ photo sent successfully! ❤ ❤ ❤";
             console.log(msg);
-            M.toast({"html": msg});
+            iziToast.destroy();
+            iziToast.success({
+                "title": msg,
+                "titleSize": "3em",
+                "messageSize": "2em",
+                "close": false,
+                "drag": false,
+                "pauseOnHover": false,
+                "position": "topCenter"
+            });
         } else {
-            msg = "😭 😭 😭 something wrong on the backend 😭 😭 😭";
+            msg = json.message;
             console.log(msg);
-            M.toast({"html": msg});
-            msg = "the server says: " + json.message;
-            console.log(msg);
-            M.toast({"html": msg});
+            iziToast.error({
+                "title": "😭 backend error 😭",
+                "message": msg,
+                "titleSize": "3em",
+                "messageSize": "2em",
+                "close": false,
+                "drag": false,
+                "pauseOnHover": false,
+                "position": "topCenter"
+            });
         }
     }).catch(function(err) {
-        msg = "something went wrong connecting to server: " + err;
         console.log(msg);
-        M.toast({"html": msg});
+        iziToast.error({
+            "title": "😭 error connecting to the server 😭",
+            "message": err,
+            "titleSize": "3em",
+            "messageSize": "2em",
+            "close": false,
+            "drag": false,
+            "pauseOnHover": false,
+            "position": "topCenter"
+        });
         cancelPhoto();
     });
 }
@@ -104,19 +175,12 @@ function sendData(data) {
 
 function cancelPhoto() {
     console.log("cancel photo");
-    document.querySelector("#sb-message").style.visibility = "hidden";
     document.querySelector("#send-photo-btn").classList.add("disabled");
     document.querySelector("#cancel-photo-btn").classList.add("disabled");
     var canvas = document.querySelector("#sb-canvas");
     var context = canvas.getContext("2d");
     context.clearRect(0, 0, canvas.width, canvas.height);
-    countdown.stop();
-}
-
-
-function updateSendCountdown() {
-    document.querySelector("#sb-countdown").innerText = "" + countdown.seconds;
-    console.log("deleting photo in " + countdown.seconds + " seconds");
+    Countdown.stop();
 }
 
 
@@ -130,23 +194,44 @@ function isBlank(canvas) {
 
 function sendPhoto() {
     console.log("send photo");
-    countdown.stop();
-    document.querySelector("#sb-message").style.visibility = "hidden";
+    Countdown.stop();
     var canvas = document.querySelector("#sb-canvas");
-    if (isBlank(canvas)) {
+    if (!canvas || isBlank(canvas)) {
         var msg = "I cowardly refuse to send a blank image.";
-        console.log(msg)
-        M.toast({"html": msg});
+        console.log(msg);
+        iziToast.warning({
+            "title": msg,
+            "titleSize": "3em",
+            "messageSize": "2em",
+            "close": false,
+            "drag": false,
+            "pauseOnHover": false,
+            "position": "topCenter"
+        });
         return;
     }
     return sendData(canvas.toDataURL("image/jpeg"));
 }
 
 
-function _takePhoto() {
+function _takePhoto(message) {
     console.log("take photo");
-    document.querySelector("#sb-message").style.visibility = "visible";
     var video = document.querySelector("#sb-video");
+    if (!(video.offsetWidth && video.offsetHeight)) {
+        var msg = "missing video";
+        console.log(msg);
+        iziToast.warning({
+            "title": msg,
+            "message": "please grant camera permissions",
+            "titleSize": "3em",
+            "messageSize": "2em",
+            "close": false,
+            "drag": false,
+            "pauseOnHover": false,
+            "position": "topCenter"
+        });
+        return;
+    }
     var canvas = document.querySelector("#sb-canvas");
     var context = canvas.getContext("2d");
     canvas.width = video.offsetWidth;
@@ -171,12 +256,22 @@ function _takePhoto() {
     context.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
     document.querySelector("#send-photo-btn").classList.remove("disabled");
     document.querySelector("#cancel-photo-btn").classList.remove("disabled");
-    countdown.start(5, cancelPhoto, updateSendCountdown);
+    iziToast.question({
+        "title": "do you like what you see?",
+        "message": message || "press \"share photo\" to publish!",
+        "titleSize": "3em",
+        "messageSize": "2em",
+        "close": false,
+        "drag": false,
+        "pauseOnHover": false,
+        "position": "topCenter"
+    });
+    Countdown.start(5, cancelPhoto);
 }
 
 
-function takePhoto() {
-    window.setTimeout(_takePhoto, 2000);
+function takePhoto(msg) {
+    window.setTimeout(function() { _takePhoto(msg); }, 1000);
 }
 
 
@@ -198,7 +293,16 @@ function initCamera() {
     }).catch(function(err) {
         console.log("unable to open camera");
         console.log(err);
-        M.toast({"html": "unable to open camera; please reload this page: " + err});
+        iziToast.error({
+            "title": "unable to open camera",
+            "message": "please reload this page: " + err,
+            "titleSize": "3em",
+            "messageSize": "2em",
+            "close": false,
+            "drag": false,
+            "pauseOnHover": false,
+            "position": "topCenter"
+        });
     });
 }
 

+ 92 - 3
toot-my-t-shirt

@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 """toot-my-t-shirt
 
-Copyright 2018 Davide Alberani <da@mimante.net>
+Copyright 2018-2019 Davide Alberani <da@mimante.net>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@ limitations under the License.
 """
 
 import os
+import re
+import json
 import base64
 import logging
 import tempfile
@@ -25,13 +27,17 @@ import tornado.httpserver
 import tornado.ioloop
 from tornado.options import define, options
 import tornado.web
+import tornado.websocket
 from tornado import gen, escape
 
 from mastodon import Mastodon
 
-
 API_VERSION = '1.0'
 
+# Keep track of WebSocket connections.
+_ws_clients = {}
+
+re_slashes = re.compile(r'//+')
 
 class Socialite:
     def __init__(self, options, logger=None):
@@ -190,6 +196,85 @@ class PublishHandler(BaseHandler):
         self.write(reply)
 
 
+class ButtonHandler(BaseHandler):
+    @gen.coroutine
+    def post(self, **kwargs):
+        reply = {'success': True}
+        self.send_ws_message('/ws', json.dumps({"source": "button", "action": "clicked"}))
+        self.write(reply)
+
+    @gen.coroutine
+    def send_ws_message(self, path, message):
+        """Send a WebSocket message to all the connected clients.
+
+        :param path: partial path used to build the WebSocket url
+        :type path: str
+        :param message: message to send
+        :type message: str
+        """
+        try:
+            url = '%s://localhost:%s/ws?uuid=bigredbutton' % ('wss' if self.ssl_options else 'ws',
+                                                              self.global_options.port)
+            self.logger.info(url)
+            req = tornado.httpclient.HTTPRequest(url, validate_cert=False)
+            ws = yield tornado.websocket.websocket_connect(req)
+            ws.write_message(message)
+            ws.close()
+        except Exception as e:
+            self.logger.error('Error yielding WebSocket message: %s', e)
+
+
+class WSHandler(tornado.websocket.WebSocketHandler):
+    def initialize(self, **kwargs):
+        """Add every passed (key, value) as attributes of the instance."""
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+    def _clean_url(self, url):
+        url = re_slashes.sub('/', url)
+        ridx = url.rfind('?')
+        if ridx != -1:
+            url = url[:ridx]
+        return url
+
+    def open(self, *args, **kwargs):
+        try:
+            self.uuid = self.get_argument('uuid')
+        except:
+            self.uuid = None
+        url = self._clean_url(self.request.uri)
+        _ws_clients.setdefault(url, {})
+        if self.uuid and self.uuid not in _ws_clients[url]:
+            _ws_clients[url][self.uuid] = self
+        self.logger.debug('WSHandler.open %s clients connected' % len(_ws_clients[url]))
+
+    def on_message(self, message):
+        url = self._clean_url(self.request.uri)
+        self.logger.debug('WSHandler.on_message url: %s' % url)
+        count = 0
+        _to_delete = set()
+        current_uuid = None
+        try:
+            current_uuid = self.get_argument('uuid')
+        except:
+            pass
+        for uuid, client in _ws_clients.get(url, {}).items():
+            if uuid and uuid == current_uuid:
+               continue
+            try:
+                client.write_message(message)
+            except:
+                _to_delete.add(uuid)
+                continue
+            count += 1
+        for uuid in _to_delete:
+            try:
+                del _ws_clients[url][uuid]
+            except KeyError:
+                pass
+        self.logger.debug('WSHandler.on_message sent message to %d clients' % count)
+
+
 def run():
     """Run the Tornado web application."""
     # command line arguments; can also be written in a configuration file,
@@ -226,12 +311,16 @@ def run():
         ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
 
     socialite = Socialite(options, logger=logger)
-    init_params = dict(global_options=options, socialite=socialite)
+    init_params = dict(global_options=options, ssl_options=ssl_options, socialite=socialite, logger=logger)
 
     _publish_path = r"/publish/?"
+    _button_path = r"/button/?"
     application = tornado.web.Application([
+            (r'/ws', WSHandler, init_params),
             (_publish_path, PublishHandler, init_params),
             (r'/v%s%s' % (API_VERSION, _publish_path), PublishHandler, init_params),
+            (_button_path, ButtonHandler, init_params),
+            (r'/v%s%s' % (API_VERSION, _button_path), ButtonHandler, init_params),
             (r"/(?:index.html)?", RootHandler, init_params),
             (r'/?(.*)', tornado.web.StaticFileHandler, {"path": "static"})
         ],

Some files were not shown because too many files changed in this diff