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