Major update and redesign
This commit is contained in:
parent
767c4dd73d
commit
8780eb228e
50 changed files with 2465 additions and 3701 deletions
85
.eslintrc
85
.eslintrc
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
120
api/casparcg/client.js
Normal 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))
|
||||
}
|
||||
}
|
5
api/casparcg/connection.js
Normal file
5
api/casparcg/connection.js
Normal 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
41
api/content/model.js
Normal 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
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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'])
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const Preset = bookshelf.createModel({
|
|||
attributes.values = JSON.parse(attributes.values)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
},
|
||||
}, {
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
35
api/schedule/model.js
Normal 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
52
api/schedule/routes.js
Normal 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)
|
||||
}
|
|
@ -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
51
api/settings/model.js
Normal 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
37
api/settings/routes.js
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
|
|
@ -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
57
app/main/add/module.js
Normal 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
|
60
app/main/common/components.js
Normal file
60
app/main/common/components.js
Normal 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
98
app/main/common/module.js
Normal 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
|
|
@ -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')
|
||||
]
|
||||
),
|
||||
])
|
||||
])
|
||||
))
|
||||
]
|
||||
}
|
|
@ -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
|
83
app/main/dagskra/module.js
Normal file
83
app/main/dagskra/module.js
Normal 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
|
|
@ -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')
|
|
@ -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'),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
260
app/main/graphic/module.js
Normal 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
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
106
app/status/index.js
Normal 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)
|
||||
// })
|
|
@ -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 */
|
||||
|
||||
.header-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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-delete {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Main */
|
||||
|
||||
.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;
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 10px 1em;
|
||||
flex-grow: 2;
|
||||
width: 300px;
|
||||
|
||||
.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
168
app/styl/status.styl
Normal 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
8
install_run.bat
Normal file
|
@ -0,0 +1,8 @@
|
|||
git pull
|
||||
npm install
|
||||
npm run build
|
||||
npm start | bunyan
|
||||
echo.
|
||||
echo EXITED
|
||||
echo.
|
||||
PAUSE
|
24
migrations/20180625133103_settings.js
Normal file
24
migrations/20180625133103_settings.js
Normal 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'),
|
||||
]);
|
||||
};
|
22
migrations/20180626143319_content.js
Normal file
22
migrations/20180626143319_content.js
Normal 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'),
|
||||
]);
|
||||
};
|
20
migrations/20180626154925_schedule.js
Normal file
20
migrations/20180626154925_schedule.js
Normal 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'),
|
||||
]);
|
||||
};
|
21
package.json
21
package.json
|
@ -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
2555
public/foundation.css
vendored
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||
|
|
190
public/main.css
190
public/main.css
|
@ -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 */
|
|
@ -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
11
public/status.html
Normal 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>
|
Loading…
Reference in a new issue