Implemented basic sup text support

dev
Jonatan Nilsson 2016-04-10 08:37:05 +00:00
parent c3e93ac603
commit 0d4fa14fec
35 changed files with 20292 additions and 3 deletions

7
.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"presets": ["es2015-node5"],
"plugins": [
"transform-async-to-generator",
"syntax-async-functions"
]
}

33
.eslintrc Normal file
View File

@ -0,0 +1,33 @@
{
"parser": "babel-eslint",
"extends": "airbnb/base",
"ecmaFeatures": {
"modules": false
},
"plugins": [
"mocha"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"semi": [2, "never"],
"max-len": [1, 120],
"prefer-const": 0,
"consistent-return": 0,
"no-param-reassign": [2, {"props": false}],
"no-use-before-define": [2, {"functions": false, "classes": true}],
"no-unused-vars": [
2,
{
"args": "none"
}
]
},
"globals": {
"describe": false,
"it": false,
"before": false,
"beforeEach": false,
"after": false,
"afterEach": false
}
}

41
app/client.js Normal file
View File

@ -0,0 +1,41 @@
const socket = require('./socket')
socket.on('client.display', (data) => {
let exists = document.getElementById(data.key)
if (exists) {
exists.tag.remove()
exists.remove()
}
let element = document.createElement('div')
element.innerHTML = data.html
element.id = data.key
element.classList.add('root-element')
let 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(() => {
element.classList.add('root-element-display')
}, 50)
})
socket.on('client.hide', (data) => {
let exists = document.getElementById(data.key)
if (exists) {
exists.classList.remove('root-element-display')
window.setTimeout(() => {
exists.tag.remove()
exists.remove()
}, 1500)
}
})

98
app/controller/content.js Normal file
View File

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

23
app/controller/menu.js Normal file
View File

@ -0,0 +1,23 @@
const m = require('mithril')
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'),
])
]),
])
},
}
module.exports = Menu

39
app/controller/store.js Normal file
View File

@ -0,0 +1,39 @@
const socket = require('../socket')
const storage = {}
const events = {}
const store = {
get: function(name) {
return storage[name]
},
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
},
unlisten: function(name) {
delete events[name]
},
}
socket.on('store', (data) => {
store.set(data.name, data.value, true)
})
module.exports = store

27
app/main.js Normal file
View File

@ -0,0 +1,27 @@
/**
* @license
* caspar-sup <https://filadelfia.is>
* Copyright 2015 Jonatan Nilsson <http://jonatan.nilsson.is/>
*
* Available under WTFPL License (http://www.wtfpl.net/txt/copying/)
*/
'use strict'
//Add debug components to window. Allows us to play with controls
//in the console.
window.components = {}
require('./socket')
require('./controller/store')
const m = require('mithril')
const Menu = require('./controller/menu')
const Content = require('./controller/content')
m.mount(document.getElementById('menu'), Menu)
m.route(document.getElementById('content'), '/', {
'/': {},
'/content': Content,
});

5
app/socket.js Normal file
View File

@ -0,0 +1,5 @@
const io = require('socket.io-client')
const socket = io()
module.exports = socket

60
config.js Normal file
View File

@ -0,0 +1,60 @@
import _ from 'lodash'
import nconf from 'nconf'
const pckg = require('./package.json')
// Helper method for global usage.
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
// Config follow the following priority check order:
// 1. Arguments
// 2. package.json
// 3. Enviroment variables
// 4. config/config.json
// 5. default settings
// Load arguments as highest priority
nconf.argv()
// Load package.json for name and such
let project = _.pick(pckg, ['name', 'version', 'description', 'author', 'license', 'homepage'])
// If we have global.it, there's a huge chance
// we're in test mode so we force node_env to be test.
if (typeof global.it === 'function') {
project.NODE_ENV = 'test'
}
// Load overrides as second priority
nconf.overrides(project)
// Load enviroment variables as third priority
nconf.env()
// Load any overrides from the appropriate config file
let configFile = 'config/config.json'
if (nconf.get('NODE_ENV') === 'test') {
configFile = 'config/config.test.json'
}
nconf.file('main', configFile)
// Load defaults
nconf.file('default', 'config/config.default.json')
// Final sanity checks
/* istanbul ignore if */
if (typeof global.it === 'function' & !nconf.inTest()) {
// eslint-disable-next-line no-console
console.log('Critical: potentially running test on production enviroment. Shutting down.')
process.exit(1)
}
module.exports = nconf

View File

@ -0,0 +1,23 @@
{
"NODE_ENV": "development",
"server": {
"port": 3000,
"host": "0.0.0.0"
},
"bunyan": {
"name": "keywe",
"streams": [{
"stream": "process.stdout",
"level": "debug"
}
]
},
"knex": {
"client": "sqlite3",
"connection": {
"filename" : "./db.sqlite"
},
"migrations": {
}
}
}

1
config/config.json Normal file
View File

@ -0,0 +1 @@
{}

BIN
db.sqlite Normal file

Binary file not shown.

38
index.js Normal file
View File

@ -0,0 +1,38 @@
'use strict'
require('babel-register')
let log = require('./log').default
function exitHandler(options, err) {
if (options.cleanup) {
log.warn('Application is shutting down')
}
if (err) {
log.error('An unhandled error occured')
log.error(err)
}
if (options.exit) {
log.warn('Application is exiting')
process.exit()
}
}
// do something when app is closing
process.on('exit', exitHandler.bind(null, { cleanup: true }))
// catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, { exit: true }))
// catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, { exit: true }))
// Run the database script automatically.
log.info('Running database integrity scan.')
let setup = require('./script/setup')
setup().then(() => {
require('./server')
}).catch((error) => {
log.error(error, 'Error while preparing database')
process.exit(1)
})

15
knexfile.js Normal file
View File

@ -0,0 +1,15 @@
'use strict'
require('babel-register')
const _ = require('lodash')
const config = require('./config')
let out = {}
// This is important for setup to run cleanly.
let knexConfig = _.cloneDeep(config.get('knex'))
knexConfig.pool = { min: 1, max: 1 }
out[config.get('NODE_ENV')] = knexConfig
module.exports = out

20
log.js Normal file
View File

@ -0,0 +1,20 @@
import _ from 'lodash'
import bunyan from 'bunyan'
import config from './config'
// Clone the settings as we will be touching
// on them slightly.
let settings = _.cloneDeep(config.get('bunyan'))
// Replace any instance of 'process.stdout' with the
// actual reference to the process.stdout.
for (let i = 0; i < settings.streams.length; i++) {
if (settings.streams[i].stream === 'process.stdout') {
settings.streams[i].stream = process.stdout
}
}
// Create our logger.
const logger = bunyan.createLogger(settings)
export default logger

View File

@ -0,0 +1,23 @@
/* eslint-disable */
'use strict';
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('store', function(table) {
table.increments()
table.text('name')
table.text('value')
}).then(() => {
return knex('store').insert({
name: 'content',
value: '{}'
})
}),
]);
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('store'),
]);
};

3
nodemon.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignore": ["app/*", "public/*"]
}

View File

@ -1,10 +1,18 @@
{
"name": "caspar-sup",
"version": "1.0.0",
"description": "",
"description": "CasparCG superimposed graphics project",
"main": "index.js",
"scripts": {
"test": "npm test"
"test": "npm test",
"build-main:js": "browserify app/main.js -o public/main.js --debug",
"watch-main:js": "watchify app/main.js -o public/main.js --debug",
"build-client:js": "browserify 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:watch": "parallelshell \"npm run watch-main:js\" \"npm run watch-client:js\"",
"start": "node index.js",
"start:dev": "nodemon index.js | bunyan"
},
"repository": {
"type": "git",
@ -20,5 +28,38 @@
"bugs": {
"url": "https://github.com/nfp-projects/caspar-sup/issues"
},
"homepage": "https://github.com/nfp-projects/caspar-sup#readme"
"homepage": "https://github.com/nfp-projects/caspar-sup#readme",
"dependencies": {
"app-root-path": "^1.0.0",
"babel-plugin-syntax-async-functions": "^6.5.0",
"babel-plugin-transform-async-to-generator": "^6.7.0",
"babel-preset-es2015-node5": "^1.1.2",
"babel-register": "^6.7.2",
"bookshelf": "^0.9.2",
"browserify": "^13.0.0",
"bunyan": "^1.7.1",
"knex": "^0.10.0",
"koa": "^2.0.0-alpha.3",
"koa-socket": "^4.3.0",
"koa-static": "^3.0.0",
"lodash": "^4.6.1",
"mithril": "^0.2.3",
"nconf": "^0.8.4",
"parallelshell": "^2.0.0",
"socket.io": "^1.4.5",
"socket.io-client": "^1.4.5",
"sqlite3": "^3.1.3"
},
"devDependencies": {
"assert-extended": "^1.0.1",
"babel-eslint": "^5.0.0",
"eslint": "^2.2.0",
"eslint-config-airbnb": "^6.1.0",
"eslint-plugin-mocha": "^2.0.0",
"live-reload": "^1.1.0",
"mocha": "^2.4.5",
"sinon": "^1.17.3",
"sinon-as-promised": "^4.0.0",
"watchify": "^3.7.0"
}
}

9
public/client.css Normal file
View File

@ -0,0 +1,9 @@
.root-element {
opacity: 0;
transition: opacity 1s;
}
.root-element-display {
opacity: 1;
transition: opacity 1s;
}

10
public/client.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CasparCG Client</title>
<link href="client.css" rel="stylesheet" />
</head>
<body>
<script src="client.js"></script>
</body>
</html>

7309
public/client.js Normal file

File diff suppressed because one or more lines are too long

2555
public/foundation.css vendored Normal file

File diff suppressed because it is too large Load Diff

15
public/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CasparCG Controller</title>
<link href="foundation.css" rel="stylesheet" />
<link href="main.css" rel="stylesheet" />
</head>
<body>
<div class="row">
<div class="small-3 columns" id="menu"></div>
<div class="small-9 columns" id="content"></div>
</div>
<script src="main.js"></script>
</body>
</html>

4
public/main.css Normal file
View File

@ -0,0 +1,4 @@
a.button {
margin: 0 1rem;
width: 10rem;
}

9604
public/main.js Normal file

File diff suppressed because one or more lines are too long

45
script/setup.js Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env node
/* eslint-disable no-console */
'use strict'
const _ = require('lodash')
const appRoot = require('app-root-path')
const config = require(appRoot.resolve('/config'))
let log = require(appRoot.resolve('/log')).default
// This is important for setup to run cleanly.
let knexConfig = _.cloneDeep(config.get('knex'))
knexConfig.pool = { min: 1, max: 1 }
let knex = require('knex')(knexConfig)
log.info(knexConfig, 'Connected to database')
let setup = module.exports = () =>
knex.migrate.latest({
directory: appRoot.resolve('/migrations'),
})
.then((result) => {
if (result[1].length === 0) {
return log.info('Database is up to date')
}
for (let i = 0; i < result[1].length; i++) {
log.info('Applied migration from', result[1][i].substr(result[1][i].lastIndexOf('\\') + 1))
}
return knex.destroy()
})
if (require.main === module) {
// Since we're running this as a script, we should output
// directly to the console.
log = console
log.info = console.log.bind(console)
setup().then(() => {
log.info('Setup ran successfully.')
}).catch((error) => {
log.error(error, 'Error while running setup.')
}).then(() => {
process.exit(0)
})
}

71
server/bookshelf.js Normal file
View File

@ -0,0 +1,71 @@
import _ from 'lodash'
import knex from 'knex'
import bookshelf from 'bookshelf'
import config from '../config'
import log from '../log'
let host = config.get('knex:connection')
/* istanbul ignore if */
if (host.match && host.match(/@[^/]+/)) {
host = host.match(/@[^/]+/)[0]
}
log.info(host, 'Connecting to DB')
const client = knex(config.get('knex'))
// Check if we're running tests while connected to
// potential production environment.
/* istanbul ignore if */
if (config.get('NODE_ENV') === 'test' &&
config.get('knex:connection:database') !== 'test' ||
config.get('knex:connection:connection')) {
// There is an offchance that we're running tests on
// production database. Exit NOW!
log.error('Critical: potentially running test on production enviroment. Shutting down.')
process.exit(1)
}
let shelf = bookshelf(client)
// Helper method to create models
shelf.createModel = (attr, opts) => {
// Create default attributes to all models
let attributes = _.defaults(attr, {
initialize() {
this.on('fetching', this.checkFetching)
},
remove() {
return this.destroy()
},
checkFetching(model, columns, options) {
// options.query.where({ is_deleted: false })
},
})
// Create default options for all models
let options = _.defaults(opts, {
create(data) {
return this.forge(data).save()
},
getSingle(id, withRelated = [], required = true) {
let where = { id: Number(id) || 0 }
return this.query({ where })
.fetch({ require, withRelated })
},
getAll(where = {}, withRelated = []) {
return this.query({ where })
.fetchAll({ withRelated })
},
})
return shelf.Model.extend(attributes, options)
}
export default shelf

1
server/index.js Normal file
View File

@ -0,0 +1 @@
import './koa'

34
server/io/connection.js Normal file
View File

@ -0,0 +1,34 @@
import _ from 'lodash'
import Store from './store/model'
export function register(ctx, name, method) {
if (_.isPlainObject(method)) {
Object.keys(method).forEach(key => {
register(ctx, [name, key].join('.'), method[key])
})
return
}
ctx.socket.on(name, async (data) => {
if (name !== 'store') {
ctx.log.info(`Got event ${name}`)
}
try {
await method(ctx, data)
}
catch (error) {
ctx.log.error(error, `Error processing ${name}`)
}
})
}
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())
)
}

View File

@ -0,0 +1,18 @@
import _ from 'lodash'
export function display(ctx, data) {
let compiled = _.template(data.html)
let html = compiled(data)
ctx.io.emit('client.display', {
key: 'content',
html,
css: data.css,
})
}
export function hide(ctx) {
ctx.io.emit('client.hide', {
key: 'content',
})
}

22
server/io/router.js Normal file
View File

@ -0,0 +1,22 @@
import logger from '../../log'
import { register, newConnection } from './connection'
import * as content from './content/routes'
import * as store from './store/routes'
function onConnection(server, data) {
const io = server.socket
const socket = data.socket
const log = logger.child({
id: socket.id,
})
let ctx = { io, socket, log }
newConnection(ctx)
register(ctx, 'content', content)
register(ctx, 'store', store.updateStore)
}
export default onConnection

24
server/io/store/model.js Normal file
View File

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

11
server/io/store/routes.js Normal file
View File

@ -0,0 +1,11 @@
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())
}

20
server/koa.js Normal file
View File

@ -0,0 +1,20 @@
import serve from 'koa-static'
import Koa from 'koa'
import socket from 'koa-socket'
import config from '../config'
import log from '../log'
import router from './io/router'
import { bunyanLogger, errorHandler } from './middlewares'
const app = new Koa()
const io = new socket()
io.attach(app)
io.on('connection', router.bind(this, io))
app.use(bunyanLogger(log))
app.use(errorHandler())
app.use(serve('public'))
app.listen(config.get('server:port'))

40
server/middlewares.js Normal file
View File

@ -0,0 +1,40 @@
export function bunyanLogger(logger) {
return async (ctx, next) => {
ctx.log = logger.child({
})
const d1 = new Date().getTime()
await next()
const d2 = new Date().getTime()
let level = 'info'
if (ctx.status >= 400) {
level = 'warn'
}
if (ctx.status >= 500) {
level = 'error'
}
ctx.log[level]({
duration: (d2 - d1),
status: ctx.res.statusCode,
}, `<-- ${ctx.request.method} ${ctx.request.url}`)
}
}
export function errorHandler() {
return async (ctx, next) => {
try {
await next()
} catch(err) {
ctx.log.error(err, 'Unknown error occured')
ctx.status = 500
ctx.render('error', {
error: err,
})
}
}
}