Implemented version 1.0

This commit is contained in:
Jonatan Nilsson 2016-04-14 04:01:51 +00:00
parent e9555b215e
commit 918f350dff
34 changed files with 1161 additions and 229 deletions

View File

@ -1,36 +1,38 @@
var socket = require('./socket') var socket = require('./socket')
socket.on('client.display', function(data) { var engines = {
var exists = document.getElementById(data.key) text: require('./frontend/text'),
countdown: require('./frontend/countdown'),
}
var current = []
function display(data) {
var exists = document.getElementById(data.graphic.name)
if (exists) { if (exists) {
exists.tag.remove() exists.tag.remove()
exists.remove() exists.remove()
current.splice(current.indexOf(data.graphic.name), 1)
} }
current.push(data.graphic.name)
var element = document.createElement('div') let engine = data.graphic.engine
element.innerHTML = data.html
element.id = data.key
element.classList.add('root-element')
var styleElement = document.createElement('style') if (engines[engine]) {
styleElement.setAttribute('type', 'text/css') engines[engine](data)
styleElement.innerHTML = data.css }
}
element.tag = styleElement socket.on('client.display', display)
document.body.appendChild(element)
document.head.appendChild(styleElement)
window.setTimeout(function (){
element.classList.add('root-element-display')
}, 50)
})
socket.on('client.hide', function (data) { socket.on('client.hide', function (data) {
var exists = document.getElementById(data.key) var exists = document.getElementById(data.name)
if (exists) { if (exists) {
current.splice(current.indexOf(data.name), 1)
exists.classList.remove('root-element-display') exists.classList.remove('root-element-display')
window.setTimeout(function () { window.setTimeout(function () {
@ -39,3 +41,7 @@ socket.on('client.hide', function (data) {
}, 1500) }, 1500)
} }
}) })
socket.on('client.reset', function(data) {
data.forEach(display)
})

52
app/controller/add.js Normal file
View File

@ -0,0 +1,52 @@
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

View File

@ -0,0 +1,41 @@
const m = require('mithril')
exports.error = function(error) {
if (!error) return null
return m('div.error-box', error)
}
exports.presetList = function(vm) {
return [
m('a.panel-graphic-preset-add.button', {
onclick: vm.addPreset.bind(vm),
}, 'Save to preset list'),
m('a.panel-graphic-display.success.button', {
onclick: vm.displayCurrent.bind(vm),
}, 'Display'),
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-2 columns' },
m('a.panel-graphic-preset-remove.button.success', {
onclick: vm.displayPreset.bind(vm, item),
}, 'Display')
),
m('div', { class: 'small-2 columns' },
m('a.panel-graphic-preset-remove.button.alert', {
onclick: vm.removePreset.bind(vm, item),
}, 'Remove')
),
])
])
))
]
}

View File

@ -1,98 +0,0 @@
const m = require('mithril')
const socket = require('../socket')
const store = require('./store')
const Content = { }
Content.vm = (function() {
let vm = {}
vm.storeUpdated = function() {
vm.content = store.get('content') || {}
m.redraw()
}
vm.init = function() {
vm.content = store.get('content') || {}
store.listen('content', vm.storeUpdated)
}
vm.onunload = function() {
store.unlisten('content')
}
vm.updated = function(name, control) {
vm.content[name] = control.target.value
store.set('content', vm.content)
}
vm.display = function() {
socket.emit('content.display', vm.content)
}
vm.hide = function() {
socket.emit('content.hide')
}
return vm
})()
Content.controller = function() {
Content.vm.init()
this.onunload = Content.vm.onunload
}
Content.view = function() {
return m('div', [
m('h3', 'Content'),
m('div', { class: 'row' }, [
m('div', { class: 'small-12 columns' }, [
m('label', [
'HTML (use <%- name %> and <%- title %> for values)',
m('textarea', {
rows: '4',
oninput: Content.vm.updated.bind(null, 'html'),
value: Content.vm.content.html || '',
})
]),
]),
m('div', { class: 'small-12 columns' }, [
m('label', [
'CSS',
m('textarea', {
rows: '4',
oninput: Content.vm.updated.bind(null, 'css'),
value: Content.vm.content.css || '',
})
]),
]),
m('div', { class: 'small-12 columns' }, [
m('label', [
'Name',
m('input[type=text]', {
oninput: Content.vm.updated.bind(null, 'name'),
value: Content.vm.content.name || '',
})
]),
]),
m('div', { class: 'small-12 columns' }, [
m('label', [
'Title',
m('input[type=text]', {
oninput: Content.vm.updated.bind(null, 'title'),
value: Content.vm.content.title || '',
})
]),
]),
m('a.button', {
onclick: Content.vm.display
}, 'Display'),
m('a.button.alert', {
onclick: Content.vm.hide
}, 'Hide'),
]),
])
}
module.exports = Content

View File

@ -0,0 +1,136 @@
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('')
},
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)
}
},
addProperty: function() {
if (!this.newProperty()) {
this.error = 'Please type in property name'
return
}
if (this.graphic.settings.properties.includes(this.newProperty())) {
this.error = 'A property with that name already exists'
return
}
this.graphic.settings.properties.push(this.newProperty())
this.newProperty('')
if (!this.graphic.settings.main) {
this.graphic.settings.main = this.graphic.settings.properties[0]
}
socket.emit('graphic.update', this.graphic)
},
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,
})
},
removeProperty: function(prop) {
this.graphic.settings.properties.splice(
this.graphic.settings.properties.indexOf(prop), 1)
socket.emit('graphic.update', this.graphic)
},
switchView: function() {
if (Graphic.vm.currentView === 'view') {
Graphic.vm.currentView = 'settings'
return
}
Graphic.vm.currentView = 'view'
},
})
module.exports = Graphic
require('./view')

View File

@ -0,0 +1,66 @@
const m = require('mithril')
const components = require('../../components')
exports.view = function(ctlr, graphic, vm) {
return [
m('label', [
'Text',
m('input[type=text]', {
value: vm.graphic.settings.text || '',
oninput: vm.updated.bind(vm, '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', [
'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'),
}),
]),
components.presetList(vm),
]
}
exports.settings = function(cltr, graphic, vm) {
return [
m('label', [
'Name',
m('input[type=text]', {
value: graphic.name,
oninput: vm.updated.bind(vm, 'name'),
}),
]),
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('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),
}, 'Delete graphic'),
]
}

View File

@ -0,0 +1,108 @@
const m = require('mithril')
const components = require('../../components')
exports.view = function(ctlr, graphic, vm) {
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.'),
]
}
return [
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'),
}),
])
),
components.presetList(vm),
]
}
exports.settings = function(cltr, graphic, vm) {
return [
m('label', [
'Name',
m('input[type=text]', {
value: graphic.name,
oninput: vm.updated.bind(vm, 'name'),
}),
]),
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('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[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')
),
]),
m('a.panel-graphic-delete.button.alert', {
onclick: vm.remove.bind(vm),
}, 'Delete graphic'),
]
}

View File

@ -0,0 +1,27 @@
const m = require('mithril')
const Graphic = require('./controller')
const components = require('../components')
const engines = {
text: require('./engine/text'),
countdown: require('./engine/countdown'),
}
Graphic.view = function(ctrl) {
graphic = Graphic.vm.graphic
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),
]
),
])
}

30
app/controller/header.js Normal file
View File

@ -0,0 +1,30 @@
const m = require('mithril')
const createModule = require('./module')
const socket = require('../socket')
const Header = createModule({
init: function() {
this.monitor('list', 'content.list', [])
},
hide: function(item) {
socket.emit('content.hide', {
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),
}, 'Hide'),
m('div.header-item-display', `${item.name} - ${item.display}`),
])
),
]),
] || '')
})
module.exports = Header

View File

@ -1,23 +1,24 @@
const m = require('mithril') const m = require('mithril')
const createModule = require('./module')
const Menu = { const Menu = createModule({
controller: function() { init: function() {
return {} this.monitor('list', 'graphic.all', [])
}, }
}, function(ctrl) {
view: function(ctrl) {
return m('div', [ return m('div', [
m('h3', 'Menu'), m('h3.container-header', 'Graphics'),
m('ul', [ m('div.container-panel.menu', [
m('li', [ m('ul.menu-list', [
m('a', { href: '/', config: m.route }, 'Home'), // m('a', { href: `/`, config: m.route }, 'Home'),
]), Menu.vm.list.map((item) =>
m('li', [ m('li.menu-item', [
m('a', { href: '/content', config: m.route }, 'Content'), m('a', { href: `/graphic/${item.id}`, config: m.route }, item.name),
]) ])
)
]),
m('a.menu-item-add', { href: '/add', config: m.route }, 'Add graphic' ),
]), ]),
]) ])
}, })
}
module.exports = Menu module.exports = Menu

51
app/controller/module.js Normal file
View File

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

View File

@ -1,39 +1,62 @@
const _ = require('lodash')
const socket = require('../socket') const socket = require('../socket')
const storage = {} const storage = {}
const events = {} const events = {}
// Listen on all events
let onevent = socket.onevent
socket.onevent = function(packet) {
let args = packet.data || []
onevent.call(this, packet) // original call
packet.data = ['*'].concat(args)
onevent.call(this, packet)
}
function genId(name, id) {
if (id) {
return `${name}:${id}`
}
return name
}
const store = { const store = {
get: function(name) { get: function(name, id) {
return storage[name] return storage[genId(name, id)]
}, },
set: function(name, value, dontSend) { listen: function(name, caller, id) {
storage[name] = value events[genId(name, id)] = caller
if (dontSend) {
if (events[name]) {
events[name]()
}
return
}
socket.emit('store', {
name,
value,
})
},
listen: function(name, caller) {
events[name] = caller
}, },
unlisten: function(name) { unlisten: function(name) {
delete events[name] delete events[name]
delete storage[name]
}, },
events: events,
storage: storage,
} }
socket.on('store', (data) => { socket.on('*', (event, data) => {
store.set(data.name, data.value, true) let name = genId(event, data && data.id)
if (events[name]) {
storage[name] = data
events[name]()
}
if (event.contains('single')) {
let check = event.replace('single', 'all')
if (events[name]) {
let index = _.findIndex(storage[check], { id: data.id })
if (index > -1) {
storage[check][index] = data
events[name]()
}
}
}
}) })
window.store = store
module.exports = store module.exports = store

67
app/frontend/countdown.js Normal file
View File

@ -0,0 +1,67 @@
module.exports = function(data) {
var element = document.createElement('div')
element.innerHTML = data.html
element.id = data.graphic.name
element.classList.add('root-element')
var styleElement = document.createElement('style')
styleElement.setAttribute('type', 'text/css')
styleElement.innerHTML = data.css
element.tag = styleElement
document.body.appendChild(element)
document.head.appendChild(styleElement)
window.setTimeout(function (){
element.classList.add('root-element-display')
}, 100)
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
}
if (timeElement) {
timeElement.tag = time
timer()
data.timer = setInterval(timer, 1000)
}
}

20
app/frontend/text.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = function(data) {
var element = document.createElement('div')
element.innerHTML = data.html
element.id = data.graphic.name
element.classList.add('root-element')
var styleElement = document.createElement('style')
styleElement.setAttribute('type', 'text/css')
styleElement.innerHTML = data.css
element.tag = styleElement
document.body.appendChild(element)
document.head.appendChild(styleElement)
window.setTimeout(function (){
element.classList.add('root-element-display')
}, 100)
}

View File

@ -16,12 +16,16 @@ require('./socket')
require('./controller/store') require('./controller/store')
const m = require('mithril') const m = require('mithril')
const Header = require('./controller/header')
const Menu = require('./controller/menu') const Menu = require('./controller/menu')
const Content = require('./controller/content') const Add = require('./controller/add')
const Graphic = require('./controller/graphic/controller')
m.mount(document.getElementById('header'), Header)
m.mount(document.getElementById('menu'), Menu) m.mount(document.getElementById('menu'), Menu)
m.route(document.getElementById('content'), '/', { m.route(document.getElementById('content'), '/', {
'/': {}, '/': {},
'/content': Content, '/add': Add,
'/graphic/:id': Graphic,
}); });

View File

@ -0,0 +1,29 @@
/* eslint-disable */
'use strict';
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('store'),
knex.schema.createTable('graphics', function(table) {
table.increments()
table.text('name')
table.text('engine')
table.text('settings')
table.boolean('is_deleted')
}),
knex.schema.createTable('presets', 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('graphics'),
knex.schema.dropTable('presets'),
]);
};

View File

@ -11,7 +11,7 @@
"watch-client:js": "watchify app/client.js -o public/client.js --debug", "watch-client:js": "watchify app/client.js -o public/client.js --debug",
"build": "npm run build-main:js && npm run build-client:js", "build": "npm run build-main:js && npm run build-client:js",
"build:watch": "parallelshell \"npm run watch-main:js\" \"npm run watch-client:js\"", "build:watch": "parallelshell \"npm run watch-main:js\" \"npm run watch-client:js\"",
"start": "node index.js", "start": "node index.js | bunyan",
"start:dev": "nodemon index.js | bunyan" "start:dev": "nodemon index.js | bunyan"
}, },
"repository": { "repository": {

View File

@ -22,6 +22,20 @@ body {
text-shadow: 2px 2px 1px #000000; text-shadow: 2px 2px 1px #000000;
} }
body {
font-family: Arial;
font-weight: normal;
text-shadow: 0px 0px 0px #000000;
font-size: 22pt;
}
html {
overflow: auto;
}
div
{
position: absolute;
}
.root-element { .root-element {
opacity: 0; opacity: 0;
transition: opacity 1s; transition: opacity 1s;

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>CasparCG Client</title> <title>CasparCG Client</title>
<link href="client.css" rel="stylesheet" /> <link href="/client.css" rel="stylesheet" />
</head> </head>
<body> <body>
<script src="client.js"></script> <script src="client.js"></script>

View File

@ -6,9 +6,12 @@
<link href="main.css" rel="stylesheet" /> <link href="main.css" rel="stylesheet" />
</head> </head>
<body> <body>
<div class="row"> <div class="expanded row">
<div class="small-3 columns" id="menu"></div> <div class="small-12 columns container" id="header"></div>
<div class="small-9 columns" id="content"></div> </div>
<div class="expanded row">
<div class="small-3 columns container" id="menu"></div>
<div class="small-9 columns container" id="content"></div>
</div> </div>
<script src="main.js"></script> <script src="main.js"></script>
</body> </body>

View File

@ -1,4 +1,181 @@
a.button { body {
background: #3f3f41;
color: #f1f1f1;
}
h4 {
margin-bottom: 2rem;
}
/* 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;
}
/* Header */
.header-list {
list-style-type: none;
margin: 0;
}
.header-item-hide {
float: right;
width: 5rem;
border-radius: 6px;
}
.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-delete {
}
.panel-graphic-settings {
float: right;
margin-right: 1rem;
}
.panel-graphic-property-item {
padding-left: 0;
}
.panel-graphic-preset-add {
margin-right: 1rem;
}
.panel-graphic-preset {
margin-top: 1rem;
list-style-type: none;
}
.panel-graphic-preset a {
width: 100%;
}
/* Components */
.error-box {
margin: 0rem 0rem 2rem 0;
color: #FF0000;
}
/* Inputs */
label {
color: #f1f1f1;
}
input[type="text"],
textarea {
background: #333337;
border-color: #3f3f3f;
color: #999999;
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: #999999;
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;
}
select
a.button {
margin: 0 1rem; margin: 0 1rem;
width: 10rem; width: 10rem;
} }

1
run.bat Normal file
View File

@ -0,0 +1 @@
npm run build && npm start

View File

@ -37,12 +37,8 @@ shelf.createModel = (attr, opts) => {
this.on('fetching', this.checkFetching) this.on('fetching', this.checkFetching)
}, },
remove() {
return this.destroy()
},
checkFetching(model, columns, options) { checkFetching(model, columns, options) {
// options.query.where({ is_deleted: false }) options.query.where({ is_deleted: false })
}, },
}) })
@ -52,7 +48,7 @@ shelf.createModel = (attr, opts) => {
return this.forge(data).save() return this.forge(data).save()
}, },
getSingle(id, withRelated = [], required = true) { getSingle(id, withRelated = [], require = true) {
let where = { id: Number(id) || 0 } let where = { id: Number(id) || 0 }
return this.query({ where }) return this.query({ where })
@ -60,6 +56,8 @@ shelf.createModel = (attr, opts) => {
}, },
getAll(where = {}, withRelated = []) { getAll(where = {}, withRelated = []) {
where.is_deleted = false
return this.query({ where }) return this.query({ where })
.fetchAll({ withRelated }) .fetchAll({ withRelated })
}, },

View File

@ -1,5 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import Store from './store/model'
import { reset, list } from './content/routes'
export function register(ctx, name, method) { export function register(ctx, name, method) {
if (_.isPlainObject(method)) { if (_.isPlainObject(method)) {
@ -10,9 +11,7 @@ export function register(ctx, name, method) {
} }
ctx.socket.on(name, async (data) => { ctx.socket.on(name, async (data) => {
if (name !== 'store') { ctx.log.info('Got event', name)
ctx.log.info(`Got event ${name}`)
}
try { try {
await method(ctx, data) await method(ctx, data)
@ -26,9 +25,6 @@ export function register(ctx, name, method) {
export async function newConnection(ctx) { export async function newConnection(ctx) {
ctx.log.info('Got new socket connection') ctx.log.info('Got new socket connection')
let data = await Store.getAll() list(ctx)
reset(ctx)
data.forEach(item =>
ctx.socket.emit('store', item.toJSON())
)
} }

View File

@ -1,18 +1,55 @@
import _ from 'lodash' import _ from 'lodash'
export const active = { }
function getSocket(ctx, all) {
if (all === true) return ctx.io
return ctx.socket
}
export function display(ctx, data) { export function display(ctx, data) {
let compiled = _.template(data.html) let compiled = _.template(data.graphic.settings.html)
let html = compiled(data) let html = compiled(data.data)
ctx.io.emit('client.display', { let payload = {
key: 'content', graphic: data.graphic,
html, html,
css: data.css, css: data.graphic.settings.css,
}) data: data.data,
}
active[data.graphic.name] = payload
ctx.io.emit('client.display', payload)
list(ctx, true)
} }
export function hide(ctx) { export function hide(ctx, data) {
delete active[data.name]
ctx.io.emit('client.hide', { ctx.io.emit('client.hide', {
key: 'content', name: data.name,
}) })
list(ctx, true)
}
function generateDisplayText(item) {
if (item.graphic.engine === 'countdown') {
return `${item.data[item.graphic.settings.main]} - ${item.data.countdown}`
}
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]),
}))
getSocket(ctx, all).emit('content.list', payload)
}
export function reset(ctx) {
ctx.socket.emit('client.reset', _.values(active))
} }

View File

@ -0,0 +1,4 @@
export function all(ctx) {
ctx.socket.emit('engine.all', ['text', 'countdown'])
}

View File

@ -0,0 +1,30 @@
import bookshelf from '../../bookshelf'
/* Graphic model:
{
id,
name,
engine,
settings,
is_deleted,
}
*/
const Graphic = bookshelf.createModel({
tableName: 'graphics',
format(attributes) {
attributes.settings = JSON.stringify(attributes.settings)
return attributes
},
parse(attributes) {
if (attributes.settings) {
attributes.settings = JSON.parse(attributes.settings)
}
return attributes
}
}, {
})
export default Graphic

View File

@ -0,0 +1,65 @@
import Graphic from './model'
function getSocket(ctx, all) {
if (all === true) return ctx.io
return ctx.socket
}
export async function all(ctx, all) {
let data = await Graphic.getAll()
getSocket(ctx, all).emit('graphic.all', data.toJSON())
}
export async function single(ctx, data, all) {
if (!data || !data.id) {
ctx.log.warn('called graphic get single but no id specified')
return
}
let graphic = await Graphic.getSingle(data.id)
getSocket(ctx, all).emit('graphic.single', graphic.toJSON())
}
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'
}
await Graphic.create(data)
await all(ctx, true)
}
export async function remove(ctx, data) {
if (!data || !data.id) {
ctx.log.warn('called graphic get single but no id specified')
return
}
let graphic = await Graphic.getSingle(data.id)
graphic.set({ is_deleted: true })
await graphic.save()
await all(ctx, true)
}
export async function update(ctx, data) {
if (!data || !data.id) {
ctx.log.warn('called graphic update but no id specified')
return
}
let graphic = await Graphic.getSingle(data.id)
graphic.set(data)
await graphic.save()
await single(ctx, data, true)
}

30
server/io/preset/model.js Normal file
View File

@ -0,0 +1,30 @@
import bookshelf from '../../bookshelf'
/* Preset model:
{
id,
graphic_id,
values,
sort,
is_deleted,
}
*/
const Preset = bookshelf.createModel({
tableName: 'presets',
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 Preset

View File

@ -0,0 +1,43 @@
import Preset from './model'
function getSocket(ctx, all) {
if (all === true) return ctx.io
return ctx.socket
}
export async function all(ctx, payload, all) {
let id = Number(payload.graphic_id || payload.id)
let data = await Preset.getAll({ graphic_id: id })
getSocket(ctx, all).emit(`preset.all:${id}`, data.toJSON())
}
export async function add(ctx, payload) {
payload.is_deleted = false
payload.sort = 1
let last = await Preset.query(q => {
q.where({ graphic_id: payload.graphic_id })
q.orderBy('sort', 'desc')
q.limit(1)
}).fetch({ require: false })
if (last) {
payload.sort = last.get('sort') + 1
}
await Preset.create(payload)
await all(ctx, payload, true)
}
export async function remove(ctx, payload) {
let preset = await Preset.getSingle(payload.id)
preset.set('is_deleted', true)
await preset.save()
await all(ctx, payload, true)
}

View File

@ -2,7 +2,9 @@ import logger from '../../log'
import { register, newConnection } from './connection' import { register, newConnection } from './connection'
import * as content from './content/routes' import * as content from './content/routes'
import * as store from './store/routes' import * as engine from './engine/routes'
import * as graphic from './graphic/routes'
import * as preset from './preset/routes'
function onConnection(server, data) { function onConnection(server, data) {
const io = server.socket const io = server.socket
@ -16,7 +18,9 @@ function onConnection(server, data) {
newConnection(ctx) newConnection(ctx)
register(ctx, 'content', content) register(ctx, 'content', content)
register(ctx, 'store', store.updateStore) register(ctx, 'engine', engine)
register(ctx, 'graphic', graphic)
register(ctx, 'preset', preset)
} }
export default onConnection export default onConnection

View File

@ -1,24 +0,0 @@
import bookshelf from '../../bookshelf'
const Store = bookshelf.createModel({
tableName: 'store',
format(attributes) {
attributes.value = JSON.stringify(attributes.value)
return attributes
},
parse(attributes) {
attributes.value = JSON.parse(attributes.value)
return attributes
}
}, {
getSingle(name, withRelated = [], required = true) {
let where = { name }
return this.query({ where })
.fetch({ require, withRelated })
},
})
export default Store

View File

@ -1,11 +0,0 @@
import Store from './model'
export async function updateStore(ctx, data) {
let item = await Store.getSingle(data.name)
item.set('value', data.value)
await item.save()
ctx.socket.broadcast.emit('store', item.toJSON())
}

2
update_run.bat Normal file
View File

@ -0,0 +1,2 @@
git pull
npm install && run