This commit is contained in:
Reaby committed 2019-01-17 01:35:47 +02:00
1 parent 1be692636d
commit a02d085d0c
68 files changed
+7615

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
node_modules
.idea
.github
+14
View File
@@ -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)
+66
View File
@@ -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
+19
View File
@@ -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" ]
+53
View File
@@ -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/>
+75
View File
@@ -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};
+94
View File
@@ -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("<<"));
}
+12
View File
@@ -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
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+18
View File
@@ -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"
}
}
View File
Whitespace-only changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+26
View File
@@ -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":{}}]}
+30
View File
@@ -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"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+18
View File
@@ -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
}
}
View File
Whitespace-only changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+18
View File
@@ -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":{}}]}
+450
View File
@@ -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;
+28
View File
@@ -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);
};
+152
View File
@@ -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;
+45
View File
@@ -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;
+48
View File
@@ -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;
}
+215
View File
@@ -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;
+84
View File
@@ -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;
};
+1362
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -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"
}
}
+280
View File
@@ -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');
}
}
+382
View File
@@ -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)
}
+255
View File
@@ -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);
});
}
+435
View File
@@ -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);
});
}
+432
View File
@@ -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);
+1763
View File
File diff suppressed because it is too large. Load diff
+175
View File
@@ -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});
});
}
+150
View File
@@ -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();
}
+150
View File
@@ -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;
}
+66
View File
@@ -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;
};
+100
View File
@@ -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;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+18
View File
@@ -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
}
}
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
[]
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+130
View File
@@ -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 %}
+109
View File
@@ -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 %}
+9
View File
@@ -0,0 +1,9 @@
{% extends 'layout/admin.twig' %}
{% block header %}
<script src="/socket.io/socket.io.js"></script>
{% endblock %}
{% block body %}
{% endblock %}
+30
View File
@@ -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 %}
+15
View File
@@ -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 %}
+36
View File
@@ -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 %}
+12
View File
@@ -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 %}
+62
View File
@@ -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 %}
+21
View File
@@ -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>
+24
View File
@@ -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>
+24
View File
@@ -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>
+35
View File
@@ -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 %}
+34
View File
@@ -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 %}