import
No files matched your search
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.idea
|
||||
.github
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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" ]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Info screen<sup>3</sup>
|
||||
|
||||
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 <br/>
|
||||
Admin interface is located at: http://localhost:8000/admin<br/>
|
||||
Defaults to user: `admin`, pass: `admin`<br/>
|
||||
It's highly encouraged to change the defaults to something else!
|
||||
|
||||
# Lite viewer
|
||||
Lite viewer is located at each display post fixed with `/lite`<br/>
|
||||
Example: http://localhost:8000/display/0/lite <br/>
|
||||
|
||||
> 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`<br/>
|
||||
set following options:<br/>
|
||||
|
||||
|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 .`<br/>
|
||||
`docker run -p 8000:8000 -e HOST=infoscreen.lan -e PORT=8000 -e ADMIN_USER=admin -e ADMIN_PASS=secret --name infoscreen reaby/infoscreen`<br/>
|
||||
@@ -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};
|
||||
@@ -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("<<"));
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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":{}}]}
|
||||
@@ -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":{}}]}
|
||||
@@ -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":{}}]}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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":{}}]}
|
||||
@@ -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":{}}]}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 += '<div class="ui green message item" id="bundle_' + bundle + '">' +
|
||||
'<div class="right floated content">' +
|
||||
'<button class="ui small basic inverted icon button" onclick="emit(\'admin.setBundle\', {bundle:\'' + bundle + '\'});"><i class="step forward icon"></i></button>' +
|
||||
'<button class="ui small basic inverted icon button" onclick="editBundle(\'' + bundle + '\')"><i class="edit outline icon"></i></button>' +
|
||||
'</div>' +
|
||||
'<div class="content">' +
|
||||
'<div>' + bundle + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
$('#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 += '<div class="ui ' + color + ' message item" id="' + slide.file + '">' +
|
||||
'<div class="right floated content">' +
|
||||
'<button class="ui small basic inverted icon button" onclick="emit(\'controls.preview\', {fileName: \'' + slide.file + '\', bundle: \'' + serverOptions.currentBundle + '\' });"><i class="search icon"></i></button>' +
|
||||
'<button class="ui small basic inverted icon button" onclick="emit(\'controls.skipTo\',{fileName: \'' + slide.file + '\'});"><i class="step forward icon"></i></button>' +
|
||||
'<button class="ui small basic inverted icon button" onclick="editSlide(\'' + slide.file + '\')"><i class="edit outline icon"></i></button>' +
|
||||
'<button class="ui small basic inverted icon button" onclick="remove({uuid: \'' + slide.file + '\'});"><i class="delete icon"></i></button>' +
|
||||
'</div>' +
|
||||
'<div class="content">' +
|
||||
'<i class="large toggle ' + status + ' icon" onclick="emit(\'controls.toggle\', {fileName: \'' + slide.file + '\'});"></i>' +
|
||||
'<div class="editable">' + slide.name.replace(".json", "") + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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});
|
||||
|
||||
});
|
||||
}
|
||||
@@ -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 = ` <div class="ui card" id="${uid}">
|
||||
<div class="image">
|
||||
<img class="" src="${src}" alt="image">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="center aligned header">${imgName}</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="ui tiny basic button" onclick="upload('${uid}', true);">Upload Original</div>
|
||||
<div class="ui tiny basic button" onclick="upload('${uid}', false);">Upload & Scale to 16:9</div>
|
||||
<div class="ui tiny basic button" onclick="cancelUpload('${uid}');">Cancel</div>
|
||||
</div>
|
||||
</div>`;
|
||||
$("#imageList").append(template);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function upload(uid, useOriginal) {
|
||||
|
||||
if (images[uid] !== null) {
|
||||
var busyTemplate = `<div class="card">
|
||||
<div class="ui active dimmer">
|
||||
<div class="ui text loader">Uploading</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$("#" + 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 = ` <div class="ui card" id="${uid}">
|
||||
<div class="image">
|
||||
<img class="" src="${image.data}" alt="image">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="center aligned header">${image.name}</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="ui tiny basic button" onclick="upload('${uid}', true);">Upload Original</div>
|
||||
<div class="ui tiny basic button" onclick="upload('${uid}', false);">Upload & Scale to 16:9</div>
|
||||
<div class="ui tiny basic button" onclick="cancelUpload('${uid}');">Cancel</div>
|
||||
</div>
|
||||
</div>`;
|
||||
$("#imageList").append(template);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelUpload(uid) {
|
||||
delete images[uid];
|
||||
$("#" + uid).remove();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends 'layout/admin.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jeditable.js/2.0.6/jquery.jeditable.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="ui primary bottom fixed pointing menu">
|
||||
<span class="header item">
|
||||
Info Screen<sup>3</sup>
|
||||
</span>
|
||||
|
||||
<div id="displays" class="ui dropdown item">
|
||||
<div class="ui small breadcrumb">
|
||||
<span class="section currentDisplay"></span>
|
||||
</div>
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
|
||||
<div id="bundles" class="ui dropdown item">
|
||||
<div class="ui small breadcrumb">
|
||||
<span class="section">Bundle</span>
|
||||
<i class="right chevron icon divider"></i>
|
||||
<span id="currentBundle" class="section"></span>
|
||||
</div>
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
|
||||
<div id="transitions" class="ui dropdown item">
|
||||
<div class="ui small breadcrumb">
|
||||
<span class="section">Transition</span>
|
||||
<i class="right chevron icon divider"></i>
|
||||
<span id="currentTransition" class="section"></span>
|
||||
</div>
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
|
||||
<div class="right menu">
|
||||
<div class="ui inverted buttons">
|
||||
<button class="ui icon button" onclick="emit('controls.previous');">
|
||||
<i class="step backward icon"></i>
|
||||
</button>
|
||||
|
||||
<button id="play" class="ui icon button" onclick="emit('controls.play');">
|
||||
<i class="play icon"></i>
|
||||
</button>
|
||||
|
||||
<button id="pause" class="ui icon button" onclick="emit('controls.pause');">
|
||||
<i class="pause icon"></i>
|
||||
</button>
|
||||
|
||||
<button class="ui icon button" onclick="emit('controls.next');">
|
||||
<i class="step forward icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="blackout" class="ui black basic button" onclick="emit('controls.blackout');">
|
||||
Blackout
|
||||
</button>
|
||||
<button id="toggleTime" class="ui teal basic button" onclick="emit('controls.time.toggle');">
|
||||
Toggle Time
|
||||
</button>
|
||||
<button id="stream" class="ui blue basic button"
|
||||
onclick="emit('controls.startStream', {streamName: 'STREAM_NAME' });">
|
||||
Start Stream
|
||||
</button>
|
||||
<button class="ui red basic button"
|
||||
onclick="forceReload()">
|
||||
Force Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- menu -->
|
||||
|
||||
<!-- Grids -->
|
||||
<div class="column">
|
||||
<div class="ui three column grid">
|
||||
|
||||
<div class="ui five wide column">
|
||||
<div class="ui inverted basic segment">
|
||||
<h1 class="ui inverted header">
|
||||
Available Bundles
|
||||
</h1>
|
||||
|
||||
<div id="allBundles" class="ui list">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui five wide column">
|
||||
<div class="ui inverted basic segment">
|
||||
<h1 class="ui inverted header">
|
||||
Slides in rotation
|
||||
</h1>
|
||||
|
||||
<button class="ui basic inverted button" onclick="createNew();">Create new</button>
|
||||
|
||||
<div id="bundleSlides" class="ui sortable list">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center aligned six wide column">
|
||||
|
||||
<div class="row">
|
||||
<div id="previewContainer" class="ui inverted segment">
|
||||
<h2 class="ui inverted nopadded header">Preview</h2>
|
||||
<iframe style="overflow-scrolling: unset; overflow: unset;" class="nopadded" id="preview"
|
||||
src="/admin/preview?uid={{ uid }}"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="programContainer" class="ui inverted segment">
|
||||
<h2 class="ui inverted nopadded header currentDisplay">Program </h2>
|
||||
<iframe class="nopadded" id="program" src="/display/0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/admin");
|
||||
</script>
|
||||
<script src="/js/dashboard.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% extends 'layout/edit.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
let bundle = "{{ bundle }}";
|
||||
let file = "{{ file }}";
|
||||
let displayId = {{ displayId }};
|
||||
</script>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="imageLayer" class="ui modal">
|
||||
<div class="header">Insert Image</div>
|
||||
<div id="dropzone" class="scrolling content">
|
||||
<div id="imageList" class="ui three stackable cards">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui cancel button">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui inverted secondary menu">
|
||||
<div class="item">
|
||||
<div class="ui inverted buttons">
|
||||
<a href="#" onclick="addHeader('header')" class="ui basic inverted icon button"><i
|
||||
class="ui heading icon"></i></a>
|
||||
<a href="#" onclick="addText('paragraph text')" class="ui basic inverted icon button"><i
|
||||
class="ui paragraph icon"></i></a>
|
||||
<a href="#" onclick="openImageBrowser()" class="ui basic inverted icon button"><i
|
||||
class="ui image icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a href="#" onclick="removeSelectedObjects()" class="ui basic inverted icon button"><i
|
||||
class="ui remove icon"></i></a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div id="order" class="ui dropdown basic inverted button">
|
||||
<div class="default text">Change order <i class="dropdown icon"></i></div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="bringToFront">Send to front</div>
|
||||
<div class="item" data-value="bringForward">Move forward</div>
|
||||
<div class="item" data-value="sendBackwards">Move backward</div>
|
||||
<div class="item" data-value="sendToBack">Send to back</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a href="#" onclick="cueSlide()" class="ui basic teal button">Fast Announce</a>
|
||||
</div>
|
||||
<div class="right menu">
|
||||
<div class="item">
|
||||
<div class="ui buttons">
|
||||
<a href="#" onclick="save()" class="ui green button">Save</a>
|
||||
<a href="#" onclick="window.close()" class="ui red button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="segment">
|
||||
<div id="matchsize">
|
||||
<canvas id="edit" width="1280" height="720" style="position: fixed; z-index:10;"></canvas>
|
||||
<div id="bg">
|
||||
<img id="bgimg" class="fsimage" src=""/>
|
||||
</div>
|
||||
<video muted loop autoplay id="bgvid2" poster="">
|
||||
<source src="" type="video/mp4">
|
||||
</video>
|
||||
<div id="time2" class="noselect delay animated">00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contextmenu" class="ui small vertical menu" style="display: none; position: absolute; z-index: 1000;">
|
||||
<a class="item" data-action="copy">
|
||||
Copy
|
||||
</a>
|
||||
<a class="item" data-action="paste">
|
||||
Paste
|
||||
</a>
|
||||
<div class="ui dropdown item">
|
||||
<i class="dropdown icon"></i>
|
||||
Order
|
||||
<div class="menu">
|
||||
<div class="item" data-action="bringToFront">Send to Front</div>
|
||||
<div class="item" data-action="bringForward">Move forward</div>
|
||||
<div class="item" data-action="sendBackwards">Move backward</div>
|
||||
<div class="item" data-action="sendToBack">Send to Back</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="item" data-action="delete">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
<canvas id="tempCanvas" width="1280" height="720" style="display: none"></canvas>
|
||||
|
||||
<script>
|
||||
var socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/admin");
|
||||
</script>
|
||||
<script src="/js/edit.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends 'layout/admin.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends 'layout/admin.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="ui primary bottom fixed pointing menu">
|
||||
<span class="header item">
|
||||
Info Screen<sup>3</sup>
|
||||
</span>
|
||||
<!--
|
||||
|
||||
-->
|
||||
<div class="right menu">
|
||||
<a href=""></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grids -->
|
||||
<div class="column">
|
||||
<div class="ui three column very padded grid">
|
||||
<div class=" column">
|
||||
<h1 class="ui inverted header">Available Bundles</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% for image in bundleImages %}
|
||||
<div class="card">
|
||||
<div class="image">
|
||||
<img src="{{ image.url }}" alt="image">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="center aligned header">{{ image.name }}</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="ui tiny basic button" onclick="addImage('{{ image.url }}', false);">Insert</div>
|
||||
<div class="ui tiny basic button" onclick="addImage('{{ image.url }}', true);">Fill</div>
|
||||
<div class="ui tiny basic negative button" onclick="deleteImage('{{ image.name }}')">Delete</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends 'layout/view.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flv.js/1.5.0/flv.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/js/flux.js" type="text/javascript" charset="utf-8"></script>
|
||||
<title>InfoScreen3 - {{ display.name }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="blackoutLayer" class="animated">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="time" class="noselect animated">00:00</div>
|
||||
<div id="networkstatus" class="noselect animated"><i class="spinner loading icon"></i></div>
|
||||
|
||||
|
||||
<div id="slider" style="overflow: hidden">
|
||||
<img src=""/>
|
||||
</div>
|
||||
|
||||
<div id="bg">
|
||||
<img id="bgimg" class="fsimage" src=""/>
|
||||
</div>
|
||||
<video muted loop autoplay id="bgvid" class="fsimage" poster="">
|
||||
<source src="" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<script>
|
||||
var socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/display-{{ displayId }}");
|
||||
</script>
|
||||
|
||||
<script src="/js/display.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'layout/admin.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
h1,h2,h3,pre {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<h1>{{ message }}</h1>
|
||||
<h2>{{ error.status }}</h2>
|
||||
<pre>{{ error.stack }}</pre>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends 'layout/view.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui center aligned inverted header">
|
||||
InfoScreen<sup>3</sup>
|
||||
</h1>
|
||||
<h2 class="ui center aligned inverted dividing header">Available displays</h2>
|
||||
|
||||
<div class="ui three stackable cards" id="displayList">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/lobby");
|
||||
var intervalId = null;
|
||||
|
||||
/** when connected **/
|
||||
socket.on('connect', function () {
|
||||
socket.emit("displays");
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(function () {
|
||||
socket.emit("displays");
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
socket.on("callback.displays", function (data) {
|
||||
var index = 0;
|
||||
var output = "";
|
||||
for (var i in data.displays) {
|
||||
var display = data.displays[i];
|
||||
var html = '<div class="ui card">' +
|
||||
'<div class="image" style="background: black;">' +
|
||||
'<img src="' + data.previewImages[i] + '" />' +
|
||||
'</div>' +
|
||||
'<div class="content">' +
|
||||
'<h3 class="ui center aligned header">' + display.name + '</h3>' +
|
||||
'<div class="ui attached buttons">' +
|
||||
'<a href="display/' + index.toString() + '" class="ui positive button">Normal</a>' +
|
||||
'<div class="or"></div>' +
|
||||
'<a href="display/' + index.toString() + '/lite" class="ui button">Lite view</a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
output += html;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
$('#displayList').html(output);
|
||||
});
|
||||
|
||||
window.onError = function (e) {
|
||||
console.log(e);
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>InfoScreen</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
|
||||
<link rel='stylesheet' href='/stylesheets/style.css'/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.6.0/fabric.min.js"></script>
|
||||
{% block header %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="ui fluid container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% block afterbody %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>InfoScreen</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
|
||||
<link rel='stylesheet' href='/stylesheets/style.css'/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.6.0/fabric.min.js"></script>
|
||||
<script src="/js/filereader.js"></script>
|
||||
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="ui fluid container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="/js/upload.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.6.0/fabric.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />
|
||||
<link rel='stylesheet' href='/stylesheets/style.css'/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
{% block header %}{% endblock %}
|
||||
</head>
|
||||
<body style="overflow: hidden;">
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends 'layout/view.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flv.js/1.5.0/flv.min.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<title>InfoScreen3 - {{ display.name }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div id="blackoutLayer" class="gpu"></div>
|
||||
|
||||
<div id="layer0" class="fsimage gpu" style="z-index: 10;">
|
||||
<img id="img0" style="width: 100%;"/>
|
||||
</div>
|
||||
<div id="layer1" class="fsimage gpu" style="z-index: 10;">
|
||||
<img id="img1" style="width: 100%;"/>
|
||||
</div>
|
||||
|
||||
<div id="time" class="noselect delay animated">00:00</div>
|
||||
<div id="networkstatus" class="noselect animated"><i class="spinner loading icon"></i></div>
|
||||
|
||||
<div id="bg">
|
||||
<img id="bgimg" class="fsimage" src=""/>
|
||||
</div>
|
||||
<video muted loop autoplay id="bgvid" poster="">
|
||||
<source src="" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<script>
|
||||
var socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/display-{{ displayId }}");
|
||||
var displayId = {{ displayId }};
|
||||
</script>
|
||||
<script src="/js/displayLite.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends 'layout/admin.twig' %}
|
||||
|
||||
{% block header %}
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<canvas id="c" width="1280" height="720" style="position: fixed; z-index: 3;"></canvas>
|
||||
|
||||
<div id="bg" style="display: none;">
|
||||
<img id="bgimg" class="fsimage" src="" />
|
||||
</div>
|
||||
<video muted loop autoplay id="bgvid" poster="" style="display: none;">
|
||||
<source src="" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<script>
|
||||
let socket = io.connect("{{ config.serverUrl }}:{{ config.serverListenPort }}/admin");
|
||||
let socketId = encodeURIComponent("{{ socketId }}");
|
||||
</script>
|
||||
<script src="/js/preview.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block afterbody %}
|
||||
<div class="ui basic modal">
|
||||
<div class="content">
|
||||
<div class="ui basic center aligned segment">
|
||||
<button class="ui button" onclick="override()">Override</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||