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 += '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + bundle + '
' + + '
' + + '
'; + } + + $('#allBundles').html(output); + + +}); + +socket.on("callback.dashboard.updateSlides", function (data) { + + console.log("update"); + if (serverOptions.currentBundle === data.bundleName) { + bundleSettings = data.bundleSettings; + console.log("slides"); + updateSlides(data.bundleSettings); + } +}); + + + +socket.on("callback.dashboard.update", function (data) { + if (data.serverOptions.displayId !== displayId) + return; + + displayId = parseInt(data.displayId); + bundleSettings = data.bundleSettings; + serverOptions = data.serverOptions; + updateControls(data.serverOptions); + updateSlides(bundleSettings); + + $('#allBundles').children().css("border", "1px solid black"); + $('#bundle_' + serverOptions.currentBundle).css("border", "1px solid #1ebc30"); + $('#currentBundle').text(serverOptions.currentBundle); + $('#currentDisplay').text(displayList[displayId].name); + $('#transitions').dropdown("set selected", serverOptions.transition); + $('#currentTransition').text(serverOptions.transition); + + $('.editable').editable(function (value, settings) { + var uuid = $(this).parent().parent().attr("id"); + emit("admin.renameSlide", {uuid: uuid, name: value, bundleName: serverOptions.currentBundle}); + return (value); + }, { + submit: 'rename', + tooltip: "Doubleclick to edit...", + event: "dblclick", + cssclass: 'ui mini nopadded form', + cancelcssclass: 'ui tiny basic negative button', + submitcssclass: 'ui tiny basic positive button', + }); +}); + +$(function () { + fixPreview(); + $(".sortable").sortable({ + beforeStop: function (event, element) { + var sortedIDs = $(".sortable").sortable("toArray"); + emit("admin.reorderSlides", {sortedIDs: sortedIDs}); + } + }).disableSelection(); +}); + +function fixPreview() { + var con = $("#programContainer"), + aspect = (0.9 / 1.6), + width = con.innerWidth(), + height = Math.floor(width * aspect); + $("#program").css("width", width + "px").css("height", height + "px"); + $("#preview").css("width", width + "px").css("height", height + "px"); +} + +$(window).bind("resize", function () { + fixPreview(); +}); + +function createNew() { + editSlide(""); +} + +function editSlide(name) { + window.open("/admin/edit/slide?bundle=" + serverOptions.currentBundle + "&file=" + name + "&displayId=" + displayId, '_blank', 'location=no,height=800,width=1304,scrollbars=no,status=no'); +} + +function emit(callback, data) { + if (data === undefined || data === null) { + data = {}; + } + Object.assign(data, {displayId: parseInt(displayId)}); + socket.emit(callback, data); +} + +/** + * + * @param {display~serverOptions} serverOptions + */ +function updateControls() { + if (serverOptions.blackout) { + $('#blackout').removeClass('basic'); + } else { + $('#blackout').addClass('basic'); + } + if (serverOptions.isStreaming) { + $('#stream').removeClass('basic'); + } else { + $('#stream').addClass('basic'); + } + + if (serverOptions.loop) { + $('#play').addClass('green'); + $('#pause').removeClass('orange'); + + } else { + $('#play').removeClass('green'); + $('#pause').addClass('orange'); + } + +} + +function updateSlides(settings) { + var slides = settings.allSlides; + + var output = ""; + var index = 0; + var currentIndex = 0; + for (slide of slides) { + var status = "on"; + var color = "green"; + if (!slide.enabled) { + status = "off"; + color = "red"; + } + var iconStatus = "play"; + if (serverOptions.currentFile === slide.file) { + iconStatus = "play"; + currentIndex = index; + } + + output += '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '' + + '
' + slide.name.replace(".json", "") + '
' + + '
' + + '
'; + + index += 1; + } + + $("#bundleSlides").html(output); + $('#bundleSlides').children().css("border", "1px solid black"); + $('#' + serverOptions.currentFile).css("border", "1px solid #1ebc30"); +} + + +function remove(obj) { + if (confirm("Really delete slide?")) { + emit('admin.removeSlide', obj); + } +} +function forceReload() { + if (confirm("Really reload all connected display clients ?")) { + emit('admin.reload'); + } +} \ No newline at end of file diff --git a/public/js/display.js b/public/js/display.js new file mode 100644 index 0000000..5aadfcf --- /dev/null +++ b/public/js/display.js @@ -0,0 +1,382 @@ +function symmetricDifference(a1, a2) { + var result = []; + for (var i = 0; i < a1.length; i++) { + if (a2.indexOf(a1[i]) === -1) { + result.push(a1[i]); + } + } + for (i = 0; i < a2.length; i++) { + if (a1.indexOf(a2[i]) === -1) { + result.push(a2[i]); + } + } + return result; +} + +var connectionTimeoutId; + +$(function () { + if (!flux.browser.supportsTransitions) + alert("Flux Slider requires a browser that supports CSS3 transitions"); + + window.f = new flux.slider('#slider', { + controls: false, + captions: false, + autoplay: false, + pagination: false, + width: $(window).width(), + height: $(window).height(), + }); + + $(window).resize(function () { + if (this.resizeTO) clearTimeout(this.resizeTO); + this.resizeTO = setTimeout(function () { + $(this).trigger('windowResize'); + }, 250); + }); + + $(window).on('windowResize', function () { + window.f.setSize($(window).width(), $(window).height()); + }); + + updateTimeout(); +}); + +var layer = 0; + +/** bundleData + * @see data/default/bundle.json + * + **/ +var bundleData; +var serverOptions; +var streamStarted = false; +bundleData = { + background: "", + duration: 10, + styleHeader: {}, + styleText: {}, + useWebFonts: false +}; + +/*************************** + * window load, init + * **************************/ + +$(function () { + fixImageSizes(); + displayTime(); + setInterval(displayTime, 1000); +}); + +$(window).bind("resize", function () { + fixImageSizes(); +}); + + +function getLayer(offset) { + if (offset === undefined) offset = 0; + return "layer" + (layer + offset) % 2; +} + + +// socketio callbacks +/** when connected **/ +socket.on('connect', function () { + socket.emit("sync"); +}); + +socket.on('callback.blackout', function (data) { + serverOptions = data.serverOptions; + checkBlackout(); +}); + +socket.on('error', function () { + $("#networkstatus").css("opacity", 1); +}); + +socket.on('disconnect', function () { + $("#networkstatus").css("opacity", 1); +}); + +socket.on('pong', function () { + updateTimeout(); +}); + +socket.on('callback.time', function (data) { + + if ($('#time').hasClass('flipOutX')) { + $('#time').removeClass('flipOutX').addClass("flipInX"); + } else { + $('#time').addClass('flipOutX').removeClass("flipInX"); + } + +}); + +/** callback Load **/ +socket.on('callbackLoad', function (data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + preloadImages(data.slides); + checkBlackout(); + setBackground(data.bundleData.background); + if (checkStream(serverOptions) === false) { + nextSlide(data); + } +}); + + +/** callback Update **/ +socket.on('callback.update', function (data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + checkBlackout(); + if (checkStream(serverOptions) === false) { + nextSlide(data); + } +}); + +socket.on('callback.reload', function () { + location.reload(true); +}); + +socket.on('callback.reloadImage', function (data) { + reloadImage(data); +}); + +socket.on('callback.announce', function (data) { + checkBlackout(); + nextSlide(data); +}); + +socket.on('callback.forceSlide', function (data) { + checkBlackout(); + nextSlide(data); +}); + +/** + * Displays the local time for bottom of screen + * hh:mm + **/ +function displayTime() { + var date = new Date(); + var min = date.getMinutes(); + if (min < 10) min = "0" + min; + var time = date.getHours() + ":" + min; + $('#time').html(time); +} + +/** resize canvas to max width keeping aspect ratio 16:9**/ +function fixImageSizes() { + var con = $(window), + aspect = (0.9 / 1.6), + width = con.innerWidth(), + height = Math.floor(width * aspect); + + + $("#layer0").css("width", width + "px").css("height", height + "px"); + $("#layer1").css("width", width + "px").css("height", height + "px"); +} + + +function checkBlackout() { + updateTimeout(); + if (serverOptions.blackout) { + $("#blackoutLayer").css("opacity", 1); + } else { + $("#blackoutLayer").css("opacity", 0); + } +} + +function nextSlide(data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + checkImages(data.slides); + + if (serverOptions.isAnnounce) { + try { + var randomId = uuidv4(); + var image = new Image(); + image.src = "/tmp/" + serverOptions.displayId + "/?randomId=" + randomId; + image.id = randomId; + image.class = "temp"; + window.f.images.push(image); + window.f.showTempImage(randomId); + } catch (err) { + console.log(err); + } + } else { + window.f.showImageById(serverOptions.currentFile, serverOptions.transition); + } + setBackground(bundleData.background); +} + +function setBackground(background) { + background = "/background/" + bundleData.bundleName + "/" + background; + if (serverOptions.isStreaming) return; + + var video = document.getElementById("bgvid"); + var bg = $("#bg"); + var bgImage = document.getElementById("bgimg"); + if (background.indexOf(".mp4") !== -1) { + + if (parseUrl(video.src) !== background) { + bg.hide(); + $(video).show(); + video.src = background; + video.play(); + } + } else { + if (parseUrl(bgImage.src) !== background) { + bgImage.src = background; + bg.show(); + $(video).hide(); + video.src = null; + video.pause(); + } + } +} + +function parseUrl(url) { + return '/background' + url.split('background')[1] +} + +function showBackgroundOnly() { + $("#" + getLayer(1)).css("opacity", 0); + $("#" + getLayer()).css("opacity", 0); +} + +function checkStream(serverOptions) { + + if (serverOptions.isStreaming && streamStarted === false) { + console.log("start stream!"); + if (flvjs.isSupported()) { + var videoElement = document.getElementById('bgvid'); + flvPlayer = flvjs.createPlayer({ + type: 'flv', + url: serverOptions.streamSource + }); + try { + flvPlayer.attachMediaElement(videoElement); + flvPlayer.load(); + flvPlayer.play(); + showBackgroundOnly(); + streamStarted = true; + return true; + + } catch (err) { + console.log(err); + streamStarted = false; + return false; + } + } + } else if (serverOptions.isStreaming === false) { + if (typeof flvPlayer !== "undefined") { + if (flvPlayer != null) { + flvPlayer.pause(); + flvPlayer.unload(); + flvPlayer.detachMediaElement(); + flvPlayer.destroy(); + flvPlayer = null; + streamStarted = false; + } + } + return false; + } + return false; +} + +/** + * preload images + * @param allSlides + */ +function preloadImages(allSlides) { + window.f.clearImages(); + + for (var i in allSlides) { + try { + var image = new Image(); + image.src = "/render/" + serverOptions.currentBundle + "/" + allSlides[i].file + ".png"; + image.id = allSlides[i].file; + window.f.images.push(image); + } catch (err) { + console.log(err); + } + } + window.f.setupImages(); +} + +function reloadImage(data) { + console.log("reloadImage"); + console.log(data); + + window.f.clearImageById(data.file); + window.f.setupImages(); + + var image = new Image(); + image.src = "/render/" + data.bundle + "/" + data.file + ".png?" + uuidv4(); + image.id = data.file; + window.f.images.push(image); + + +} + +function checkImages(allSlides) { + var fluxIds = []; + var allIds = []; + + var fImages = window.f.images; + for (var j in fImages) { + fluxIds.push(fImages[j].id); + } + for (var k in allSlides) { + allIds.push(allSlides[k].file); + } + + // new slides count is less than slides in rotation + // remove slides + if (allSlides.length < fluxIds.length) { + console.log("less"); + var diffIds = symmetricDifference(fluxIds, allIds); + for (var l in diffIds) { + window.f.clearImageById(diffIds[l]); + } + } else + // else count is greater, so add slides + { + for (var i in allSlides) { + try { + if (fluxIds.indexOf(allSlides[i].file) < 0) { + console.log("adding"); + var image = new Image(); + image.src = "/render/" + serverOptions.currentBundle + "/" + allSlides[i].file + ".png"; + image.id = allSlides[i].file; + window.f.images.push(image); + } + } catch (err) { + console.log(err); + } + } + } + + +} + +/** 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); + }); +} + +function updateTimeout() { + clearTimeout(connectionTimeoutId); + $("#networkstatus").css("opacity", 0); + connectionTimeoutId = setTimeout(function () { + $("#networkstatus").css("opacity", 1); + }, 30000) +} + + diff --git a/public/js/displayLite.js b/public/js/displayLite.js new file mode 100644 index 0000000..6d0632e --- /dev/null +++ b/public/js/displayLite.js @@ -0,0 +1,255 @@ +var layer = 0; + +/** bundleData + * @see data/default/bundle.json + * + **/ +var connectionTimeoutId; +var bundleData; +var serverOptions; +var streamStarted = false; +bundleData = { + background: "", + duration: 10, + styleHeader: {}, + styleText: {}, + useWebFonts: false +}; + + +/*************************** + * window load, init + * **************************/ + +$(function () { + + fixImageSizes(); + displayTime(); + setInterval(displayTime, 1000); +}); + +$(window).bind("resize", function () { + fixImageSizes(); +}); + + +function getLayer(offset) { + if (offset === undefined) offset = 0; + return "layer" + (layer + offset) % 2; +} + + +// socketio callbacks +/** when connected **/ +socket.on('connect', function () { + console.log("connect, calling sync!"); + socket.emit("sync"); +}); + +socket.on('error', function () { + $("#networkstatus").css("opacity", 1); +}); + +socket.on('disconnect', function () { + $("#networkstatus").css("opacity", 1); +}); + +socket.on('pong', function () { + updateTimeout(); +}); + +socket.on('callback.blackout', function (data) { + serverOptions = data.serverOptions; + checkBlackout(); +}); + +socket.on('callback.time', function (data) { + + if ($('#time').hasClass('flipOutX')) { + $('#time').removeClass('flipOutX').addClass("flipInX"); + } else { + $('#time').addClass('flipOutX').removeClass("flipInX"); + } + +}); + +/** callback Load **/ +socket.on('callbackLoad', function (data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + checkBlackout(); + nextSlide(data); +}); + + +/** callback Update **/ +socket.on('callback.update', function (data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + checkBlackout(); + nextSlide(data); +}); + +socket.on('callback.reload', function () { + location.reload(true); +}); + +socket.on('callback.announce', function (data) { + checkBlackout(); + nextSlide(data); +}); + +socket.on('callback.forceSlide', function (data) { + checkBlackout(); + nextSlide(data); +}); + + +/** + * Displays the local time for bottom of screen + * hh:mm + **/ +function displayTime() { + var date = new Date(); + var min = date.getMinutes(); + if (min < 10) min = "0" + min; + var time = date.getHours() + ":" + min; + $('#time').html(time); +} + +/** resize canvas to max width keeping aspect ratio 16:9**/ +function fixImageSizes() { + var con = $(window), + aspect = (0.9 / 1.6), + width = con.innerWidth(), + height = Math.floor(width * aspect); + + + $("#layer0").css("width", width + "px").css("height", height + "px"); + $("#layer1").css("width", width + "px").css("height", height + "px"); +} + + +function checkBlackout() { + updateTimeout(); + if (serverOptions.blackout) { + $("#blackoutLayer").css("opacity", 1); + } else { + $("#blackoutLayer").css("opacity", 0); + } +} + +function nextSlide(data) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + + var elem = document.getElementById("img" + layer); + if (serverOptions.isAnnounce) { + try { + var randomId = uuidv4(); + elem.src = "/tmp/" + serverOptions.displayId + "/?randomId=" + randomId; + } catch (err) { + console.log(err); + } + } else { + elem.src = "/render/" + serverOptions.currentBundle + "/" + serverOptions.currentFile + ".png"; + } + + setBackground(bundleData.background); + $("#" + getLayer(1)).css("opacity", 0); + $("#" + getLayer()).css("opacity", 1); + + layer++; + if (layer > 1) { + layer = 0; + } + +} + +function setBackground(background) { + background = "/background/" + bundleData.bundleName + "/" + background; + + var video = document.getElementById("bgvid"); + var bg = $("#bg"); + var bgImage = document.getElementById("bgimg"); + if (background.indexOf(".mp4") !== -1) { + if (parseUrl(video.src) !== background) { + bg.hide(); + $(video).show(); + video.src = background; + video.play(); + } + } else { + if (parseUrl(bgImage.src) !== background) { + bgImage.src = background; + bg.show(); + $(video).hide(); + video.src = ""; + video.pause(); + } + } +} + +function parseUrl(url) { + return '/background' + url.split('background')[1] +} + +function showBackgroundOnly() { + $("#" + getLayer(1)).css("opacity", 0); + $("#" + getLayer()).css("opacity", 0); +} + +function checkStream(serverOptions) { + + if (serverOptions.isStreaming && streamStarted === false) { + if (flvjs.isSupported()) { + var videoElement = document.getElementById('bgvid'); + flvPlayer = flvjs.createPlayer({ + type: 'flv', + url: serverOptions.streamSource + }); + try { + flvPlayer.attachMediaElement(videoElement); + flvPlayer.load(); + flvPlayer.play(); + showBackgroundOnly(); + streamStarted = true; + return true; + } catch (err) { + console.log(err); + streamStarted = false; + return false; + } + } + } else if (serverOptions.isStreaming === false) { + if (typeof flvPlayer !== "undefined") { + if (flvPlayer != null) { + flvPlayer.pause(); + flvPlayer.unload(); + flvPlayer.detachMediaElement(); + flvPlayer.destroy(); + flvPlayer = null; + streamStarted = false; + } + } + return false; + } + return false; +} + +function updateTimeout() { + clearTimeout(connectionTimeoutId); + $("#networkstatus").css("opacity", 0); + connectionTimeoutId = setTimeout(function () { + $("#networkstatus").css("opacity", 1); + }, 30000) +} + +/** 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); + }); +} diff --git a/public/js/edit.js b/public/js/edit.js new file mode 100644 index 0000000..ac35153 --- /dev/null +++ b/public/js/edit.js @@ -0,0 +1,435 @@ +let canvas; +let bundleData; + +let _clipboard = null; + +bundleData = { + background: "", + duration: 10, + styleHeader: {}, + styleText: {}, + useWebFonts: false +}; + +$(function () { + + $('html').keyup(function (e) { + if (e.keyCode === 46) { + obj = canvas.getActiveObject(); + if (!obj.isEditing) { + removeSelectedObjects(); + } + } + }); + + $('#order') + .dropdown({ + action: 'hide', + onChange: function (value, text, $selectedItem) { + setZindex(value); + } + }); + + + $('#contextmenu .dropdown').dropdown({ + on: 'hover', + }); + + displayTime(); + setInterval(displayTime, 1000); +}); + + +// Trigger action when the contexmenu is about to be shown +$(document).bind("contextmenu", function (event) { + + // Avoid the real one + event.preventDefault(); + + var objects = canvas.getActiveObjects(); + + if (objects.length > 0) { + // Show contextmenu + $("#contextmenu").finish().toggle(100).// In the right position (the mouse) + css({ + top: event.pageY + "px", + left: event.pageX + "px" + }); + } + +}); + + +// If the document is clicked somewhere +$(document).bind("mousedown", function (e) { + + // If the clicked element is not the menu + if (!$(e.target).parents("#contextmenu").length > 0) { + if (!$(e.target).attr("data-action")) { + // Hide it + $("#contextmenu").hide(100); + } + } +}); + + +// If the menu element is clicked +$("#contextmenu .item").click(function () { + + // This is the triggered action name + switch ($(this).attr("data-action")) { + + // A case for each action. Your actions here + case "copy": + copyObject(); + break; + case "paste": + pasteObject(); + break; + case "delete": + removeSelectedObjects(); + break; + case "bringToFront": + setZindex("bringToFront"); + break; + case "sendToBack": + setZindex("sendToBack"); + break; + case "bringForward": + setZindex("bringForward"); + break; + case "sendBackwards": + setZindex("sendBackwards"); + break; + } + + // Hide it AFTER the action was triggered + $("#contextmenu").hide(100); +}); + +/** + * Displays the local time for bottom of screen + * hh:mm + **/ +function displayTime() { + var date = new Date(); + var min = date.getMinutes(); + if (min < 10) min = "0" + min; + var time = date.getHours() + ":" + min; + $('#time2').html(time); +} + + +function setZindex(value) { + switch (value) { + case "sendToBack": + canvas.sendToBack(canvas.getActiveObject()); + break; + case "bringToFront": + canvas.bringToFront(canvas.getActiveObject()); + break; + case "bringForward": + canvas.bringForward(canvas.getActiveObject()); + break; + case "sendBackwards": + canvas.sendBackwards(canvas.getActiveObject()); + break; + } + canvas.renderAll(); +} + +// socketio +fabric.Image.prototype.toObject = (function (toObject) { + return function () { + return fabric.util.object.extend(toObject.call(this), { + src: '/images' + this.getSrc().split('images')[1] + }); + } +})(fabric.Image.prototype.toObject); + + +socket.on('connect', function () { + canvas = new fabric.Canvas('edit'); + canvas.includeDefaultValues = false; + socket.emit('admin.editSlide', {bundleName: bundle, fileName: file}); +}); + +socket.on('callback.save', function (data) { + if (data.error) { + alert(data.error); + } else { + window.close(); + } +}); + +socket.on('callback.edit.updateFileList', function (data) { + if (data.error) { + console.log(data); + alert(data.error); + } else { + openImageBrowser(); + } +}); + +socket.on('callbackEdit', function (data) { + canvas.clear(); + if (data.bundleData.useWebFonts) { + if (data.bundleData.styleHeader.fontFamily !== bundleData.styleHeader.fontFamily || data.bundleData.styleText.fontFamily !== bundleData.styleText.fontFamily) { + WebFont.load({ + google: { + families: [data.bundleData.styleHeader.fontFamily, data.bundleData.styleText.fontFamily] + }, + timeout: 2000, + active: function () { + nextSlide(data, true); + }, + inactive: function () { + nextSlide(data, true); + } + }); + } else { + nextSlide(data, true); + } + } else { + nextSlide(data, true); + } + + bundleData = data.bundleData; + setBackground(bundleData.background); +}); + +function setBackground(background) { + background = "/background/" + bundleData.bundleName + "/" + background; + + var video = document.getElementById("bgvid2"); + var bg = $("#bg"); + var bgImage = document.getElementById("bgimg"); + if (background.indexOf(".mp4") !== -1) { + if (parseUrl(video.src) !== background) { + bg.hide(); + $(video).show(); + video.src = background; + video.play(); + } + } else { + if (parseUrl(bgImage.src) !== background) { + bgImage.src = background; + bg.show(); + $(video).hide(); + video.src = null; + video.pause(); + } + } +} + +function parseUrl(url) { + return '/background' + url.split('background')[1] +} + +function nextSlide(data) { + canvas.loadFromJSON(data.json, function () { + canvas.renderAll(); + }, function (o, object) { + + if (object.type === "i-text") { + object.setOptions(bundleData.styleText); + + if (object.id === "header") { + object.setOptions(bundleData.styleHeader); + } + object.setShadow({color: "#000", blur: 3, offsetX: 0, offsetY: 0}); + + object.lockRotation = true; + object.hasControls = false; + object.hasRotatingPoint = false; + } + + object.lockUniScaling = true; + }); +} + + +function copyObject() { + // clone what are you copying since you + // may want copy and paste on different moment. + // and you do not want the changes happened + // later to reflect on the copy. + canvas.getActiveObject().clone(function (cloned) { + _clipboard = cloned; + }); +} + +function pasteObject() { + if (_clipboard === null) return; + // clone again, so you can do multiple copies. + _clipboard.clone(function (clonedObj) { + canvas.discardActiveObject(); + clonedObj.set({ + left: clonedObj.left + 10, + top: clonedObj.top + 10, + evented: true, + }); + if (clonedObj.type === 'activeSelection') { + // active selection needs a reference to the canvas. + clonedObj.canvas = canvas; + clonedObj.forEachObject(function (obj) { + canvas.add(obj); + }); + // this should solve the unselectability + clonedObj.setCoords(); + } else { + canvas.add(clonedObj); + } + _clipboard.top += 10; + _clipboard.left += 10; + canvas.setActiveObject(clonedObj); + canvas.requestRenderAll(); + }); +} + +function addText(content, isHeader) { + let text = new fabric.IText(content, { + id: "normal", + }); + + let y = 0; + var objects = canvas.getObjects('i-text'); + for (let i in objects) { + if (objects[i].top > y) { + y = objects[i].top + objects[i].height; + } + } + + if (y === 0) { + y = 30; + } + text.setOptions(bundleData.styleText); + text.setOptions({top: y + 10, left: 250}); + + if (isHeader) { + text.id = "header"; + text.setOptions(bundleData.styleHeader); + text.setOptions({top: y + 10, left: 150}); + } + + text.lockRotation = true; + text.hasControls = false; + text.lockUniScaling = true; + text.hasRotatingPoint = false; + + text.setShadow({color: "rgba(0,0,0,0.8)", blur: 5, offsetX: 2, offsetY: 2}); + + canvas.add(text); +} + +function addImage(imageUrl, isFullSized) { + + fabric.Image.fromURL(imageUrl, function (bgImage) { + + var canvasAspect = canvas.width / canvas.height; + var imgAspect = bgImage.width / bgImage.height; + var left, top, scaleFactor; + + if (canvasAspect >= imgAspect) { + scaleFactor = canvas.width / bgImage.width; + left = 0; + top = -((bgImage.height * scaleFactor) - canvas.height) / 2; + } else { + scaleFactor = canvas.height / bgImage.height; + top = 0; + left = -((bgImage.width * scaleFactor) - canvas.width) / 2; + } + + if (isFullSized) { + bgImage.set({ + top: top, + left: left, + originX: 'left', + originY: 'top', + scaleX: scaleFactor, + scaleY: scaleFactor + }); + } else { + bgImage.set({ + scaleY: 0.5, + scaleX: 0.5, + }); + } + + bgImage.lockUniScaling = true; + canvas.add(bgImage); + }); + + $("#imageLayer").modal("hide"); + +} + +function addHeader(content) { + addText(content, true); +} + +function removeSelectedObjects() { + let obj = canvas.getActiveObjects(); + for (var i in obj) { + canvas.remove(obj[i]); + } + canvas.discardActiveObject(); + canvas.requestRenderAll(); + +} + +function cueSlide() { + if (confirm('Pause show and force display of the current slide?')) { + let obj = {json: canvas.toJSON(['id']), png: canvas.toDataURL('png'), displayId: displayId}; + socket.emit("admin.override", obj); + } +} + + +function save() { + canvas.setBackgroundImage(null, null, null); + var name = "untitled"; + var texts = canvas.getObjects('i-text'); + for (let i in texts) { + if (texts[i].id == "header") { + name = texts[i].text; + break; + } + } + socket.emit("edit.save", { + bundleName: bundle, + name: name, + fileName: file, + json: canvas.toJSON(['id']), + png: canvas.toDataURL('png'), + displayId: displayId + }); +} + + +function deleteImage(name) { + if (confirm("Really delete image?")) { + socket.emit("edit.deleteImage", { + bundleName: bundle, + name: name, + }); + } +} + + +function openImageBrowser() { + $("#imageList").load("/admin/ajax/imagelist?bundle=" + bundle + "&nocache=" + uuidv4(), null, function () { + addImagesFromUploadQueue(); + $('#imageLayer').modal('show'); + }); +} + +/** 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); + }); +} + diff --git a/public/js/filereader.js b/public/js/filereader.js new file mode 100644 index 0000000..ddeb815 --- /dev/null +++ b/public/js/filereader.js @@ -0,0 +1,432 @@ +/*! +FileReader.js - v0.99 +A lightweight wrapper for common FileReader usage. +Copyright 2014 Brian Grinstead - MIT License. +See http://github.com/bgrins/filereader.js for documentation. +*/ + +(function(window, document) { + + var FileReader = window.FileReader; + var FileReaderSyncSupport = false; + var workerScript = "self.addEventListener('message', function(e) { var data=e.data; try { var reader = new FileReaderSync; postMessage({ result: reader[data.readAs](data.file), extra: data.extra, file: data.file})} catch(e){ postMessage({ result:'error', extra:data.extra, file:data.file}); } }, false);"; + var syncDetectionScript = "onmessage = function(e) { postMessage(!!FileReaderSync); };"; + var fileReaderEvents = ['loadstart', 'progress', 'load', 'abort', 'error', 'loadend']; + var sync = false; + var FileReaderJS = window.FileReaderJS = { + enabled: false, + setupInput: setupInput, + setupDrop: setupDrop, + setupClipboard: setupClipboard, + setSync: function (value) { + sync = value; + + if (sync && !FileReaderSyncSupport) { + checkFileReaderSyncSupport(); + } + }, + getSync: function() { + return sync && FileReaderSyncSupport; + }, + output: [], + opts: { + dragClass: "drag", + accept: false, + readAsDefault: 'DataURL', + readAsMap: { + }, + on: { + loadstart: noop, + progress: noop, + load: noop, + abort: noop, + error: noop, + loadend: noop, + skip: noop, + groupstart: noop, + groupend: noop, + beforestart: noop + } + } + }; + + // Setup jQuery plugin (if available) + if (typeof(jQuery) !== "undefined") { + jQuery.fn.fileReaderJS = function(opts) { + return this.each(function() { + if (jQuery(this).is("input")) { + setupInput(this, opts); + } + else { + setupDrop(this, opts); + } + }); + }; + + jQuery.fn.fileClipboard = function(opts) { + return this.each(function() { + setupClipboard(this, opts); + }); + }; + } + + // Not all browsers support the FileReader interface. Return with the enabled bit = false. + if (!FileReader) { + return; + } + + + // makeWorker is a little wrapper for generating web workers from strings + function makeWorker(script) { + var URL = window.URL || window.webkitURL; + var Blob = window.Blob; + var Worker = window.Worker; + + if (!URL || !Blob || !Worker || !script) { + return null; + } + + var blob = new Blob([script]); + var worker = new Worker(URL.createObjectURL(blob)); + return worker; + } + + // setupClipboard: bind to clipboard events (intended for document.body) + function setupClipboard(element, opts) { + + if (!FileReaderJS.enabled) { + return; + } + var instanceOptions = extend(extend({}, FileReaderJS.opts), opts); + + element.addEventListener("paste", onpaste, false); + + function onpaste(e) { + var files = []; + var clipboardData = e.clipboardData || {}; + var items = clipboardData.items || []; + + for (var i = 0; i < items.length; i++) { + var file = items[i].getAsFile(); + + if (file) { + + // Create a fake file name for images from clipboard, since this data doesn't get sent + var matches = new RegExp("/\(.*\)").exec(file.type); + if (!file.name && matches) { + var extension = matches[1]; + file.name = "clipboard" + i + "." + extension; + } + + files.push(file); + } + } + + if (files.length) { + processFileList(e, files, instanceOptions); + e.preventDefault(); + e.stopPropagation(); + } + } + } + + // setupInput: bind the 'change' event to an input[type=file] + function setupInput(input, opts) { + + if (!FileReaderJS.enabled) { + return; + } + var instanceOptions = extend(extend({}, FileReaderJS.opts), opts); + + input.addEventListener("change", inputChange, false); + input.addEventListener("drop", inputDrop, false); + + function inputChange(e) { + processFileList(e, input.files, instanceOptions); + } + + function inputDrop(e) { + e.stopPropagation(); + e.preventDefault(); + processFileList(e, e.dataTransfer.files, instanceOptions); + } + } + + // setupDrop: bind the 'drop' event for a DOM element + function setupDrop(dropbox, opts) { + + if (!FileReaderJS.enabled) { + return; + } + var instanceOptions = extend(extend({}, FileReaderJS.opts), opts); + var dragClass = instanceOptions.dragClass; + var initializedOnBody = false; + + // Bind drag events to the dropbox to add the class while dragging, and accept the drop data transfer. + dropbox.addEventListener("dragenter", onlyWithFiles(dragenter), false); + dropbox.addEventListener("dragleave", onlyWithFiles(dragleave), false); + dropbox.addEventListener("dragover", onlyWithFiles(dragover), false); + dropbox.addEventListener("drop", onlyWithFiles(drop), false); + + // Bind to body to prevent the dropbox events from firing when it was initialized on the page. + document.body.addEventListener("dragstart", bodydragstart, true); + document.body.addEventListener("dragend", bodydragend, true); + document.body.addEventListener("drop", bodydrop, false); + + function bodydragend(e) { + initializedOnBody = false; + } + + function bodydragstart(e) { + initializedOnBody = true; + } + + function bodydrop(e) { + if (e.dataTransfer.files && e.dataTransfer.files.length ){ + e.stopPropagation(); + e.preventDefault(); + } + } + + function onlyWithFiles(fn) { + return function() { + if (!initializedOnBody) { + fn.apply(this, arguments); + } + }; + } + + function drop(e) { + e.stopPropagation(); + e.preventDefault(); + if (dragClass) { + removeClass(dropbox, dragClass); + } + processFileList(e, e.dataTransfer.files, instanceOptions); + } + + function dragenter(e) { + e.stopPropagation(); + e.preventDefault(); + if (dragClass) { + addClass(dropbox, dragClass); + } + } + + function dragleave(e) { + if (dragClass) { + removeClass(dropbox, dragClass); + } + } + + function dragover(e) { + e.stopPropagation(); + e.preventDefault(); + if (dragClass) { + addClass(dropbox, dragClass); + } + } + } + + // setupCustomFileProperties: modify the file object with extra properties + function setupCustomFileProperties(files, groupID) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + file.extra = { + nameNoExtension: file.name.substring(0, file.name.lastIndexOf('.')), + extension: file.name.substring(file.name.lastIndexOf('.') + 1), + fileID: i, + uniqueID: getUniqueID(), + groupID: groupID, + prettySize: prettySize(file.size) + }; + } + } + + // getReadAsMethod: return method name for 'readAs*' - http://www.w3.org/TR/FileAPI/#reading-a-file + function getReadAsMethod(type, readAsMap, readAsDefault) { + for (var r in readAsMap) { + if (type.match(new RegExp(r))) { + return 'readAs' + readAsMap[r]; + } + } + return 'readAs' + readAsDefault; + } + + // processFileList: read the files with FileReader, send off custom events. + function processFileList(e, files, opts) { + + var filesLeft = files.length; + var group = { + groupID: getGroupID(), + files: files, + started: new Date() + }; + + function groupEnd() { + group.ended = new Date(); + opts.on.groupend(group); + } + + function groupFileDone() { + if (--filesLeft === 0) { + groupEnd(); + } + } + + FileReaderJS.output.push(group); + setupCustomFileProperties(files, group.groupID); + + opts.on.groupstart(group); + + // No files in group - end immediately + if (!files.length) { + groupEnd(); + return; + } + + var supportsSync = sync && FileReaderSyncSupport; + var syncWorker; + + // Only initialize the synchronous worker if the option is enabled - to prevent the overhead + if (supportsSync) { + syncWorker = makeWorker(workerScript); + syncWorker.onmessage = function(e) { + var file = e.data.file; + var result = e.data.result; + + // Workers seem to lose the custom property on the file object. + if (!file.extra) { + file.extra = e.data.extra; + } + + file.extra.ended = new Date(); + + // Call error or load event depending on success of the read from the worker. + opts.on[result === "error" ? "error" : "load"]({ target: { result: result } }, file); + groupFileDone(); + }; + } + + Array.prototype.forEach.call(files, function(file) { + + file.extra.started = new Date(); + + if (opts.accept && !file.type.match(new RegExp(opts.accept))) { + opts.on.skip(file); + groupFileDone(); + return; + } + + if (opts.on.beforestart(file) === false) { + opts.on.skip(file); + groupFileDone(); + return; + } + + var readAs = getReadAsMethod(file.type, opts.readAsMap, opts.readAsDefault); + + if (syncWorker) { + syncWorker.postMessage({ + file: file, + extra: file.extra, + readAs: readAs + }); + } + else { + + var reader = new FileReader(); + reader.originalEvent = e; + + fileReaderEvents.forEach(function(eventName) { + reader['on' + eventName] = function(e) { + if (eventName == 'load' || eventName == 'error') { + file.extra.ended = new Date(); + } + opts.on[eventName](e, file); + if (eventName == 'loadend') { + groupFileDone(); + } + }; + }); + reader[readAs](file); + } + }); + } + + // checkFileReaderSyncSupport: Create a temporary worker and see if FileReaderSync exists + function checkFileReaderSyncSupport() { + var worker = makeWorker(syncDetectionScript); + if (worker) { + worker.onmessage =function(e) { + FileReaderSyncSupport = e.data; + }; + worker.postMessage({}); + } + } + + // noop: do nothing + function noop() { + + } + + // extend: used to make deep copies of options object + function extend(destination, source) { + for (var property in source) { + if (source[property] && source[property].constructor && + source[property].constructor === Object) { + destination[property] = destination[property] || {}; + arguments.callee(destination[property], source[property]); + } + else { + destination[property] = source[property]; + } + } + return destination; + } + + // hasClass: does an element have the css class? + function hasClass(el, name) { + return new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)").test(el.className); + } + + // addClass: add the css class for the element. + function addClass(el, name) { + if (!hasClass(el, name)) { + el.className = el.className ? [el.className, name].join(' ') : name; + } + } + + // removeClass: remove the css class from the element. + function removeClass(el, name) { + if (hasClass(el, name)) { + var c = el.className; + el.className = c.replace(new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)", "g"), " ").replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + } + } + + // prettySize: convert bytes to a more readable string. + function prettySize(bytes) { + var s = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB']; + var e = Math.floor(Math.log(bytes)/Math.log(1024)); + return (bytes/Math.pow(1024, Math.floor(e))).toFixed(2)+" "+s[e]; + } + + // getGroupID: generate a unique int ID for groups. + var getGroupID = (function(id) { + return function() { + return id++; + }; + })(0); + + // getUniqueID: generate a unique int ID for files + var getUniqueID = (function(id) { + return function() { + return id++; + }; + })(0); + + // The interface is supported, bind the FileReaderJS callbacks + FileReaderJS.enabled = true; + +})(this, document); \ No newline at end of file diff --git a/public/js/flux.js b/public/js/flux.js new file mode 100644 index 0000000..2f81a00 --- /dev/null +++ b/public/js/flux.js @@ -0,0 +1,1763 @@ +/** + * @preserve Flux Slider v1.4.4 + * http://www.joelambert.co.uk/flux + * + * Copyright 2011, Joe Lambert. + * Free to use under the MIT license. + * http://www.opensource.org/licenses/mit-license.php + */ + +/** + * Contains additional changes for infoScreen3 + * Free to use under the MIT license. + * http://www.opensource.org/licenses/mit-license.php + */ + +// Flux namespace +window.flux = { + version: '1.4.4' +}; + +(function ($) { + flux.slider = function (elem, opts) { + // Setup the flux.browser singleton to perform feature detection + flux.browser.init(); + + if (!flux.browser.supportsTransitions) { + if (window.console && window.console.error) + console.error("Flux Slider requires a browser that supports CSS3 transitions"); + } + + var _this = this; + + this.element = $(elem); + + // Make a list of all available transitions + this.transitions = []; + for (var fx in flux.transitions) + this.transitions.push(fx); + + this.options = $.extend({ + autoplay: true, + transitions: this.transitions, + delay: 4000, + pagination: true, + controls: false, + captions: false, + width: null, + height: null, + onTransitionEnd: null + }, opts); + + // Set the height/width if given [EXPERIMENTAL!] + this.height = this.options.height ? this.options.height : null; + this.width = this.options.width ? this.options.width : null; + + // Filter out non compatible transitions + var newTrans = []; + $(this.options.transitions).each(function (index, tran) { + var t = new flux.transitions[tran](this), + compatible = true; + + if (t.options.requires3d && !flux.browser.supports3d) + compatible = false; + + if (t.options.compatibilityCheck) + compatible = t.options.compatibilityCheck(); + + if (compatible) + newTrans.push(tran); + }); + + this.options.transitions = newTrans; + + this.images = []; + this.imageLoadedCount = 0; + this.currentImageIndex = 0; + this.nextImageIndex = 1; + this.playing = false; + this.isTransition = false; + + this.container = $('
').appendTo(this.element); + + this.surface = $('
').appendTo(this.container); + + // Listen for click events as we may want to follow a link + this.container.bind('click', function (event) { + if ($(event.target).hasClass('hasLink')) + window.location = $(event.target).data('href'); + }); + + this.imageContainer = $('
').css({ + 'position': 'relative', + 'overflow': 'hidden', + 'min-height': '100px', + 'background-repeat': 'no-repeat', + }).appendTo(this.surface); + + // If the height/width is already set then resize the container + if (this.width && this.height) { + this.imageContainer.css({ + width: this.width + 'px', + height: this.height + 'px' + }); + } + + // Create the placeholders for the current and next image + this.image1 = $('
').appendTo(this.imageContainer); + this.image2 = $('
').appendTo(this.imageContainer); + + $(this.image1).add(this.image2).css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px' + }); + + // Get a list of the images to use + this.element.find('img, a img').each(function (index, found_img) { + var imgClone = found_img.cloneNode(false), + link = $(found_img).parent(); + + // If this img is directly inside a link then save the link for later use + if (link.is('a')) + $(imgClone).data('href', link.attr('href')); + + _this.images.push(imgClone); + + // Remove the images from the DOM + $(found_img).remove(); + }); + + // Load the images afterwards as IE seems to load images synchronously + for (var i = 0; i < this.images.length; i++) { + var image = new Image(); + image.onload = function () { + _this.imageLoadedCount++; + + _this.width = _this.width ? _this.width : this.width; + _this.height = _this.height ? _this.height : this.height; + + if (_this.imageLoadedCount >= _this.images.length) { + _this.finishedLoading(); + _this.setupImages(); + } + }; + + // Load the image to ensure its cached by the browser + image.src = this.images[i].src; + } + + // Catch when a transition has finished + this.element.bind('fluxTransitionEnd', function (event, data) { + // If the slider is currently playing then set the timeout for the next transition + // if(_this.isPlaying()) + // _this.start(); + + // Are we using a callback instead of events for notifying about transition ends? + if (_this.options.onTransitionEnd) { + event.preventDefault(); + + _this.options.onTransitionEnd(data); + } + }); + + // Should we auto start the slider? + if (this.options.autoplay) + this.start(); + + // Handle swipes + /* this.element.bind('swipeLeft', function(event){ + _this.next(null, {direction: 'left'}); + }).bind('swipeRight', function(event){ + _this.prev(null, {direction: 'right'}); + }); */ + + // Under FF7 autoplay breaks when the current tab loses focus + setTimeout(function () { + $(window).focus(function () { + if (_this.isPlaying()) + _this.next(); + }); + }, 100); + }; + + flux.slider.prototype = { + constructor: flux.slider, + playing: false, + start: function () { + var _this = this; + this.playing = true; + this.interval = setInterval(function () { + _this.transition(); + }, this.options.delay); + }, + stop: function () { + this.playing = false; + clearInterval(this.interval); + this.interval = null; + }, + isPlaying: function () { + return this.playing; + //return this.interval != null; + }, + setSize(w, h) { + this.width = w; + this.height = h; + this.options.width = w; + this.options.height = h; + this.container.css({"width": w + "px", "height": h + "px"}); + this.imageContainer.css({"width": w + "px", "height": h + "px"}); + this.image1.css({"width": w + "px", "height": h + "px"}); + this.image2.css({"width": w + "px", "height": h + "px"}); + this.setupImages(); + }, + next: function (trans, opts) { + opts = opts || {}; + opts.direction = 'left'; + this.showImage(this.currentImageIndex + 1, trans, opts); + }, + + prev: function (trans, opts) { + opts = opts || {}; + opts.direction = 'right'; + this.showImage(this.currentImageIndex - 1, trans, opts); + }, + transitionPlaying: function () { + return this.isTransition; + }, + showImage: function (index, trans, opts) { + this.setNextIndex(index); + + // Temporarily stop the transition interval + //clearInterval(this.interval); + //this.interval = null; + + this.setupImages(); + this.transition(trans, opts); + }, + showImageById: function (id, trans, opts) { + let i = 0; + for (var img of this.images) { + if (img.id === id) { + this.showImage(i, trans, opts); + break; + } + i++; + } + this.clearTempImages(); + }, + showTempImage: function (id, trans, opts) { + let i = 0; + for (var img of this.images) { + if (img.class === "temp" && img.id === id) { + this.showImage(i, trans, opts); + break; + } + i++; + } + }, + clearImages: function () { + this.images = []; + }, + clearTempImages: function () { + let newImages = []; + for (var i in this.images) { + if (this.images[i].class !== "temp") { + newImages.push(this.images[i]); + } else { + delete this.images[i]; + console.log("cleared temp"); + } + } + this.images = newImages; + }, + clearImageById: function (id) { + let newImages = []; + for (var i in this.images) { + if (this.images[i].id !== id) { + newImages.push(this.images[i]); + } else { + console.log("cleared image by id: " + id); + delete this.images[i]; + } + } + this.images = newImages; + }, + finishedLoading: function () { + var _this = this; + + this.container.css({ + width: this.width + 'px', + height: this.height + 'px' + }); + + this.imageContainer.removeClass('loading'); + + // Should we setup a pagination view? + if (this.options.pagination) { + // TODO: Attach to touch events if appropriate + this.pagination = $('').css({ + margin: '0px', + padding: '0px', + 'text-align': 'center' + }); + + this.pagination.bind('click', function (event) { + event.preventDefault(); + _this.showImage($(event.target).data('index')); + }); + + $(this.images).each(function (index, image) { + var li = $('
  • ' + (index + 1) + '
  • ').css({ + display: 'inline-block', + 'margin-left': '0.5em', + 'cursor': 'pointer' + }).appendTo(_this.pagination); + + if (index == 0) + li.css('margin-left', 0).addClass('current'); + }); + + this.container.append(this.pagination); + } + + // Resize + $(this.imageContainer).css({ + width: this.width + 'px', + height: this.height + 'px' + }); + + $(this.image1).css({ + width: this.width + 'px', + height: this.height + 'px' + }); + + $(this.image2).css({ + width: this.width + 'px', + height: this.height + 'px' + }); + + this.container.css({ + width: this.width + 'px', + height: this.height + (this.options.pagination ? this.pagination.height() : 0) + 'px' + }); + + // Should we add prev/next controls? + if (this.options.controls) { + var css = { + padding: '4px 10px 10px', + 'font-size': '60px', + 'font-family': 'arial, sans-serif', + 'line-height': '1em', + 'font-weight': 'bold', + color: '#FFF', + 'text-decoration': 'none', + background: 'rgba(0,0,0,0.5)', + position: 'absolute', + 'z-index': 2000 + }; + + this.nextButton = $('»').css(css).css3({ + 'border-radius': '4px' + }).appendTo(this.surface).bind('click', function (event) { + event.preventDefault(); + _this.next(); + }); + + this.prevButton = $('«').css(css).css3({ + 'border-radius': '4px' + }).appendTo(this.surface).bind('click', function (event) { + event.preventDefault(); + _this.prev(); + }); + + var top = (this.height - this.nextButton.height()) / 2; + this.nextButton.css({ + top: top + 'px', + right: '10px' + }); + + this.prevButton.css({ + top: top + 'px', + left: '10px' + }); + } + + // Should we use captions? + if (this.options.captions) { + this.captionBar = $('
    ').css({ + background: 'rgba(0,0,0,0.6)', + color: '#FFF', + 'font-size': '16px', + 'font-family': 'helvetica, arial, sans-serif', + 'text-decoration': 'none', + 'font-weight': 'bold', + padding: '1.5em 1em', + opacity: 0, + position: 'absolute', + 'z-index': 110, + width: '100%', + bottom: 0 + }).css3({ + 'transition-property': 'opacity', + 'transition-duration': '800ms', + 'box-sizing': 'border-box' + }).prependTo(this.surface); + } + + this.updateCaption(); + }, + setupImages: function () { + var img1 = this.getImage(this.currentImageIndex); + css1 = { + 'background-image': 'url("' + img1.src + '")', + 'z-index': 101, + 'cursor': 'auto', + 'background-repeat': 'no-repeat', + 'background-size': this.options.width + "px " + this.options.height + "px" + }; + + // Does this image have an associated link? + if ($(img1).data('href')) { + css1.cursor = 'pointer'; + this.image1.addClass('hasLink'); + this.image1.data('href', $(img1).data('href')); + } + else { + this.image1.removeClass('hasLink'); + this.image1.data('href', null); + } + + this.image1.css(css1).children().remove(); + + this.image2.css({ + 'background-image': 'url("' + this.getImage(this.nextImageIndex).src + '")', + 'z-index': 100, + 'background-repeat': 'no-repeat', + 'background-size': this.options.width + "px " + this.options.height + "px", + }).hide(); + + if (this.options.pagination && this.pagination) { + this.pagination.find('li.current').removeClass('current'); + $(this.pagination.find('li')[this.currentImageIndex]).addClass('current'); + } + }, + transition: function (transition, opts) { + // Allow a transition to be picked from ALL available transitions (not just the reduced set) + if (transition == undefined || !flux.transitions[transition]) { + // Pick a transition at random from the (possibly reduced set of) transitions + var index = Math.floor(Math.random() * (this.options.transitions.length)); + transition = this.options.transitions[index]; + } + + var tran = null; + + try { + tran = new flux.transitions[transition](this, $.extend(this.options[transition] ? this.options[transition] : {}, opts)); + } + catch (e) { + // If an invalid transition has been provided then use the fallback (default is to just switch the image) + tran = new flux.transition(this, {fallback: true}); + } + this.isTransition = true; + tran.run(); + + this.currentImageIndex = this.nextImageIndex; + this.setNextIndex(this.currentImageIndex + 1); + this.updateCaption(); + }, + updateCaption: function () { + var str = $(this.getImage(this.currentImageIndex)).attr('title'); + if (this.options.captions && this.captionBar) { + if (str !== "") + this.captionBar.html(str); + + this.captionBar.css('opacity', str === "" ? 0 : 1); + } + }, + getImage: function (index) { + index = index % this.images.length; + + return this.images[index]; + }, + setNextIndex: function (nextIndex) { + if (nextIndex == undefined) + nextIndex = this.currentImageIndex + 1; + + this.nextImageIndex = nextIndex; + + if (this.nextImageIndex > this.images.length - 1) + this.nextImageIndex = 0; + + if (this.nextImageIndex < 0) + this.nextImageIndex = this.images.length - 1; + }, + increment: function () { + this.currentImageIndex++; + if (this.currentImageIndex > this.images.length - 1) + this.currentImageIndex = 0; + } + }; +})(window.jQuery || window.Zepto); + +/** + * Helper object to determine support for various CSS3 functions + * @author Joe Lambert + */ + +(function ($) { + flux.browser = { + init: function () { + // Have we already been initialised? + if (flux.browser.supportsTransitions !== undefined) + return; + + var div = document.createElement('div'), + prefixes = ['-webkit', '-moz', '-o', '-ms'], + domPrefixes = ['Webkit', 'Moz', 'O', 'Ms']; + + // Does the current browser support CSS Transitions? + if (window.Modernizr && Modernizr.csstransitions !== undefined) + flux.browser.supportsTransitions = Modernizr.csstransitions; + else { + flux.browser.supportsTransitions = this.supportsCSSProperty('Transition'); + } + + // Does the current browser support 3D CSS Transforms? + if (window.Modernizr && Modernizr.csstransforms3d !== undefined) + flux.browser.supports3d = Modernizr.csstransforms3d; + else { + // Custom detection when Modernizr isn't available + flux.browser.supports3d = this.supportsCSSProperty("Perspective"); + + if (flux.browser.supports3d && 'webkitPerspective' in $('body').get(0).style) { + // Double check with a media query (similar to how Modernizr does this) + var div3D = $('
    '); + var mq = $(''); + + $('body').append(div3D); + $('head').append(mq); + + flux.browser.supports3d = div3D.get(0).offsetLeft == 9; + + div3D.remove(); + mq.remove(); + } + } + + }, + supportsCSSProperty: function (prop) { + var div = document.createElement('div'), + prefixes = ['', '-webkit', '-moz', '-o', '-ms'], + domPrefixes = ['', 'Webkit', 'Moz', 'O', 'Ms']; + + var support = false; + for (var i = 0; i < domPrefixes.length; i++) { + if (domPrefixes[i] + prop in div.style) + support = support || true; + } + + return support; + }, + translate: function (x, y, z) { + x = (x != undefined) ? x : 0; + y = (y != undefined) ? y : 0; + z = (z != undefined) ? z : 0; + + return 'translate' + (flux.browser.supports3d ? '3d(' : '(') + x + 'px,' + y + (flux.browser.supports3d ? 'px,' + z + 'px)' : 'px)'); + }, + + rotateX: function (deg) { + return flux.browser.rotate('x', deg); + }, + + rotateY: function (deg) { + return flux.browser.rotate('y', deg); + }, + + rotateZ: function (deg) { + return flux.browser.rotate('z', deg); + }, + + rotate: function (axis, deg) { + if (!axis in {'x': '', 'y': '', 'z': ''}) + axis = 'z'; + + deg = (deg != undefined) ? deg : 0; + + if (flux.browser.supports3d) + return 'rotate3d(' + (axis == 'x' ? '1' : '0') + ', ' + (axis == 'y' ? '1' : '0') + ', ' + (axis == 'z' ? '1' : '0') + ', ' + deg + 'deg)'; + else { + if (axis == 'z') + return 'rotate(' + deg + 'deg)'; + else + return ''; + } + } + }; + + $(function () { + // To continue to work with legacy code, ensure that flux.browser is initialised on document ready at the latest + flux.browser.init(); + }); +})(window.jQuery || window.Zepto); + +(function ($) { + /** + * Helper function for cross-browser CSS3 support, prepends all possible prefixes to all properties passed in + * @param {Object} props Ker/value pairs of CSS3 properties + */ + $.fn.css3 = function (props) { + var css = {}; + var prefixes = ['webkit', 'moz', 'ms', 'o']; + + for (var prop in props) { + // Add the vendor specific versions + for (var i = 0; i < prefixes.length; i++) + css['-' + prefixes[i] + '-' + prop] = props[prop]; + + // Add the actual version + css[prop] = props[prop]; + } + + this.css(css); + return this; + }; + + /** + * Helper function to bind to the correct transition end event + * @param {function} callback The function to call when the event fires + */ + $.fn.transitionEnd = function (callback) { + var _this = this; + + var events = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd']; + + for (var i = 0; i < events.length; i++) { + this.bind(events[i], function (event) { + // Automatically stop listening for the event + for (var j = 0; j < events.length; j++) + $(this).unbind(events[j]); + + // Perform the callback function + if (callback) + callback.call(this, event); + }); + } + + return this; + }; + + flux.transition = function (fluxslider, opts) { + this.options = $.extend({ + requires3d: false, + after: function () { + // Default callback for after the transition has completed + } + }, opts); + + this.slider = fluxslider; + + // We need to ensure transitions degrade gracefully if the transition is unsupported or not loaded + if ((this.options.requires3d && !flux.browser.supports3d) || !flux.browser.supportsTransitions || this.options.fallback === true) { + var _this = this; + + this.options.after = undefined; + + this.options.setup = function () { + //console.error("Fallback setup()"); + _this.fallbackSetup(); + }; + + this.options.execute = function () { + //console.error("Fallback execute()"); + _this.fallbackExecute(); + }; + } + }; + + flux.transition.prototype = { + constructor: flux.transition, + hasFinished: false, // This is a lock to ensure that the fluxTransitionEnd event is only fired once per tansition + run: function () { + var _this = this; + flux.slider.isTransition = true; + + // do something + if (this.options.setup !== undefined) + this.options.setup.call(this); + + // Remove the background image from the top image + this.slider.image1.css({ + 'background-image': 'none', + + }); + + this.slider.imageContainer.css('overflow', this.options.requires3d ? 'visible' : 'hidden'); + + // For some of the 3D effects using Zepto we need to delay the transitions for some reason + setTimeout(function () { + if (_this.options.execute !== undefined) + _this.options.execute.call(_this); + }, 5); + }, + finished: function () { + if (this.hasFinished) + return; + + this.hasFinished = true; + this.slider.isTransition = false; + if (this.options.after) + this.options.after.call(this); + + this.slider.imageContainer.css('overflow', 'hidden'); + + this.slider.setupImages(); + + // Trigger an event to signal the end of a transition + this.slider.element.trigger('fluxTransitionEnd', { + currentImage: this.slider.getImage(this.slider.currentImageIndex) + }); + }, + fallbackSetup: function () { + + }, + fallbackExecute: function () { + this.finished(); + } + }; + + flux.transitions = {}; + + // Flux grid transition + + flux.transition_grid = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + columns: 6, + rows: 6, + forceSquare: false, + setup: function () { + var imgWidth = this.slider.options.width, + imgHeight = this.slider.options.height; + + var colWidth = Math.floor(imgWidth / this.options.columns), + rowHeight = Math.floor(imgHeight / this.options.rows); + + if (this.options.forceSquare) { + rowHeight = colWidth; + this.options.rows = Math.floor(imgHeight / rowHeight); + } + + // Work out how much space remains with the adjusted barWidth + var colRemainder = imgWidth - (this.options.columns * colWidth), + colAddPerLoop = Math.floor(colRemainder / this.options.columns), + + rowRemainder = imgHeight - (this.options.rows * rowHeight), + rowAddPerLoop = Math.floor(rowRemainder / this.options.rows), + + delayBetweenBars = 150, + height = this.slider.options.height; + totalLeft = 0, + totalTop = 0, + fragment = document.createDocumentFragment(); + + for (var i = 0; i < this.options.columns; i++) { + var thisColWidth = colWidth, + totalTop = 0; + + if (colRemainder > 0) { + var add = colRemainder >= colAddPerLoop ? colAddPerLoop : colRemainder; + thisColWidth += add; + colRemainder -= add; + } + + for (var j = 0; j < this.options.rows; j++) { + var thisRowHeight = rowHeight, + thisRowRemainder = rowRemainder; + + if (thisRowRemainder > 0) { + var add = thisRowRemainder >= rowAddPerLoop ? rowAddPerLoop : thisRowRemainder; + thisRowHeight += add; + thisRowRemainder -= add; + } + + var tile = $('
    ').css({ + width: thisColWidth + 'px', + height: thisRowHeight + 'px', + position: 'absolute', + top: totalTop + 'px', + left: totalLeft + 'px' + }); + + this.options.renderTile.call(this, tile, i, j, thisColWidth, thisRowHeight, totalLeft, totalTop); + + fragment.appendChild(tile.get(0)); + + totalTop += thisRowHeight; + } + + totalLeft += thisColWidth; + } + + // Append the fragement to the surface + this.slider.image1.get(0).appendChild(fragment); + }, + execute: function () { + var _this = this, + height = this.slider.options.height, + bars = this.slider.image1.find('div.barcontainer'); + + this.slider.image2.hide(); + + // Get notified when the last transition has completed + bars.last().transitionEnd(function (event) { + _this.slider.image2.show(); + + _this.finished(); + }); + + bars.css3({ + 'transform': flux.browser.rotateX(-90) + ' ' + flux.browser.translate(0, height / 2, height / 2) + }); + }, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.bars = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + columns: 10, + rows: 1, + delayBetweenBars: 40, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + $(elem).css({ + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px 0px', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'transition-duration': '400ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'all', + 'transition-delay': (colIndex * this.options.delayBetweenBars) + 'ms' + }); + }, + execute: function () { + var _this = this; + + var height = this.slider.options.height; + + var bars = this.slider.image1.find('div.tile'); + + // Get notified when the last transition has completed + $(bars[bars.length - 1]).transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + bars.css({ + 'opacity': '0.5' + }).css3({ + 'transform': flux.browser.translate(0, height) + }); + }, 50); + + } + }, opts)); + }; +})(window.jQuery || window.Zepto); +/* +(function ($) { + flux.transitions.bars3d = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + requires3d: true, + columns: 7, + rows: 1, + delayBetweenBars: 150, + perspective: 1000, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + var bar = $('
    ').css({ + width: colWidth + 'px', + height: this.slider.options.height + "px", + position: 'absolute', + top: '0px', + left: '0px', + 'z-index': 200, + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px 0px', + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px ", + }).css3({ + 'backface-visibility': 'hidden' + }), + + bar2 = $(bar.get(0).cloneNode(false)).css({ + 'background-image': this.slider.image2.css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px ", + }).css3({ + 'transform': flux.browser.rotateX(90) + ' ' + flux.browser.translate(0, -rowHeight / 2, rowHeight / 2) + }), + + left = $('
    ').css({ + width: rowHeight + 'px', + height: rowHeight + 'px', + position: 'absolute', + top: '0px', + left: '0px', + background: '#222', + 'z-index': 190 + }).css3({ + 'transform': flux.browser.rotateY(90) + ' ' + flux.browser.translate(rowHeight / 2, 0, -rowHeight / 2) + ' ' + flux.browser.rotateY(180), + 'backface-visibility': 'hidden' + }), + + right = $(left.get(0).cloneNode(false)).css3({ + 'transform': flux.browser.rotateY(90) + ' ' + flux.browser.translate(rowHeight / 2, 0, colWidth - rowHeight / 2) + }); + + $(elem).css({ + width: colWidth + 'px', + height: this.slider.options.height + "px", + position: 'absolute', + top: '0px', + left: leftOffset + 'px', + 'z-index': colIndex > this.options.columns / 2 ? 1000 - colIndex : 1000 // Fix for Chrome to ensure that the z-index layering is correct! + }).css3({ + 'transition-duration': '800ms', + 'transition-timing-function': 'linear', + 'transition-property': 'all', + 'transition-delay': (colIndex * this.options.delayBetweenBars) + 'ms', + 'transform-style': 'preserve-3d' + }).append(bar).append(bar2).append(left).append(right); + }, + execute: function () { + this.slider.image1.css3({ + 'perspective': this.options.perspective, + 'perspective-origin': '50% 50%' + }).css({ + '-moz-transform': 'perspective(' + this.options.perspective + 'px)', + '-moz-perspective': 'none', + '-moz-transform-style': 'preserve-3d' + }); + + var _this = this, + height = this.slider.options.height, + bars = this.slider.image1.find('div.tile'); + + this.slider.image2.hide(); + + // Get notified when the last transition has completed + bars.last().transitionEnd(function (event) { + _this.slider.image1.css3({ + 'transform-style': 'flat' + }); + + _this.slider.image2.show(); + + _this.finished(); + }); + + setTimeout(function () { + bars.css3({ + 'transform': flux.browser.rotateX(-90) + ' ' + flux.browser.translate(0, height / 2, height / 2) + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); +*/ + +(function ($) { + flux.transitions.blinds = function (fluxslider, opts) { + return new flux.transitions.bars(fluxslider, $.extend({ + execute: function () { + var _this = this; + + var height = this.slider.options.height; + + var bars = this.slider.image1.find('div.tile'); + + // Get notified when the last transition has completed + $(bars[bars.length - 1]).transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + bars.css({ + 'opacity': '0.5' + }).css3({ + 'transform': 'scalex(0.0001)' + }); + }, 50); + } + }, opts)); + } +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.blinds3d = function (fluxslider, opts) { + return new flux.transitions.tiles3d(fluxslider, $.extend({ + forceSquare: false, + rows: 1, + columns: 6 + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.zip = function (fluxslider, opts) { + return new flux.transitions.bars(fluxslider, $.extend({ + execute: function () { + var _this = this; + + var height = this.slider.options.height; + + var bars = this.slider.image1.find('div.tile'); + + // Get notified when the last transition has completed + $(bars[bars.length - 1]).transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + bars.each(function (index, bar) { + $(bar).css({ + 'opacity': '0.3' + }).css3({ + 'transform': flux.browser.translate(0, (index % 2 ? '-' + (2 * height) : height)) + }); + }); + }, 20); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.blocks = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + forceSquare: true, + delayBetweenBars: 100, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + var delay = Math.floor(Math.random() * 10 * this.options.delayBetweenBars); + + $(elem).css({ + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px -' + topOffset + 'px', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px" + }).css3({ + 'transition-duration': '350ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'all', + 'transition-delay': delay + 'ms' + }); + + // Keep track of the last elem to fire + if (this.maxDelay === undefined) + this.maxDelay = 0; + + if (delay > this.maxDelay) { + this.maxDelay = delay; + this.maxDelayTile = elem; + } + }, + execute: function () { + var _this = this; + + var blocks = this.slider.image1.find('div.tile'); + + // Get notified when the last transition has completed + this.maxDelayTile.transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + blocks.each(function (index, block) { + $(block).css({ + 'opacity': '0' + }).css3({ + 'transform': 'scale(0.8)' + }); + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.blocks2 = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + cols: 12, + forceSquare: true, + delayBetweenDiagnols: 150, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + var delay = Math.floor(Math.random() * 10 * this.options.delayBetweenBars); + + $(elem).css({ + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px -' + topOffset + 'px', + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'transition-duration': '350ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'all', + 'transition-delay': (colIndex + rowIndex) * this.options.delayBetweenDiagnols + 'ms', + 'backface-visibility': 'hidden' // trigger hardware acceleration + }); + }, + execute: function () { + var _this = this; + + var blocks = this.slider.image1.find('div.tile'); + + // Get notified when the last transition has completed + blocks.last().transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + blocks.each(function (index, block) { + $(block).css({ + 'opacity': '0' + }).css3({ + 'transform': 'scale(0.8)' + }); + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.concentric = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + blockSize: 60, + delay: 150, + alternate: false, + setup: function () { + var w = this.slider.options.width, + h = this.slider.options.height, + largestLength = Math.sqrt(w * w + h * h), // Largest length is the diagonal + + // How many blocks do we need? + blockCount = Math.ceil(((largestLength - this.options.blockSize) / 2) / this.options.blockSize) + 1, // 1 extra to account for the round border + fragment = document.createDocumentFragment(); + + for (var i = 0; i < blockCount; i++) { + var thisBlockSize = (2 * i * this.options.blockSize) + this.options.blockSize; + + var block = $('
    ').attr('class', 'block block-' + i).css({ + width: thisBlockSize + 'px', + height: thisBlockSize + 'px', + position: 'absolute', + top: ((h - thisBlockSize) / 2) + 'px', + left: ((w - thisBlockSize) / 2) + 'px', + 'z-index': 100 + (blockCount - i), + 'background-image': this.slider.image1.css('background-image'), + 'background-position': 'center center', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px ", + }).css3({ + 'border-radius': thisBlockSize + 'px', + 'transition-duration': '800ms', + 'transition-timing-function': 'linear', + 'transition-property': 'all', + 'transition-delay': ((blockCount - i) * this.options.delay) + 'ms' + }); + + fragment.appendChild(block.get(0)); + } + + //this.slider.image1.append($(fragment)); + this.slider.image1.get(0).appendChild(fragment); + }, + execute: function () { + var _this = this; + + var blocks = this.slider.image1.find('div.block'); + + // Get notified when the last transition has completed + $(blocks[0]).transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + blocks.each(function (index, block) { + $(block).css({ + 'opacity': '0' + }).css3({ + 'transform': flux.browser.rotateZ((!_this.options.alternate || index % 2 ? '' : '-') + '90') + }); + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.warp = function (fluxslider, opts) { + return new flux.transitions.concentric(fluxslider, $.extend({ + delay: 30, + alternate: true + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.cube = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + requires3d: true, + barWidth: 100, + direction: 'left', + perspective: 1000, + setup: function () { + var width = this.slider.options.width; + var height = this.slider.options.height; + + // Setup the container to allow 3D perspective + + this.slider.image1.css3({ + 'perspective': this.options.perspective, + 'perspective-origin': '50% 50%' + }); + + this.cubeContainer = $('
    ').css({ + width: width + 'px', + height: height + 'px', + position: 'relative' + }).css3({ + 'transition-duration': '800ms', + 'transition-timing-function': 'linear', + 'transition-property': 'all', + 'transform-style': 'preserve-3d' + }); + + var css = { + height: this.slider.options.height + "px", + width: this.slider.options.width + "px", + position: 'absolute', + top: '0px', + left: '0px' + }; + + var currentFace = $('
    ').css($.extend(css, { + background: this.slider.image1.css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + })).css3({ + 'backface-visibility': 'hidden' + }); + + this.cubeContainer.append(currentFace); + + var nextFace = $('').css($.extend(css, { + background: this.slider.image2.css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + })).css3({ + 'transform': this.options.transitionStrings.call(this, this.options.direction, 'nextFace'), + 'backface-visibility': 'hidden' + }); + + this.cubeContainer.append(nextFace); + + this.slider.image1.append(this.cubeContainer); + }, + execute: function () { + var _this = this; + + var width = this.slider.options.width; + var height = this.slider.options.height; + + this.slider.image2.hide(); + this.cubeContainer.transitionEnd(function () { + _this.slider.image2.show(); + + _this.finished(); + }); + + setTimeout(function () { + _this.cubeContainer.css3({ + 'transform': _this.options.transitionStrings.call(_this, _this.options.direction, 'container') + }); + }, 50); + }, + transitionStrings: function (direction, elem) { + var width = this.slider.options.width; + var height = this.slider.options.height; + + // Define the various transforms that are required to perform various cube rotations + var t = { + 'up': { + 'nextFace': flux.browser.rotateX(-90) + ' ' + flux.browser.translate(0, height / 2, height / 2), + 'container': flux.browser.rotateX(90) + ' ' + flux.browser.translate(0, -height / 2, height / 2) + }, + 'down': { + 'nextFace': flux.browser.rotateX(90) + ' ' + flux.browser.translate(0, -height / 2, height / 2), + 'container': flux.browser.rotateX(-90) + ' ' + flux.browser.translate(0, height / 2, height / 2) + }, + 'left': { + 'nextFace': flux.browser.rotateY(90) + ' ' + flux.browser.translate(width / 2, 0, width / 2), + 'container': flux.browser.rotateY(-90) + ' ' + flux.browser.translate(-width / 2, 0, width / 2) + }, + 'right': { + 'nextFace': flux.browser.rotateY(-90) + ' ' + flux.browser.translate(-width / 2, 0, width / 2), + 'container': flux.browser.rotateY(90) + ' ' + flux.browser.translate(width / 2, 0, width / 2) + } + }; + + return (t[direction] && t[direction][elem]) ? t[direction][elem] : false; + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.tiles3d = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + requires3d: true, + forceSquare: false, + columns: 6, + rows: 4, + perspective: 600, + delayBetweenBarsX: 200, + delayBetweenBarsY: 150, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + var tile = $('
    ').css({ + width: colWidth + 'px', + height: rowHeight + 'px', + position: 'absolute', + top: '0px', + left: '0px', + //'z-index': 200, // Removed to make compatible with FF10 (Chrome bug seems to have been fixed) + + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px -' + topOffset + 'px', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + + + }).css3({ + 'backface-visibility': 'hidden' + }); + + var tile2 = $(tile.get(0).cloneNode(false)).css({ + 'background-image': this.slider.image2.css('background-image'), + + //'z-index': 190 // Removed to make compatible with FF10 (Chrome bug seems to have been fixed) + }).css3({ + + 'transform': flux.browser.rotateY(180), + 'backface-visibility': 'hidden' + }); + + $(elem).css({ + 'z-index': (colIndex > this.options.columns / 2 ? 500 - colIndex : 500) + (rowIndex > this.options.rows / 2 ? 500 - rowIndex : 500), // Fix for Chrome to ensure that the z-index layering is correct! + + }).css3({ + 'transition-duration': '800ms', + 'transition-timing-function': 'ease-out', + 'transition-property': 'all', + 'transition-delay': (colIndex * this.options.delayBetweenBarsX + rowIndex * this.options.delayBetweenBarsY) + 'ms', + 'transform-style': 'preserve-3d' + }).append(tile).append(tile2); + + }, + execute: function () { + this.slider.image1.css3({ + 'perspective': this.options.perspective, + 'perspective-origin': '50% 50%' + }); + + var _this = this; + + var tiles = this.slider.image1.find('div.tile'); + + this.slider.image2.hide(); + + // Get notified when the last transition has completed + tiles.last().transitionEnd(function (event) { + _this.slider.image2.show(); + _this.finished(); + }); + + setTimeout(function () { + tiles.css3({ + 'transform': flux.browser.rotateY(180) + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.tiles3dprev = function (fluxslider, opts) { + return new flux.transition_grid(fluxslider, $.extend({ + requires3d: true, + forceSquare: false, + columns: 6, + rows: 4, + perspective: 600, + delayBetweenBarsX: 200, + delayBetweenBarsY: 150, + renderTile: function (elem, colIndex, rowIndex, colWidth, rowHeight, leftOffset, topOffset) { + var tile = $('
    ').css({ + width: colWidth + 'px', + height: rowHeight + 'px', + position: 'absolute', + top: '0px', + left: '0px', + //'z-index': 200, // Removed to make compatible with FF10 (Chrome bug seems to have been fixed) + + 'background-image': this.slider.image1.css('background-image'), + 'background-position': '-' + leftOffset + 'px -' + topOffset + 'px', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + + + }).css3({ + 'backface-visibility': 'hidden' + }); + + var tile2 = $(tile.get(0).cloneNode(false)).css({ + 'background-image': this.slider.image2.css('background-image'), + + //'z-index': 190 // Removed to make compatible with FF10 (Chrome bug seems to have been fixed) + }).css3({ + + 'transform': flux.browser.rotateY(180), + 'backface-visibility': 'hidden' + }); + + $(elem).css({ + 'z-index': (colIndex > this.options.columns / 2 ? 500 - colIndex : 500) + (rowIndex > this.options.rows / 2 ? 500 - rowIndex : 500), // Fix for Chrome to ensure that the z-index layering is correct! + + }).css3({ + 'transition-duration': '800ms', + 'transition-timing-function': 'ease-out', + 'transition-property': 'all', + 'transition-delay': ((this.options.columns - 1 - colIndex) * this.options.delayBetweenBarsX + (this.options.rows - 1 - rowIndex) * this.options.delayBetweenBarsY) + 'ms', + 'transform-style': 'preserve-3d' + }).append(tile).append(tile2); + }, + execute: function () { + this.slider.image1.css3({ + 'perspective': this.options.perspective, + 'perspective-origin': '50% 50%' + }); + + var _this = this; + + var tiles = this.slider.image1.find('div.tile'); + + this.slider.image2.hide(); + + // Get notified when the last transition has completed + tiles.first().transitionEnd(function (event) { + _this.slider.image2.show(); + _this.finished(); + }); + + setTimeout(function () { + tiles.css3({ + 'transform': flux.browser.rotateY(-180) + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); +/* (function ($) { + flux.transitions.turn = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + requires3d: true, + perspective: 1300, + direction: 'left', + setup: function () { + var tab = $('
    ').css({ + width: '50%', + height: '100%', + position: 'absolute', + top: '0px', + left: this.options.direction == 'left' ? '50%' : '0%', + 'z-index': 101, + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px" + }).css3({ + 'transform-style': 'preserve-3d', + 'transition-duration': '1000ms', + 'transition-timing-function': 'ease-out', + 'transition-property': 'all', + 'transform-origin': this.options.direction == 'left' ? 'left center' : 'right center' + }), + + front = $('
    ').appendTo(tab).css({ + 'background-image': this.slider.image1.css('background-image'), + 'background-position': (this.options.direction == 'left' ? '-' + (this.slider.options.width / 2) : 0) + 'px 0', + width: "100%", + height: "100%", + position: 'absolute', + top: '0', + left: '0', + '-moz-transform': 'translateZ(1px)', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + + }).css3({ + 'backface-visibility': 'hidden' + }), + + back = $('
    ').appendTo(tab).css({ + 'background-image': this.slider.image2.css('background-image'), + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + 'background-position': (this.options.direction == 'left' ? 0 : '-' + (this.slider.options.width / 2)) + 'px 0', + width: '100%', + height: '100%', + position: 'absolute', + top: '0', + left: '0' + }).css3({ + transform: flux.browser.rotateY(180), + 'backface-visibility': 'hidden' + }), + + current = $('
    ').css({ + position: 'absolute', + top: '0', + left: this.options.direction == 'left' ? '0' : '50%', + width: '50%', + height: '100%', + 'background-image': this.slider.image1.css('background-image'), + 'background-position': (this.options.direction == 'left' ? 0 : '-' + (this.slider.options.width / 2)) + 'px 0', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + 'z-index': 100 + }), + + overlay = $('
    ').css({ + position: 'absolute', + top: '0', + left: this.options.direction == 'left' ? '50%' : '0', + width: '50%', + height: '100%', + background: '#000', + opacity: 1, + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'transition-duration': '800ms', + 'transition-timing-function': 'linear', + 'transition-property': 'opacity' + }), + + container = $('
    ').css3({ + width: '100%', + height: '100%', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'perspective': this.options.perspective, + 'perspective-origin': '50% 50%' + }).append(tab).append(current).append(overlay); + + this.slider.image1.append(container); + }, + execute: function () { + var _this = this; + + this.slider.image1.find('div.tab').first().transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + _this.slider.image1.find('div.tab').css3({ + // 179 not 180 so that the tab rotates the correct way in Firefox + transform: flux.browser.rotateY(_this.options.direction == 'left' ? -179 : 179) + }); + _this.slider.image1.find('div.overlay').css({ + opacity: 0 + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); +*/ + +(function ($) { + flux.transitions.slide = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + direction: 'left', + setup: function () { + var width = this.slider.options.width, + height = this.slider.options.height, + + currentImage = $('
    ').css({ + height: height + 'px', + width: width + 'px', + position: 'absolute', + top: '0px', + left: '0px', + background: this.slider[this.options.direction == 'left' ? 'image1' : 'image2'].css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': 'cover' + }).css3({ + 'backface-visibility': 'hidden' + }), + + nextImage = $('').css({ + height: height + 'px', + width: width + 'px', + position: 'absolute', + top: '0px', + left: width + 'px', + background: this.slider[this.options.direction == 'left' ? 'image2' : 'image1'].css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': 'cover' + }).css3({ + 'backface-visibility': 'hidden' + }); + + this.slideContainer = $('
    ').css({ + width: (2 * width) + 'px', + height: height + 'px', + position: 'relative', + left: this.options.direction == 'left' ? '0px' : -width + 'px', + 'z-index': 101, + 'background-repeat': 'no-repeat' + }).css3({ + 'transition-duration': '600ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'all' + }); + + this.slideContainer.append(currentImage).append(nextImage); + + this.slider.image1.append(this.slideContainer); + }, + execute: function () { + var _this = this, + delta = this.slider.options.width; + + if (this.options.direction == 'left') + delta = -delta; + + this.slideContainer.transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + _this.slideContainer.css3({ + 'transform': flux.browser.translate(delta) + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.swipe = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + setup: function () { + var svgmask = $('' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + var img = $('
    ').css({ + width: this.slider.options.width + "px ", + height: this.slider.options.height + "px ", + 'background-image': this.slider.image1.css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'transition-duration': '1600ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'all', + 'mask-image': '-webkit-linear-gradient(left, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 48%, rgba(0,0,0,1) 52%, rgba(0,0,0,1) 100%)', + 'mask-position': '70%', + 'mask-size': '400%' + }); + + this.slider.image1.append(img); + this.slider.image1.append(svgmask); + }, + execute: function () { + //return; + var _this = this, + img = this.slider.image1.find('div'); + + // Get notified when the last transition has completed + $(img).transitionEnd(function () { + _this.finished(); + }).get()[0].addEventListener('end', function () { + _this.finished(); + }, false); + + setTimeout(function () { + if (flux.browser.supportsCSSProperty('MaskImage')) { + $(img).css3({ + 'mask-position': '30%' + }); + } else if (flux.browser.supportsCSSProperty('mask')) { + $(img).css3({ + 'mask': 'url(#myMask)' + }); + } + }, 50); + }, + compatibilityCheck: function () { + return flux.browser.supportsCSSProperty('MaskImage') || + flux.browser.supportsCSSProperty('mask'); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + +(function ($) { + flux.transitions.dissolve = function (fluxslider, opts) { + return new flux.transition(fluxslider, $.extend({ + setup: function () { + var img = $('
    ').css({ + width: '100%', + height: '100%', + 'background-image': this.slider.image1.css('background-image'), + 'background-repeat': 'no-repeat', + 'background-size': this.slider.options.width + "px " + this.slider.options.height + "px", + }).css3({ + 'transition-duration': '600ms', + 'transition-timing-function': 'ease-in', + 'transition-property': 'opacity' + }); + + this.slider.image1.append(img); + }, + execute: function () { + var _this = this, + img = this.slider.image1.find('div.image'); + + // Get notified when the last transition has completed + $(img).transitionEnd(function () { + _this.finished(); + }); + + setTimeout(function () { + $(img).css({ + 'opacity': '0.0' + }); + }, 50); + } + }, opts)); + }; +})(window.jQuery || window.Zepto); + diff --git a/public/js/preview.js b/public/js/preview.js new file mode 100644 index 0000000..ae7f8a9 --- /dev/null +++ b/public/js/preview.js @@ -0,0 +1,175 @@ +/** fabric **/ +var canvas; +var displayId = 0; + +/** bundleData + * @see data/default/bundle.json + * + **/ +var bundleData; +bundleData = { + background: "", + duration: 10, + styleHeader: {}, + styleText: {}, + useWebFonts: false +}; +/** displayGroup + * contains displayed slide items + */ +var displayGroup; +displayGroup = []; + +/** background image **/ +var bgImage; + +/*************************** + * window load, init + * **************************/ +$(function () { + canvas = new fabric.StaticCanvas('c'); + canvas.backgroundColor = null; + canvas.backgroundImage = null; + canvas.renderAll(); + var ctx = canvas.getContext("2d"); + if (ctx) { + ctx.translate(0.5, 0.5); + } + fixCanvas(); + + /* $(document).mouseenter(function () { + $('.ui.basic.modal') + .modal('show') + ; + }); + + $(document).mouseleave(function () { + $('.ui.basic.modal') + .modal('hide') + ; + }); + */ + +}); + +$(window).bind("resize", function () { + fixCanvas(); +}); + +// socketio callbacks +/** when connected **/ +socket.on('connect', function () { + socket.emit("sync.preview", socketId); +}); + +/** callback Load **/ +socket.on('callback.preview', function (data) { + canvas.clear(); + displayId = data.displayId; + + if (data.bundleData.useWebFonts) { + if (bundleData.styleHeader.fontFamily != data.bundleData.styleHeader.fontFamily || bundleData.styleText.fontFamily != data.bundleData.styleText.fontFamily) { + WebFont.load({ + google: { + families: [data.bundleData.styleHeader.fontFamily, data.bundleData.styleText.fontFamily] + }, + timeout: 2000, + active: function () { + + nextSlide(data); + }, + inactive: function () { + + nextSlide(data); + } + }); + } else { + nextSlide(data); + } + } else { + nextSlide(data); + } + + bundleData = data.bundleData; + serverOptions = data.serverOptions; + + displayGroup = new fabric.Group(); + + setBackground(bundleData.background); +}); + + +function setBackground(background) { + background = "/background/" + bundleData.bundleName + "/" + background; + + var video = document.getElementById("bgvid"); + var bg = $("#bg"); + var bgImage = document.getElementById("bgimg"); + if (background.indexOf(".mp4") !== -1) { + if (parseUrl(video.src) !== background) { + bg.hide(); + $(video).show(); + video.src = background; + video.play(); + } + } else { + if (parseUrl(bgImage.src) !== background) { + bgImage.src = background; + bg.show(); + $(video).hide(); + video.pause(); + video.src = ""; + } + } +} + +function parseUrl(url) { + return '/background' + url.split('background')[1] +} + + +/** resize canvas to max width keeping aspect ratio 16:9**/ +function fixCanvas() { + var con = $(window), + aspect = (0.9 / 1.6), + width = con.innerWidth(), + height = Math.floor(width * aspect); + + canvas.setWidth(1280).setHeight(720); + $("#c").css("width", width + "px").css("height", height + "px"); + canvas.calcOffset(); +} + +function override() { + var obj = canvas.toJSON(['id']); + var video = document.getElementById("bgvid"); + var bg = ""; + if (video.src.indexOf(".mp4") !== -1) { + bg = video.src; + } else { + bg = video.poster; + } + + socket.emit("admin.override", {json: obj, displayId: displayId, background: bg}); +} + + +function nextSlide(data, updateOptions) { + serverOptions = data.serverOptions; + bundleData = data.bundleData; + + + setBackground(data.bundleData.background); + + canvas.loadFromJSON(data.json, function () { + + }, function (o, object) { + object.set(data.bundleData.styleText); + if (object.id === "header") { + object.set(data.bundleData.styleHeader); + } + + object.setShadow({color: "#000", blur: 3, offsetX: 0, offsetY: 0}); + + }); +} \ No newline at end of file diff --git a/public/js/upload.js b/public/js/upload.js new file mode 100644 index 0000000..4939508 --- /dev/null +++ b/public/js/upload.js @@ -0,0 +1,150 @@ +var images = {}; + +$(document).ready(function () { + var opts = { + dragClass: "active", + on: { + load: function (e, file) { + // check file type + var imageType = /image.(jpeg|png)/; + if (!file.type.match(imageType)) { + alert("File \"" + file.name + "\" is not a valid image file, only jpeg and png are supported"); + return false; + } + + // check file size + if (parseInt(file.size / 1024) > 2050) { + alert("File \"" + file.name + "\" is too big.Max allowed size is 2 MB."); + return false; + } + + createBox(e, file); + }, + } + }; + + FileReaderJS.setupDrop(document.getElementById('dropzone'), opts); + FileReaderJS.setupClipboard(document.body, opts); +}); + + +function createBox(e, file) { + var uid = uuidv4(); + var imgName = file.name; + + var src = e.target.result; + var tempCanvas = new fabric.StaticCanvas("tempCanvas", {width: 1280, height: 720}); + tempCanvas.backgroundColor = null; + tempCanvas.backgroundImage = null; + + fabric.Image.fromURL(src, function (bgImage) { + + var canvasAspect = tempCanvas.width / tempCanvas.height; + var imgAspect = bgImage.width / bgImage.height; + var left, top, scaleFactor; + + if (canvasAspect >= imgAspect) { + scaleFactor = tempCanvas.width / bgImage.width; + left = 0; + top = -((bgImage.height * scaleFactor) - tempCanvas.height) / 2; + } else { + scaleFactor = tempCanvas.height / bgImage.height; + top = 0; + left = -((bgImage.width * scaleFactor) - tempCanvas.width) / 2; + } + + bgImage.set({ + top: top, + left: left, + originX: 'left', + originY: 'top', + scaleX: scaleFactor, + scaleY: scaleFactor + }); + + bgImage.lockUniScaling = true; + tempCanvas.add(bgImage); + tempCanvas.renderAll(); + + images[uid] = {data: src, crop: tempCanvas.toDataURL(), name: imgName, type: file.type}; + + var template = `
    +
    + image +
    +
    +
    ${imgName}
    +
    +
    +
    Upload Original
    +
    Upload & Scale to 16:9
    +
    Cancel
    +
    +
    `; + $("#imageList").append(template); + }); + +} + + +function upload(uid, useOriginal) { + + if (images[uid] !== null) { + var busyTemplate = `
    +
    +
    Uploading
    +
    +
    `; + + $("#" + uid).html(busyTemplate); + let filename = images[uid].name.replace(/\.[^/.]+$/, ""); + + if (useOriginal) { + socket.emit("edit.uploadImage", { + bundleName: bundle, + name: filename, + imageData: images[uid].data, + type: images[uid].type, + displayId: displayId + }); + } else { + socket.emit("edit.uploadImage", { + bundleName: bundle, + name: filename + "_crop", + imageData: images[uid].crop, + type: "image/png", + displayId: displayId + }); + } + + delete (images[uid]); + + } +} + +function addImagesFromUploadQueue() { + + for (var i in images) { + var uid = i; + var image = images[i]; + var template = `
    +
    + image +
    +
    +
    ${image.name}
    +
    +
    +
    Upload Original
    +
    Upload & Scale to 16:9
    +
    Cancel
    +
    +
    `; + $("#imageList").append(template); + } +} + +function cancelUpload(uid) { + delete images[uid]; + $("#" + uid).remove(); +} \ No newline at end of file diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..54b9e8d --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,150 @@ +body { + padding: 0; + margin: 0; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; + height: 100%; + background: #1B1C1D; +} + +html { + min-height: 100%; +} + +iframe { + border: 1px solid black; +} + +#edit { + border: 1px solid #0f0f10; + padding: 0; + margin: 0; +} + +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} + +#networkstatus { + position: absolute; + z-index: 10; + top: 1vw; + right: 1vw; + font-family: 'Open Sans', sans-serif; + font-weight: bolder; + color: white; + text-align: center; + opacity: 0; +} + +#time { + position: absolute; + z-index: 10; + bottom: 2vw; + right: 3vw; + font-family: 'Open Sans', sans-serif; + font-weight: bolder; + font-size: 5vw; + color: white; + text-shadow: 2px 2px 2px black; + text-align: right; +} + +#time2 { + position: absolute; + z-index: 10; + bottom: 2vw; + right: 3vw; + font-family: 'Open Sans', sans-serif; + font-weight: bolder; + font-size: 5vw; + color: white; + text-shadow: 2px 2px 2px black; + text-align: right; +} + +#bgvid, #bg, canvas, .fsimage { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#bgvid, #bg { + z-index: 0; +} + +#bgvid2 { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; +} + +#matchsize { + position: absolute; +} + +#blackoutLayer { + position: fixed; + background: black; + opacity: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; +} + +#bundleSlides .item, #allBundles .item { + padding: 0.5rem; + margin: 3px 0; + box-shadow: none; +} + +#bundleSlides .green, #allBundles .green { + background: #08200b; +} + +#bundleSlides .red { + background: #200c0c; +} + +.nopadded { + padding: 0 !important; + margin: 0 !important; +} + +#preview { + overflow: hidden; +} + +.indexIcon { + opacity: 0; +} + +.gpu { + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + transform: translateZ(0); + + -moz-transition: opacity 2s !important; + -webkit-transition: opacity 2s !important; + -o-transition: opacity 2s !important; + transition: opacity 2s !important; +} + +#slider { + position: fixed; + z-index: 1; +} \ No newline at end of file diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..3ff7e7f --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,66 @@ +let express = require('express'); +let authMiddleWare = require('../modules/auth.js'); +let router = express.Router(); + +let fs = require('fs'); +let path = require('path'); +let config = require("../config.js"); + +module.exports = function (displays) { + + router.use(authMiddleWare); + + router.get('/', function (req, res, next) { + res.render('admin/dashboard', {config: config}); + }); + + router.get('/preview', function (req, res, next) { + let socketId = req.query['socket']; + let displayId = req.query['displayId'] || 0; + res.render('preview', {config: config, socketId: socketId, displayId: displayId}); + }); + + + router.get('/edit/slide', function (req, res, next) { + let bundle = req.query['bundle']; + let file = req.query['file']; + let displayId = req.query['displayId'] || 0; + res.render('admin/edit', {config: config, bundle: bundle, displayId: displayId, file: file}); + }); + + router.get('/bundle', function (req, res, next) { + let bundles = getDirectories("./data"); + res.render('admin/listBundle', {config: config, bundles: bundles}); + }); + + + router.get('/edit/bundle', function (req, res, next) { + let bundle = req.query['bundle']; + let file = req.query['file']; + let displayId = req.query['displayId'] || 0; + res.render('admin/editBundle', {config: config, bundle: bundle, displayId: displayId, file: file}); + }); + + router.get('/ajax/imagelist', function (req, res, next) { + let bundle = req.query['bundle']; + let bundleRoot = path.normalize("./data/" + bundle); + let bundleImages = fs.readdirSync(bundleRoot + "/images", { + dotfiles: false + }); + + let output = []; + for (let file of bundleImages) { + output.push({url: "/images/" + bundle + "/" + file, name: file}); + } + + res.render('ajax/bundleImageList', {bundleImages: output}); + }); + + function getDirectories(path) { + return fs.readdirSync(path).filter(function (file) { + return fs.statSync(path + '/' + file).isDirectory(); + }); + } + + return router; +}; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..7931154 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,100 @@ +let express = require('express'); +let router = express.Router(); +let fs = require("fs"); +let config = require("../config.js"); +let displayMeta = require("../data/meta.json"); +const cli = require('../modules/cli.js'); + +module.exports = function (displays) { + + router.get('/', function (req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.render('index', {config: config}); + }); + + router.get('/display/:id/lite', function (req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + let idx = parseInt(req.params.id); + res.render('liteDisplay', {config: config, display: displayMeta.displays[idx], displayId: idx}); + }); + + router.get('/display/:id', function (req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + let idx = parseInt(req.params.id); + res.render('display', {config: config, display: displayMeta.displays[idx], displayId: idx}); + }); + + router.get('/images/:dir/:name', function (req, res, next) { + let options = { + root: './data/' + req.params.dir + '/images/', + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true + } + }; + + res.sendFile(req.params.name, options, function (err) { + if (err) { + cli.error(err); + res.end(); + } + }); + }); + + router.get('/render/:dir/:name', function (req, res, next) { + let options = { + root: './data/' + req.params.dir + '/render/', + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true + } + }; + + res.sendFile(req.params.name, options, function (err) { + if (err) { + cli.error(err); + res.end(); + } + }); + }); + + router.get('/tmp/:displayId', function (req, res, next) { + let options = { + root: './tmp', + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true + } + }; + + res.sendFile("display_"+req.params.displayId+".png", options, function (err) { + if (err) { + cli.error(err); + res.end(); + } + }); + }); + + router.get('/background/:dir/:name', function (req, res, next) { + let options = { + root: './data/' + req.params.dir + '/backgrounds/', + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true + } + }; + + res.sendFile(req.params.name, options, function (err) { + if (err) { + cli.error(err); + res.end(); + } + }); + }); + + return router; +}; \ No newline at end of file diff --git a/templates/bundle/backgrounds/bg.jpg b/templates/bundle/backgrounds/bg.jpg new file mode 100644 index 0000000..f3cc047 Binary files /dev/null and b/templates/bundle/backgrounds/bg.jpg differ diff --git a/templates/bundle/bundle.json b/templates/bundle/bundle.json new file mode 100644 index 0000000..c3e04ae --- /dev/null +++ b/templates/bundle/bundle.json @@ -0,0 +1,18 @@ +{ + "bundleName": "bundle", + "background": "bg.jpg", + "duration": 20, + "useWebFonts": false, + "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/templates/bundle/images/.gitkeep b/templates/bundle/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/bundle/render/.gitkeep b/templates/bundle/render/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/bundle/slides.json b/templates/bundle/slides.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/templates/bundle/slides.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/templates/bundle/slides/.gitkeep b/templates/bundle/slides/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/views/admin/dashboard.twig b/views/admin/dashboard.twig new file mode 100644 index 0000000..44d3d6a --- /dev/null +++ b/views/admin/dashboard.twig @@ -0,0 +1,130 @@ +{% extends 'layout/admin.twig' %} + +{% block header %} + + +{% endblock %} + +{% block body %} + + + + +
    +
    + +
    +
    +

    + Available Bundles +

    + +
    +
    +
    +
    + +
    +
    +

    + Slides in rotation +

    + + + +
    + +
    +
    +
    + +
    + +
    +
    +

    Preview

    + +
    +
    + +
    +
    +

    Program

    + +
    +
    + +
    +
    +
    + + + +{% endblock %} \ No newline at end of file diff --git a/views/admin/edit.twig b/views/admin/edit.twig new file mode 100644 index 0000000..6904413 --- /dev/null +++ b/views/admin/edit.twig @@ -0,0 +1,109 @@ +{% extends 'layout/edit.twig' %} + +{% block header %} + + + + +{% endblock %} + +{% block body %} + + + + +
    +
    + +
    + +
    + +
    00:00
    +
    +
    + + + + + + +{% endblock %} + diff --git a/views/admin/editBundle.twig b/views/admin/editBundle.twig new file mode 100644 index 0000000..f217b98 --- /dev/null +++ b/views/admin/editBundle.twig @@ -0,0 +1,9 @@ +{% extends 'layout/admin.twig' %} + +{% block header %} + +{% endblock %} + +{% block body %} + +{% endblock %} \ No newline at end of file diff --git a/views/admin/listBundle.twig b/views/admin/listBundle.twig new file mode 100644 index 0000000..d33ea31 --- /dev/null +++ b/views/admin/listBundle.twig @@ -0,0 +1,30 @@ +{% extends 'layout/admin.twig' %} + +{% block header %} + +{% endblock %} + +{% block body %} + + + +
    +
    +
    +

    Available Bundles

    + +
    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/views/ajax/bundleImageList.twig b/views/ajax/bundleImageList.twig new file mode 100644 index 0000000..6ea59e8 --- /dev/null +++ b/views/ajax/bundleImageList.twig @@ -0,0 +1,15 @@ +{% for image in bundleImages %} +
    +
    + image +
    +
    +
    {{ image.name }}
    +
    +
    +
    Insert
    +
    Fill
    +
    Delete
    +
    +
    +{% endfor %} \ No newline at end of file diff --git a/views/display.twig b/views/display.twig new file mode 100644 index 0000000..dc702cc --- /dev/null +++ b/views/display.twig @@ -0,0 +1,36 @@ +{% extends 'layout/view.twig' %} + +{% block header %} + + + + InfoScreen3 - {{ display.name }} +{% endblock %} + +{% block body %} +
    + +
    + +
    00:00
    +
    + + +
    + +
    + +
    + +
    + + + + + +{% endblock %} + diff --git a/views/error.twig b/views/error.twig new file mode 100644 index 0000000..143ff6a --- /dev/null +++ b/views/error.twig @@ -0,0 +1,12 @@ +{% extends 'layout/admin.twig' %} + +{% block body %} + +

    {{ message }}

    +

    {{ error.status }}

    +
    {{ 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 %} +
    +
    +

    + InfoScreen3 +

    +

    Available displays

    + +
    +
    +
    +
    + + +{% endblock %} diff --git a/views/layout/admin.twig b/views/layout/admin.twig new file mode 100644 index 0000000..e6213c2 --- /dev/null +++ b/views/layout/admin.twig @@ -0,0 +1,21 @@ + + + + InfoScreen + + + + + + + + + {% block header %}{% endblock %} + + +
    + {% block body %}{% endblock %} +
    +{% block afterbody %}{% endblock %} + + diff --git a/views/layout/edit.twig b/views/layout/edit.twig new file mode 100644 index 0000000..f32c793 --- /dev/null +++ b/views/layout/edit.twig @@ -0,0 +1,24 @@ + + + + InfoScreen + + + + + + + + + + + {% block header %}{% endblock %} + + +
    + {% block body %}{% endblock %} +
    + + + + diff --git a/views/layout/view.twig b/views/layout/view.twig new file mode 100644 index 0000000..fa30839 --- /dev/null +++ b/views/layout/view.twig @@ -0,0 +1,24 @@ + + + + + + + + + + + {% block header %}{% endblock %} + + +{% block body %}{% endblock %} + + diff --git a/views/liteDisplay.twig b/views/liteDisplay.twig new file mode 100644 index 0000000..a0319c7 --- /dev/null +++ b/views/liteDisplay.twig @@ -0,0 +1,35 @@ +{% extends 'layout/view.twig' %} + +{% block header %} + + + InfoScreen3 - {{ display.name }} +{% endblock %} + +{% block body %} + +
    + +
    + +
    +
    + +
    + +
    00:00
    +
    + +
    + +
    + + + + +{% endblock %} diff --git a/views/preview.twig b/views/preview.twig new file mode 100644 index 0000000..c379ea2 --- /dev/null +++ b/views/preview.twig @@ -0,0 +1,34 @@ +{% extends 'layout/admin.twig' %} + +{% block header %} + + +{% endblock %} + +{% block body %} + + + + + + + + +{% endblock %} + +{% block afterbody %} + +{% endblock %} \ No newline at end of file