Browse Source

fix #1 introduce the /button API to control the workflow with a single big red button

Davide Alberani 4 years ago
parent
commit
1576411103
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
 # 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");
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with 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="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-icons.css">
         <link rel="stylesheet" type="text/css" href="/static/css/materialize.min.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">
         <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/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>
         <script type="text/javascript" src="/static/js/sb.js"></script>
     </head>
     </head>
     <body>
     <body>
         <div id="main" class="container">
         <div id="main" class="container">
-            <div id="header" class="row">
+            <div id="header" class="row center-align">
                 <h1>toot-my-t-shirt</h1>
                 <h1>toot-my-t-shirt</h1>
                 Share the love for your geek t-shirt.
                 Share the love for your geek t-shirt.
             </div>
             </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()">
                 <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>
                 <button id="send-photo-btn" class="btn waves-effect waves-light disabled" name="send-photo-btn" onClick="sendPhoto()">
                 <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
                     <i class="material-icons left">share</i>Share photo
@@ -32,10 +34,7 @@
                 </button>
                 </button>
             </div>
             </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">
                 <div id="canvas-container">
                     <video id="sb-video" autoplay="true" muted="muted"></video>
                     <video id="sb-video" autoplay="true" muted="muted"></video>
                     <canvas id="sb-canvas"></canvas>
                     <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,
     _timeout: null,
     _stepCb: null,
     _stepCb: null,
     _timeoutCb: null,
     _timeoutCb: null,
@@ -7,48 +7,89 @@ var countdown = {
     _initial_seconds: 5,
     _initial_seconds: 5,
 
 
     start: function(seconds, timeoutCb, stepCb) {
     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() {
     stop: function() {
-        if (countdown._timeout) {
-            window.clearTimeout(countdown._timeout);
+        if (Countdown._timeout) {
+            window.clearTimeout(Countdown._timeout);
         }
         }
-        countdown.running = false;
+        Countdown.running = false;
     },
     },
 
 
     restart: function() {
     restart: function() {
-        countdown.start(countdown._initial_seconds);
+        Countdown.start(Countdown._initial_seconds);
     },
     },
 
 
     _step: function() {
     _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 {
         } else {
-            countdown._decrement();
+            Countdown._decrement();
         }
         }
     },
     },
 
 
     _decrement: function() {
     _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);
         }, 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) {
 function runCamera(stream) {
     console.log("initialize the camera");
     console.log("initialize the camera");
     var video = document.querySelector("#sb-video");
     var video = document.querySelector("#sb-video");
@@ -62,8 +103,6 @@ function runCamera(stream) {
 
 
 
 
 function sendData(data) {
 function sendData(data) {
-    var xhr = new XMLHttpRequest();
-    var boundary = "youarenotsupposedtolookatthis";
     var formData = new FormData();
     var formData = new FormData();
     var msg = "";
     var msg = "";
     formData.append("selfie", new Blob([data]), "selfie.jpeg");
     formData.append("selfie", new Blob([data]), "selfie.jpeg");
@@ -72,9 +111,18 @@ function sendData(data) {
         body: formData
         body: formData
     }).then(function(response) {
     }).then(function(response) {
         if (response.status !== 200) {
         if (response.status !== 200) {
-            msg = "something went wrong sending the data: " + response.status;
+            msg = response.status;
             console.log(msg);
             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();
         cancelPhoto();
         return response.json();
         return response.json();
@@ -84,19 +132,42 @@ function sendData(data) {
         if (json && json.success) {
         if (json && json.success) {
             msg = "❤ ❤ ❤ photo sent successfully! ❤ ❤ ❤";
             msg = "❤ ❤ ❤ photo sent successfully! ❤ ❤ ❤";
             console.log(msg);
             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 {
         } else {
-            msg = "😭 😭 😭 something wrong on the backend 😭 😭 😭";
+            msg = json.message;
             console.log(msg);
             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) {
     }).catch(function(err) {
-        msg = "something went wrong connecting to server: " + err;
         console.log(msg);
         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();
         cancelPhoto();
     });
     });
 }
 }
@@ -104,19 +175,12 @@ function sendData(data) {
 
 
 function cancelPhoto() {
 function cancelPhoto() {
     console.log("cancel photo");
     console.log("cancel photo");
-    document.querySelector("#sb-message").style.visibility = "hidden";
     document.querySelector("#send-photo-btn").classList.add("disabled");
     document.querySelector("#send-photo-btn").classList.add("disabled");
     document.querySelector("#cancel-photo-btn").classList.add("disabled");
     document.querySelector("#cancel-photo-btn").classList.add("disabled");
     var canvas = document.querySelector("#sb-canvas");
     var canvas = document.querySelector("#sb-canvas");
     var context = canvas.getContext("2d");
     var context = canvas.getContext("2d");
     context.clearRect(0, 0, canvas.width, canvas.height);
     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() {
 function sendPhoto() {
     console.log("send photo");
     console.log("send photo");
-    countdown.stop();
-    document.querySelector("#sb-message").style.visibility = "hidden";
+    Countdown.stop();
     var canvas = document.querySelector("#sb-canvas");
     var canvas = document.querySelector("#sb-canvas");
-    if (isBlank(canvas)) {
+    if (!canvas || isBlank(canvas)) {
         var msg = "I cowardly refuse to send a blank image.";
         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;
     }
     }
     return sendData(canvas.toDataURL("image/jpeg"));
     return sendData(canvas.toDataURL("image/jpeg"));
 }
 }
 
 
 
 
-function _takePhoto() {
+function _takePhoto(message) {
     console.log("take photo");
     console.log("take photo");
-    document.querySelector("#sb-message").style.visibility = "visible";
     var video = document.querySelector("#sb-video");
     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 canvas = document.querySelector("#sb-canvas");
     var context = canvas.getContext("2d");
     var context = canvas.getContext("2d");
     canvas.width = video.offsetWidth;
     canvas.width = video.offsetWidth;
@@ -171,12 +256,22 @@ function _takePhoto() {
     context.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
     context.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
     document.querySelector("#send-photo-btn").classList.remove("disabled");
     document.querySelector("#send-photo-btn").classList.remove("disabled");
     document.querySelector("#cancel-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) {
     }).catch(function(err) {
         console.log("unable to open camera");
         console.log("unable to open camera");
         console.log(err);
         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 -*-
 # -*- coding: utf-8 -*-
 """toot-my-t-shirt
 """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");
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with 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 os
+import re
+import json
 import base64
 import base64
 import logging
 import logging
 import tempfile
 import tempfile
@@ -25,13 +27,17 @@ import tornado.httpserver
 import tornado.ioloop
 import tornado.ioloop
 from tornado.options import define, options
 from tornado.options import define, options
 import tornado.web
 import tornado.web
+import tornado.websocket
 from tornado import gen, escape
 from tornado import gen, escape
 
 
 from mastodon import Mastodon
 from mastodon import Mastodon
 
 
-
 API_VERSION = '1.0'
 API_VERSION = '1.0'
 
 
+# Keep track of WebSocket connections.
+_ws_clients = {}
+
+re_slashes = re.compile(r'//+')
 
 
 class Socialite:
 class Socialite:
     def __init__(self, options, logger=None):
     def __init__(self, options, logger=None):
@@ -190,6 +196,85 @@ class PublishHandler(BaseHandler):
         self.write(reply)
         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():
 def run():
     """Run the Tornado web application."""
     """Run the Tornado web application."""
     # command line arguments; can also be written in a configuration file,
     # 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)
         ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
 
 
     socialite = Socialite(options, logger=logger)
     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/?"
     _publish_path = r"/publish/?"
+    _button_path = r"/button/?"
     application = tornado.web.Application([
     application = tornado.web.Application([
+            (r'/ws', WSHandler, init_params),
             (_publish_path, PublishHandler, init_params),
             (_publish_path, PublishHandler, init_params),
             (r'/v%s%s' % (API_VERSION, _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"/(?:index.html)?", RootHandler, init_params),
             (r'/?(.*)', tornado.web.StaticFileHandler, {"path": "static"})
             (r'/?(.*)', tornado.web.StaticFileHandler, {"path": "static"})
         ],
         ],

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