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",
"extends": "airbnb-base",
"ecmaFeatures": {
"modules": false
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"extends": "eslint:recommended",
"plugins": [
"mocha"
],
"env": {
"mocha": true,
"node": true,
"es6": true
},
"rules": {
"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"],
"max-len": [1, 120],
"prefer-const": 0,
"consistent-return": 0,
"no-param-reassign": [2, {"props": false}],
"no-use-before-define": [2, {"functions": false, "classes": true}],
"no-unused-vars": [
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": {
"describe": false,
"it": false,
"before": false,
"beforeEach": false,
"after": false,
"afterEach": false
globals: {
"window": true,
"document": true
}
}

View file

@ -73,10 +73,11 @@ shelf.createModel = (attr, opts) => {
.fetch({ require, withRelated })
},
getAll(where = {}, withRelated = []) {
getAll(where = {}, withRelated = [], orderBy = 'id') {
where.is_deleted = false
return this.query({ where })
.orderBy(orderBy, 'ASC')
.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 Content from './model'
export const active = { }
@ -7,25 +8,48 @@ function getSocket(ctx, all) {
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 html = compiled(data.data)
let payload = {
graphic: data.graphic,
html,
css: data.graphic.settings.css,
data: data.data,
let old = await Content.getSingle(data.graphic.name)
if (old) {
await old.destroy()
}
active[data.graphic.name] = payload
ctx.io.emit('client.display', payload)
let 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)
}
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', {
name: data.name,
@ -35,21 +59,41 @@ export function hide(ctx, data) {
}
function generateDisplayText(item) {
if (item.graphic.engine === 'countdown') {
return `${item.data[item.graphic.settings.main]} - ${item.data.countdown}`
// if (item.graphic.engine === '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 => ({
name: active[key].graphic.name,
display: generateDisplayText(active[key]),
}))
/*
* Event: 'content.list'
* Runs on start of every new connection
*
* 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)
}
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) {
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'
/*
* Event: 'graphic.all'
*
* Request all graphics in store
*/
export async function all(ctx) {
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) {
if (!data || !data.id) {
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)
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) {
data.settings = {}
data.is_deleted = false
if (data.engine === 'countdown') {
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) {
if (!data || !data.id) {
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 })
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) {
if (!data || !data.id) {
ctx.log.warn('called graphic update but no id specified')
@ -56,5 +92,5 @@ export async function update(ctx, data) {
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)
}
return attributes
}
},
}, {
})

View file

@ -3,7 +3,7 @@ import Preset from './model'
export async function all(ctx, payload) {
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())
}
@ -27,6 +27,18 @@ export async function add(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) {
let preset = await Preset.getSingle(payload.id)

View file

@ -1,11 +1,14 @@
import logger from '../log'
import { register } from './io/helper'
import { contentConnection } from './content/connection'
import { casparConnection } from './casparcg/connection'
import * as content from './content/routes'
import * as engine from './engine/routes'
import * as graphic from './graphic/routes'
import * as preset from './preset/routes'
import * as settings from './settings/routes'
import * as schedule from './schedule/routes'
function onConnection(server, data) {
const io = server.socket
@ -17,11 +20,14 @@ function onConnection(server, data) {
let ctx = { io, socket, log }
contentConnection(ctx)
casparConnection(ctx)
register(ctx, 'content', content)
register(ctx, 'engine', engine)
register(ctx, 'graphic', graphic)
register(ctx, 'preset', preset)
register(ctx, 'settings', settings)
register(ctx, 'schedule', schedule)
}
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 serve from 'koa-better-serve'
import socket from 'koa-socket'
import * as casparcg from './casparcg/client'
import config from '../config'
import log from '../log'
@ -14,9 +15,19 @@ io.attach(app)
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(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 => {
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')
element.innerHTML = data.html
element.id = data.graphic.name
@ -18,50 +61,19 @@ module.exports = function(data) {
element.classList.add('root-element-display')
}, 100)
module.exports.update(data)
}
module.exports.update = function(data) {
var timeElement = document.getElementById(data.graphic.name + '-countdown-timer')
var 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
}
data.time = new Date(data.data.countdown.replace(' ', 'T'))
if (timeElement) {
timeElement.tag = time
timer()
data.timer = setInterval(timer, 1000)
timeElement.tag = data
timer(data.graphic.name)
if (currentActiveTimer) {
clearInterval(currentActiveTimer)
}
currentActiveTimer = setInterval(timer.bind(null, data.graphic.name), 1000)
}
}

View file

@ -6,23 +6,21 @@ var engines = {
schedule: require('./schedule'),
}
var current = []
function display(data) {
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
if (exists) {
exists.innerHtml = data.html
exists.tag.innerHtml = data.css
engines[engine].update(data)
return
}
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)
if (exists) {
current.splice(current.indexOf(data.name), 1)
exists.classList.remove('root-element-display')
window.setTimeout(function () {

View file

@ -1,5 +1,5 @@
module.exports = function(data) {
module.exports.init = function(data) {
var element = document.createElement('div')
element.innerHTML = data.html
element.id = data.graphic.name
@ -18,3 +18,7 @@ module.exports = function(data) {
element.classList.add('root-element-display')
}, 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')
element.innerHTML = data.html
element.id = data.graphic.name
@ -18,3 +18,7 @@ module.exports = function(data) {
element.classList.add('root-element-display')
}, 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 components = require('../../components')
const components = require('../../common/components')
exports.view = function(ctlr, graphic, vm) {
exports.view = function(module, graphic) {
return [
m('label', [
'Text',
m('input[type=text]', {
value: vm.graphic.settings.text || '',
oninput: vm.updated.bind(vm, 'settings.text'),
m('div.graphic-presetadd', [
m('h3.graphic-presetadd-header', 'Start countdown'),
m('label', { for: `countdown-text` }, 'Text'),
m(`input#countdown-text[type=text]`, {
value: graphic.settings.text || '',
oninput: module.updated.bind(module, 'settings.text'),
}),
]),
m('label', [
'Count to (format: "YYYY-MM-DD hh:mm")',
m('input[type=text]', {
value: vm.graphic.settings.countdown || '',
oninput: vm.updated.bind(vm, 'settings.countdown'),
m('label', { for: `countdown-countdown` }, 'Count to (format: "YYYY-MM-DD hh:mm")'),
m(`input#countdown-countdown[type=text]`, {
value: graphic.settings.countdown || '',
oninput: module.updated.bind(module, 'settings.countdown'),
}),
]),
m('label', [
'Finished (gets displayed in the countdown upon reaching 0)',
m('input[type=text]', {
value: vm.graphic.settings.finished || '',
oninput: vm.updated.bind(vm, 'settings.finished'),
m('label', { for: `countdown-finished` }, 'Finished (gets displayed in the countdown upon reaching 0)'),
m(`input#countdown-finished[type=text]`, {
value: graphic.settings.finished || '',
oninput: module.updated.bind(module, '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 [
m('label', [
'Name',
m('input[type=text]', {
value: graphic.name,
oninput: vm.updated.bind(vm, 'name'),
}),
// Name
m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input#graphic-name[type=text]', {
value: graphic.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', [
'HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: <%- text %>',
')',
m('p', `<div id="${graphic.name}">`),
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p', `</div>`),
m('p.graphic-helper', `<div id="${graphic.name}">`),
m('textarea#graphic-html', {
rows: '4',
oninput: module.updated.bind(module, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p.graphic-helper.bottom', `</div>`),
// CSS
m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
m('p.graphic-helper', '<style type="text/css">'),
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', [
'CSS',
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.css'),
value: graphic.settings.css || '',
})
]),
m('a.panel-graphic-delete.button.alert', {
onclick: vm.remove.bind(vm),
m('input#graphic-main[type=text]', {
value: graphic.settings.main,
oninput: module.updated.bind(module, 'settings.main'),
}),
components.error(module.mainTemplateError),
// Remove
m('button.red.graphic-delete', {
onclick: module.remove.bind(module),
}, 'Delete graphic'),
]
}

View file

@ -1,162 +1,121 @@
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) {
graphic.settings.properties = []
}
if (!graphic.settings.textfields) {
graphic.settings.textfields = []
}
if (graphic.settings.properties.length === 0) {
return [
m('p', 'No properties have been defined.'),
m('p', 'Click settings to create and define properties to display.'),
m('p.settings-empty', `
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 [
components.presetOnlyList(vm),
graphic.settings.properties.map((prop, index) =>
m('label', { key: index }, [
prop,
m('input[type=text]', {
value: vm.current[prop] || '',
oninput: vm.updated.bind(vm, prop, 'current'),
m('div.graphic-presetadd', [
m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
m('label', { for: `preset-add-${index}` }, prop),
m(`input#preset-add-${index}[type=text]`, {
value: module.current[prop] || '',
oninput: module.updated.bind(module, prop, 'current'),
}),
])
),
graphic.settings.textfields.map((prop, index) =>
m('label', { key: index }, [
prop,
m('textarea', {
rows: '6',
oninput: vm.updated.bind(vm, prop, 'current'),
value: vm.current[prop] || '',
}),
])
),
components.presetButtons(vm),
])),
components.presetButtons(module, 'Display live now', 'Add to preset list'),
]),
components.presetOnlyList(module, graphic, 'Presets'),
]
}
exports.settings = function(cltr, graphic, vm) {
exports.settings = function(module, graphic) {
return [
m('label', [
'Name',
m('input[type=text]', {
value: graphic.name,
oninput: vm.updated.bind(vm, 'name'),
}),
// Name
m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input#graphic-name[type=text]', {
value: graphic.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', [
'HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
', ',
graphic.settings.textfields.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
m('p', `<div id="${graphic.name}">`),
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p', `</div>`),
m('p.graphic-helper', `<div id="${graphic.name}">`),
m('textarea#graphic-html', {
rows: '4',
oninput: module.updated.bind(null, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p.graphic-helper.bottom', `</div>`),
// CSS
m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
m('p.graphic-helper', '<style type="text/css">'),
m('textarea#graphic-css', {
rows: '4',
oninput: module.updated.bind(null, '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: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]),
m('label', [
'CSS',
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.css'),
value: graphic.settings.css || '',
})
]),
/* -------- Simple Properties -------- */
m('label', 'Simple Properties'),
m('label', [
'Main',
m('select', {
onchange: vm.updated.bind(vm, 'settings.main'),
}, graphic.settings.properties.map((prop, index) =>
m('option', {
key: 'prop-list-' + index,
m('input#graphic-main[type=text]', {
value: graphic.settings.main,
oninput: module.updated.bind(module, 'settings.main'),
}),
// Property list
m('label.graphic-label', 'Properties'),
graphic.settings.properties.map((prop, index) =>
m('div.graphic-property', { key: `prop-${index}` }, [
m('input[type=text]', {
readonly: true,
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 -------- */
m('label', 'List'),
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]', {
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),
components.error(module.mainTemplateError),
// Remove
m('button.red.graphic-delete', {
onclick: module.remove.bind(module),
}, 'Delete graphic'),
]
}

View file

@ -1,109 +1,121 @@
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) {
graphic.settings.properties = []
}
if (graphic.settings.properties.length === 0) {
return [
m('p', 'No properties have been defined.'),
m('p', 'Click settings to create and define properties to display.'),
m('p.settings-empty', `
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 [
components.presetOnlyList(vm),
graphic.settings.properties.map((prop, index) =>
m('label', { key: index }, [
prop,
m('input[type=text]', {
value: vm.current[prop] || '',
oninput: vm.updated.bind(vm, prop, 'current'),
m('div.graphic-presetadd', [
m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
m('label', { for: `preset-add-${index}` }, prop),
m(`input#preset-add-${index}[type=text]`, {
value: module.current[prop] || '',
oninput: module.updated.bind(module, prop, 'current'),
}),
])
),
components.presetButtons(vm),
])),
components.presetButtons(module, 'Display live now', 'Add to preset list'),
]),
components.presetOnlyList(module, graphic, 'Presets'),
]
}
exports.settings = function(cltr, graphic, vm) {
exports.settings = function(module, graphic) {
return [
m('label', [
'Name',
m('input[type=text]', {
value: graphic.name,
oninput: vm.updated.bind(vm, 'name'),
}),
// Name
m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'),
m('input#graphic-name[type=text]', {
value: graphic.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', [
'HTML (',
m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'),
' available: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
m('p', `<div id="${graphic.name}">`),
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p', `</div>`),
m('p.graphic-helper', `<div id="${graphic.name}">`),
m('textarea#graphic-html', {
rows: '4',
oninput: module.updated.bind(null, 'settings.html'),
value: graphic.settings.html || '',
}),
m('p.graphic-helper.bottom', `</div>`),
// CSS
m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'),
m('p.graphic-helper', '<style type="text/css">'),
m('textarea#graphic-css', {
rows: '4',
oninput: module.updated.bind(null, '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: ',
graphic.settings.properties.map(prop =>
`<%- ${prop} %>`
).join(', '),
')',
]),
m('label', [
'CSS',
m('textarea', {
rows: '4',
oninput: vm.updated.bind(null, 'settings.css'),
value: graphic.settings.css || '',
})
]),
m('label', [
'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#graphic-main[type=text]', {
value: graphic.settings.main,
oninput: module.updated.bind(module, 'settings.main'),
}),
// Property list
m('label.graphic-label', 'Properties'),
graphic.settings.properties.map((prop, index) =>
m('div.graphic-property', { key: `prop-${index}` }, [
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')
),
readonly: true,
value: 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'),
]),
m('a.panel-graphic-delete.button.alert', {
onclick: vm.remove.bind(vm),
components.error(module.mainTemplateError),
// Remove
m('button.red.graphic-delete', {
onclick: module.remove.bind(module),
}, '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 Graphic = require('./controller')
const components = require('../components')
const components = require('../common/components')
const engines = {
text: require('./engine/text'),
@ -8,21 +7,20 @@ const engines = {
schedule: require('./engine/schedule'),
}
Graphic.view = function(ctrl) {
graphic = Graphic.vm.graphic
module.exports = function() {
let graphic = this.graphic
let currentView = graphic.engine && engines[graphic.engine][this.currentView] || null
return m('div', [
m('h3.container-header', 'Graphic'),
m('div.container-panel.panel-graphic',
!graphic.name && m('p', 'Loading...') ||
[
m('a.panel-graphic-settings.button', {
onclick: Graphic.vm.switchView
}, Graphic.vm.currentView === 'view' && 'Settings' || 'Control'),
m('h4', graphic.name),
components.error(Graphic.vm.error),
engines[graphic.engine][Graphic.vm.currentView](ctrl, graphic, Graphic.vm),
]
),
])
return [
m('h4.header', 'Graphic'),
m('header', [
m('h3', graphic.name),
m('button', {
onclick: () => this.switchView(),
}, this.changeViewTitle()),
]),
components.error(this.error),
!currentView && m('p', 'Loading...')
|| currentView(this, graphic),
]
}

View file

@ -1,10 +1,22 @@
const m = require('mithril')
const createModule = require('./module')
const createModule = require('./common/module')
const socket = require('../socket')
const Header = createModule({
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) {
@ -12,19 +24,38 @@ const Header = createModule({
name: item.name,
})
},
}, function(ctrl) {
return m('div.header', Header.vm.list.length > 0 && [
m('h3.container-header', 'Currently active'),
m('ul.header-list', [
Header.vm.list.map((item, index) =>
m('li.header-item', { key: 'header-' + index, }, [
m('a.header-item-hide.button.alert', {
onclick: Header.vm.hide.bind(Header.vm, item),
onupdate: function() {
if (!this.updateMargin) return
this.updateMargin = false
let header = document.getElementById('header')
let container = document.getElementById('container')
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'),
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

View file

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

View file

@ -1,24 +1,70 @@
const m = require('mithril')
const createModule = require('./module')
const createModule = require('./common/module')
const socket = require('../socket')
const Menu = createModule({
init: function() {
this.monitor('list', 'graphic.all', [])
}
}, function(ctrl) {
return m('div', [
m('h3.container-header', 'Graphics'),
m('div.container-panel.menu', [
m('ul.menu-list', [
// m('a', { href: `/`, config: m.route }, 'Home'),
Menu.vm.list.map((item) =>
m('li.menu-item', [
m('a', { href: `/graphic/${item.id}`, config: m.route }, item.name),
])
)
]),
m('a.menu-item-add', { href: '/add', config: m.route }, 'Add graphic' ),
]),
])
this.monitor('settings', 'settings.all', {})
this.monitor('schedule', 'schedule.total', { total: 0 })
this.monitor('status', 'casparcg.status', {
connected: false,
playing: false,
})
this.newHost = ''
this.enableEdit = false
},
setHost(value) {
this.newHost = value
this.enableEdit = true
},
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

View file

@ -5,6 +5,7 @@ const events = {}
// Listen on all events
let onevent = socket.onevent
let disconnected = false
socket.onevent = function(packet) {
let args = packet.data || []
@ -13,6 +14,25 @@ socket.onevent = function(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) {
if (id) {
return `${name}:${id}`
@ -21,6 +41,10 @@ function genId(name, id) {
}
const store = {
getId: function(name, id) {
return genId(name, id)
},
get: function(name, id) {
return storage[genId(name, id)]
},
@ -39,18 +63,24 @@ const store = {
}
socket.on('*', (event, data) => {
let name = genId(event, data && data.id)
let id = data && data.id
let name = genId(event, id)
if (events[name]) {
storage[name] = data
events[name]()
}
if (event.indexOf('single') >= 0) {
let check = event.replace('single', 'all')
if (events[name]) {
if (events[check]) {
let index = _.findIndex(storage[check], { id: data.id })
if (index > -1) {
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 {
background: #3f3f41;
color: #f1f1f1;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
h4 {
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 {
padding: 1rem;
}
.container-header {
font-size: 1.5rem;
margin-left: 1rem;
color: #777777;
}
.container-panel {
border: 1px solid #3f3f3f;
background: #2d2d30;
padding: 1rem;
border-radius: 5px;
display: flex;
align-items: stretch;
flex-grow: 2;
}
/* Header */
section.current {
padding: 0 13px;
background: black;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 10;
.header-list {
list-style-type: none;
margin: 0;
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 {
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;
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
font-size: 1em;
display: flex;
justify-content: center;
align-items: center;
}
/* Menu */
.menu-list {
list-style-type: none;
margin: 0;
nav {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px;
background: #2d2d30;
text-align: center;
.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;
}
}
}
.menu a {
color: #007acc;
display: block;
border: 1px solid #2d2d30;
padding: 0.2rem 0.5rem;
}
/* Main */
.menu a:hover {
color: #f1f1f1;
border: 1px solid #007acc;
}
main {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px 1em;
flex-grow: 2;
width: 300px;
.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 {
}
.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: #FF0000;
.header {
color: $header-color;
font-size: $header-size;
margin-bottom: 10px;
}
}
/* Inputs */
.panel-graphic-property-item input {
display: inline-block;
}
label {
margin-top: 0.6em;
color: #f1f1f1;
font-size: 0.7em;
& a,
& a:hover,
& a:visited {
color: #aaa;
text-decoration: underline;
}
}
input[type="text"],
textarea {
input[type='text'],
textarea,
select {
font-size: 0.6em;
padding: 0.5em;
margin: 0.5em 0;
background: #333337;
border-color: #3f3f3f;
border: 1px solid #2d2d30;
color: #999999;
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,
textarea:hover {
input[type=submit] {
margin-top: 0.6em;
border: none;
color: #f1f1f1;
border-color: #007acc;
}
input[type="text"]:focus,
textarea:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
background: #2199e8;
font-size: 0.6em;
line-height: 3em;
}
input[readonly],
@ -160,14 +310,14 @@ h4 {
}
select {
background: #333337;
border-color: #3f3f3f;
color: #999999;
height: 2.5em;
-webkit-appearance: none;
border-radius: 0;
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>')
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 {
@ -187,55 +337,210 @@ h4 {
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 */
body {
font-size: 1.5rem;
}
@media only screen and (max-device-width: 1280px) {
.header-item-hide {
width: 9rem;
line-height: 0rem;
@media only screen and (max-device-width: 600px) {
#container {
flex-direction: column;
}
a.button {
font-size: 2rem;
line-height: 0;
nav {
width: auto;
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;
}
.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",
"build-client:styl": "stylus -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:js": "browserify app/main.js -o public/main.js --debug -t [ babelify ]",
"watch-client:js": "watchify -t babelify app/client.js -o public/client.js --debug",
"build-client:js": "browserify app/client.js -o public/client.js --debug -t [ babelify ]",
"build-status:styl": "stylus -m app/styl/status.styl --out public",
"watch-status:styl": "stylus -w -m app/styl/status.styl --out public",
"watch:js": "watchify -t babelify app/main/index.js -o public/main.js --debug",
"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",
"start": "node index.js",
"dev-run": "run-p watch:js watch-client:js watch:server watch:styl watch-client:styl",
"prod-run": "npm run build:js && npm run build-client:js && npm run build:styl && npm run build-client:styl && npm start",
"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-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",
"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",
@ -43,6 +47,9 @@
"babel-register": "^6.26.0",
"bookshelf": "^0.11.1",
"bunyan": "^1.8.12",
"casparcg-connection": "^4.1.0",
"dragula": "^3.7.2",
"ip": "^1.1.5",
"knex": "^0.14.2",
"koa": "^2.4.1",
"koa-better-serve": "^2.0.7",
@ -54,6 +61,8 @@
"sqlite3": "^3.1.13"
},
"devDependencies": {
"eslint": "^4.16.0",
"eslint-plugin-mocha": "^4.11.0",
"babelify": "^8.0.0",
"mocha": "^4.0.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>
<title>CasparCG Controller</title>
<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>
<body>
<div class="expanded row">
<div class="small-12 columns container" id="header"></div>
<section class="current" id="header"></section>
<div id="container" class="container">
<nav id="menu"></nav>
<main id="content"></main>
</div>
<div class="expanded row">
<div class="small-3 columns container" id="menu"></div>
<div class="small-9 columns container" id="content"></div>
</div>
<script src="/public/main.js"></script>
<div id="dragcontainer"></div>
<script src="/main.js"></script>
</body>
</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>