Major update and redesign

This commit is contained in:
Jonatan Nilsson 2018-06-26 18:35:12 +00:00
parent 767c4dd73d
commit 8780eb228e
50 changed files with 2465 additions and 3701 deletions

View file

@ -1,33 +1,88 @@
{ {
"parser": "babel-eslint", "parserOptions": {
"extends": "airbnb-base", "ecmaVersion": 9,
"ecmaFeatures": { "sourceType": "module",
"modules": false "ecmaFeatures": {
"impliedStrict": true
}
}, },
"extends": "eslint:recommended",
"plugins": [ "plugins": [
"mocha" "mocha"
], ],
"env": {
"mocha": true,
"node": true,
"es6": true
},
"rules": { "rules": {
"mocha/no-exclusive-tests": 2, "mocha/no-exclusive-tests": 2,
"require-await": 0,
"no-invalid-this": 0,
"array-callback-return": 2,
"block-scoped-var": 2,
"complexity": ["error", 20],
"eqeqeq": [2, "smart"],
"no-else-return": ["error", { "allowElseIf": false }],
"no-extra-bind": 2,
"no-implicit-coercion": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-new": 2,
"no-param-reassign": [2, {"props": false}],
"no-return-assign": 2,
"no-return-await": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-unmodified-loop-condition": 2,
"no-useless-call": 2,
"no-useless-concat": 2,
"no-useless-return": 2,
"no-void": 2,
"no-warning-comments": 2,
"prefer-promise-reject-errors": 2,
"no-catch-shadow": 2,
"no-shadow": 2,
"no-undef-init": 2,
"no-undefined": 2,
"no-use-before-define": 0,
"no-new-require": 2,
"no-sync": 2,
"array-bracket-newline": [2, "consistent"],
"block-spacing": [2, "always"],
"brace-style": [2, "1tbs"],
"comma-dangle": [2, "always-multiline"],
"comma-spacing": 2,
"comma-style": 2,
"computed-property-spacing": 2,
"eol-last": 2,
"func-call-spacing": 2,
"key-spacing": 2,
"keyword-spacing": 2,
"semi": [2, "never"], "semi": [2, "never"],
"max-len": [1, 120], "max-len": [1, 120],
"prefer-const": 0, "prefer-const": 0,
"consistent-return": 0, "consistent-return": 0,
"no-param-reassign": [2, {"props": false}],
"no-use-before-define": [2, {"functions": false, "classes": true}],
"no-unused-vars": [ "no-unused-vars": [
2, 2,
{ {
"args": "none" "args": "after-used",
"argsIgnorePattern": "next|res|req"
} }
] ],
"generator-star-spacing": 0,
"global-require": 0,
"import/prefer-default-export": 0,
"no-underscore-dangle": 0,
"strict": 0,
"require-yield": 0
}, },
"globals": { globals: {
"describe": false, "window": true,
"it": false, "document": true
"before": false,
"beforeEach": false,
"after": false,
"afterEach": false
} }
} }

View file

@ -73,10 +73,11 @@ shelf.createModel = (attr, opts) => {
.fetch({ require, withRelated }) .fetch({ require, withRelated })
}, },
getAll(where = {}, withRelated = []) { getAll(where = {}, withRelated = [], orderBy = 'id') {
where.is_deleted = false where.is_deleted = false
return this.query({ where }) return this.query({ where })
.orderBy(orderBy, 'ASC')
.fetchAll({ withRelated }) .fetchAll({ withRelated })
}, },
}) })

120
api/casparcg/client.js Normal file
View file

@ -0,0 +1,120 @@
import Settings from '../settings/model'
import { address } from 'ip'
import { CasparCG, AMCP } from 'casparcg-connection'
const timeoutDuration = 60000
let io
let logger
let connection
let casparIsPlaying
let casparIsConnected
let checkTimeout
let currentHost
export async function initialise(log, socket) {
io = socket.socket
logger = log
return connect()
}
export async function connect() {
currentHost = await Settings.getValue('casparcg')
casparIsPlaying = false
casparIsConnected = false
logger.info('CasparCG: Connectiong to', currentHost + ':' + 5250)
if (connection) {
await connection.close()
}
connection = new CasparCG({
host: currentHost,
port: 5250,
queueMode: 2,
autoReconnectInterval: timeoutDuration,
onError: err => {
logger.error('CasparCG: Error', err.message)
},
onConnectionStatus: data => {
casparIsConnected = data.connected
if (!data.connected) {
casparIsPlaying = false
logger.warn('CasparCG: connection closed, retrying in 60 seconds', connection.connected)
io.emit('casparcg.status', currentStatus())
if (checkTimeout) clearInterval(checkTimeout)
checkTimeout = null
}
},
onConnected: async connected => {
logger.info('CasparCG: connected', connected)
io.emit('casparcg.status', currentStatus())
checkClientPlaying(false, true)
// Run our check on hourly interval
if (checkTimeout) clearInterval(checkTimeout)
checkTimeout = setInterval(() => checkClientPlaying(), timeoutDuration * 60)
},
})
}
export function currentStatus(e) {
return {
connected: casparIsConnected,
playing: casparIsPlaying,
error: e,
}
}
export async function checkClientPlaying(starting = false, first = false) {
let ip
if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
ip = 'localhost'
} else {
ip = address()
}
// Check if we lost connection while attempting to start playing
if (!connection.connected) {
logger.error('CasparCG: Attempted to play but connection was lost')
}
try {
// Check if we're already playing
let output = await connection.info(1, 100)
if (output.response.data.status !== 'playing') {
casparIsPlaying = false
if (starting) {
// We are not playing, check to see if we've already attempted
// to issue a play command and delay trying for a minute
await new Promise(res => {
logger.warn('CasparCG: Play did not start playing, retrying in 60 seconds')
setTimeout(res, timeoutDuration)
})
}
// Send a play command and retry checking again
logger.info(`CasparCG: Sending play command for ${ip}:3000`)
await connection.do(new AMCP.CustomCommand(`PLAY 1-100 [HTML] "http://${ip}:3000/client.html" CUT 1 LINEAR RIGHT`))
return checkClientPlaying(true)
}
casparIsPlaying = true
// We are playing, notify all clients
io.emit('casparcg.status', currentStatus())
if (starting || first) {
logger.info('CasparCG: client is up and playing')
}
} catch (e) {
// Unknown error occured
casparIsPlaying = true
logger.error(e, 'CasparCG: Error starting play on client')
io.emit('casparcg.status', currentStatus(e))
}
}

View file

@ -0,0 +1,5 @@
import { currentStatus } from './client'
export async function casparConnection(ctx) {
ctx.socket.emit('casparcg.status', currentStatus())
}

41
api/content/model.js Normal file
View file

@ -0,0 +1,41 @@
import bookshelf from '../bookshelf'
/* Content model:
{
id,
name,
graphic,
html,
css,
data,
}
*/
const Content = bookshelf.createModel({
tableName: 'content',
format(attributes) {
attributes.graphic = JSON.stringify(attributes.graphic)
attributes.data = JSON.stringify(attributes.data)
return attributes
},
parse(attributes) {
if (attributes.graphic) {
attributes.graphic = JSON.parse(attributes.graphic)
}
if (attributes.data) {
attributes.data = JSON.parse(attributes.data)
}
return attributes
},
}, {
getSingle(name, withRelated = [], require = false) {
let where = { name }
return this.query({ where })
.fetch({ require, withRelated })
},
})
export default Content

View file

@ -1,4 +1,5 @@
import _ from 'lodash' import _ from 'lodash'
import Content from './model'
export const active = { } export const active = { }
@ -7,25 +8,48 @@ function getSocket(ctx, all) {
return ctx.socket return ctx.socket
} }
export function display(ctx, data) { /*
* Event: 'content.display'
*
* Display a specific graphic content
*/
export async function display(ctx, data) {
let compiled = _.template(data.graphic.settings.html) let compiled = _.template(data.graphic.settings.html)
let html = compiled(data.data) let html = compiled(data.data)
let payload = { let old = await Content.getSingle(data.graphic.name)
graphic: data.graphic,
html, if (old) {
css: data.graphic.settings.css, await old.destroy()
data: data.data,
} }
active[data.graphic.name] = payload let payload = {
ctx.io.emit('client.display', payload) graphic: data.graphic,
name: data.graphic.name,
html: html || '',
css: data.graphic.settings.css || '',
data: data.data,
is_deleted: false,
}
let content = await Content.create(payload)
ctx.io.emit('client.display', content.toJSON())
list(ctx, true) list(ctx, true)
} }
export function hide(ctx, data) { /*
delete active[data.name] * Event: 'content.hide'
*
* Hide a specific graphic content
*/
export async function hide(ctx, data) {
let content = await Content.getSingle(data.name)
if (!content) return
await content.destroy()
ctx.io.emit('client.hide', { ctx.io.emit('client.hide', {
name: data.name, name: data.name,
@ -35,21 +59,41 @@ export function hide(ctx, data) {
} }
function generateDisplayText(item) { function generateDisplayText(item) {
if (item.graphic.engine === 'countdown') { // if (item.graphic.engine === 'countdown') {
return `${item.data[item.graphic.settings.main]} - ${item.data.countdown}` // return `${item.data.text} - ${item.data.finished}`
// }
try {
return _.template(item.graphic.settings.main)(item.data)
} catch (e) {
return `Error creating display: ${e.message}`
} }
return item.data[item.graphic.settings.main]
} }
export function list(ctx, all) { /*
let payload = Object.keys(active).map(key => ({ * Event: 'content.list'
name: active[key].graphic.name, * Runs on start of every new connection
display: generateDisplayText(active[key]), *
})) * Send a name list of all active graphics
*/
export async function list(ctx, all) {
let allContent = await Content.getAll()
let payload = await Promise.all(allContent.map(item => ({
name: item.get('name'),
display: generateDisplayText(item.toJSON()),
})))
getSocket(ctx, all).emit('content.list', payload) getSocket(ctx, all).emit('content.list', payload)
} }
export function reset(ctx) { /*
ctx.socket.emit('client.reset', _.values(active)) * Event: 'content.list'
* Runs on start of every new connection
*
* Send actual graphics of all active graphics
*/
export async function reset(ctx) {
let allContent = await Content.getAll()
ctx.socket.emit('client.reset', allContent.toJSON())
} }

View file

@ -1,4 +1,9 @@
/*
* Event: 'engine.all'
*
* Return all supported graphic engines.
*/
export function all(ctx) { export function all(ctx) {
ctx.socket.emit('engine.all', ['text', 'countdown', 'schedule']) ctx.socket.emit('engine.all', ['text', 'countdown'])
} }

View file

@ -1,11 +1,21 @@
import Graphic from './model' import Graphic from './model'
/*
* Event: 'graphic.all'
*
* Request all graphics in store
*/
export async function all(ctx) { export async function all(ctx) {
let data = await Graphic.getAll() let data = await Graphic.getAll()
ctx.io.emit('graphic.all', data.toJSON()) ctx.socket.emit('graphic.all', data.toJSON())
} }
/*
* Event: 'graphic.single'
*
* Request a single graphic
*/
export async function single(ctx, data) { export async function single(ctx, data) {
if (!data || !data.id) { if (!data || !data.id) {
ctx.log.warn('called graphic get single but no id specified') ctx.log.warn('called graphic get single but no id specified')
@ -14,23 +24,38 @@ export async function single(ctx, data) {
let graphic = await Graphic.getSingle(data.id) let graphic = await Graphic.getSingle(data.id)
ctx.io.emit('graphic.single', graphic.toJSON()) ctx.socket.emit('graphic.single', graphic.toJSON())
} }
/*
* Event: 'graphic.create'
*
* Create a single graphic and emit to all clients.
*
* @body {string} engine - Engine for the graphic
* @body {string} name - Name of graphic
*/
export async function create(ctx, data) { export async function create(ctx, data) {
data.settings = {} data.settings = {}
data.is_deleted = false data.is_deleted = false
if (data.engine === 'countdown') { if (data.engine === 'countdown') {
data.settings.html = `<span id="${data.name}-countdown-timer">countdown appears here</span>` data.settings.html = `<span id="${data.name}-countdown-timer">countdown appears here</span>`
data.settings.main = 'text' data.settings.main = '<%- text %> - <%- finished %>'
} }
await Graphic.create(data) let graphic = await Graphic.create(data)
await all(ctx) ctx.io.emit('graphic.single', graphic.toJSON())
} }
/*
* Event: 'graphic.remove'
*
* Remove a single graphic
*
* @body {int} id - Id of the graphic to remove
*/
export async function remove(ctx, data) { export async function remove(ctx, data) {
if (!data || !data.id) { if (!data || !data.id) {
ctx.log.warn('called graphic get single but no id specified') ctx.log.warn('called graphic get single but no id specified')
@ -41,9 +66,20 @@ export async function remove(ctx, data) {
graphic.set({ is_deleted: true }) graphic.set({ is_deleted: true })
await graphic.save() await graphic.save()
await all(ctx) let output = await Graphic.getAll()
ctx.io.emit('graphic.all', output.toJSON())
} }
/*
* Event: 'graphic.update'
*
* Update a single graphic
*
* @body {int} id - Id of the graphic to update
* @body {string} [name] - Name of the graphic
* @body {string} [engine] - Engine for the graphic
* @body {object} [settings] - Settings for the graphic, JSON object
*/
export async function update(ctx, data) { export async function update(ctx, data) {
if (!data || !data.id) { if (!data || !data.id) {
ctx.log.warn('called graphic update but no id specified') ctx.log.warn('called graphic update but no id specified')
@ -56,5 +92,5 @@ export async function update(ctx, data) {
await graphic.save() await graphic.save()
await single(ctx, data) ctx.io.emit('graphic.single', graphic.toJSON())
} }

View file

@ -23,7 +23,7 @@ const Preset = bookshelf.createModel({
attributes.values = JSON.parse(attributes.values) attributes.values = JSON.parse(attributes.values)
} }
return attributes return attributes
} },
}, { }, {
}) })

View file

@ -3,7 +3,7 @@ import Preset from './model'
export async function all(ctx, payload) { export async function all(ctx, payload) {
let id = Number(payload.graphic_id || payload.id) let id = Number(payload.graphic_id || payload.id)
let data = await Preset.getAll({ graphic_id: id }) let data = await Preset.getAll({ graphic_id: id }, [], 'sort')
ctx.io.emit(`preset.all:${id}`, data.toJSON()) ctx.io.emit(`preset.all:${id}`, data.toJSON())
} }
@ -27,6 +27,18 @@ export async function add(ctx, payload) {
await all(ctx, payload) await all(ctx, payload)
} }
export async function patch(ctx, payload) {
await Promise.all(payload.map(async item => {
let preset = await Preset.getSingle(item.id)
preset.set({ sort: item.sort })
await preset.save()
}))
await all(ctx, payload[0])
}
export async function remove(ctx, payload) { export async function remove(ctx, payload) {
let preset = await Preset.getSingle(payload.id) let preset = await Preset.getSingle(payload.id)

View file

@ -1,11 +1,14 @@
import logger from '../log' import logger from '../log'
import { register } from './io/helper' import { register } from './io/helper'
import { contentConnection } from './content/connection' import { contentConnection } from './content/connection'
import { casparConnection } from './casparcg/connection'
import * as content from './content/routes' import * as content from './content/routes'
import * as engine from './engine/routes' import * as engine from './engine/routes'
import * as graphic from './graphic/routes' import * as graphic from './graphic/routes'
import * as preset from './preset/routes' import * as preset from './preset/routes'
import * as settings from './settings/routes'
import * as schedule from './schedule/routes'
function onConnection(server, data) { function onConnection(server, data) {
const io = server.socket const io = server.socket
@ -17,11 +20,14 @@ function onConnection(server, data) {
let ctx = { io, socket, log } let ctx = { io, socket, log }
contentConnection(ctx) contentConnection(ctx)
casparConnection(ctx)
register(ctx, 'content', content) register(ctx, 'content', content)
register(ctx, 'engine', engine) register(ctx, 'engine', engine)
register(ctx, 'graphic', graphic) register(ctx, 'graphic', graphic)
register(ctx, 'preset', preset) register(ctx, 'preset', preset)
register(ctx, 'settings', settings)
register(ctx, 'schedule', schedule)
} }
export default onConnection export default onConnection

35
api/schedule/model.js Normal file
View file

@ -0,0 +1,35 @@
import bookshelf from '../bookshelf'
import Graphic from '../graphic/model'
/* Schedule model:
{
id,
graphic_id,
values,
sort,
is_deleted,
}
*/
const Schedule = bookshelf.createModel({
tableName: 'schedule',
graphic() {
return this.belongsTo(Graphic, 'graphic_id')
},
format(attributes) {
attributes.values = JSON.stringify(attributes.values)
return attributes
},
parse(attributes) {
if (attributes.values) {
attributes.values = JSON.parse(attributes.values)
}
return attributes
},
}, {
})
export default Schedule

52
api/schedule/routes.js Normal file
View file

@ -0,0 +1,52 @@
import Schedule from './model'
export async function all(ctx) {
let data = await Schedule.getAll({ }, ['graphic'], 'sort')
ctx.io.emit('schedule.all', data.toJSON())
total(ctx)
}
export async function total(ctx) {
let data = await Schedule.getAll({ }, ['graphic'], 'sort')
ctx.io.emit('schedule.total', { total: data.length })
}
export async function add(ctx, payload) {
payload.is_deleted = false
payload.sort = 1
let last = await Schedule.query(q => {
q.orderBy('sort', 'desc')
q.limit(1)
}).fetch({ require: false })
if (last) {
payload.sort = last.get('sort') + 1
}
await Schedule.create(payload)
await all(ctx)
}
export async function patch(ctx, payload) {
await Promise.all(payload.map(async item => {
let scheduleItem = await Schedule.getSingle(item.id)
scheduleItem.set({ sort: item.sort })
await scheduleItem.save()
}))
await all(ctx)
}
export async function remove(ctx, payload) {
let scheduleItem = await Schedule.getSingle(payload.id)
await scheduleItem.destroy()
await all(ctx)
}

View file

@ -1,6 +1,7 @@
import Koa from 'koa' import Koa from 'koa'
import serve from 'koa-better-serve' import serve from 'koa-better-serve'
import socket from 'koa-socket' import socket from 'koa-socket'
import * as casparcg from './casparcg/client'
import config from '../config' import config from '../config'
import log from '../log' import log from '../log'
@ -14,9 +15,19 @@ io.attach(app)
io.on('connection', onConnection.bind(this, io)) io.on('connection', onConnection.bind(this, io))
casparcg.initialise(log, io).catch(e => {
log.error(e, 'Critical error initialising casparcg')
})
app.use(bunyanLogger(log)) app.use(bunyanLogger(log))
app.use(errorHandler()) app.use(errorHandler())
app.use(serve('./public', '/public')) app.use(async (ctx, next) => {
if (ctx.url === '/') {
return ctx.redirect('/index.html')
}
await next()
})
app.use(serve('./public', ''))
app.listen(config.get('server:port'), err => { app.listen(config.get('server:port'), err => {
if (err) return log.critical(err) if (err) return log.critical(err)

51
api/settings/model.js Normal file
View file

@ -0,0 +1,51 @@
import bookshelf from '../bookshelf'
/* Settings model:
{
id,
name,
value,
}
*/
const Settings = bookshelf.createModel({
tableName: 'settings',
}, {
getValue(name) {
return this.query({ where: { name: name } })
.fetch({ require: false })
.then(item => item && item.get('value') || item)
},
setValue(name, value) {
return this.query({ where: { name } })
.fetch({ require: false })
.then(item => {
if (item) {
item.set({ value })
return item.save()
}
return this.create({
name,
value,
is_deleted: false,
})
})
},
getSettings() {
return this.query({ where: { }})
.fetchAll({ })
.then(data => {
let out = { }
data.forEach(item => {
out[item.get('name')] = item.get('value')
})
return out
})
},
})
export default Settings

37
api/settings/routes.js Normal file
View file

@ -0,0 +1,37 @@
import Settings from './model'
import { connect } from '../casparcg/client'
/*
* Event: 'settings.all'
*
* Request all settings in store
*/
export async function all(ctx) {
let data = await Settings.getSettings()
ctx.socket.emit('settings.all', data)
}
/*
* Event: 'settings.update'
*
* Update a single setting value
*
* @body {string} [name] - Name of the settings
* @body {string} [value] - Value of the settings
*/
export async function update(ctx, data) {
if (!data || data.name == null || data.value == null) {
ctx.log.warn(data, 'called settings update but no name or value specified, body was:')
return
}
await Settings.setValue(data.name, data.value)
let output = await Settings.getSettings()
ctx.io.emit('settings.all', output)
if (data.name === 'casparcg') {
connect()
}
}

View file

@ -1,5 +1,48 @@
module.exports = function(data) { var currentActiveTimer = null
function pad(n) { return (n < 10) ? ('0' + n) : n }
function timer(name) {
var days = 0
var hours = 0
var mins = 0
var secs = 0
var now = new Date()
var timeElement = document.getElementById(name + '-countdown-timer')
if (!timeElement) {
clearInterval(currentActiveTimer)
currentActiveTimer = null
return
}
var data = timeElement.tag
var time = data.time
var difference = (time - now)
if (difference <= 0) {
clearInterval(currentActiveTimer)
currentActiveTimer = null
timeElement.innerHTML = data.data.finished || ''
return
}
days = Math.floor(difference / (60 * 60 * 1000 * 24) * 1)
hours = Math.floor((difference % (60 * 60 * 1000 * 24)) / (60 * 60 * 1000) )
mins = Math.floor(((difference % (60 * 60 * 1000 * 24)) % (60 * 60 * 1000)) / (60 * 1000) * 1)
secs = Math.floor((((difference % (60 * 60 * 1000 * 24)) % (60 * 60 * 1000)) % (60 * 1000)) / 1000 * 1)
var text = pad(hours) + ':' + pad(mins) + ':' + pad(secs)
if (days > 0) {
text = days.toString() + ' dag' + (days > 1 && 'a' || '') + ' ' + text
}
timeElement.innerHTML = text
}
module.exports.init = function(data) {
var element = document.createElement('div') var element = document.createElement('div')
element.innerHTML = data.html element.innerHTML = data.html
element.id = data.graphic.name element.id = data.graphic.name
@ -18,50 +61,19 @@ module.exports = function(data) {
element.classList.add('root-element-display') element.classList.add('root-element-display')
}, 100) }, 100)
module.exports.update(data)
}
module.exports.update = function(data) {
var timeElement = document.getElementById(data.graphic.name + '-countdown-timer') var timeElement = document.getElementById(data.graphic.name + '-countdown-timer')
var time = new Date(data.data.countdown.replace(' ', 'T')) data.time = new Date(data.data.countdown.replace(' ', 'T'))
function pad(n) { return (n < 10) ? ('0' + n) : n }
function timer() {
var days = 0
var hours = 0
var mins = 0
var secs = 0
now = new Date()
difference = (time - now)
timeElement = document.getElementById(data.graphic.name + '-countdown-timer')
if (difference < 0 || !timeElement) {
clearInterval(data.timer)
if (timeElement) {
timeElement.innerHTML = data.data.finished || ''
}
return
}
if (timeElement.tag !== time) {
clearInterval(data.timer)
return
}
days = Math.floor(difference / (60 * 60 * 1000 * 24) * 1);
hours = Math.floor((difference % (60 * 60 * 1000 * 24)) / (60 * 60 * 1000) );
mins = Math.floor(((difference % (60 * 60 * 1000 * 24)) % (60 * 60 * 1000)) / (60 * 1000) * 1);
secs = Math.floor((((difference % (60 * 60 * 1000 * 24)) % (60 * 60 * 1000)) % (60 * 1000)) / 1000 * 1);
var text = pad(hours) + ':' + pad(mins) + ':' + pad(secs);
if (days > 0) {
text = days.toString() + ' dag' + (days > 1 && 'a' || '') + ' ' + text;
}
timeElement.innerHTML = text
}
if (timeElement) { if (timeElement) {
timeElement.tag = time timeElement.tag = data
timer() timer(data.graphic.name)
data.timer = setInterval(timer, 1000) if (currentActiveTimer) {
clearInterval(currentActiveTimer)
}
currentActiveTimer = setInterval(timer.bind(null, data.graphic.name), 1000)
} }
} }

View file

@ -6,23 +6,21 @@ var engines = {
schedule: require('./schedule'), schedule: require('./schedule'),
} }
var current = []
function display(data) { function display(data) {
var exists = document.getElementById(data.graphic.name) var exists = document.getElementById(data.graphic.name)
if (exists) {
exists.tag.remove()
exists.remove()
current.splice(current.indexOf(data.graphic.name), 1)
}
current.push(data.graphic.name)
var engine = data.graphic.engine var engine = data.graphic.engine
if (exists) {
exists.innerHtml = data.html
exists.tag.innerHtml = data.css
engines[engine].update(data)
return
}
if (engines[engine]) { if (engines[engine]) {
engines[engine](data) engines[engine].init(data)
} }
} }
@ -32,8 +30,6 @@ socket.on('client.hide', function (data) {
var exists = document.getElementById(data.name) var exists = document.getElementById(data.name)
if (exists) { if (exists) {
current.splice(current.indexOf(data.name), 1)
exists.classList.remove('root-element-display') exists.classList.remove('root-element-display')
window.setTimeout(function () { window.setTimeout(function () {

View file

@ -1,5 +1,5 @@
module.exports = function(data) { module.exports.init = function(data) {
var element = document.createElement('div') var element = document.createElement('div')
element.innerHTML = data.html element.innerHTML = data.html
element.id = data.graphic.name element.id = data.graphic.name
@ -18,3 +18,7 @@ module.exports = function(data) {
element.classList.add('root-element-display') element.classList.add('root-element-display')
}, 100) }, 100)
} }
module.exports.update = function() {
}

View file

@ -1,5 +1,5 @@
module.exports = function(data) { module.exports.init = function(data) {
var element = document.createElement('div') var element = document.createElement('div')
element.innerHTML = data.html element.innerHTML = data.html
element.id = data.graphic.name element.id = data.graphic.name
@ -18,3 +18,7 @@ module.exports = function(data) {
element.classList.add('root-element-display') element.classList.add('root-element-display')
}, 100) }, 100)
} }
module.exports.update = function() {
}

View file

@ -1,52 +0,0 @@
const m = require('mithril')
const createModule = require('./module')
const components = require('./components')
const socket = require('../socket')
const Add = createModule({
init: function() {
this.monitor('engines', 'engine.all', [])
this.graphic = { }
},
updated: function(name, control) {
this.graphic[name] = control.target.value
},
create: function() {
if (!Add.vm.graphic.engine) {
Add.vm.graphic.engine = Add.vm.engines[0]
}
if (!Add.vm.graphic.name) {
this.error = 'Name cannot be empty'
return
}
socket.emit('graphic.create', Add.vm.graphic)
},
}, function(ctrl) {
return m('div', [
m('h3.container-header', 'Add graphics'),
m('div.container-panel.panel-add', [
components.error(Add.vm.error),
m('label', [
'Name',
m('input[type=text]', {
oninput: Add.vm.updated.bind(Add.vm, 'name'),
})
]),
m('label', [
'Engine',
m('select', {
onchange: Add.vm.updated.bind(Add.vm, 'engine'),
}, Add.vm.engines.map(engine =>
m('option', { key: engine, value: engine }, engine)
))
]),
m('a.button', {
onclick: Add.vm.create.bind(Add.vm)
}, 'Create'),
]),
])
})
module.exports = Add

57
app/main/add/module.js Normal file
View file

@ -0,0 +1,57 @@
const m = require('mithril')
const createModule = require('../common/module')
const components = require('../common/components')
const socket = require('../../socket')
const store = require('../store')
const Add = createModule({
init: function() {
this.monitor('engines', 'engine.all', [])
store.listen('graphic.single', data => {
if (data.name === this.graphic.name) {
m.route.set(`/graphic/${data.id}`)
}
})
this.graphic = { }
},
updated: function(name, control) {
this.graphic[name] = control.target.value
},
create: function() {
if (!this.graphic.engine) {
this.graphic.engine = this.engines[0]
}
if (!this.graphic.name) {
this.error = 'Name cannot be empty'
return
}
socket.emit('graphic.create', this.graphic)
},
removing: function() {
store.unlisten('graphic.single')
},
}, function() {
return [
m('h4.header', 'Add graphic'),
components.error(this.error),
m('label', { for: 'create-name' }, 'Name'),
m('input#create-name[type=text]', {
oninput: (control) => this.updated('name', control),
}),
m('label', { for: 'create-engine' }, 'Engine'),
m('select', {
onchange: (control) => this.updated('engine', control),
}, this.engines.map(engine =>
m('option', { key: engine, value: engine }, engine)
)),
m('input[type=submit]', {
value: 'Create',
onclick: () => this.create(),
}),
]
})
module.exports = Add

View file

@ -0,0 +1,60 @@
const m = require('mithril')
exports.error = function(error) {
if (!error) return null
return m('div.error-box', error)
}
exports.presetOnlyList = function(module, graphic, title, color = 'green', button = 'Display now', schedule = 'Schedule') {
return [
m('label.graphic-label', { key: 'first' }, title),
m('div.graphic-presetlist', {
key: `second-${graphic.id}`,
oncreate: control => module.presetlistInit(control),
},
module.presets.map(item =>
m('div.graphic-preset', {
key: `preset-${graphic.id}-${item.id}`,
data: item.id,
}, [
m('div.graphic-preset-reorder'),
m('input[type=text]', {
readonly: true,
value: module.mainTemplate(item.values),
}),
schedule && m(`button`, {
onclick: module.schedulePreset.bind(module, item),
}, schedule) || null,
m(`button.${color}`, {
onclick: module.displayPreset.bind(module, item),
}, button),
module.displayRemove && m('button.red', {
onclick: module.removePreset.bind(module, item),
}, 'Remove') || null,
]),
),
),
module.presets.length &&
m('button.red.graphic-presetremove', {
key: 'third',
onclick: () => (module.displayRemove = !module.displayRemove),
}, 'Remove entries') || null,
]
}
exports.presetButtons = function(module, green, blue) {
return [
m('div.graphic-presetadd-buttons', [
green && m('button.green', {
onclick: module.displayCurrent.bind(module),
}, green) || null,
blue && m('button', {
onclick: module.addPreset.bind(module),
}, blue) || null,
m('button', {
onclick: module.scheduleCurrent.bind(module),
}, 'Add to schedule'),
]),
]
}

98
app/main/common/module.js Normal file
View file

@ -0,0 +1,98 @@
const m = require('mithril')
const _ = require('lodash')
const store = require('../store')
const socket = require('../../socket')
const dragula = require('dragula')
function createModule(component, view) {
let newModule = { }
newModule = _.defaults(component, {
oninit: function(vnode) {
this.error = null
this.listening = []
this.init(vnode)
},
_storeUpdated: function(key, name, id, cb) {
this[key] = store.get(name, id)
if (cb) cb(store.get(name, id))
m.redraw()
},
init: function() { },
removing: function() { },
monitor: function(key, name, fallback, id, cb) {
this[key] = store.get(name, id) || fallback || { }
this.listening.push(store.getId(name, id))
store.listen(name, this._storeUpdated.bind(this, key, name, id, cb), id)
socket.emit(name, { id: id })
},
unmonitor: function(name, id) {
store.unlisten(store.getId(name, id))
this.listening.splice(this.listening.indexOf(store.getId(name, id)), 1)
},
initDragula: function(control, cb) {
let dragContainer = document.getElementById('dragcontainer')
let out = dragula([control.dom], {
mirrorContainer: dragContainer,
invalid: el => el.className !== 'graphic-preset-reorder'
&& el.className !== 'graphic-preset',
})
out.on('dragend', () => {
if (is_touch_device()) {
document.body.style.cssText = ''
window.scroll(0, document.body.data)
}
})
out.on('drag', () => {
if (is_touch_device()) {
document.body.data = window.scrollY
document.body.style.cssText = `position: fixed; left: 0; right: 0; overflow: hidden; top: -${window.scrollY}px;`
dragContainer.style.marginTop = `${document.body.data}px`
}
})
out.on('drop', (a, b, c, d) => {
cb(a, d)
})
},
onremove: function() {
this.listening.forEach((item) => {
store.unlisten(item)
})
this.removing()
},
view: view,
})
return newModule
}
// https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886
// LOL
function is_touch_device() {
var prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
var mq = function(query) {
return window.matchMedia(query).matches
}
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
return true
}
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
// https://git.io/vznFH
var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('')
return mq(query)
}
module.exports = createModule

View file

@ -1,80 +0,0 @@
const m = require('mithril')
exports.error = function(error) {
if (!error) return null
return m('div.error-box', error)
}
exports.presetOnlyList = function(vm) {
return [
m('label', 'Presets'),
m('ul.panel-graphic-preset', vm.presets.map((item, index) =>
m('li', { key: index }, [
m('.row', { key: index }, [
m('div', { class: 'small-8 columns panel-graphic-property-item' },
m('input[type=text]', {
readonly: true,
value: item.values[graphic.settings.main],
})
),
m('div', { class: 'small-4 columns' },
[
m('a.panel-graphic-preset-remove.button.success', {
onclick: vm.displayPreset.bind(vm, item),
}, 'Display'),
m('a.panel-graphic-preset-remove.button.alert', {
onclick: vm.removePreset.bind(vm, item),
}, 'X')
]
),
])
])
))
]
}
exports.presetButtons = function(vm) {
return [
m('a.panel-graphic-preset-add.button', {
onclick: vm.addPreset.bind(vm),
}, 'Save Preset'),
m('a.panel-graphic-display.success.button', {
onclick: vm.displayCurrent.bind(vm),
}, 'Display Live'),
]
}
exports.presetList = function(vm) {
return [
m('a.panel-graphic-preset-add.button', {
onclick: vm.addPreset.bind(vm),
}, 'Save Preset'),
m('a.panel-graphic-display.success.button', {
onclick: vm.displayCurrent.bind(vm),
}, 'Display Live'),
m('label', 'Presets'),
m('ul.panel-graphic-preset', vm.presets.map((item, index) =>
m('li', { key: index }, [
m('.row', { key: index }, [
m('div', { class: 'small-8 columns panel-graphic-property-item' },
m('input[type=text]', {
readonly: true,
value: item.values[graphic.settings.main],
})
),
m('div', { class: 'small-4 columns' },
[
m('a.panel-graphic-preset-remove.button.success', {
onclick: vm.displayPreset.bind(vm, item),
}, 'Display'),
m('a.panel-graphic-preset-remove.button.alert', {
onclick: vm.removePreset.bind(vm, item),
}, 'X')
]
),
])
])
))
]
}

View file

@ -1,51 +0,0 @@
const m = require('mithril')
const _ = require('lodash')
const store = require('./store')
const socket = require('../socket')
function createModule(vm, view) {
let newModule = { }
let listening = []
newModule.vm = _.defaults(vm, {
_init: function() {
this.error = null
newModule.vm.init()
},
_storeUpdated: function(key, name, id) {
this[key] = store.get(name, id)
m.redraw()
},
init: function() { },
monitor: function(key, name, fallback, id) {
this[key] = store.get(name, id) || fallback || { }
listening.push(name)
store.listen(name, this._storeUpdated.bind(this, key, name, id), id)
socket.emit(name, { id: id })
},
onunload: function() {
listening.forEach((item) => {
store.unlisten(item)
})
},
})
newModule.controller = function() {
newModule.vm._init()
this.onunload = newModule.vm.onunload
}
newModule.view = view
return newModule
}
module.exports = createModule

View file

@ -0,0 +1,83 @@
const _ = require('lodash')
const m = require('mithril')
const createModule = require('../common/module')
const socket = require('../../socket')
const Dagskra = createModule({
init: function() {
this.error = ''
this.displayRemove = false
this.monitor('schedule', 'schedule.all', [])
},
scheduleListInit: function(control) {
this.initDragula(control, (source, target) => {
let dragOldIndex = _.findIndex(this.schedule, { id: Number(source.getAttribute('data')) })
let targetOldIndex = this.schedule.length - 1
if (target) {
targetOldIndex = _.findIndex(this.schedule, { id: Number(target.getAttribute('data')) })
}
this.schedule.splice(targetOldIndex, 0, this.schedule.splice(dragOldIndex, 1)[0])
this.schedule.forEach((item, i) => {
item.sort = i + 1
})
socket.emit('schedule.patch', this.schedule)
})
},
displaySchedule: function(item) {
socket.emit('content.display', {
graphic: item.graphic,
data: item.values,
})
},
removeSchedule: function(item) {
socket.emit('schedule.remove', item)
},
}, function() {
this.schedule.forEach(item => {
if (!item.cachedDisplay) {
try {
item.cachedDisplay = _.template(item.graphic.settings.main || '')(item.values)
} catch (e) {
item.cachedDisplay = `ERROR WITH TEMPLATE: ${e.message}`
}
item.cachedDisplay = `[${item.graphic.name}] ${item.cachedDisplay}`
}
})
return [
m('h4.header', 'Schedule'),
m('div.graphic-presetlist', {
oncreate: control => this.scheduleListInit(control),
},
this.schedule.map(item =>
m('div.graphic-preset', {
key: `preset-${item.id}`,
data: item.id,
}, [
m('div.graphic-preset-reorder'),
m('input[type=text]', {
readonly: true,
value: item.cachedDisplay,
}),
m(`button.green`, {
onclick: () => this.displaySchedule(item),
}, 'Display'),
this.displayRemove && m('button.red', {
onclick: () => this.removeSchedule(item),
}, 'Remove') || null,
]),
),
),
this.schedule.length
&& m('button.red.graphic-presetremove', {
onclick: () => (this.displayRemove = !this.displayRemove),
}, 'Remove entries')
|| m('div.schedule-empty', 'Schedule is empty'),
]
})
module.exports = Dagskra

View file

@ -1,158 +0,0 @@
const _ = require('lodash')
const m = require('mithril')
const createModule = require('../module')
const socket = require('../../socket')
const Graphic = createModule({
init: function() {
this.monitor('graphic', 'graphic.single', {}, m.route.param('id'))
this.monitor('presets', 'preset.all', [], m.route.param('id'))
this.currentView = 'view'
this.current = {}
this.newProperty = m.prop('')
this.newTextField = m.prop('')
},
updated: function(name, variable, control) {
if (!control) {
control = variable
variable = 'graphic'
}
_.set(this[variable], name, control.target.value)
if (variable === 'graphic') {
socket.emit('graphic.update', this.graphic)
}
},
addDataField: function(type, name) {
if (!name) {
return 'Please type in proper name'
}
if (this.graphic.settings[type].includes(name)) {
return 'A property with that name already exists'
}
this.graphic.settings[type].push(name)
socket.emit('graphic.update', this.graphic)
return null
},
addProperty: function() {
this.error = this.addDataField('properties', this.newProperty())
if (!this.error) {
this.newProperty('')
if (!this.graphic.settings.main) {
this.graphic.settings.main = this.graphic.settings.properties[0]
socket.emit('graphic.update', this.graphic)
}
}
},
addTextField: function() {
this.error = this.addDataField('textfields', this.newTextField())
if (!this.error) {
this.newTextField('')
}
},
removeDataField: function(type, name) {
this.graphic.settings[type].splice(
this.graphic.settings[type].indexOf(name), 1)
socket.emit('graphic.update', this.graphic)
},
removeProperty: function(prop) {
this.removeDataField('properties', prop)
},
cleanCurrent: function() {
if (this.graphic.engine === 'countdown') {
this.current.text = this.graphic.settings.text
this.current.countdown = this.graphic.settings.countdown
this.current.finished = this.graphic.settings.finished
if (!this.current.countdown) {
this.error = 'Count to had to be defined'
}
else {
let test = new Date(this.current.countdown.replace(' ', 'T'))
if (!test.getTime()) {
this.error = 'Count to has to be valid date and time'
}
}
} else {
this.graphic.settings.properties.forEach(prop => {
if (!this.current[prop]) {
this.current[prop] = ''
}
})
}
if (this.graphic.settings.main &&
!this.current[this.graphic.settings.main]) {
this.error = `Property "${this.graphic.settings.main}" cannot be empty`
return
}
},
addPreset: function() {
this.error = ''
this.cleanCurrent()
if (this.error) return
socket.emit('preset.add', {
graphic_id: this.graphic.id,
values: this.current,
})
},
removePreset: function(preset) {
socket.emit('preset.remove', preset)
},
remove: function() {
socket.emit('graphic.remove', this.graphic)
m.route('/')
},
displayPreset: function(preset) {
socket.emit('content.display', {
graphic: this.graphic,
data: preset.values,
})
},
displayCurrent: function() {
this.error = ''
this.cleanCurrent()
if (this.error) return
socket.emit('content.display', {
graphic: this.graphic,
data: this.current,
})
},
switchView: function() {
if (Graphic.vm.currentView === 'view') {
Graphic.vm.currentView = 'settings'
return
}
Graphic.vm.currentView = 'view'
},
})
module.exports = Graphic
require('./view')

View file

@ -1,65 +1,82 @@
const m = require('mithril') const m = require('mithril')
const components = require('../../components') const components = require('../../common/components')
exports.view = function(ctlr, graphic, vm) { exports.view = function(module, graphic) {
return [ return [
m('label', [ m('div.graphic-presetadd', [
'Text', m('h3.graphic-presetadd-header', 'Start countdown'),
m('input[type=text]', {
value: vm.graphic.settings.text || '', m('label', { for: `countdown-text` }, 'Text'),
oninput: vm.updated.bind(vm, 'settings.text'), m(`input#countdown-text[type=text]`, {
value: graphic.settings.text || '',
oninput: module.updated.bind(module, 'settings.text'),
}), }),
]),
m('label', [ m('label', { for: `countdown-countdown` }, 'Count to (format: "YYYY-MM-DD hh:mm")'),
'Count to (format: "YYYY-MM-DD hh:mm")', m(`input#countdown-countdown[type=text]`, {
m('input[type=text]', { value: graphic.settings.countdown || '',
value: vm.graphic.settings.countdown || '', oninput: module.updated.bind(module, 'settings.countdown'),
oninput: vm.updated.bind(vm, 'settings.countdown'),
}), }),
]),
m('label', [ m('label', { for: `countdown-finished` }, 'Finished (gets displayed in the countdown upon reaching 0)'),
'Finished (gets displayed in the countdown upon reaching 0)', m(`input#countdown-finished[type=text]`, {
m('input[type=text]', { value: graphic.settings.finished || '',
value: vm.graphic.settings.finished || '', oninput: module.updated.bind(module, 'settings.finished'),
oninput: vm.updated.bind(vm, 'settings.finished'),
}), }),
components.presetButtons(module, 'Display live now', 'Add to template'),
]), ]),
components.presetList(vm), components.presetOnlyList(module, graphic, 'Templates', '', 'Fill top', ''),
] ]
} }
exports.settings = function(cltr, graphic, vm) { exports.settings = function(module, graphic) {
return [ return [
m('label', [ // Name
'Name', m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input[type=text]', { m('input#graphic-name[type=text]', {
value: graphic.name, value: graphic.name,
oninput: vm.updated.bind(vm, 'name'), oninput: module.updated.bind(module, 'name'),
}), }),
// HTML
m('label.graphic-label', { for: 'graphic-html' }, [
'Graphic HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: <%- text %>)',
]), ]),
m('label', [ m('p.graphic-helper', `<div id="${graphic.name}">`),
'HTML (', m('textarea#graphic-html', {
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), rows: '4',
' available: <%- text %>', oninput: module.updated.bind(module, 'settings.html'),
')', value: graphic.settings.html || '',
m('p', `<div id="${graphic.name}">`), }),
m('textarea', { m('p.graphic-helper.bottom', `</div>`),
rows: '4',
oninput: vm.updated.bind(null, 'settings.html'), // CSS
value: graphic.settings.html || '', m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
}), m('p.graphic-helper', '<style type="text/css">'),
m('p', `</div>`), m('textarea#graphic-css', {
rows: '4',
oninput: module.updated.bind(module, 'settings.css'),
value: graphic.settings.css || '',
}),
m('p.graphic-helper.bottom', '</style>'),
// Main display template
m('label.graphic-label', { for: 'graphic-main' }, [
'Graphic control display template (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: <%- text %>, <%- countdown %>, <%- finished %>)',
]), ]),
m('label', [ m('input#graphic-main[type=text]', {
'CSS', value: graphic.settings.main,
m('textarea', { oninput: module.updated.bind(module, 'settings.main'),
rows: '4', }),
oninput: vm.updated.bind(null, 'settings.css'), components.error(module.mainTemplateError),
value: graphic.settings.css || '',
}) // Remove
]), m('button.red.graphic-delete', {
m('a.panel-graphic-delete.button.alert', { onclick: module.remove.bind(module),
onclick: vm.remove.bind(vm),
}, 'Delete graphic'), }, 'Delete graphic'),
] ]
} }

View file

@ -1,162 +1,121 @@
const m = require('mithril') const m = require('mithril')
const components = require('../../components') const components = require('../../common/components')
exports.view = function(ctlr, graphic, vm) { exports.view = function(module, graphic) {
if (!graphic.settings.properties) { if (!graphic.settings.properties) {
graphic.settings.properties = [] graphic.settings.properties = []
} }
if (!graphic.settings.textfields) {
graphic.settings.textfields = []
}
if (graphic.settings.properties.length === 0) { if (graphic.settings.properties.length === 0) {
return [ return [
m('p', 'No properties have been defined.'), m('p.settings-empty', `
m('p', 'Click settings to create and define properties to display.'), No properties have been defined.
This graphic needs properties to be defined before usage.
Click the settings button to define the properties for this graphic.
`),
m('button.settings-empty-button', {
onclick: () => module.switchView(),
}, module.changeViewTitle()),
] ]
} }
return [ return [
components.presetOnlyList(vm), m('div.graphic-presetadd', [
graphic.settings.properties.map((prop, index) => m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
m('label', { key: index }, [ graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
prop, m('label', { for: `preset-add-${index}` }, prop),
m('input[type=text]', { m(`input#preset-add-${index}[type=text]`, {
value: vm.current[prop] || '', value: module.current[prop] || '',
oninput: vm.updated.bind(vm, prop, 'current'), oninput: module.updated.bind(module, prop, 'current'),
}), }),
]) ])),
), components.presetButtons(module, 'Display live now', 'Add to preset list'),
graphic.settings.textfields.map((prop, index) => ]),
m('label', { key: index }, [ components.presetOnlyList(module, graphic, 'Presets'),
prop,
m('textarea', {
rows: '6',
oninput: vm.updated.bind(vm, prop, 'current'),
value: vm.current[prop] || '',
}),
])
),
components.presetButtons(vm),
] ]
} }
exports.settings = function(cltr, graphic, vm) { exports.settings = function(module, graphic) {
return [ return [
m('label', [ // Name
'Name', m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input[type=text]', { m('input#graphic-name[type=text]', {
value: graphic.name, value: graphic.name,
oninput: vm.updated.bind(vm, 'name'), oninput: module.updated.bind(module, 'name'),
}), }),
// HTML
m('label.graphic-label', { for: 'graphic-html' }, [
'Graphic HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]), ]),
m('label', [ m('p.graphic-helper', `<div id="${graphic.name}">`),
'HTML (', m('textarea#graphic-html', {
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), rows: '4',
' available: ', oninput: module.updated.bind(null, 'settings.html'),
graphic.settings.properties.map(prop => value: graphic.settings.html || '',
`<%- ${prop} %>` }),
).join(', '), m('p.graphic-helper.bottom', `</div>`),
', ',
graphic.settings.textfields.map(prop => // CSS
`<%- ${prop} %>` m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
).join(', '), m('p.graphic-helper', '<style type="text/css">'),
')', m('textarea#graphic-css', {
m('p', `<div id="${graphic.name}">`), rows: '4',
m('textarea', { oninput: module.updated.bind(null, 'settings.css'),
rows: '4', value: graphic.settings.css || '',
oninput: vm.updated.bind(null, 'settings.html'), }),
value: graphic.settings.html || '', m('p.graphic-helper.bottom', '</style>'),
}),
m('p', `</div>`), // Main display template
m('label.graphic-label', { for: 'graphic-main' }, [
'Graphic control display template (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]), ]),
m('label', [ m('input#graphic-main[type=text]', {
'CSS', value: graphic.settings.main,
m('textarea', { oninput: module.updated.bind(module, 'settings.main'),
rows: '4', }),
oninput: vm.updated.bind(null, 'settings.css'),
value: graphic.settings.css || '', // Property list
}) m('label.graphic-label', 'Properties'),
]), graphic.settings.properties.map((prop, index) =>
/* -------- Simple Properties -------- */ m('div.graphic-property', { key: `prop-${index}` }, [
m('label', 'Simple Properties'), m('input[type=text]', {
m('label', [ readonly: true,
'Main',
m('select', {
onchange: vm.updated.bind(vm, 'settings.main'),
}, graphic.settings.properties.map((prop, index) =>
m('option', {
key: 'prop-list-' + index,
value: prop, value: prop,
selected: prop === graphic.settings.main, }),
}, prop) m('button.red', {
)) onclick: module.removeProperty.bind(module, prop),
}, 'Remove'),
]),
),
graphic.settings.properties.length === 0 && m('p.graphic-empty', 'No properties exist yet.') || [],
// Add a new property
m('label.graphic-label', { for: 'graphic-newproperty' }, 'Add new graphic property'),
m('div.graphic-property', [
m('input#graphic-newproperty[type=text]', {
value: module.newProperty,
oninput: m.withAttr('value', val => (module.newProperty = val)),
}),
m('button', {
onclick: module.addProperty.bind(module),
}, 'Add'),
]), ]),
/* -------- Simple Properties List -------- */ components.error(module.mainTemplateError),
m('label', 'List'),
m('div', [ // Remove
graphic.settings.properties.map((prop, index) => m('button.red.graphic-delete', {
m('.row', { key: 'add-prop-' + index }, [ onclick: module.remove.bind(module),
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', {
readonly: true,
value: prop,
})
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-property-remove.button.alert', {
onclick: vm.removeProperty.bind(vm, prop),
}, 'Remove')
)
])
),
]),
m('.row', [
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', {
value: vm.newProperty(),
oninput: m.withAttr('value', vm.newProperty),
})
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-property-add.button', {
onclick: vm.addProperty.bind(vm),
}, 'Add')
),
]),
/* -------- Text Properties -------- */
m('label', 'Text Fields'),
m('div', [
graphic.settings.textfields.map((prop, index) =>
m('.row', { key: 'add-prop-' + index }, [
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', {
readonly: true,
value: prop,
})
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-property-remove.button.alert', {
onclick: vm.removeDataField.bind(vm, 'textfields', prop),
}, 'Remove')
)
])
),
]),
m('.row', [
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', {
value: vm.newTextField(),
oninput: m.withAttr('value', vm.newTextField),
})
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-property-add.button', {
onclick: vm.addTextField.bind(vm),
}, 'Add')
),
]),
/* -------- Delete -------- */
m('a.panel-graphic-delete.button.alert', {
onclick: vm.remove.bind(vm),
}, 'Delete graphic'), }, 'Delete graphic'),
] ]
} }

View file

@ -1,109 +1,121 @@
const m = require('mithril') const m = require('mithril')
const components = require('../../components') const components = require('../../common/components')
exports.view = function(ctlr, graphic, vm) { exports.view = function(module, graphic) {
if (!graphic.settings.properties) { if (!graphic.settings.properties) {
graphic.settings.properties = [] graphic.settings.properties = []
} }
if (graphic.settings.properties.length === 0) { if (graphic.settings.properties.length === 0) {
return [ return [
m('p', 'No properties have been defined.'), m('p.settings-empty', `
m('p', 'Click settings to create and define properties to display.'), No properties have been defined.
This graphic needs properties to be defined before usage.
Click the settings button to define the properties for this graphic.
`),
m('button.settings-empty-button', {
onclick: () => module.switchView(),
}, module.changeViewTitle()),
] ]
} }
return [ return [
components.presetOnlyList(vm), m('div.graphic-presetadd', [
graphic.settings.properties.map((prop, index) => m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
m('label', { key: index }, [ graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
prop, m('label', { for: `preset-add-${index}` }, prop),
m('input[type=text]', { m(`input#preset-add-${index}[type=text]`, {
value: vm.current[prop] || '', value: module.current[prop] || '',
oninput: vm.updated.bind(vm, prop, 'current'), oninput: module.updated.bind(module, prop, 'current'),
}), }),
]) ])),
), components.presetButtons(module, 'Display live now', 'Add to preset list'),
components.presetButtons(vm), ]),
components.presetOnlyList(module, graphic, 'Presets'),
] ]
} }
exports.settings = function(cltr, graphic, vm) { exports.settings = function(module, graphic) {
return [ return [
m('label', [ // Name
'Name', m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input[type=text]', { m('input#graphic-name[type=text]', {
value: graphic.name, value: graphic.name,
oninput: vm.updated.bind(vm, 'name'), oninput: module.updated.bind(module, 'name'),
}), }),
// HTML
m('label.graphic-label', { for: 'graphic-html' }, [
'Graphic HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]), ]),
m('label', [ m('p.graphic-helper', `<div id="${graphic.name}">`),
'HTML (', m('textarea#graphic-html', {
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), rows: '4',
' available: ', oninput: module.updated.bind(null, 'settings.html'),
graphic.settings.properties.map(prop => value: graphic.settings.html || '',
`<%- ${prop} %>` }),
).join(', '), m('p.graphic-helper.bottom', `</div>`),
')',
m('p', `<div id="${graphic.name}">`), // CSS
m('textarea', { m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
rows: '4', m('p.graphic-helper', '<style type="text/css">'),
oninput: vm.updated.bind(null, 'settings.html'), m('textarea#graphic-css', {
value: graphic.settings.html || '', rows: '4',
}), oninput: module.updated.bind(null, 'settings.css'),
m('p', `</div>`), value: graphic.settings.css || '',
}),
m('p.graphic-helper.bottom', '</style>'),
// Main display template
m('label.graphic-label', { for: 'graphic-main' }, [
'Graphic control display template (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]), ]),
m('label', [ m('input#graphic-main[type=text]', {
'CSS', value: graphic.settings.main,
m('textarea', { oninput: module.updated.bind(module, 'settings.main'),
rows: '4', }),
oninput: vm.updated.bind(null, 'settings.css'),
value: graphic.settings.css || '', // Property list
}) m('label.graphic-label', 'Properties'),
]), graphic.settings.properties.map((prop, index) =>
m('label', [ m('div.graphic-property', { key: `prop-${index}` }, [
'Main property',
m('select', {
onchange: vm.updated.bind(vm, 'settings.main'),
}, graphic.settings.properties.map((prop, index) =>
m('option', {
key: 'prop-list-' + index,
value: prop,
selected: prop === graphic.settings.main,
}, prop)
))
]),
m('label', 'Properties'),
m('div', [
graphic.settings.properties.map((prop, index) =>
m('.row', { key: 'add-prop-' + index }, [
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', {
readonly: true,
value: prop,
})
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-property-remove.button.alert', {
onclick: vm.removeProperty.bind(vm, prop),
}, 'Remove')
)
])
),
]),
m('.row', [
m('div', { class: 'small-10 columns panel-graphic-property-item' },
m('input[type=text]', { m('input[type=text]', {
value: vm.newProperty(), readonly: true,
oninput: m.withAttr('value', vm.newProperty), value: prop,
}) }),
), m('button.red', {
m('div', { class: 'small-2 columns' }, onclick: module.removeProperty.bind(module, prop),
m('a.panel-graphic-property-add.button', { }, 'Remove'),
onclick: vm.addProperty.bind(vm), ]),
}, 'Add') ),
), graphic.settings.properties.length === 0 && m('p.graphic-empty', 'No properties exist yet.') || [],
// Add a new property
m('label.graphic-label', { for: 'graphic-newproperty' }, 'Add new graphic property'),
m('div.graphic-property', [
m('input#graphic-newproperty[type=text]', {
value: module.newProperty,
oninput: m.withAttr('value', val => (module.newProperty = val)),
}),
m('button', {
onclick: module.addProperty.bind(module),
}, 'Add'),
]), ]),
m('a.panel-graphic-delete.button.alert', { components.error(module.mainTemplateError),
onclick: vm.remove.bind(vm),
// Remove
m('button.red.graphic-delete', {
onclick: module.remove.bind(module),
}, 'Delete graphic'), }, 'Delete graphic'),
] ]
} }

260
app/main/graphic/module.js Normal file
View file

@ -0,0 +1,260 @@
const _ = require('lodash')
const m = require('mithril')
const createModule = require('../common/module')
const socket = require('../../socket')
const view = require('./view')
const dragula = require('dragula')
const Graphic = createModule({
init: function(vnode) {
this.fetchData(vnode)
},
onupdate: function(vnode) {
this.fetchData(vnode)
},
fetchData: function(vnode) {
if (this.currentId === vnode.attrs.id) return
if (this.currentId && this.currentId !== vnode.attrs.id) {
this.unmonitor('graphic.single', this.currentId)
this.unmonitor('preset.all', this.currentId)
}
this.currentId = vnode.attrs.id
this.monitor(
'graphic',
'graphic.single',
{},
vnode.attrs.id,
() => this.recheckTemplate()
)
this.monitor('presets', 'preset.all', [], vnode.attrs.id)
this.currentView = 'view'
this.current = {}
this.displayRemove = false
this.newProperty = ''
this.newTextField = ''
this.mainTemplateString = ''
this.mainTemplateError = ''
this.mainTemplate = _.template('')
},
recheckTemplate: function() {
if (this.graphic.settings.main !== this.mainTemplateString) {
this.mainTemplateError = ''
this.mainTemplateString = this.graphic.settings.main
try {
this.mainTemplate = _.template(this.mainTemplateString)
} catch (e) {
this.mainTemplateError = `Invalid template: ${e.message}`
}
}
},
updated: function(name, variable, cont) {
let target = variable
let control = cont
if (!control) {
control = variable
target = 'graphic'
}
_.set(this[target], name, control.target.value)
this.recheckTemplate()
if (target === 'graphic') {
socket.emit('graphic.update', this.graphic)
}
},
addDataField: function(type, name) {
if (!name) {
return 'Please type in proper name'
}
if (this.graphic.settings[type].includes(name)) {
return 'A property with that name already exists'
}
this.graphic.settings[type].push(name)
socket.emit('graphic.update', this.graphic)
return null
},
addProperty: function() {
this.error = this.addDataField('properties', this.newProperty)
if (!this.error) {
this.newProperty = ''
if (!this.graphic.settings.main) {
this.graphic.settings.main = `<%- ${ this.graphic.settings.properties[0] } %>`
this.recheckTemplate()
socket.emit('graphic.update', this.graphic)
}
}
},
addTextField: function() {
this.error = this.addDataField('textfields', this.newTextField)
if (!this.error) {
this.newTextField = ''
}
},
removeDataField: function(type, name) {
this.graphic.settings[type].splice(
this.graphic.settings[type].indexOf(name), 1)
if (type === 'properties' && this.graphic.settings.properties.length === 0) {
this.graphic.settings.main = ''
this.recheckTemplate()
}
socket.emit('graphic.update', this.graphic)
},
removeProperty: function(prop) {
this.removeDataField('properties', prop)
},
presetlistInit: function(control) {
this.initDragula(control, (source, target) => {
let dragOldIndex = _.findIndex(this.presets, { id: Number(source.getAttribute('data')) })
let targetOldIndex = this.presets.length - 1
if (target) {
targetOldIndex = _.findIndex(this.presets, { id: Number(target.getAttribute('data')) })
}
this.presets.splice(targetOldIndex, 0, this.presets.splice(dragOldIndex, 1)[0])
this.presets.forEach((item, i) => {
item.sort = i + 1
})
socket.emit('preset.patch', this.presets)
})
},
cleanCurrent: function() {
if (this.graphic.engine === 'countdown') {
this.current.text = this.graphic.settings.text
this.current.countdown = this.graphic.settings.countdown
this.current.finished = this.graphic.settings.finished
if (!this.current.countdown) {
this.error = '"Count to" needs to be defined'
} else {
let test = new Date(this.current.countdown.replace(' ', 'T'))
if (!test.getTime()) {
this.error = '"Count to" has to be valid date and time'
}
}
} else {
this.graphic.settings.properties.forEach(prop => {
if (!this.current[prop]) {
this.current[prop] = ''
}
})
}
},
addPreset: function() {
this.error = ''
this.cleanCurrent()
if (this.error) return
if (this.graphic.engine === 'countdown') {
this.current.countdown = null
}
socket.emit('preset.add', {
graphic_id: this.graphic.id,
values: this.current,
})
},
removePreset: function(preset) {
socket.emit('preset.remove', preset)
},
remove: function() {
socket.emit('graphic.remove', this.graphic)
m.route.set('/')
},
displayPreset: function(preset) {
if (this.graphic.engine === 'countdown') {
this.graphic.settings.text = preset.values.text
this.graphic.settings.finished = preset.values.finished
socket.emit('graphic.update', this.graphic)
return
}
socket.emit('content.display', {
graphic: this.graphic,
data: preset.values,
})
},
schedulePreset: function(preset) {
socket.emit('schedule.add', {
graphic_id: this.graphic.id,
values: preset.values,
})
},
scheduleCurrent: function() {
this.error = ''
this.cleanCurrent()
if (this.error) return
socket.emit('schedule.add', {
graphic_id: this.graphic.id,
values: this.current,
})
},
displayCurrent: function() {
this.error = ''
this.cleanCurrent()
if (this.error) return
socket.emit('content.display', {
graphic: this.graphic,
data: this.current,
})
},
switchView: function() {
if (this.currentView === 'view') {
this.currentView = 'settings'
} else {
this.currentView = 'view'
}
},
changeViewTitle: function() {
if (this.currentView === 'view') {
return 'Settings'
}
return 'Control'
},
}, view)
module.exports = Graphic

View file

@ -1,6 +1,5 @@
const m = require('mithril') const m = require('mithril')
const Graphic = require('./controller') const components = require('../common/components')
const components = require('../components')
const engines = { const engines = {
text: require('./engine/text'), text: require('./engine/text'),
@ -8,21 +7,20 @@ const engines = {
schedule: require('./engine/schedule'), schedule: require('./engine/schedule'),
} }
Graphic.view = function(ctrl) { module.exports = function() {
graphic = Graphic.vm.graphic let graphic = this.graphic
let currentView = graphic.engine && engines[graphic.engine][this.currentView] || null
return m('div', [ return [
m('h3.container-header', 'Graphic'), m('h4.header', 'Graphic'),
m('div.container-panel.panel-graphic', m('header', [
!graphic.name && m('p', 'Loading...') || m('h3', graphic.name),
[ m('button', {
m('a.panel-graphic-settings.button', { onclick: () => this.switchView(),
onclick: Graphic.vm.switchView }, this.changeViewTitle()),
}, Graphic.vm.currentView === 'view' && 'Settings' || 'Control'), ]),
m('h4', graphic.name), components.error(this.error),
components.error(Graphic.vm.error), !currentView && m('p', 'Loading...')
engines[graphic.engine][Graphic.vm.currentView](ctrl, graphic, Graphic.vm), || currentView(this, graphic),
] ]
),
])
} }

View file

@ -1,10 +1,22 @@
const m = require('mithril') const m = require('mithril')
const createModule = require('./module') const createModule = require('./common/module')
const socket = require('../socket') const socket = require('../socket')
const Header = createModule({ const Header = createModule({
init: function() { init: function() {
this.monitor('list', 'content.list', []) this.currentLength = 0
this.updateMargin = false
this.connected = socket.connected
this.monitor('list', 'content.list', [], null, () => this.checkChanged())
socket.on('connect', () => {
this.connected = true
m.redraw()
})
socket.on('disconnect', () => {
this.connected = false
m.redraw()
})
}, },
hide: function(item) { hide: function(item) {
@ -12,19 +24,38 @@ const Header = createModule({
name: item.name, name: item.name,
}) })
}, },
}, function(ctrl) {
return m('div.header', Header.vm.list.length > 0 && [ onupdate: function() {
m('h3.container-header', 'Currently active'), if (!this.updateMargin) return
m('ul.header-list', [ this.updateMargin = false
Header.vm.list.map((item, index) =>
m('li.header-item', { key: 'header-' + index, }, [ let header = document.getElementById('header')
m('a.header-item-hide.button.alert', { let container = document.getElementById('container')
onclick: Header.vm.hide.bind(Header.vm, item),
container.style.marginTop = `${ header.clientHeight - 1}px`
},
checkChanged: function() {
if (this.currentLength === this.list.length) return
this.currentLength = this.list.length
this.updateMargin = true
},
}, function() {
return [
this.list.length > 0 && [
m('h4', 'Active graphics'),
this.list.map(item =>
m('div.item', { key: `header-${item.id}` }, [
m('h3', `${item.name} - ${item.display}`),
m('button.red', {
onclick: () => this.hide(item),
}, 'Hide'), }, 'Hide'),
m('div.header-item-display', `${item.name} - ${item.display}`),
]) ])
), ),
]), ] || null,
] || '') !this.connected && m('div.disconnected', `
Lost connection with server, Attempting to reconnect
`) || null,
]
}) })
module.exports = Header module.exports = Header

View file

@ -19,14 +19,15 @@ const m = require('mithril')
const Header = require('./header') const Header = require('./header')
const Menu = require('./menu') const Menu = require('./menu')
const Add = require('./add') const Add = require('./add/module')
const Graphic = require('./graphic') const Graphic = require('./graphic/module')
const Dagskra = require('./dagskra/module')
m.mount(document.getElementById('header'), Header) m.mount(document.getElementById('header'), Header)
m.mount(document.getElementById('menu'), Menu) m.mount(document.getElementById('menu'), Menu)
m.route(document.getElementById('content'), '/', { m.route(document.getElementById('content'), '/', {
'/': {}, '/': Dagskra,
'/add': Add, '/add': Add,
'/graphic/:id': Graphic, '/graphic/:id': Graphic,
}); })

View file

@ -1,24 +1,70 @@
const m = require('mithril') const m = require('mithril')
const createModule = require('./module') const createModule = require('./common/module')
const socket = require('../socket')
const Menu = createModule({ const Menu = createModule({
init: function() { init: function() {
this.monitor('list', 'graphic.all', []) this.monitor('list', 'graphic.all', [])
} this.monitor('settings', 'settings.all', {})
}, function(ctrl) { this.monitor('schedule', 'schedule.total', { total: 0 })
return m('div', [ this.monitor('status', 'casparcg.status', {
m('h3.container-header', 'Graphics'), connected: false,
m('div.container-panel.menu', [ playing: false,
m('ul.menu-list', [ })
// m('a', { href: `/`, config: m.route }, 'Home'), this.newHost = ''
Menu.vm.list.map((item) => this.enableEdit = false
m('li.menu-item', [ },
m('a', { href: `/graphic/${item.id}`, config: m.route }, item.name),
]) setHost(value) {
) this.newHost = value
]), this.enableEdit = true
m('a.menu-item-add', { href: '/add', config: m.route }, 'Add graphic' ), },
]),
]) saveNewHost() {
socket.emit('settings.update', {
name: 'casparcg',
value: this.newHost,
})
this.newHost = ''
this.enableEdit = false
},
}, function() {
return [
m('a', {
href: '/',
oncreate: m.route.link,
class: m.route.get() === '/' && 'active' || '',
}, `Schedule (${this.schedule.total})` ),
m('h4.header.header--space', 'Graphics'),
this.list.map((item) =>
m('a', {
href: `/graphic/${item.id}`,
oncreate: m.route.link,
class: m.route.get() === `/graphic/${item.id}` && 'active' || '',
}, item.name)
),
m('h5.header.header--space', 'Other'),
m('a', {
href: '/add',
oncreate: m.route.link,
class: m.route.get() === '/add' && 'active' || '',
}, 'Add graphic' ),
m('h5.header.header--space', 'CasparCG Status'),
m('input[type=text]', {
placeholder: 'Host IP',
value: this.newHost || this.settings.casparcg || '',
oninput: control => this.setHost(control.target.value),
}),
this.enableEdit && m('button', {
onclick: () => this.saveNewHost(),
}, 'Connect'),
m('div.status', {
class: this.status.connected && 'green',
}, 'connected'),
m('div.status', {
class: this.status.playing && 'green',
}, 'playing'),
]
}) })
module.exports = Menu module.exports = Menu

View file

@ -5,6 +5,7 @@ const events = {}
// Listen on all events // Listen on all events
let onevent = socket.onevent let onevent = socket.onevent
let disconnected = false
socket.onevent = function(packet) { socket.onevent = function(packet) {
let args = packet.data || [] let args = packet.data || []
@ -13,6 +14,25 @@ socket.onevent = function(packet) {
onevent.call(this, packet) onevent.call(this, packet)
} }
socket.on('disconnect', () => {
disconnected = true
})
socket.on('connect', () => {
if (disconnected) {
Object.keys(events).forEach(event => {
let name = event
let id = null
if (event.indexOf(':') > 0) {
name = event.split(':')[0]
id = Number(event.split(':')[1])
}
socket.emit(name, { id: id })
})
}
disconnected = false
})
function genId(name, id) { function genId(name, id) {
if (id) { if (id) {
return `${name}:${id}` return `${name}:${id}`
@ -21,6 +41,10 @@ function genId(name, id) {
} }
const store = { const store = {
getId: function(name, id) {
return genId(name, id)
},
get: function(name, id) { get: function(name, id) {
return storage[genId(name, id)] return storage[genId(name, id)]
}, },
@ -39,18 +63,24 @@ const store = {
} }
socket.on('*', (event, data) => { socket.on('*', (event, data) => {
let name = genId(event, data && data.id) let id = data && data.id
let name = genId(event, id)
if (events[name]) { if (events[name]) {
storage[name] = data storage[name] = data
events[name]() events[name]()
} }
if (event.indexOf('single') >= 0) { if (event.indexOf('single') >= 0) {
let check = event.replace('single', 'all') let check = event.replace('single', 'all')
if (events[name]) { if (events[check]) {
let index = _.findIndex(storage[check], { id: data.id }) let index = _.findIndex(storage[check], { id: data.id })
if (index > -1) { if (index > -1) {
storage[check][index] = data storage[check][index] = data
events[name]() events[check]()
} else {
storage[check].push(data)
events[check]()
} }
} }
} }

106
app/status/index.js Normal file
View file

@ -0,0 +1,106 @@
const socket = require('../socket')
const m = require('mithril')
const Status = {
oninit: function() {
this.current = []
this.connected = socket.connected
this.status = {
connected: false,
playing: false,
}
socket.on('casparcg.status', data => {
this.status = data
m.redraw()
})
socket.on('content.list', data => {
this.current = data
m.redraw()
})
socket.on('connect', () => this.updateConnected(true))
socket.on('disconnect', () => this.updateConnected(false))
},
updateConnected: function(connected) {
this.connected = connected
m.redraw()
},
hide: function(item) {
socket.emit('content.hide', {
name: item.name,
})
},
view: function() {
return [
m('header', [
m('h2', 'Active graphics:'),
m('div.status', {
class: this.status.connected && 'green',
}, 'connected'),
m('div.status', {
class: this.status.playing && 'green',
}, 'playing'),
]),
this.current.map(item =>
m('div.item', { key: `header-${item.id}` }, [
m('h3', `${item.name} - ${item.display}`),
m('button', {
onclick: () => this.hide(item),
}, 'Hide'),
])
),
this.current.length === 0 && m('div.empty', 'No active graphics') || null,
!this.connected && m('div.disconnected', `
Lost connection with server, Attempting to reconnect
`) || null,
]
},
}
m.mount(document.getElementById('container'), Status)
// var engines = {
// text: require('./text'),
// countdown: require('./countdown'),
// schedule: require('./schedule'),
// }
// function display(data) {
// var exists = document.getElementById(data.graphic.name)
// var engine = data.graphic.engine
// if (exists) {
// exists.innerHtml = data.html
// exists.tag.innerHtml = data.css
// engines[engine].update(data)
// return
// }
// if (engines[engine]) {
// engines[engine].init(data)
// }
// }
// socket.on('client.display', display)
// socket.on('client.hide', function (data) {
// var exists = document.getElementById(data.name)
// if (exists) {
// exists.classList.remove('root-element-display')
// window.setTimeout(function () {
// exists.tag.remove()
// exists.remove()
// }, 1500)
// }
// })
// socket.on('client.reset', function(data) {
// data.forEach(display)
// })

View file

@ -1,156 +1,306 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input {
font-size: 16px;
-webkit-appearance: none;
border-radius: 0;
}
button {
outline: none;
border: none;
cursor: pointer;
}
a {
text-decoration: none;
}
body { body {
background: #3f3f41; background: #3f3f41;
color: #f1f1f1; color: #f1f1f1;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
} }
h4 { h4 {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
/* Components */
button {
border: none;
color: #f1f1f1;
background: #2199e8;
font-size: 0.6em;
height: 3em;
&.green {
background: #3adb78;
}
&.red {
background: #ec5840;
}
}
.error-box {
margin: 1rem 0rem 2rem 0;
padding: 1rem;
background: #FF0000;
color: white;
font-size: 0.7em;
line-height: 1em;
}
$header-size = 0.8em;
$header-color = #777777;
/* Container */ /* Container */
.container { .container {
padding: 1rem; display: flex;
} align-items: stretch;
flex-grow: 2;
.container-header {
font-size: 1.5rem;
margin-left: 1rem;
color: #777777;
}
.container-panel {
border: 1px solid #3f3f3f;
background: #2d2d30;
padding: 1rem;
border-radius: 5px;
} }
/* Header */ /* Header */
section.current {
.header-list { padding: 0 13px;
list-style-type: none; background: black;
margin: 0; position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 10;
h4 {
color: $header-color;
font-size: 0.7em;
padding: 0.2em;
margin: 0;
}
h3 {
font-size: 1em;
line-height: 2em;
color: #eb6e00;
flex-grow: 2;
height: 2em;
padding-right: 0.5em;
overflow: hidden;
word-break: break-all;
}
button {
width: 80px;
flex-shrink: 0;
}
.item {
display: flex;
margin-bottom: 5px;
}
} }
.header-item { .disconnected {
margin-bottom: 1rem; position: fixed;
} top: 0;
left: 0;
.header-item-hide { right: 0;
float: right; bottom: 0;
width: 5rem; background: rgba(0,0,0,0.8);
border-radius: 6px; color: white;
margin: 0; font-size: 1em;
} display: flex;
justify-content: center;
.header-item-display { align-items: center;
background: #070707;
color: #eb6e00;
border-radius: 6px;
padding: 0.5rem 1rem;
margin-right: 5.5rem;
} }
/* Menu */ /* Menu */
.menu-list { nav {
list-style-type: none; width: 200px;
margin: 0; flex-shrink: 0;
} display: flex;
flex-direction: column;
.menu a { align-items: stretch;
color: #007acc; padding: 10px;
display: block; background: #2d2d30;
border: 1px solid #2d2d30; text-align: center;
padding: 0.2rem 0.5rem;
}
.menu a:hover {
color: #f1f1f1;
border: 1px solid #007acc;
}
.menu-item-add {
margin-top: 3rem;
}
/* Add */
.panel-add {
padding: 2rem;
}
.panel-graphic-property-add,
.panel-graphic-property-remove {
width: 100%;
}
/* Graphic */
.panel-graphic-delete {
.header {
color: $header-color;
font-size: $header-size;
margin-bottom: 10px;
&--space {
margin-top: 2em;
}
}
a {
font-size: $header-size;
line-height: 2.6em;
display: block;
border: 4px solid #2d2d30;
background: #007acc;
color: white;
&.active {
background: transparent;
border: 4px solid #007acc;
}
&:hover {
border: 4px solid #007acc;
}
}
input[type=text] {
text-align: center;
}
.status {
padding: 5px 20px;
font-size: 0.8em;
color: $header-color;
text-align: left;
position: relative;
margin-left: 1.8em;
&::after {
position: absolute;
left: 0;
top: calc(50% - 5px);
content: '';
border: 6px solid #ec5840;
}
&.green::after {
border-color: #008000;
}
}
} }
/* Main */
.panel-graphic-settings { main {
float: right; display: flex;
margin-right: 1rem; flex-direction: column;
} align-items: stretch;
padding: 10px 1em;
.panel-graphic-property-item { flex-grow: 2;
padding-left: 0; width: 300px;
}
.header {
.panel-graphic-preset-add { color: $header-color;
margin-right: 1rem; font-size: $header-size;
} margin-bottom: 10px;
}
.panel-graphic-preset {
margin-top: 1rem;
list-style-type: none;
}
.panel-graphic-preset a {
width: 100%;
}
/* Components */
.error-box {
margin: 0rem 0rem 2rem 0;
color: #FF0000;
} }
/* Inputs */ /* Inputs */
.panel-graphic-property-item input {
display: inline-block;
}
label { label {
margin-top: 0.6em;
color: #f1f1f1; color: #f1f1f1;
font-size: 0.7em;
& a,
& a:hover,
& a:visited {
color: #aaa;
text-decoration: underline;
}
} }
input[type="text"], input[type='text'],
textarea { textarea,
select {
font-size: 0.6em;
padding: 0.5em;
margin: 0.5em 0;
background: #333337; background: #333337;
border-color: #3f3f3f; border: 1px solid #2d2d30;
color: #999999; color: #999999;
transition-property: none !important; transition-property: none !important;
outline: none;
&:hover {
color: #f1f1f1;
border-color: #007acc;
}
&:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
} }
input[type="text"]:hover, input[type=submit] {
textarea:hover { margin-top: 0.6em;
border: none;
color: #f1f1f1; color: #f1f1f1;
border-color: #007acc; background: #2199e8;
} font-size: 0.6em;
line-height: 3em;
input[type="text"]:focus,
textarea:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
} }
input[readonly], input[readonly],
@ -160,14 +310,14 @@ h4 {
} }
select { select {
background: #333337; height: 2.5em;
border-color: #3f3f3f; -webkit-appearance: none;
color: #999999; border-radius: 0;
background-position: right center; background-position: right center;
background-size: 9px 6px; background-size: 9px 6px;
background-origin: content-box; background-origin: content-box;
background-repeat: no-repeat; background-repeat: no-repeat;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="24" viewBox="0 0 32 24"><polygon points="0,0 32,0 16,24" style="fill: rgb%28138, 138, 138%29"></polygon></svg>') background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28138, 138, 138%29'></polygon></svg>")
} }
select:hover { select:hover {
@ -187,55 +337,210 @@ h4 {
width: 7rem; width: 7rem;
} }
/* Graphic */
header {
display: flex;
h3 {
font-size: 1em;
flex-grow: 2;
border-bottom: 1px solid #2d2d30;
padding-top: 10px;
margin-right: 30px;
}
button {
border: 0;
width: 100px;
}
}
.graphic {
&-presetlist {
display: flex;
flex-direction: column;
align-items: stretch;
}
&-presetadd {
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid #2d2d30;
margin: 30px 0 10px;
padding: 20px;
position: relative;
&-header {
background: #3f3f41;
position: absolute;
top: -1.3em;
left: 10px;
font-size: 0.8em;
padding: 0.8em 10px;
}
&-buttons {
display: flex;
margin-top: 10px;
& button {
margin-right: 10px;
width: 150px;
}
}
}
&-presetremove {
align-self: center;
margin-top: 50px;
width: 150px;
}
&-empty {
font-size: 0.7em;
color: #999;
text-align: center;
margin: 20px 0;
}
&-delete {
align-self: center;
margin-top: 30px;
width: 150px;
}
&-label {
margin-top: 30px;
padding-bottom: 0.5em;
}
&-helper {
font-size: 0.7em;
color: #999;
margin: 5px 0 0;
&.bottom {
margin: 0;
}
}
&-property,
&-preset {
display: flex;
&-reorder {
width: 30px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODQ5NDcyMzY3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODQ5NDcyMzc3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4NDk0NzIzNDcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4NDk0NzIzNTcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pla8e2wAAABFUExURXd3d////3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0brzhMAAAAWdFJOUwAAN0NEVXJzdHV2d3h/iImKmaq73e5w9SzPAAAAlElEQVRIx+XT2wqDMBRE0Witt2piW53//9SCoETMwJkXkWY/CgtNBp0jDa/086JgwgNeIwEghpFVAMFOJoCaNNkFMNlIJBImRcrPodJ8ybQ/I285B7mLSCWX+/qL3EVTznK5T/mUu++P7OVyX/9U9T30sLyliUeobR8WmcZ6lnYTrf34HRX8xlbTaZfcA726yziyXX5Joj0NgTfsUQAAAABJRU5ErkJggg==') no-repeat transparent;
background-size: 25px;
background-position: left center;
touch-action: none;
}
& input {
flex-grow: 2;
margin: 0;
}
& button {
width: 100px;
border: 1px solid #3f3f41;
border-left: none;
}
}
}
.schedule {
&-empty {
margin-top: 2em;
font-size: 1em;
text-align: center;
}
}
.settings {
&-empty {
text-align: center;
margin: 50px 0 30px;
font-size: 0.8em;
color: #999;
&-button {
align-self: center;
width: 200px;
}
}
}
/* Dragula */
@css {
#dragcontainer {
position: fixed;
top: 0;
left: 0;
}
.gu-mirror {
position: absolute !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
}
.gu-hide {
display: none !important;
}
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}
}
/* Media queries */ /* Media queries */
body { body {
font-size: 1.5rem; font-size: 1.5rem;
} }
@media only screen and (max-device-width: 1280px) { @media only screen and (max-device-width: 600px) {
.header-item-hide { #container {
width: 9rem; flex-direction: column;
line-height: 0rem;
} }
a.button { nav {
font-size: 2rem; width: auto;
line-height: 0; flex-direction: row;
flex-wrap: wrap;
justify-content: center;
.header {
width: 100%;
}
a {
width: calc(50% - 8px);
}
input[type=text] {
width: 100%;
}
.status {
align-self: center;
width: 120px;
}
}
#content {
width: auto; width: auto;
} }
.panel-graphic-preset {
margin: 0;
}
.panel-graphic-display,
.panel-graphic-preset-add {
margin-bottom: 3rem !important;
}
.panel-graphic-preset-remove {
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.panel-graphic-preset-remove.alert {
padding-right: 1rem;
padding-left: 1rem;
}
.panel-graphic-settings {
font-size: 1.3rem !important;
}
.header-item-display {
font-size: 2rem;
margin-right: 12.5rem;
padding: 0.2rem 1rem;
}
.panel-graphic-property-item input {
font-size: 2rem;
height: 3.5rem;
}
} }

168
app/styl/status.styl Normal file
View file

@ -0,0 +1,168 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input {
font-size: 16px;
-webkit-appearance: none;
border-radius: 0;
}
button {
outline: none;
border: none;
cursor: pointer;
}
a {
text-decoration: none;
}
body {
background: #3f3f41;
color: #eb6e00;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
$header-size = 2.3em;
#container {
display: flex;
align-items: stretch;
flex-direction: column;
flex-grow: 2;
h3 {
background: black;
font-size: $header-size;
line-height: ($header-size);
color: #eb6e00;
height: ($header-size);
overflow: hidden;
padding: 0 0.3em;
flex-grow: 2;
border-radius: 5px 0 0 5px;
word-break: break-all;
}
button {
border: none;
color: black;
background: #eb6e00;
font-size: 2em;
width: 80px;
flex-shrink: 0;
border-radius: 0 5px 5px 0;
}
.item {
display: flex;
margin: 5px;
}
.empty {
flex-grow: 2;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
font-size: 2em;
}
}
header {
display: flex;
margin-bottom: 5px;
color: #ccc;
h2 {
font-size: 3em;
line-height: 64px;
padding: 0 0.3em;
flex-grow: 2;
}
.status {
padding: 0 20px 0 30px;
line-height: 64px;
font-size: 2em;
color: $header-color;
text-align: left;
position: relative;
&::after {
position: absolute;
left: 0;
top: calc(50% - 7px);
content: '';
border: 10px solid #ec5840;
}
&.green::after {
border-color: #00FF00;
}
}
}
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
font-size: 3em;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
}

8
install_run.bat Normal file
View file

@ -0,0 +1,8 @@
git pull
npm install
npm run build
npm start | bunyan
echo.
echo EXITED
echo.
PAUSE

View file

@ -0,0 +1,24 @@
/* eslint-disable */
'use strict';
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('settings', function(table) {
table.increments()
table.text('name')
table.text('value')
table.boolean('is_deleted')
}).then(() => {
return knex('settings').insert({
name: 'casparcg',
value: ''
})
}),
]);
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('settings'),
]);
};

View file

@ -0,0 +1,22 @@
/* eslint-disable */
'use strict';
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('content', function(table) {
table.increments()
table.text('graphic')
table.text('name')
table.text('html')
table.text('css')
table.text('data')
table.boolean('is_deleted')
}),
]);
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('content'),
]);
};

View file

@ -0,0 +1,20 @@
/* eslint-disable */
'use strict';
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('schedule', function(table) {
table.increments()
table.integer('graphic_id').references('graphics.id')
table.text('values')
table.integer('sort')
table.boolean('is_deleted')
}),
]);
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('schedule'),
]);
};

View file

@ -8,14 +8,18 @@
"watch:styl": "stylus -w -m app/styl/main.styl --out public", "watch:styl": "stylus -w -m app/styl/main.styl --out public",
"build-client:styl": "stylus -m app/styl/client.styl --out public", "build-client:styl": "stylus -m app/styl/client.styl --out public",
"watch-client:styl": "stylus -w -m app/styl/client.styl --out public", "watch-client:styl": "stylus -w -m app/styl/client.styl --out public",
"watch:js": "watchify -t babelify app/main.js -o public/main.js --debug", "build-status:styl": "stylus -m app/styl/status.styl --out public",
"build:js": "browserify app/main.js -o public/main.js --debug -t [ babelify ]", "watch-status:styl": "stylus -w -m app/styl/status.styl --out public",
"watch-client:js": "watchify -t babelify app/client.js -o public/client.js --debug", "watch:js": "watchify -t babelify app/main/index.js -o public/main.js --debug",
"build-client:js": "browserify app/client.js -o public/client.js --debug -t [ babelify ]", "build:js": "browserify app/main/index.js -o public/main.js --debug -t [ babelify ]",
"watch-client:js": "watchify -t babelify app/client/index.js -o public/client.js --debug",
"build-client:js": "browserify app/client/index.js -o public/client.js --debug -t [ babelify ]",
"watch-status:js": "watchify -t babelify app/status/index.js -o public/status.js --debug",
"build-status:js": "browserify app/status/index.js -o public/status.js --debug -t [ babelify ]",
"watch:server": "nodemon index.js", "watch:server": "nodemon index.js",
"start": "node index.js", "start": "node index.js",
"dev-run": "run-p watch:js watch-client:js watch:server watch:styl watch-client:styl", "dev-run": "run-p watch:js watch-client:js watch-status:js watch:server watch:styl watch-client:styl watch-status:styl",
"prod-run": "npm run build:js && npm run build-client:js && npm run build:styl && npm run build-client:styl && npm start", "prod-run": "npm run build:js && npm run build-client:js && npm run build-status:js && npm run build:styl && npm run build-client:styl && npm run build-status:styl && npm start",
"test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot", "test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot",
"docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine", "docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine",
"docker:test": "npm run docker -- npm install && npm run test", "docker:test": "npm run docker -- npm install && npm run test",
@ -43,6 +47,9 @@
"babel-register": "^6.26.0", "babel-register": "^6.26.0",
"bookshelf": "^0.11.1", "bookshelf": "^0.11.1",
"bunyan": "^1.8.12", "bunyan": "^1.8.12",
"casparcg-connection": "^4.1.0",
"dragula": "^3.7.2",
"ip": "^1.1.5",
"knex": "^0.14.2", "knex": "^0.14.2",
"koa": "^2.4.1", "koa": "^2.4.1",
"koa-better-serve": "^2.0.7", "koa-better-serve": "^2.0.7",
@ -54,6 +61,8 @@
"sqlite3": "^3.1.13" "sqlite3": "^3.1.13"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^4.16.0",
"eslint-plugin-mocha": "^4.11.0",
"babelify": "^8.0.0", "babelify": "^8.0.0",
"mocha": "^4.0.1", "mocha": "^4.0.1",
"nodemon": "^1.12.1", "nodemon": "^1.12.1",

2555
public/foundation.css vendored

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,15 @@
<head> <head>
<title>CasparCG Controller</title> <title>CasparCG Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href="/public/main.css" rel="stylesheet" /> <link href="/main.css" rel="stylesheet" />
</head> </head>
<body> <body>
<div class="expanded row"> <section class="current" id="header"></section>
<div class="small-12 columns container" id="header"></div> <div id="container" class="container">
<nav id="menu"></nav>
<main id="content"></main>
</div> </div>
<div class="expanded row"> <div id="dragcontainer"></div>
<div class="small-3 columns container" id="menu"></div> <script src="/main.js"></script>
<div class="small-9 columns container" id="content"></div>
</div>
<script src="/public/main.js"></script>
</body> </body>
</html> </html>

View file

@ -1,190 +0,0 @@
body {
background: #3f3f41;
color: #f1f1f1;
}
h4 {
margin-bottom: 2rem;
}
/* Container */
.container {
padding: 1rem;
}
.container-header {
font-size: 1.5rem;
margin-left: 1rem;
color: #777;
}
.container-panel {
border: 1px solid #3f3f3f;
background: #2d2d30;
padding: 1rem;
border-radius: 5px;
}
/* Header */
.header-list {
list-style-type: none;
margin: 0;
}
.header-item {
margin-bottom: 1rem;
}
.header-item-hide {
float: right;
width: 5rem;
border-radius: 6px;
margin: 0;
}
.header-item-display {
background: #070707;
color: #eb6e00;
border-radius: 6px;
padding: 0.5rem 1rem;
margin-right: 5.5rem;
}
/* Menu */
.menu-list {
list-style-type: none;
margin: 0;
}
.menu a {
color: #007acc;
display: block;
border: 1px solid #2d2d30;
padding: 0.2rem 0.5rem;
}
.menu a:hover {
color: #f1f1f1;
border: 1px solid #007acc;
}
.menu-item-add {
margin-top: 3rem;
}
/* Add */
.panel-add {
padding: 2rem;
}
.panel-graphic-property-add,
.panel-graphic-property-remove {
width: 100%;
}
/* Graphic */
.panel-graphic-settings {
float: right;
margin-right: 1rem;
}
.panel-graphic-property-item {
padding-left: 0;
}
.panel-graphic-preset-add {
margin-right: 1rem;
}
.panel-graphic-preset {
margin-top: 1rem;
list-style-type: none;
}
.panel-graphic-preset a {
width: 100%;
}
/* Components */
.error-box {
margin: 0rem 0rem 2rem 0;
color: #f00;
}
/* Inputs */
.panel-graphic-property-item input {
display: inline-block;
}
label {
color: #f1f1f1;
}
input[type="text"],
textarea {
background: #333337;
border-color: #3f3f3f;
color: #999;
transition-property: none !important;
}
input[type="text"]:hover,
textarea:hover {
color: #f1f1f1;
border-color: #007acc;
}
input[type="text"]:focus,
textarea:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
input[readonly],
input[readonly]:hover {
background: #2d2d30 !important;
border-color: #3f3f3f;
}
select {
background: #333337;
border-color: #3f3f3f;
color: #999;
background-position: right center;
background-size: 9px 6px;
background-origin: content-box;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="24" viewBox="0 0 32 24"><polygon points="0,0 32,0 16,24" style="fill: rgb%28138, 138, 138%29"></polygon></svg>");
}
select:hover {
color: #f1f1f1;
border-color: #007acc;
}
select:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
a.button {
margin: 0 1rem 0 0;
width: 7rem;
}
/* Media queries */
body {
font-size: 1.5rem;
}
@media only screen and (max-device-width: 1280px) {
.header-item-hide {
width: 9rem;
line-height: 0rem;
}
a.button {
font-size: 2rem;
line-height: 0;
width: auto;
}
.panel-graphic-preset {
margin: 0;
}
.panel-graphic-display,
.panel-graphic-preset-add {
margin-bottom: 3rem !important;
}
.panel-graphic-preset-remove {
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.panel-graphic-preset-remove.alert {
padding-right: 1rem;
padding-left: 1rem;
}
.panel-graphic-settings {
font-size: 1.3rem !important;
}
.header-item-display {
font-size: 2rem;
margin-right: 12.5rem;
padding: 0.2rem 1rem;
}
.panel-graphic-property-item input {
font-size: 2rem;
height: 3.5rem;
}
}
/*# sourceMappingURL=main.css.map */

View file

@ -1 +0,0 @@
{"version":3,"sources":["../app/styl/main.styl"],"names":[],"mappings":"AAAA;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;;AAGF;EACE,eAAe,KAAf;;AAGF;AAEE;EACE,SAAS,KAAT;;AAGF;EACE,WAAW,OAAX;EACA,aAAa,KAAb;EACA,OAAO,KAAP;;AAGF;EACE,QAAQ,kBAAR;EACA,YAAY,QAAZ;EACA,SAAS,KAAT;EACA,eAAe,IAAf;;AAGJ;AAEE;EACE,iBAAiB,KAAjB;EACA,QAAQ,EAAR;;AAGF;EACE,eAAe,KAAf;;AAGF;EACE,OAAO,MAAP;EACA,OAAO,KAAP;EACA,eAAe,IAAf;EACA,QAAQ,EAAR;;AAGF;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,eAAe,IAAf;EACA,SAAS,YAAT;EACA,cAAc,OAAd;;AAGJ;AAEE;EACE,iBAAiB,KAAjB;EACA,QAAQ,EAAR;;AAGF;EACE,OAAO,QAAP;EACA,SAAS,MAAT;EACA,QAAQ,kBAAR;EACA,SAAS,cAAT;;AAGF;EACE,OAAO,QAAP;EACA,QAAQ,kBAAR;;AAGF;EACE,YAAY,KAAZ;;AAGJ;AAEE;EACE,SAAS,KAAT;;AAGF;AAA4B;EAE1B,OAAO,KAAP;;AAGJ;AAME;EACE,OAAO,MAAP;EACA,cAAc,KAAd;;AAGF;EACE,cAAc,EAAd;;AAGF;EACE,cAAc,KAAd;;AAGF;EACE,YAAY,KAAZ;EACA,iBAAiB,KAAjB;;AAGF;EACE,OAAO,KAAP;;AAGJ;AAEE;EACE,QAAQ,iBAAR;EACA,OAAO,KAAP;;AAGJ;AAEE;EACE,SAAS,aAAT;;AAGF;EACE,OAAO,QAAP;;AAGF;AAAmB;EAEjB,YAAY,QAAZ;EACA,cAAc,QAAd;EACA,OAAO,KAAP;EACA,qBAAqB,gBAArB;;AAGF;AAAyB;EAEvB,OAAO,QAAP;EACA,cAAc,QAAd;;AAGF;AAAyB;EAEvB,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,cAAc,QAAd;EACA,YAAY,KAAZ;;AAGF;AAAgB;EAEd,YAAY,mBAAZ;EACA,cAAc,QAAd;;AAGF;EACE,YAAY,QAAZ;EACA,cAAc,QAAd;EACA,OAAO,KAAP;EACA,qBAAqB,aAArB;EACA,iBAAiB,QAAjB;EACA,mBAAmB,YAAnB;EACA,mBAAmB,UAAnB;EACA,kBAAuO,uNAAvO;;AAGF;EACE,OAAO,QAAP;EACA,cAAc,QAAd;;AAGF;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,cAAc,QAAd;EACA,YAAY,KAAZ;;AAGF;EACE,QAAQ,WAAR;EACA,OAAO,KAAP;;AAGJ;AAEE;EACE,WAAW,OAAX;;AAG8C;AAC9C;IACE,OAAO,KAAP;IACA,aAAa,KAAb;;AAGF;IACE,WAAW,KAAX;IACA,aAAa,EAAb;IACA,OAAO,KAAP;;AAGF;IACE,QAAQ,EAAR;;AAGF;AAAuB;IAErB,eAAe,gBAAf;;AAGF;IACE,eAAe,OAAf;IACA,cAAc,OAAd;;AAGF;IACE,eAAe,KAAf;IACA,cAAc,KAAd;;AAGF;IACE,WAAW,kBAAX;;AAGF;IACE,WAAW,KAAX;IACA,cAAc,QAAd;IACA,SAAS,YAAT;;AAGF;IACE,WAAW,KAAX;IACA,QAAQ,OAAR","file":"main.css"}

11
public/status.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CasparCG Client</title>
<link href="/status.css" rel="stylesheet" />
</head>
<body>
<div id="container"></div>
<script src="status.js"></script>
</body>
</html>