diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..7a34581
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+.idea
+.github
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..a88e819
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,14 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+
+**Desktop (please complete the following information):**
+ - OS: [Windows/Linux/Mac]
+ - Browser: [Firefox, opera, chrome]
+ - Infoscreen Version: [1.0]
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3f5eeb9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,9 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+(Add any other context or screenshots about the feature request)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f026f2d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,66 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+# own additions
+.idea
+config.js
+tmp/*.png
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..260d991
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+# Nice fresh and new base
+FROM node:10-alpine
+
+# Create directory for our app
+WORKDIR /usr/src/infoscreen
+
+# Install app dependencies
+COPY package*.json ./
+RUN npm install
+
+# Bundle app source
+COPY . .
+COPY config-default.js config.js
+
+# Port will be 8000 by default unless run wih ex. -e PORT=8080
+EXPOSE 8000 8001 1935
+
+# Let's GO!
+CMD [ "npm", "start" ]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cd30711
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# Info screen3
+
+is useful tool when you need displaying same content to multiple locations or need to remotely control a slide show display.
+
+Usage is very easy after you have setup the server at local network. The viewer is simply a web page so you need just computer with a browser to setup your displays.
+
+# Setup
+1. npm install
+2. copy `config-default.js` to `config.js`
+3. check config settings and if you don't need local rtmp media server, set `mediaServer` to `false`
+4. (edit `/data/meta.json`, add, remove or rename the displays you need or use the defaults)
+5. npm start
+
+# Default usage
+Viewer is located at: http://localhost:8000
+Admin interface is located at: http://localhost:8000/admin
+Defaults to user: `admin`, pass: `admin`
+It's highly encouraged to change the defaults to something else!
+
+# Lite viewer
+Lite viewer is located at each display post fixed with `/lite`
+Example: http://localhost:8000/display/0/lite
+
+> notice: Lite viewer supports only .png images on slides!
+> backgrounds can be only jpg.
+
+# Local stream support for OBS
+> Works only when config has `mediaServer` set to `true`
+
+Go to: `Settings` -> `Stream`
+set following options:
+
+|Setting|Value|
+|:---|:---|
+|Stream Type:| Custom Streaming Server|
+|URL:| `rtmp://localhost/live`|
+|Stream key:|`STREAM_NAME`|
+
+Later it is possible to define other stream names/keys too, but for now only this one is supported.
+
+# Environment variables
+|ENV|default|Usage|
+|:---|:---|:---|
+|PORT|8000|Server listen port|
+|HOST|localhost|Host or ip from where to server is externally accessible|
+|ADMIN_USER|admin|Username to access admin interface|
+|ADMIN_PASS|admin|Password for the admin interface|
+
+# Dockerfile
+Dockerfile is provided for building a docker container. Docker container accepts the same ENV variables as the standard setup (see them above).
+
+`docker build -t reaby/infoscreen .`
+`docker run -p 8000:8000 -e HOST=infoscreen.lan -e PORT=8000 -e ADMIN_USER=admin -e ADMIN_PASS=secret --name infoscreen reaby/infoscreen`
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..7567064
--- /dev/null
+++ b/app.js
@@ -0,0 +1,75 @@
+let express = require('express');
+let createError = require('http-errors');
+let path = require('path');
+let cookieParser = require('cookie-parser');
+let logger = require('morgan');
+let lessMiddleware = require('less-middleware');
+const EventEmitter = require('events');
+let config = require("./config.js");
+
+class Dispatcher extends EventEmitter {
+}
+
+const eventDispatcher = new Dispatcher();
+
+let app = express();
+let server = require('http').Server(app);
+let io = require('socket.io')(server);
+
+if (config.mediaServer) {
+ const {NodeMediaServer} = require('node-media-server');
+
+ const nodeServerConfig = {
+ rtmp: {
+ port: 1935,
+ chunk_size: 60000,
+ gop_cache: true,
+ ping: 60,
+ ping_timeout: 30
+ },
+ http: {
+ port: (parseInt(config.serverListenPort) + 1),
+ allow_origin: '*'
+ }
+ };
+
+ let nms = new NodeMediaServer(nodeServerConfig);
+ nms.run();
+}
+
+// view engine setup
+app.set('port', parseInt(config.serverListenPort));
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'twig');
+app.use(logger('dev'));
+app.use(express.json());
+app.use(express.urlencoded({extended: false}));
+app.use(cookieParser());
+app.use(lessMiddleware(path.join(__dirname, 'public')));
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+let displays = require("./modules/websocket")(server, app, io, eventDispatcher);
+let indexRouter = require('./routes/index.js')(displays);
+let adminRouter = require('./routes/admin.js')(displays);
+
+app.use('/', indexRouter);
+app.use('/admin', adminRouter);
+
+// catch 404 and forward to error handler
+app.use(function (req, res, next) {
+ next(createError(404));
+});
+
+// error handler
+app.use(function (err, req, res, next) {
+ // set locals, only providing error in development
+ res.locals.message = err.message;
+ res.locals.error = req.app.get('env') === 'development' ? err : {};
+
+ // render the error page
+ res.status(err.status || 500);
+ res.render('error');
+});
+
+module.exports = {app: app, server: server, io: io, eventDispatcher: eventDispatcher};
diff --git a/bin/www b/bin/www
new file mode 100644
index 0000000..7cdfeb9
--- /dev/null
+++ b/bin/www
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+let cli = require('../modules/cli');
+let chalk = require('chalk');
+let debug = require('debug')('screen-info:server');
+let config = require('../config.js');
+let app = require('../app').app;
+
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+let port = normalizePort(config.serverListenPort || '8000');
+app.set('port', port);
+
+/**
+ * Create HTTP server.
+ */
+
+let server = require("../app").server;
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+ let port = parseInt(val, 10);
+
+ if (isNaN(port)) {
+ // named pipe
+ return val;
+ }
+
+ if (port >= 0) {
+ // port number
+ return port;
+ }
+
+ return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+ if (error.syscall !== 'listen') {
+ throw error;
+ }
+
+ let bind = typeof port === 'string'
+ ? 'Pipe ' + port
+ : 'Port ' + port;
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case 'EACCES':
+ console.error(bind + ' requires elevated privileges');
+ process.exit(1);
+ break;
+ case 'EADDRINUSE':
+ console.error(bind + ' is already in use');
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+ let addr = server.address();
+ let bind = typeof addr === 'string'
+ ? 'pipe ' + addr
+ : 'port ' + addr.port;
+
+ cli.info('Infoscreen is now accessible at ' + chalk.bold.white(config.serverUrl + ":" + config.serverListenPort));
+ console.log(chalk.green(">>") + chalk.default("Start complete.") + chalk.green("<<"));
+}
diff --git a/config-default.js b/config-default.js
new file mode 100644
index 0000000..83eabd9
--- /dev/null
+++ b/config-default.js
@@ -0,0 +1,12 @@
+module.exports = {
+ "serverListenPort": process.env.PORT || 8000,
+ "serverHost": process.env.HOST || "localhost",
+ "serverUrl": "http://" + (process.env.HOST || "localhost"),
+ "admins": [
+ {
+ "name": process.env.ADMIN_USER || "admin",
+ "pass": process.env.ADMIN_PASS || "admin"
+ }
+ ],
+ "mediaServer": false
+};
diff --git a/data/default/backgrounds/bg.jpg b/data/default/backgrounds/bg.jpg
new file mode 100644
index 0000000..f3cc047
Binary files /dev/null and b/data/default/backgrounds/bg.jpg differ
diff --git a/data/default/bundle.json b/data/default/bundle.json
new file mode 100644
index 0000000..6551bc5
--- /dev/null
+++ b/data/default/bundle.json
@@ -0,0 +1,18 @@
+{
+ "bundleName": "default",
+ "background": "bg.jpg",
+ "duration": 20,
+ "useWebFonts": false,
+ "styleHeader": {
+ "fill": "#f90",
+ "fontSize": 56,
+ "fontWeight": 900,
+ "fontFamily": "Arial Black"
+ },
+ "styleText": {
+ "fill": "#fff",
+ "fontSize": 32,
+ "fontWeight": 900,
+ "fontFamily": "Arial Black"
+ }
+}
\ No newline at end of file
diff --git a/data/default/images/.gitkeep b/data/default/images/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/data/default/render/03191688-bb93-4376-a3ab-53ed790b2155.png b/data/default/render/03191688-bb93-4376-a3ab-53ed790b2155.png
new file mode 100644
index 0000000..5403346
Binary files /dev/null and b/data/default/render/03191688-bb93-4376-a3ab-53ed790b2155.png differ
diff --git a/data/default/render/39e5205f-9ace-4597-9873-f9ea5846dff0.png b/data/default/render/39e5205f-9ace-4597-9873-f9ea5846dff0.png
new file mode 100644
index 0000000..d957681
Binary files /dev/null and b/data/default/render/39e5205f-9ace-4597-9873-f9ea5846dff0.png differ
diff --git a/data/default/render/6a298b8c-9246-411f-b364-2d73c7e292d0.png b/data/default/render/6a298b8c-9246-411f-b364-2d73c7e292d0.png
new file mode 100644
index 0000000..ae26c43
Binary files /dev/null and b/data/default/render/6a298b8c-9246-411f-b364-2d73c7e292d0.png differ
diff --git a/data/default/slides.json b/data/default/slides.json
new file mode 100644
index 0000000..ad4b588
--- /dev/null
+++ b/data/default/slides.json
@@ -0,0 +1,26 @@
+[
+ {
+ "file": "39e5205f-9ace-4597-9873-f9ea5846dff0",
+ "name": "header",
+ "duration": null,
+ "enabled": true,
+ "displayTime": true,
+ "index": 0
+ },
+ {
+ "file": "6a298b8c-9246-411f-b364-2d73c7e292d0",
+ "name": "header 3",
+ "duration": null,
+ "enabled": true,
+ "displayTime": true,
+ "index": 1
+ },
+ {
+ "file": "03191688-bb93-4376-a3ab-53ed790b2155",
+ "name": "2 header",
+ "duration": null,
+ "enabled": true,
+ "displayTime": true,
+ "index": 2
+ }
+]
\ No newline at end of file
diff --git a/data/default/slides/03191688-bb93-4376-a3ab-53ed790b2155 b/data/default/slides/03191688-bb93-4376-a3ab-53ed790b2155
new file mode 100644
index 0000000..7fd13ac
--- /dev/null
+++ b/data/default/slides/03191688-bb93-4376-a3ab-53ed790b2155
@@ -0,0 +1 @@
+{"version":"2.4.0","objects":[{"type":"i-text","version":"2.4.0","left":150,"top":40,"width":206.85,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"2 header","fontSize":56,"fontWeight":900,"fontFamily":"Arial Black","id":"header","styles":{}},{"type":"i-text","version":"2.4.0","left":150,"top":113.28,"width":164.85,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"header","fontSize":56,"fontWeight":900,"fontFamily":"Arial Black","id":"header","styles":{}}]}
\ No newline at end of file
diff --git a/data/default/slides/39e5205f-9ace-4597-9873-f9ea5846dff0 b/data/default/slides/39e5205f-9ace-4597-9873-f9ea5846dff0
new file mode 100644
index 0000000..15d83e3
--- /dev/null
+++ b/data/default/slides/39e5205f-9ace-4597-9873-f9ea5846dff0
@@ -0,0 +1 @@
+{"version":"2.4.0","objects":[{"type":"i-text","version":"2.4.0","left":150,"top":40,"width":225.39,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"header","fontSize":56,"fontWeight":900,"fontFamily":"Audiowide","id":"header","styles":{}},{"type":"i-text","version":"2.4.0","left":250,"top":113.28,"width":225.86,"height":36.16,"fill":"#fff","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"paragraph text","fontSize":32,"fontWeight":900,"fontFamily":"Exo 2","id":"normal","styles":{}},{"type":"i-text","version":"2.4.0","left":250,"top":159.44,"width":225.86,"height":36.16,"fill":"#fff","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"paragraph text","fontSize":32,"fontWeight":900,"fontFamily":"Exo 2","id":"normal","styles":{}}]}
\ No newline at end of file
diff --git a/data/default/slides/6a298b8c-9246-411f-b364-2d73c7e292d0 b/data/default/slides/6a298b8c-9246-411f-b364-2d73c7e292d0
new file mode 100644
index 0000000..b13d40b
--- /dev/null
+++ b/data/default/slides/6a298b8c-9246-411f-b364-2d73c7e292d0
@@ -0,0 +1 @@
+{"version":"2.4.0","objects":[{"type":"i-text","version":"2.4.0","left":151,"top":40,"width":267.64,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"header 3","fontSize":56,"fontWeight":900,"fontFamily":"Arial Black","id":"header","styles":{}}]}
\ No newline at end of file
diff --git a/data/meta.json b/data/meta.json
new file mode 100644
index 0000000..160cebb
--- /dev/null
+++ b/data/meta.json
@@ -0,0 +1,30 @@
+{
+ "bundleNames": [
+ {
+ "dir": "default",
+ "name": "default"
+ }
+ ],
+ "displays": [
+ {
+ "name": "main screen",
+ "bundle": "default"
+ },
+ {
+ "name": "2nd screen",
+ "bundle": "set1"
+ },
+ {
+ "name": "3rd screen",
+ "bundle": "default"
+ },
+ {
+ "name": "4th screen",
+ "bundle": "set1"
+ },
+ {
+ "name": "5th screen",
+ "bundle": "default"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/set1/backgrounds/bg.jpg b/data/set1/backgrounds/bg.jpg
new file mode 100644
index 0000000..f3cc047
Binary files /dev/null and b/data/set1/backgrounds/bg.jpg differ
diff --git a/data/set1/bundle.json b/data/set1/bundle.json
new file mode 100644
index 0000000..417cc07
--- /dev/null
+++ b/data/set1/bundle.json
@@ -0,0 +1,18 @@
+{
+ "bundleName": "set1",
+ "background": "bg.jpg",
+ "useWebFonts": false,
+ "duration": 20,
+ "styleHeader": {
+ "fill": "#f90",
+ "fontSize": 56,
+ "fontWeight": 900,
+ "fontFamily": "Arial Black"
+ },
+ "styleText": {
+ "fill": "#fff",
+ "fontFamily": "Arial Black",
+ "fontSize": 42,
+ "fontWeight": 900
+ }
+}
\ No newline at end of file
diff --git a/data/set1/images/.gitkeep b/data/set1/images/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/data/set1/render/21f0451b-52ca-4da2-bdbb-dce3b8092c3e.png b/data/set1/render/21f0451b-52ca-4da2-bdbb-dce3b8092c3e.png
new file mode 100644
index 0000000..cd41054
Binary files /dev/null and b/data/set1/render/21f0451b-52ca-4da2-bdbb-dce3b8092c3e.png differ
diff --git a/data/set1/render/cfc55645-fd10-4f4d-b6f0-3dd34b68192e.png b/data/set1/render/cfc55645-fd10-4f4d-b6f0-3dd34b68192e.png
new file mode 100644
index 0000000..52c9fd0
Binary files /dev/null and b/data/set1/render/cfc55645-fd10-4f4d-b6f0-3dd34b68192e.png differ
diff --git a/data/set1/slides.json b/data/set1/slides.json
new file mode 100644
index 0000000..cdf2bd1
--- /dev/null
+++ b/data/set1/slides.json
@@ -0,0 +1,18 @@
+[
+ {
+ "file": "cfc55645-fd10-4f4d-b6f0-3dd34b68192e",
+ "name": "bundle2",
+ "duration": null,
+ "enabled": true,
+ "displayTime": true,
+ "index": 0
+ },
+ {
+ "file": "21f0451b-52ca-4da2-bdbb-dce3b8092c3e",
+ "name": "dia 3",
+ "duration": null,
+ "enabled": true,
+ "displayTime": true,
+ "index": 1
+ }
+]
\ No newline at end of file
diff --git a/data/set1/slides/21f0451b-52ca-4da2-bdbb-dce3b8092c3e b/data/set1/slides/21f0451b-52ca-4da2-bdbb-dce3b8092c3e
new file mode 100644
index 0000000..67bb21f
--- /dev/null
+++ b/data/set1/slides/21f0451b-52ca-4da2-bdbb-dce3b8092c3e
@@ -0,0 +1 @@
+{"version":"2.4.0","objects":[{"type":"i-text","version":"2.4.0","left":150,"top":40,"width":149.38,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"dia 3","fontSize":56,"fontWeight":900,"fontFamily":"Arial Black","id":"header","styles":{}},{"type":"i-text","version":"2.4.0","left":250,"top":113.28,"width":342.89,"height":47.46,"fill":"#fff","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"paragraph text","fontSize":42,"fontWeight":900,"fontFamily":"Arial Black","id":"normal","styles":{}},{"type":"i-text","version":"2.4.0","left":250,"top":170.74,"width":342.89,"height":47.46,"fill":"#fff","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"paragraph text","fontSize":42,"fontWeight":900,"fontFamily":"Arial Black","id":"normal","styles":{}}]}
\ No newline at end of file
diff --git a/data/set1/slides/cfc55645-fd10-4f4d-b6f0-3dd34b68192e b/data/set1/slides/cfc55645-fd10-4f4d-b6f0-3dd34b68192e
new file mode 100644
index 0000000..31ed41c
--- /dev/null
+++ b/data/set1/slides/cfc55645-fd10-4f4d-b6f0-3dd34b68192e
@@ -0,0 +1 @@
+{"version":"2.4.0","objects":[{"type":"i-text","version":"2.4.0","left":150,"top":36,"width":242.76,"height":63.28,"fill":"#f90","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"bundle2","fontSize":56,"fontWeight":900,"fontFamily":"Arial Black","styles":{}},{"type":"i-text","version":"2.4.0","left":250,"top":113.28,"width":342.89,"height":47.46,"fill":"#fff","shadow":{"color":"rgba(0,0,0,0.8)","blur":5,"offsetX":2,"offsetY":2,"affectStroke":false},"text":"paragraph text","fontSize":42,"fontWeight":900,"fontFamily":"Arial Black","styles":{}}]}
\ No newline at end of file
diff --git a/modules/admin.js b/modules/admin.js
new file mode 100644
index 0000000..e9e8ca1
--- /dev/null
+++ b/modules/admin.js
@@ -0,0 +1,450 @@
+let fs = require("fs");
+let config = require("../config.js");
+let cli = require('./cli.js');
+
+/**
+ * @module infoscreen3/admin
+ * @class
+ */
+class admin {
+
+ /**
+ *
+ * @param sharedIO
+ * @param dispatcher
+ * @param screenViews
+ * @param bundleManager
+ */
+ constructor(sharedIO, dispatcher, screenViews, bundleManager) {
+ cli.info("Starting admin socket interface");
+
+ /**
+ * @typedef {object} previewInstance
+ * @property {string} adminId is string from admin socket.Id
+ * @property {object} preview, pure previews socket
+ *
+ * @type {previewInstance[]}
+ */
+ let previewInstances = [];
+ let io = sharedIO.of("/admin");
+ this.io = io;
+ this.dispatcher = dispatcher;
+
+ this.bundleManager = bundleManager;
+ let self = this;
+ // helper callback for global announce
+ dispatcher.on("announce", function (obj) {
+ if (obj.screens == null) {
+ io.emit(obj.event, obj.data);
+ } else {
+ for (let screen of obj.screens) {
+ if (screen === "admin") {
+ io.emit(obj.event, obj.data);
+ }
+ }
+ }
+ });
+
+ /** helper function to call announce easier **/
+ function announce(screens, event, data) {
+ self.dispatcher.emit("announce", {screens: screens, event: event, data: data});
+ }
+
+ io.on("connection", function (socket) {
+ cli.success("WS " + socket.conn.remoteAddress + " connect with id:" + socket.id);
+
+
+ socket.on('error', function (error) {
+ cli.error(error, "WS error on socket id:" + socket.id);
+ for (let i in previewInstances) {
+ if (previewInstances[i].adminId === socket.id || previewInstances[i].preview === socket) {
+ delete previewInstances[i];
+ break;
+ }
+ }
+ });
+
+ socket.on('disconnect', function (reason) {
+ for (let i in previewInstances) {
+ if (previewInstances[i].adminId === socket.id || previewInstances[i].preview === socket) {
+ delete previewInstances[i];
+ break;
+ }
+ }
+ cli.success("WS " + reason + " " + socket.conn.remoteAddress + " socket id:" + socket.id);
+ });
+
+ socket.on('sync.preview', function (socketId) {
+ for (let i in previewInstances) {
+ if (previewInstances[i].adminId === decodeURIComponent(socketId)) {
+ previewInstances[i].preview = socket;
+ }
+ }
+ });
+
+ socket.on('admin.reload', function (data) {
+ announce([data.displayId], "callback.reload", {});
+ });
+
+ // toggle time
+ socket.on('controls.time.toggle', function (data) {
+ let view = getView(data.displayId);
+ view.serverOptions.timeDisplay = !view.serverOptions.timeDisplay;
+ view.io.emit("callback.time", view.serverOptions.timeDisplay);
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on('controls.previous', function (data) {
+ let view = getView(data.displayId);
+ view.serverOptions.loopIndex -= 2;
+ view.mainLoop();
+ });
+
+ socket.on('controls.play', function (data) {
+ getServerOptions(data.displayId).loop = true;
+ getView(data.displayId).mainLoop();
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on('controls.startStream', function (data) {
+ let view = getView(data.displayId);
+ let serverOptions = getServerOptions(data.displayId);
+
+ if (serverOptions.isStreaming === false) {
+ serverOptions.loop = false;
+ serverOptions.streamSource = "http://" + config.serverHost + ":" + (config.serverListenPort + 1) + "/live/" + data.streamName + ".flv";
+ serverOptions.isStreaming = true;
+ cli.success("start stream");
+ } else {
+
+ serverOptions.loop = true;
+ serverOptions.isStreaming = false;
+ serverOptions.streamSource = "";
+ cli.success("stop stream");
+ }
+ view.io.emit("callbackLoad", view.getSlideData());
+ updateDashboard(io, data.displayId);
+ });
+
+
+ socket.on('controls.pause', function (data) {
+ let view = getView(data.displayId);
+ view.clearTimers();
+ getServerOptions(data.displayId).loop = false;
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on('controls.next', function (data) {
+ getView(data.displayId).mainLoop();
+ });
+
+ socket.on('controls.blackout', function (data) {
+ let view = getView(data.displayId);
+ view.serverOptions.blackout = view.serverOptions.blackout === false;
+
+ view.io.emit("callback.blackout", {serverOptions: view.serverOptions});
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on("controls.preview", function (data) {
+ let bundle = self.bundleManager.getBundle(data.bundle);
+ let json = "{}";
+ if (data.fileName) {
+ json = bundle.getSlideJsonFile(data.fileName);
+ }
+
+ try {
+ getPreview(socket).preview.emit("callback.preview",
+ {
+ bundleData: bundle.getBundleData(),
+ json: json,
+ displayId: data.displayId
+ }
+ );
+ } catch (err) {
+ cli.error("preview instance not found");
+ }
+
+ });
+
+ socket.on("controls.skipTo", function (data) {
+ let bundleSettings = getView(data.displayId).getBundle();
+ let slides = bundleSettings.enabledSlides;
+
+ let idx = slides.indexOf(data.fileName);
+ if (idx >= 0) {
+ getServerOptions(data.displayId).loopIndex = idx;
+ }
+ getView(data.displayId).mainLoop();
+ });
+
+ socket.on('controls.toggle', function (data) {
+ let fileName = data.fileName;
+ let bundleSettings = getView(data.displayId).getBundle();
+ let idx = bundleSettings.disabledSlides.indexOf(fileName);
+
+ if (idx > -1) {
+ bundleSettings.setSlideStatus(fileName, true);
+ } else {
+ bundleSettings.setSlideStatus(fileName, false);
+ }
+
+ updateDashboard(socket, data.displayId);
+ });
+
+ // override
+ socket.on('admin.override', function (data) {
+ let view = getView(parseInt(data.displayId));
+ view.overrideSlide(data.json, data.png);
+ view.displayCurrentSlide();
+ });
+
+ socket.on('admin.setBundle', function (data) {
+ let view = getView(data.displayId);
+ view.changeBundle(data.bundle);
+ view.displayCurrentSlide();
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on('admin.dashboard.sync', function (data) {
+ syncDashboard(socket, 0);
+ updateDashboard(socket, data.displayId);
+ });
+
+ socket.on('admin.setDisplay', function (data) {
+ syncDashboard(socket, data.display);
+ updateDashboard(socket, data.display);
+ });
+
+ socket.on('admin.listSlides', function (data) {
+ updateDashboard(socket, data.displayId);
+ });
+
+ socket.on('admin.listBundles', function (data) {
+ updateDashboard(socket, data.displayId);
+ });
+
+
+ socket.on('admin.reorderSlides', function (data) {
+ let view = getView(data.displayId);
+ let serverOptions = view.serverOptions;
+ let bundle = view.getBundle();
+
+ let i = 0;
+ console.log(data.sortedIDs);
+
+ for (let uuid of data.sortedIDs) {
+ bundle.setIndex(uuid, i);
+ i += 1;
+ }
+
+ for (let slide of bundle.allSlides) {
+ // calculate new index for next slide;
+ if (slide.file === serverOptions.currentFile) {
+ serverOptions.loopIndex = slide.index + 1;
+ }
+ }
+
+ bundle.save();
+ updateDashboard(io, data.displayId);
+ });
+
+ /** remove slide **/
+ socket.on('admin.removeSlide', function (data) {
+ let view = getView(data.displayId);
+ let bundle = view.getBundle();
+ bundle.removeUuid(data.uuid);
+ updateSlides(io, bundle.getBundleData().bundleName, bundle);
+ });
+
+ /** edit **/
+ socket.on('admin.editSlide', function (data) {
+ let bundle = self.bundleManager.getBundle(data.bundleName);
+ let bundleData = bundle.getBundleData();
+ let json = "{}";
+ if (data.fileName) {
+ json = bundle.getSlideJsonFile(data.fileName);
+ }
+ socket.emit("callbackEdit", {bundleData: bundleData, json: json});
+ });
+
+ /** rename **/
+ socket.on('admin.renameSlide', function (data) {
+ getView(data.displayId).getBundle().setName(data.uuid, data.name);
+ updateDashboard(io, data.displayId);
+ });
+ /** change transition **/
+ socket.on('admin.setTransition', function (data) {
+ getServerOptions(data.displayId).transition = data.transition;
+ updateDashboard(io, data.displayId);
+ });
+
+ socket.on('edit.save', function (data) {
+
+ let filename = data.fileName.replace(".json", "");
+ if (filename === "") {
+ filename = uuidv4();
+ }
+
+ try {
+ fs.writeFileSync("./data/" + data.bundleName + "/render/" + filename + ".png", data.png.replace(/^data:image\/png;base64,/, ""), "base64");
+ fs.writeFileSync("./data/" + data.bundleName + "/slides/" + filename, JSON.stringify(data.json));
+
+ let bundle = self.bundleManager.getBundle(data.bundleName);
+
+ let template = {
+ file: filename,
+ name: data.name,
+ duration: null,
+ enabled: true,
+ displayTime: true,
+ index: bundle.allSlides.length,
+ };
+
+ let obj = bundle.findSlideByUuid(filename);
+ if (Object.keys(obj).length === 0 && obj.constructor === Object) {
+ bundle.allSlides.push(template);
+ }
+
+ bundle.save();
+
+ cli.success("save new slide data");
+ socket.emit("callback.save", {});
+ announce([parseInt(data.displayId)], "callback.reloadImage", {
+ bundle: data.bundleName,
+ file: filename
+ });
+ updateSlides(io, data.bundleName, bundle);
+
+ } catch (err) {
+ cli.error(err, "save new slide date");
+ socket.emit("callback.save", {error: err});
+ }
+ });
+
+ socket.on('edit.uploadImage', function (data) {
+
+ let filename = data.name.replace(/\.[^/.]+$/, "");
+ let ext = "." + data.type.replace("image/", "");
+ if (filename === "") {
+ filename = uuidv4();
+ }
+ try {
+ let replace = "^data:" + data.type + ";base64,";
+ let re = new RegExp(replace, "g");
+
+ fs.writeFileSync("./data/" + data.bundleName + "/images/" + filename + ext, data.imageData.replace(re, ""), "base64");
+ cli.success("upload of " + filename + ext);
+ socket.emit("callback.edit.updateFileList", {});
+ } catch (err) {
+ cli.error("upload of " + filename + ext);
+ cli.error(err);
+ socket.emit("callback.edit.updateFileList", {error: err});
+ }
+ });
+
+ socket.on('edit.deleteImage', function (data) {
+ try {
+ fs.unlinkSync("./data/" + data.bundleName + "/images/" + data.name);
+ cli.success("delete " + data.name);
+ socket.emit("callback.edit.updateFileList", {});
+ } catch (err) {
+ cli.error(err, "delete image");
+ socket.emit("callback.edit.updateFileList", {error: err});
+ }
+ });
+
+ }); // io
+
+
+ /** helper functions */
+
+ /**
+ * @param {object} socket
+ * @param {number} displayId
+ */
+ function updateDashboard(socket, displayId) {
+ let view = getView(displayId);
+ let serverOptions = view.serverOptions;
+ let bundleSettings = view.getBundle();
+
+ socket.emit("callback.dashboard.update", {
+ bundleSettings: bundleSettings,
+ allSlides: bundleSettings.allSlides,
+ serverOptions: serverOptions,
+ displayId: displayId
+ });
+ }
+
+ function updateSlides(socket, bundlename, bundle) {
+ socket.emit("callback.dashboard.updateSlides", {
+ bundleName: bundlename,
+ bundleSettings: bundle
+ });
+ }
+
+ function syncDashboard(socket, displayId) {
+
+ previewInstances.push({adminId: socket.id, preview: {}, currentView: {}});
+ let serverOptions = getView(displayId).serverOptions;
+ let bundleDirs = getDirectories("./data");
+ let meta = JSON.parse(fs.readFileSync("./data/meta.json").toString());
+
+ socket.emit("callback.dashboard.sync", {
+ displayId: displayId,
+ bundleDirs: bundleDirs,
+ serverOptions: serverOptions,
+ displays: meta.displays
+ });
+ }
+
+ function getDirectories(path) {
+ return fs.readdirSync(path).filter(function (file) {
+ return fs.statSync(path + '/' + file).isDirectory();
+ });
+ }
+
+ function getPreview(socket) {
+ for (let i in previewInstances) {
+ if (previewInstances[i].adminId === socket.id) {
+ return previewInstances[i];
+ }
+ }
+
+ throw "error";
+ }
+
+ /** Generate an uuid
+ * @url https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#2117523 **/
+ function uuidv4() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ /**
+ * @param {number} displayId
+ */
+ function getServerOptions(displayId) {
+ return getView(displayId).serverOptions;
+
+ }
+
+ /**
+ *
+ * @param displayId
+ */
+ function getView(displayId) {
+ try {
+ return screenViews[parseInt(displayId)];
+ } catch (err) {
+ throw "display not found";
+ }
+ }
+ }
+
+} // admin
+
+
+module.exports = admin;
\ No newline at end of file
diff --git a/modules/auth.js b/modules/auth.js
new file mode 100644
index 0000000..be973a3
--- /dev/null
+++ b/modules/auth.js
@@ -0,0 +1,28 @@
+
+let basicAuth = require("basic-auth");
+let config = require("../config.js");
+
+module.exports = function (req, res, next) {
+ function unauthorized(res) {
+ res.set("WWW-Authenticate", "Basic realm=Authorization Required");
+ return res.sendStatus(401);
+ }
+
+ let user = basicAuth(req);
+
+ if (!user || !user.name || !user.pass) {
+ return unauthorized(res);
+ }
+
+ for (let i in config.admins) {
+ let admin = config.admins[i];
+ if (user.name === admin.name && user.pass === admin.pass) {
+ return next('route');
+ }
+ }
+
+ return unauthorized(res);
+};
+
+
+
diff --git a/modules/bundle.js b/modules/bundle.js
new file mode 100644
index 0000000..46f2ba8
--- /dev/null
+++ b/modules/bundle.js
@@ -0,0 +1,152 @@
+let cli = require("./cli.js");
+let fs = require("fs");
+
+class bundleClass {
+
+ constructor(name, data, slides) {
+ this.name = name;
+ this.bundleData = data;
+ this.allSlides = slides;
+ this.enabledSlides = [];
+ this.disabledSlides = [];
+ this.sync();
+ }
+
+ getBundleData() {
+ return this.bundleData;
+ }
+
+ getSlideJsonFile(fileName) {
+ try {
+ return fs.readFileSync("./data/" + this.name + "/slides/" + fileName).toString();
+ } catch (err) {
+ return "{}";
+ }
+ }
+
+ findSlideByUuid(uuid) {
+ for (let slide of this.allSlides) {
+ if (slide.file === uuid) {
+ return slide;
+ }
+ }
+ return {};
+ }
+
+ removeUuid(uuid) {
+ // delete files from fs
+ let newList = [];
+ let x = 0;
+ for (let slide of this.allSlides) {
+ if (slide.file !== uuid) {
+ let ent = slide;
+ ent.index = x;
+ newList.push(ent);
+ x++;
+ }
+ if (slide.file === uuid) {
+ let path = "./data/" + this.name;
+
+ try {
+ if (fs.existsSync(path + "/render/" + uuid + ".png")) {
+ fs.unlinkSync(path + "/render/" + uuid + ".png");
+ cli.success("removing file:" + path + "/render/" + uuid + ".png");
+ }
+ if (fs.existsSync(path + "/slides/" + uuid)) {
+ fs.unlinkSync(path + "/slides/" + uuid);
+ cli.success("removing file:" + path + "/slides/" + uuid);
+ }
+ } catch (err) {
+ cli.error("error while deleting file:" + uuid, err);
+ }
+ }
+ }
+ this.allSlides = newList;
+ this.save();
+ }
+
+ setSlideStatus(uuid, status) {
+ for (let i in this.allSlides) {
+ if (this.allSlides[i].file === uuid) {
+ this.allSlides[i].enabled = status;
+ this.sync();
+ break;
+ }
+ }
+ }
+
+ setIndex(uuid, index) {
+ for (let i in this.allSlides) {
+ if (this.allSlides[i].file === uuid) {
+ this.allSlides[i].index = index;
+ this.sync();
+ return;
+ }
+ }
+ }
+
+ setName(uuid, name) {
+ for (let i in this.allSlides) {
+ if (this.allSlides[i].file === uuid) {
+ this.allSlides[i].name = name;
+ this.save();
+ return;
+ }
+ }
+ }
+
+ sync() {
+ this.enabledSlides = [];
+ this.disabledSlides = [];
+ this.allSlides.sort(sortByProperty('index'));
+ for (let slide of this.allSlides) {
+ if (slide.enabled) {
+ this.enabledSlides.push(slide.file.replace(".json", ""));
+ } else {
+ this.disabledSlides.push(slide.file.replace(".json", ""));
+ }
+ }
+ }
+
+ toJson() {
+ this.sync();
+ return JSON.stringify(this.allSlides, null, "\t");
+ }
+
+ save() {
+ try {
+ fs.writeFileSync("./data/" + this.name + "/slides.json", this.toJson());
+ } catch (e) {
+ cli.error("error while saving file:", e);
+ }
+ }
+
+}
+
+/**
+ * helper function to clone object
+ * @returns {*}
+ */
+function clone(object) {
+ return JSON.parse(JSON.stringify(object));
+}
+
+
+/**
+ * helper function to sort object by property.
+ * @param property
+ * @returns {function(*, *): number}
+ */
+function sortByProperty(property) {
+ let sortOrder = 1;
+ if (property[0] === "-") {
+ sortOrder = -1;
+ property = property.substr(1);
+ }
+ return function (a, b) {
+ let result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
+ return result * sortOrder;
+ }
+}
+
+module.exports = bundleClass;
\ No newline at end of file
diff --git a/modules/bundleManager.js b/modules/bundleManager.js
new file mode 100644
index 0000000..66fd216
--- /dev/null
+++ b/modules/bundleManager.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const bundle = require('./bundle.js');
+let fs = require("fs");
+
+/**
+ * @module infoscreen3/bundleManager
+ */
+class bundleManager {
+
+ constructor() {
+ this.bundles = {};
+ let dirs = getDirectories("./data");
+
+ for (let name of dirs) {
+ let data = getJson("./data/" + name + "/bundle.json");
+ let slides = getJson("./data/" + name + "/slides.json");
+
+ this.bundles[name] = new bundle(name, data, slides);
+ }
+ }
+
+ /**
+ *
+ * @param {string} name
+ * @returns {bundle}
+ */
+ getBundle(name) {
+ return this.bundles[name];
+ }
+
+}
+
+function getDirectories(path) {
+ return fs.readdirSync(path).filter(function (file) {
+ return fs.statSync(path + '/' + file).isDirectory();
+ });
+}
+
+
+function getJson(file) {
+ return JSON.parse(fs.readFileSync(file).toString());
+}
+
+module.exports = bundleManager;
\ No newline at end of file
diff --git a/modules/cli.js b/modules/cli.js
new file mode 100644
index 0000000..66cd475
--- /dev/null
+++ b/modules/cli.js
@@ -0,0 +1,48 @@
+let chalk = require('chalk');
+
+module.exports = {
+ error: function (error, title) {
+ if (title == null) {
+ title = "";
+ }
+ this.log(chalk.bold.red("[Error]" + title) + " " + error);
+ },
+
+ info: function (string) {
+ this.log(chalk.cyan("[Info] ") + string);
+ },
+
+ success: function (string) {
+ this.log(string + chalk.green(" [Success]"));
+ },
+
+ log: function (string) {
+
+ console.log(getDateTime() + " " + string);
+ }
+};
+
+function getDateTime() {
+
+ var date = new Date();
+
+ var hour = date.getHours();
+ hour = (hour < 10 ? "0" : "") + hour;
+
+ var min = date.getMinutes();
+ min = (min < 10 ? "0" : "") + min;
+
+ var sec = date.getSeconds();
+ sec = (sec < 10 ? "0" : "") + sec;
+
+ var year = date.getFullYear();
+
+ var month = date.getMonth() + 1;
+ month = (month < 10 ? "0" : "") + month;
+
+ var day = date.getDate();
+ day = (day < 10 ? "0" : "") + day;
+
+ return year + "-" + month + "-" + day + " " + hour + ":" + min + ":" + sec;
+
+}
\ No newline at end of file
diff --git a/modules/display.js b/modules/display.js
new file mode 100644
index 0000000..1d784e9
--- /dev/null
+++ b/modules/display.js
@@ -0,0 +1,215 @@
+let fs = require("fs");
+let path = require("path");
+let cli = require("./cli.js");
+let bundleManager = require('./bundleManager.js');
+
+/**
+ * handles one instance of display
+ * @module infoscreen3/display
+ */
+class display {
+
+ /**
+ * @param {object} sharedIO - socketIO
+ * @param {object} dispatcher - dispatcher
+ * @param {object} meta - reads from ./data/meta.json --> as display[displayId]
+ * @param {number} displayId - this display instances id
+ * @param {object} bundleManager
+ */
+ constructor(sharedIO, dispatcher, meta, displayId, bundleManager) {
+ cli.info("Starting display" + displayId + ": " + meta.name + "...");
+
+ /** @property {Array} timeoutId - holds setTimeout id's for mainLoop */
+ this.timeoutId = [];
+ this.bundleManager = bundleManager;
+ let self = this;
+
+ /**
+ * @property {number} displayId - this display id
+ * @property {number} currentId - index number @see getBundle()
+ * @property {string} currentBundle - current bundle directory name at ./data/
+ * @property {string} currentFile - current filename at bundle
+ * @property {string} currentJSON - current slides json data
+ * @property {boolean} loop - runs main loop
+ * @property {number} loopIndex - index number used to calculate slide at enabledSlides @see getBundle()
+ * @property {boolean} blackout - should the blackout be enabled
+ * @property {boolean} isStreaming - is media server stream enabled at client
+ * @property {string} streamSource - URL for FLV media stream
+ * @property {string} transition values supported: bars,blinds,blinds3d,zip,blocks,blocks2,concentric,warp,cube,tiles3d,tiles3dprev,slide,swipe,dissolve
+ */
+ this.serverOptions = {
+ displayId: displayId,
+ currentId: -1,
+ currentBundle: "default",
+ currentFile: null,
+ slideDuration: 10,
+ loop: true,
+ loopIndex: 0,
+ blackout: false,
+ isStreaming: false,
+ streamSource: "",
+ isAnnounce: false,
+ transition: "tiles3d",
+ displayTime: true
+ };
+
+ let io = sharedIO.of("/display-" + displayId.toString());
+ this.io = io;
+ this.name = meta.name;
+ this.dispatcher = dispatcher;
+ this.id = displayId;
+
+ /**
+ * helper callback for global announce
+ /**
+ * @listens event:announce
+ *
+ */
+ dispatcher.on("announce", function (obj) {
+ // if global announce, ie screens is null
+ if (obj.screens == null) {
+ io.emit(obj.event, obj.data);
+ } else {
+ // screens is array
+ for (let screen of obj.screens) {
+ // so if match, emit data from here
+ if (screen === displayId) {
+ io.emit(obj.event, obj.data);
+ }
+ }
+ }
+ });
+
+ io.on("connection",
+ /**
+ * @param socket
+ */
+ function (socket) {
+ console.log("WS display" + displayId + ":" + socket.conn.remoteAddress + " connect");
+
+ socket.on('error', function (error) {
+ console.log("WS display" + displayId + ": error");
+ console.log(error);
+ });
+
+ socket.on('disconnect', function (reason) {
+ console.log("WS display" + displayId + ": disconnect " + reason + " " + socket.conn.remoteAddress);
+ });
+
+ socket.on('sync', function () {
+ cli.info("request sync " + self.name);
+ socket.emit("callbackLoad", self.getSlideData());
+ });
+
+ }); // io
+
+ this.init(meta);
+ cli.info("done.");
+ } // display
+
+
+ init(meta) {
+ this.changeBundle(meta.bundle);
+ this.mainLoop();
+ }
+
+ /**
+ * change displayed bundle
+ * @param {string} bundleName
+ */
+ changeBundle(bundleName) {
+ let bundle = this.getBundle();
+ this.serverOptions.currentBundle = bundleName;
+ this.serverOptions.loopIndex = 0;
+ this.serverOptions.currentFile = bundle.enabledSlides[0] || "";
+ this.serverOptions.slideDuration = bundle.findSlideByUuid(bundle.enabledSlides[0]).duration || bundle.getBundleData().duration;
+ this.serverOptions.currentId = bundle.enabledSlides.indexOf(this.serverOptions.currentFile);
+ }
+
+ getSlideData() {
+ let bundle = this.getBundle();
+ return {bundleData: bundle.getBundleData(), slides: bundle.allSlides, serverOptions: this.serverOptions};
+ }
+
+ /**
+ * clears timers
+ */
+ clearTimers() {
+ for (let i in this.timeoutId) {
+ clearTimeout(this.timeoutId[i]);
+ delete this.timeoutId[i];
+ }
+ }
+
+ /**
+ * @returns {bundle}
+ */
+ getBundle() {
+ return this.bundleManager.getBundle(this.serverOptions.currentBundle);
+ }
+
+
+ /**
+ * emits custom event for screens
+ * @fires event:announce
+ * @param screens list of screenId's use "admin" for admin interface
+ * @param event string event to io.emit on the selected screens
+ * @param data object object to send
+ */
+ announce(screens, event, data) {
+ /**
+ * @event event:announce
+ */
+ this.dispatcher.emit("announce", {screens: screens, event: event, data: data});
+ }
+
+ /**
+ * display main loop, which updates the display periodically
+ */
+ mainLoop() {
+ if (this.serverOptions.loop) {
+ this.clearTimers();
+ }
+ this.serverOptions.isAnnounce = false;
+
+ let bundle = this.getBundle();
+ let slides = bundle.enabledSlides;
+
+ if (slides.length >= 0) {
+ let lIndex = this.serverOptions.loopIndex;
+ if (lIndex < 0) lIndex = slides.length - Math.abs(lIndex); // negative values should shift from end
+ let idx = (lIndex % slides.length); // make sure id is normalized to slide count
+ this.serverOptions.loopIndex = idx;
+ this.serverOptions.currentFile = slides[idx];
+ this.serverOptions.slideDuration = bundle.findSlideByUuid(slides[idx]).duration || bundle.getBundleData().duration;
+ this.serverOptions.currentId = bundle.enabledSlides.indexOf(this.serverOptions.currentFile);
+ }
+
+ this.displayCurrentSlide();
+
+
+ this.serverOptions.loopIndex += 1;
+
+ if (this.serverOptions.loop) {
+ let that = this;
+ this.timeoutId.push(
+ setTimeout(function () {
+ that.mainLoop();
+ }, this.serverOptions.slideDuration * 1000));
+ }
+ }
+
+ overrideSlide(json, pngData) {
+ this.clearTimers();
+ this.serverOptions.loop = false;
+ this.serverOptions.isAnnounce = true;
+ // save temporarily png data...
+ fs.writeFileSync("./tmp/display_" + this.serverOptions.displayId + ".png", pngData.replace(/^data:image\/png;base64,/, ""), "base64");
+ }
+
+ displayCurrentSlide() {
+ this.announce([this.serverOptions.displayId, "admin"], "callback.update", this.getSlideData());
+ }
+}
+
+module.exports = display;
\ No newline at end of file
diff --git a/modules/websocket.js b/modules/websocket.js
new file mode 100644
index 0000000..009375b
--- /dev/null
+++ b/modules/websocket.js
@@ -0,0 +1,84 @@
+let meta = require("../data/meta.json");
+let display = require("./display.js");
+let admin = require("./admin.js");
+let cli = require('./cli.js');
+let bundleManager = require('./bundleManager.js');
+let chalk = require('chalk');
+let fs = require('fs');
+
+/**
+ *
+ * @type {module:infoscreen3/display[]}
+ */
+let screenView = [];
+
+/**
+ * @exports infoscreen3/websocket
+ */
+module.exports = function (server, app, io, dispatcher) {
+ console.log(chalk.green(">> ") + "InfoScreen3" + chalk.green("<<"));
+ cli.log("Checking for write permissions...");
+
+ try {
+ fs.accessSync("./data", fs.W_OK);
+ cli.success("data directory (./data) is writable");
+ } catch (err) {
+ cli.error("data directory (./data) is not writable", err);
+ process.exit(1);
+ }
+
+ try {
+ fs.accessSync("./tmp", fs.W_OK);
+ cli.success("temp directory (./tmp) is writable ");
+ let tempFiles = fs.readdirSync("./tmp/", {
+ dotfiles: false
+ });
+ cli.info("removing temp-files...");
+ for (let file of tempFiles) {
+ if (file !== ".gitkeep") {
+ fs.unlinkSync("./tmp/" + file);
+ }
+ }
+ cli.info("done.");
+
+ } catch (err) {
+ cli.error("temp directory (./tmp) is not writable", err);
+ process.exit(1);
+ }
+
+ cli.info("Starting bundle manager...");
+
+ let bundler = new bundleManager();
+
+ cli.info("Starting websocket backends...");
+
+ let screenId = 0;
+ for (let metadata of meta.displays) {
+ screenView.push(new display(io, dispatcher, metadata, screenId, bundler));
+ screenId += 1;
+ }
+
+ let adminView = new admin(io, dispatcher, screenView, bundler);
+
+ io.of("/lobby").on("connection", function (socket) {
+ cli.info("WS/" + socket.conn.remoteAddress + " connect");
+
+ socket.on('error', function (error) {
+ cli.error(error, "WS/ error");
+ });
+
+ socket.on('disconnect', function (reason) {
+ cli.info("WS/ " + reason + " " + socket.conn.remoteAddress);
+ });
+
+ socket.on('displays', function () {
+ let previewImages = [];
+ for (let display of screenView) {
+ previewImages.push("/render/" + display.serverOptions.currentBundle + "/" + display.serverOptions.currentFile + ".png");
+ }
+ socket.emit("callback.displays", {displays: meta.displays, previewImages: previewImages});
+ });
+ });
+
+ return screenView;
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6f7c3a0
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1362 @@
+{
+ "name": "infoscreen3",
+ "version": "0.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+ "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "requires": {
+ "mime-types": "~2.1.18",
+ "negotiator": "0.6.1"
+ }
+ },
+ "after": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+ },
+ "ajv": {
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "optional": true,
+ "requires": {
+ "co": "^4.6.0",
+ "fast-deep-equal": "^1.0.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.3.0"
+ }
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "arraybuffer.slice": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+ },
+ "asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+ "optional": true
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "optional": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ },
+ "async-limiter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "optional": true
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "optional": true
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+ "optional": true
+ },
+ "backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ },
+ "base64-arraybuffer": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+ },
+ "base64id": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
+ },
+ "basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ }
+ }
+ },
+ "basic-auth-connect": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz",
+ "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI="
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "better-assert": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+ "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "requires": {
+ "callsite": "1.0.0"
+ }
+ },
+ "blob": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+ },
+ "body-parser": {
+ "version": "1.18.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+ "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "~1.6.3",
+ "iconv-lite": "0.4.23",
+ "on-finished": "~2.3.0",
+ "qs": "6.5.2",
+ "raw-body": "2.3.3",
+ "type-is": "~1.6.16"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "busboy": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+ "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+ "requires": {
+ "dicer": "0.2.5",
+ "readable-stream": "1.1.x"
+ }
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "optional": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+ "optional": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "combined-stream": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+ "optional": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "component-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+ },
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+ },
+ "component-inherit": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "connect-busboy": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/connect-busboy/-/connect-busboy-0.0.2.tgz",
+ "integrity": "sha1-rFyclmchcYheV2xmsr/ZXTuxEJc=",
+ "requires": {
+ "busboy": "*"
+ }
+ },
+ "content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "cookie-parser": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz",
+ "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=",
+ "requires": {
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6"
+ }
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "dateformat": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
+ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "dicer": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+ "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+ "requires": {
+ "readable-stream": "1.1.x",
+ "streamsearch": "0.1.2"
+ }
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "engine.io": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz",
+ "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==",
+ "requires": {
+ "accepts": "~1.3.4",
+ "base64id": "1.0.0",
+ "cookie": "0.3.1",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.0",
+ "ws": "~6.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-client": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.1.tgz",
+ "integrity": "sha512-q66JBFuQcy7CSlfAz9L3jH+v7DTT3i6ZEadYcVj2pOs8/0uJHLxKX3WBkGTvULJMdz0tUCyJag0aKT/dpXL9BQ==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "component-inherit": "0.0.3",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.1",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~6.1.0",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-parser": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+ "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+ "requires": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
+ }
+ },
+ "errno": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+ "optional": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
+ "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.18.3",
+ "content-disposition": "0.5.2",
+ "content-type": "~1.0.4",
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.1.1",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.4",
+ "qs": "6.5.2",
+ "range-parser": "~1.2.0",
+ "safe-buffer": "5.1.2",
+ "send": "0.16.2",
+ "serve-static": "1.13.2",
+ "setprototypeof": "1.1.0",
+ "statuses": "~1.4.0",
+ "type-is": "~1.6.16",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "optional": true
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+ },
+ "fast-deep-equal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+ "optional": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+ "optional": true
+ },
+ "finalhandler": {
+ "version": "1.1.1",
+ "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+ "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.4.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "foreachasync": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz",
+ "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY="
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "optional": true
+ },
+ "form-data": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+ "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+ "optional": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "dependencies": {
+ "combined-stream": {
+ "version": "1.0.6",
+ "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+ "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+ "optional": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ }
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+ "optional": true
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+ "optional": true
+ },
+ "har-validator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
+ "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+ "optional": true,
+ "requires": {
+ "ajv": "^5.3.0",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has-binary2": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+ "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+ "requires": {
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ }
+ }
+ },
+ "has-cors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+ "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+ "optional": true
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+ "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
+ },
+ "is": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz",
+ "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU="
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "optional": true
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "optional": true
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "optional": true
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "optional": true
+ },
+ "json-schema-traverse": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+ "optional": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "optional": true
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "less": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/less/-/less-3.0.4.tgz",
+ "integrity": "sha512-q3SyEnPKbk9zh4l36PGeW2fgynKu+FpbhiUNx/yaiBUQ3V0CbACCgb9FzYWcRgI2DJlP6eI4jc8XPrCTi55YcQ==",
+ "requires": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "mime": "^1.4.1",
+ "mkdirp": "^0.5.0",
+ "promise": "^7.1.1",
+ "request": "^2.83.0",
+ "source-map": "~0.6.0"
+ }
+ },
+ "less-middleware": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/less-middleware/-/less-middleware-3.0.1.tgz",
+ "integrity": "sha512-FCG4zGd7c/lKMWGDzzkfU9Lcndfjc4TcSnuRwUtJ4E+NxPMuDGe7lJA1rOn+SI9x2lak+s4b6sQkcPa45eqKiw==",
+ "requires": {
+ "less": "~3.0.4",
+ "mkdirp": "~0.5.1",
+ "node.extend": "~2.0.0"
+ }
+ },
+ "locutus": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.10.tgz",
+ "integrity": "sha512-AZg2kCqrquMJ5FehDsBidV0qHl98NrsYtseUClzjAQ3HFnsDBJTCwGVplSQ82t9/QfgahqvTjaKcZqZkHmS0wQ=="
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ },
+ "mime-db": {
+ "version": "1.36.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
+ "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw=="
+ },
+ "mime-types": {
+ "version": "2.1.20",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
+ "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+ "requires": {
+ "mime-db": "~1.36.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "morgan": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
+ "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
+ "requires": {
+ "basic-auth": "~2.0.0",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.1"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ },
+ "node-media-server": {
+ "version": "1.4.9",
+ "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-1.4.9.tgz",
+ "integrity": "sha512-lqWtbJDaL9DEJrRAdyMcwvsaJ1SFn23vDu6gItk0t6x1Y4GTVDRXl3R/VHqV6mpSFioi3qeacPkxCJIorbN83g==",
+ "requires": {
+ "basic-auth-connect": "^1.0.0",
+ "chalk": "^2.4.1",
+ "dateformat": "^3.0.3",
+ "express": "^4.16.3",
+ "lodash": "^4.17.10",
+ "mkdirp": "^0.5.1",
+ "ws": "^5.1.1"
+ },
+ "dependencies": {
+ "ws": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ }
+ }
+ },
+ "node.extend": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.0.tgz",
+ "integrity": "sha1-dSWih1Z36lNHhKXhCseJVhOWFN8=",
+ "requires": {
+ "is": "^3.2.1"
+ }
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "object-component": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+ "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
+ },
+ "parseqs": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+ "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseuri": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+ "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+ "optional": true
+ },
+ "promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "optional": true,
+ "requires": {
+ "asap": "~2.0.3"
+ }
+ },
+ "proxy-addr": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+ "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.8.0"
+ }
+ },
+ "prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+ "optional": true
+ },
+ "psl": {
+ "version": "1.1.29",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+ "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+ "optional": true
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "optional": true
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ },
+ "raw-body": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+ "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.3",
+ "iconv-lite": "0.4.23",
+ "unpipe": "1.0.0"
+ }
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "request": {
+ "version": "2.88.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "optional": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "optional": true
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "optional": true
+ }
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+ "optional": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
+ }
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
+ "socket.io": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz",
+ "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==",
+ "requires": {
+ "debug": "~4.1.0",
+ "engine.io": "~3.3.1",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.2.0",
+ "socket.io-parser": "~3.3.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
+ "socket.io-adapter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+ "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
+ },
+ "socket.io-client": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz",
+ "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==",
+ "requires": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "engine.io-client": "~3.3.1",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.3.0",
+ "to-array": "0.1.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "socket.io-parser": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
+ "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ }
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true
+ },
+ "sshpk": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+ "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+ "optional": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ },
+ "streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "to-array": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+ },
+ "tough-cookie": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "optional": true,
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ }
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "optional": true
+ },
+ "twig": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/twig/-/twig-0.10.3.tgz",
+ "integrity": "sha1-Z2BOCOGSDr8vr4CpAeJWGJyKPGc=",
+ "requires": {
+ "locutus": "^2.0.5",
+ "minimatch": "3.0.x",
+ "walk": "2.3.x"
+ }
+ },
+ "type-is": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.18"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+ "optional": true
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "walk": {
+ "version": "2.3.14",
+ "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz",
+ "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==",
+ "requires": {
+ "foreachasync": "^3.0.0"
+ }
+ },
+ "ws": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
+ "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ },
+ "xmlhttprequest-ssl": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+ },
+ "yeast": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..dcdc177
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "infoscreen3",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "start": "node --expose-gc ./bin/www"
+ },
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^2.4.2",
+ "connect-busboy": "0.0.2",
+ "cookie-parser": "~1.4.3",
+ "cors": "^2.8.5",
+ "debug": "~2.6.9",
+ "express": "^4.16.4",
+ "http-errors": "~1.6.2",
+ "less-middleware": "^3.0.1",
+ "morgan": "~1.9.0",
+ "node-media-server": "~1.4.9",
+ "socket.io": "^2.2.0",
+ "twig": "~0.10.3"
+ }
+}
diff --git a/public/js/dashboard.js b/public/js/dashboard.js
new file mode 100644
index 0000000..1a1c6e3
--- /dev/null
+++ b/public/js/dashboard.js
@@ -0,0 +1,280 @@
+var serverOptions;
+var bundleSettings;
+var bundleDirs;
+var displayId = 0;
+var displayList;
+
+
+socket.on('connect', function () {
+ canvas = new fabric.Canvas('edit');
+ canvas.includeDefaultValues = false;
+ emit('admin.dashboard.sync');
+});
+
+socket.on("callback.update", function (data) {
+ // filter out other displays updates than the current one
+ if (data.serverOptions.displayId === displayId) {
+ serverOptions = data.serverOptions;
+ $('#bundleSlides').children().css("border", "1px solid black");
+ $('#' + serverOptions.currentFile).css("border", "1px solid #1ebc30");
+ }
+ updateControls(data.serverOptions);
+});
+
+
+socket.on("callback.dashboard.sync", function (data) {
+ displayId = parseInt(data.displayId);
+ serverOptions = data.serverOptions;
+ bundleDirs = data.bundleDirs;
+ displayList = data.displays;
+ updateControls(data.serverOptions);
+
+ // update dropdown at menu with bundles
+ var valueArray = [];
+ for (var dir of bundleDirs) {
+ valueArray.push({name: dir, value: dir});
+ }
+
+ $('#bundles')
+ .dropdown({
+ direction: "upward",
+ values: valueArray,
+ action: function (text, value) {
+ $('#bundles')
+ .dropdown("hide");
+ emit("admin.setBundle", {bundle: value});
+ }
+ }).dropdown("set selected", {bundle: serverOptions.currentBundle});
+ $('#currentBundle').text(serverOptions.currentBundle);
+
+ // update dropdown at menu with bundles
+ valueArray = [];
+ var x = 0;
+ for (var display of data.displays) {
+ valueArray.push({name: display.name, value: x});
+ x++;
+ }
+
+ $('#displays')
+ .dropdown({
+ direction: "upward",
+ values: valueArray,
+ action: function (text, value) {
+ $('#displays')
+ .dropdown("hide");
+
+ displayId = parseInt(value);
+ emit("admin.setDisplay", {display: value});
+ var program = document.getElementById('program');
+ program.src = "/display/" + value;
+ }
+ }).dropdown("set selected", displayId);
+
+ $('.currentDisplay').text(displayList[displayId].name);
+
+ var transitionArray = [];
+ var values = ["bars", "blinds", "blinds3d", "zip", "blocks", "blocks2", "concentric", "warp", "cube", "tiles3d", "tiles3dprev", "slide", "swipe", "dissolve"];
+
+ for (var i in values) {
+ transitionArray.push({name: values[i], value: values[i]});
+ }
+
+ $('#transitions')
+ .dropdown({
+ direction: "upward",
+ values: transitionArray,
+ action: function (text, value) {
+ $('#displays')
+ .dropdown("hide");
+
+ emit("admin.setTransition", {transition: value});
+ $('.currentTransition').text(value);
+
+ }
+ }).dropdown("set selected", serverOptions.transition);
+
+ $('#currentTransition').text(serverOptions.transition);
+
+ var preview = document.getElementById('preview');
+ preview.src = "/admin/preview?displayId=" + displayId + "&socket=" + encodeURIComponent(socket.id);
+
+
+ let output = "";
+ for (var i in data.bundleDirs) {
+ let bundle = data.bundleDirs[i];
+ output += '
{{ error.stack }}
+{% endblock %}
diff --git a/views/index.twig b/views/index.twig
new file mode 100644
index 0000000..202f7ab
--- /dev/null
+++ b/views/index.twig
@@ -0,0 +1,62 @@
+{% extends 'layout/view.twig' %}
+
+{% block header %}
+
+{% endblock %}
+
+{% block body %}
+