From 918f350dffd3839fc09d01b26882e99da60c88b8 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Thu, 14 Apr 2016 04:01:51 +0000 Subject: [PATCH] Implemented version 1.0 --- app/client.js | 44 ++--- app/controller/add.js | 52 ++++++ app/controller/components.js | 41 +++++ app/controller/content.js | 98 ----------- app/controller/graphic/controller.js | 136 +++++++++++++++ app/controller/graphic/engine/countdown.js | 66 ++++++++ app/controller/graphic/engine/text.js | 108 ++++++++++++ app/controller/graphic/view.js | 27 +++ app/controller/header.js | 30 ++++ app/controller/menu.js | 39 ++--- app/controller/module.js | 51 ++++++ app/controller/store.js | 67 +++++--- app/frontend/countdown.js | 67 ++++++++ app/frontend/text.js | 20 +++ app/main.js | 8 +- migrations/20160412123858_graphics.js | 29 ++++ package.json | 2 +- public/client.css | 14 ++ public/client.html | 2 +- public/index.html | 9 +- public/main.css | 185 ++++++++++++++++++++- run.bat | 1 + server/bookshelf.js | 10 +- server/io/connection.js | 14 +- server/io/content/routes.js | 53 +++++- server/io/engine/routes.js | 4 + server/io/graphic/model.js | 30 ++++ server/io/graphic/routes.js | 65 ++++++++ server/io/preset/model.js | 30 ++++ server/io/preset/routes.js | 43 +++++ server/io/router.js | 8 +- server/io/store/model.js | 24 --- server/io/store/routes.js | 11 -- update_run.bat | 2 + 34 files changed, 1161 insertions(+), 229 deletions(-) create mode 100644 app/controller/add.js create mode 100644 app/controller/components.js delete mode 100644 app/controller/content.js create mode 100644 app/controller/graphic/controller.js create mode 100644 app/controller/graphic/engine/countdown.js create mode 100644 app/controller/graphic/engine/text.js create mode 100644 app/controller/graphic/view.js create mode 100644 app/controller/header.js create mode 100644 app/controller/module.js create mode 100644 app/frontend/countdown.js create mode 100644 app/frontend/text.js create mode 100644 migrations/20160412123858_graphics.js create mode 100644 run.bat create mode 100644 server/io/engine/routes.js create mode 100644 server/io/graphic/model.js create mode 100644 server/io/graphic/routes.js create mode 100644 server/io/preset/model.js create mode 100644 server/io/preset/routes.js delete mode 100644 server/io/store/model.js delete mode 100644 server/io/store/routes.js create mode 100644 update_run.bat diff --git a/app/client.js b/app/client.js index 09af0b4..2fd077a 100644 --- a/app/client.js +++ b/app/client.js @@ -1,36 +1,38 @@ var socket = require('./socket') -socket.on('client.display', function(data) { - var exists = document.getElementById(data.key) +var engines = { + text: require('./frontend/text'), + countdown: require('./frontend/countdown'), +} + +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 element = document.createElement('div') - element.innerHTML = data.html - element.id = data.key - element.classList.add('root-element') + let engine = data.graphic.engine - var styleElement = document.createElement('style') - styleElement.setAttribute('type', 'text/css') - styleElement.innerHTML = data.css + if (engines[engine]) { + engines[engine](data) + } +} - element.tag = styleElement - - document.body.appendChild(element) - document.head.appendChild(styleElement) - - window.setTimeout(function (){ - element.classList.add('root-element-display') - }, 50) -}) +socket.on('client.display', display) socket.on('client.hide', function (data) { - var exists = document.getElementById(data.key) + var exists = document.getElementById(data.name) if (exists) { + current.splice(current.indexOf(data.name), 1) + exists.classList.remove('root-element-display') window.setTimeout(function () { @@ -39,3 +41,7 @@ socket.on('client.hide', function (data) { }, 1500) } }) + +socket.on('client.reset', function(data) { + data.forEach(display) +}) diff --git a/app/controller/add.js b/app/controller/add.js new file mode 100644 index 0000000..cb39b18 --- /dev/null +++ b/app/controller/add.js @@ -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 diff --git a/app/controller/components.js b/app/controller/components.js new file mode 100644 index 0000000..c6c7781 --- /dev/null +++ b/app/controller/components.js @@ -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') + ), + ]) + ]) + )) + ] +} diff --git a/app/controller/content.js b/app/controller/content.js deleted file mode 100644 index 27c031e..0000000 --- a/app/controller/content.js +++ /dev/null @@ -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 diff --git a/app/controller/graphic/controller.js b/app/controller/graphic/controller.js new file mode 100644 index 0000000..70fcdae --- /dev/null +++ b/app/controller/graphic/controller.js @@ -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') diff --git a/app/controller/graphic/engine/countdown.js b/app/controller/graphic/engine/countdown.js new file mode 100644 index 0000000..96691a5 --- /dev/null +++ b/app/controller/graphic/engine/countdown.js @@ -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', `
`), + m('textarea', { + rows: '4', + oninput: vm.updated.bind(null, 'settings.html'), + value: graphic.settings.html || '', + }), + m('p', `
`), + ]), + 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'), + ] +} + diff --git a/app/controller/graphic/engine/text.js b/app/controller/graphic/engine/text.js new file mode 100644 index 0000000..d2a6d16 --- /dev/null +++ b/app/controller/graphic/engine/text.js @@ -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', `
`), + m('textarea', { + rows: '4', + oninput: vm.updated.bind(null, 'settings.html'), + value: graphic.settings.html || '', + }), + m('p', `
`), + ]), + 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'), + ] +} diff --git a/app/controller/graphic/view.js b/app/controller/graphic/view.js new file mode 100644 index 0000000..9376c7e --- /dev/null +++ b/app/controller/graphic/view.js @@ -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), + ] + ), + ]) +} diff --git a/app/controller/header.js b/app/controller/header.js new file mode 100644 index 0000000..555d21b --- /dev/null +++ b/app/controller/header.js @@ -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 diff --git a/app/controller/menu.js b/app/controller/menu.js index 1bce181..333b05b 100644 --- a/app/controller/menu.js +++ b/app/controller/menu.js @@ -1,23 +1,24 @@ const m = require('mithril') +const createModule = require('./module') -const Menu = { - controller: function() { - return {} - }, - - view: function(ctrl) { - return m('div', [ - m('h3', 'Menu'), - m('ul', [ - m('li', [ - m('a', { href: '/', config: m.route }, 'Home'), - ]), - m('li', [ - m('a', { href: '/content', config: m.route }, 'Content'), - ]) +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' ), + ]), + ]) +}) module.exports = Menu diff --git a/app/controller/module.js b/app/controller/module.js new file mode 100644 index 0000000..b9e9aef --- /dev/null +++ b/app/controller/module.js @@ -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 diff --git a/app/controller/store.js b/app/controller/store.js index 2cb4ecd..069b7f0 100644 --- a/app/controller/store.js +++ b/app/controller/store.js @@ -1,39 +1,62 @@ +const _ = require('lodash') const socket = require('../socket') const storage = {} 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 = { - get: function(name) { - return storage[name] + get: function(name, id) { + return storage[genId(name, id)] }, - set: function(name, value, dontSend) { - storage[name] = value - - if (dontSend) { - if (events[name]) { - events[name]() - } - return - } - - socket.emit('store', { - name, - value, - }) - }, - - listen: function(name, caller) { - events[name] = caller + listen: function(name, caller, id) { + events[genId(name, id)] = caller }, unlisten: function(name) { delete events[name] + delete storage[name] }, + + events: events, + storage: storage, } -socket.on('store', (data) => { - store.set(data.name, data.value, true) +socket.on('*', (event, data) => { + 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 diff --git a/app/frontend/countdown.js b/app/frontend/countdown.js new file mode 100644 index 0000000..308d2d1 --- /dev/null +++ b/app/frontend/countdown.js @@ -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) + } +} diff --git a/app/frontend/text.js b/app/frontend/text.js new file mode 100644 index 0000000..0fb2e92 --- /dev/null +++ b/app/frontend/text.js @@ -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) +} diff --git a/app/main.js b/app/main.js index 4363236..58fbbdb 100644 --- a/app/main.js +++ b/app/main.js @@ -16,12 +16,16 @@ require('./socket') require('./controller/store') const m = require('mithril') +const Header = require('./controller/header') 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.route(document.getElementById('content'), '/', { '/': {}, - '/content': Content, + '/add': Add, + '/graphic/:id': Graphic, }); diff --git a/migrations/20160412123858_graphics.js b/migrations/20160412123858_graphics.js new file mode 100644 index 0000000..f186ea9 --- /dev/null +++ b/migrations/20160412123858_graphics.js @@ -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'), + ]); +}; diff --git a/package.json b/package.json index 5d3a377..558149b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "watch-client:js": "watchify app/client.js -o public/client.js --debug", "build": "npm run build-main:js && npm run build-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" }, "repository": { diff --git a/public/client.css b/public/client.css index b878957..2fce657 100644 --- a/public/client.css +++ b/public/client.css @@ -22,6 +22,20 @@ body { 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 { opacity: 0; transition: opacity 1s; diff --git a/public/client.html b/public/client.html index 1bd9189..ef20204 100644 --- a/public/client.html +++ b/public/client.html @@ -2,7 +2,7 @@ CasparCG Client - + diff --git a/public/index.html b/public/index.html index 975a48b..146028a 100644 --- a/public/index.html +++ b/public/index.html @@ -6,9 +6,12 @@ -
- -
+
+ +
+
+ +
diff --git a/public/main.css b/public/main.css index b3d3482..5cff87d 100644 --- a/public/main.css +++ b/public/main.css @@ -1,4 +1,181 @@ -a.button { - margin: 0 1rem; - width: 10rem; -} \ No newline at end of file +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,') + } + + 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; + width: 10rem; + } diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..062fd9c --- /dev/null +++ b/run.bat @@ -0,0 +1 @@ +npm run build && npm start \ No newline at end of file diff --git a/server/bookshelf.js b/server/bookshelf.js index 42e98b2..c62472a 100644 --- a/server/bookshelf.js +++ b/server/bookshelf.js @@ -37,12 +37,8 @@ shelf.createModel = (attr, opts) => { this.on('fetching', this.checkFetching) }, - remove() { - return this.destroy() - }, - 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() }, - getSingle(id, withRelated = [], required = true) { + getSingle(id, withRelated = [], require = true) { let where = { id: Number(id) || 0 } return this.query({ where }) @@ -60,6 +56,8 @@ shelf.createModel = (attr, opts) => { }, getAll(where = {}, withRelated = []) { + where.is_deleted = false + return this.query({ where }) .fetchAll({ withRelated }) }, diff --git a/server/io/connection.js b/server/io/connection.js index 4b94905..1521650 100644 --- a/server/io/connection.js +++ b/server/io/connection.js @@ -1,5 +1,6 @@ import _ from 'lodash' -import Store from './store/model' + +import { reset, list } from './content/routes' export function register(ctx, name, method) { if (_.isPlainObject(method)) { @@ -10,9 +11,7 @@ export function register(ctx, name, method) { } ctx.socket.on(name, async (data) => { - if (name !== 'store') { - ctx.log.info(`Got event ${name}`) - } + ctx.log.info('Got event', name) try { await method(ctx, data) @@ -26,9 +25,6 @@ export function register(ctx, name, method) { export async function newConnection(ctx) { ctx.log.info('Got new socket connection') - let data = await Store.getAll() - - data.forEach(item => - ctx.socket.emit('store', item.toJSON()) - ) + list(ctx) + reset(ctx) } diff --git a/server/io/content/routes.js b/server/io/content/routes.js index 9e6607d..d9daf7f 100644 --- a/server/io/content/routes.js +++ b/server/io/content/routes.js @@ -1,18 +1,55 @@ import _ from 'lodash' +export const active = { } + +function getSocket(ctx, all) { + if (all === true) return ctx.io + return ctx.socket +} + export function display(ctx, data) { - let compiled = _.template(data.html) - let html = compiled(data) + let compiled = _.template(data.graphic.settings.html) + let html = compiled(data.data) - ctx.io.emit('client.display', { - key: 'content', + let payload = { + graphic: data.graphic, 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', { - 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)) } diff --git a/server/io/engine/routes.js b/server/io/engine/routes.js new file mode 100644 index 0000000..9b3f709 --- /dev/null +++ b/server/io/engine/routes.js @@ -0,0 +1,4 @@ + +export function all(ctx) { + ctx.socket.emit('engine.all', ['text', 'countdown']) +} diff --git a/server/io/graphic/model.js b/server/io/graphic/model.js new file mode 100644 index 0000000..bd9064d --- /dev/null +++ b/server/io/graphic/model.js @@ -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 diff --git a/server/io/graphic/routes.js b/server/io/graphic/routes.js new file mode 100644 index 0000000..f2302f2 --- /dev/null +++ b/server/io/graphic/routes.js @@ -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 = `countdown appears here` + 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) +} diff --git a/server/io/preset/model.js b/server/io/preset/model.js new file mode 100644 index 0000000..9597676 --- /dev/null +++ b/server/io/preset/model.js @@ -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 diff --git a/server/io/preset/routes.js b/server/io/preset/routes.js new file mode 100644 index 0000000..65b5752 --- /dev/null +++ b/server/io/preset/routes.js @@ -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) +} diff --git a/server/io/router.js b/server/io/router.js index ffa7788..d5adf07 100644 --- a/server/io/router.js +++ b/server/io/router.js @@ -2,7 +2,9 @@ import logger from '../../log' import { register, newConnection } from './connection' 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) { const io = server.socket @@ -16,7 +18,9 @@ function onConnection(server, data) { newConnection(ctx) register(ctx, 'content', content) - register(ctx, 'store', store.updateStore) + register(ctx, 'engine', engine) + register(ctx, 'graphic', graphic) + register(ctx, 'preset', preset) } export default onConnection diff --git a/server/io/store/model.js b/server/io/store/model.js deleted file mode 100644 index 0c0e5d0..0000000 --- a/server/io/store/model.js +++ /dev/null @@ -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 diff --git a/server/io/store/routes.js b/server/io/store/routes.js deleted file mode 100644 index d234e4f..0000000 --- a/server/io/store/routes.js +++ /dev/null @@ -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()) -} diff --git a/update_run.bat b/update_run.bat new file mode 100644 index 0000000..ea530d4 --- /dev/null +++ b/update_run.bat @@ -0,0 +1,2 @@ +git pull +npm install && run \ No newline at end of file