refactored everything to use lowdb instead of sqlite

This commit is contained in:
Jonatan Nilsson 2020-04-06 22:47:58 +00:00
parent c1f87628d4
commit 27871c9ed4
53 changed files with 5339 additions and 1163 deletions

6
.gitignore vendored
View file

@ -33,12 +33,6 @@ node_modules
.node_repl_history
db.sqlite
public/client.css
public/client.css.map
public/client.js
public/main.css
public/main.css.map
public/main.js
public/status.css
public/status.css.map
public/status.js

View file

@ -1,9 +1,9 @@
import _ from 'lodash'
import knex from 'knex'
import bookshelf from 'bookshelf'
import config from '../config'
import log from '../log'
import defaults from './defaults.mjs'
import config from './config.mjs'
import log from './log.mjs'
let host = config.get('knex:connection')
@ -28,7 +28,7 @@ let shelf = bookshelf(client)
// Helper method to create models
shelf.createModel = (attr, opts) => {
// Create default attributes to all models
let attributes = _.defaults(attr, {
let attributes = defaults(attr, {
/**
* Initialize a new instance of model. This does not get called when
* relations to this model is being fetched though.
@ -55,7 +55,7 @@ shelf.createModel = (attr, opts) => {
})
// Create default options for all models
let options = _.defaults(opts, {
let options = defaults(opts, {
/**
* Create new model object in database.
*

View file

@ -1,5 +1,7 @@
import Settings from '../settings/model'
import { CasparCG, AMCP } from 'casparcg-connection'
import CasparConnection from 'casparcg-connection'
const CasparCG = CasparConnection.CasparCG
const AMCP = CasparConnection.AMCP
const timeoutDuration = 5000
@ -11,23 +13,20 @@ let casparIsPlaying
let casparIsConnected
let currentHost
export async function initialise(log, socket) {
export function initialise(log, db, socket) {
io = socket.socket
logger = log
db = db
return connect()
connect(db)
}
export async function connect() {
currentHost = await Settings.getValue('casparcg')
export function connect(db) {
currentHost = db.get('settings').value().casparhost
casparIsPlaying = false
casparIsConnected = false
logger.info('CasparCG: Connectiong to', currentHost + ':' + 5250)
if (connection && connection.close) {
await connection.close()
}
connection = new CasparCG({
host: currentHost,
port: 5250,
@ -37,6 +36,7 @@ export async function connect() {
logger.error(err, 'CasparCG: Error')
},
onConnectionStatus: data => {
if (casparIsPlaying) return
casparIsConnected = data.connected
if (!casparIsConnected) {
@ -45,9 +45,10 @@ export async function connect() {
}
},
onConnected: async connected => {
if (casparIsPlaying) return
logger.info('CasparCG: connected', connected)
if (!casparIsPlaying) {
startPlaying().then()
startPlaying(db).then()
} else {
logger.warn('CasparCG: Stopped from starting play again.')
}
@ -63,8 +64,8 @@ export function currentStatus(e) {
}
}
export async function startPlaying() {
let ip = 'localhost'
export async function startPlaying(db) {
let ip = db.get('settings').value().casparplayhost
// Check if we lost connection while attempting to start playing
if (!connection.connected) {
@ -75,7 +76,7 @@ export async function startPlaying() {
try {
// Send a play command
let command = `PLAY 1-100 [HTML] "http://${ip}:3000/client.html" CUT 1 LINEAR RIGHT`
let command = `PLAY 1-100 [HTML] "http://${ip}/client.html" CUT 1 LINEAR RIGHT`
logger.info(`CasparCG Command: ${command}`)
await connection.do(new AMCP.CustomCommand(command))
success = true
@ -94,6 +95,12 @@ export async function startPlaying() {
// We are playing, notify all clients
io.emit('casparcg.status', currentStatus())
logger.info('CasparCG: client is up and playing')
/* console.log(connection)
for (var key in connection) {
console.log(key, '=', typeof(connection[key]))
} */
connection.autoConnect = false
// connection.close()
} else {
// Unknown error occured
casparIsPlaying = false

View file

@ -1,4 +1,4 @@
import { currentStatus } from './client'
import { currentStatus } from './client.mjs'
export async function casparConnection(ctx) {
ctx.socket.emit('casparcg.status', currentStatus())

View file

@ -1,6 +1,5 @@
import _ from 'lodash'
import nconf from 'nconf'
const pckg = require('./package.json')
import { readFileSync } from 'fs'
// Helper method for global usage.
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
@ -18,8 +17,15 @@ nconf.argv()
// Load package.json for name and such
let project = _.pick(pckg, ['name', 'version', 'description', 'author', 'license', 'homepage'])
let pckg = JSON.parse(readFileSync('./package.json'))
let project = {
name: pckg.name,
version: pckg.version,
description: pckg.description,
author: pckg.author,
license: pckg.license,
homepage: pckg.homepage,
}
// If we have global.it, there's a huge chance
// we're in test mode so we force node_env to be test.
@ -57,4 +63,5 @@ if (typeof global.it === 'function' & !nconf.inTest()) {
console.log('Critical: potentially running test on production enviroment. Shutting down.')
process.exit(1)
}
module.exports = nconf
export default nconf

View file

@ -1,4 +1,4 @@
import { reset, list } from './routes'
import { reset, list } from './routes.mjs'
export async function contentConnection(ctx) {
ctx.log.info('Got new socket connection')

View file

@ -1,4 +1,4 @@
import bookshelf from '../bookshelf'
import bookshelf from '../bookshelf.mjs'
/* Content model:
{

View file

@ -1,5 +1,4 @@
import _ from 'lodash'
import Content from './model'
import template from 'lodash.template'
export const active = { }
@ -14,13 +13,17 @@ function getSocket(ctx, all) {
* Display a specific graphic content
*/
export async function display(ctx, data) {
let compiled = _.template(data.graphic.settings.html)
let compiled = template(data.graphic.settings.html)
let html = compiled(data.data)
let old = await Content.getSingle(data.graphic.name)
// let old = await Content.getSingle(data.graphic.name)
let playing = ctx.db.get('playing')
let old = playing.find({ name: data.graphic.name }).value()
if (old) {
await old.destroy()
await playing.removeById(old.id).write()
}
let payload = {
@ -32,9 +35,9 @@ export async function display(ctx, data) {
is_deleted: false,
}
let content = await Content.create(payload)
await playing.insert(payload).write()
ctx.io.emit('client.display', content.toJSON())
ctx.io.emit('client.display', playing.find({ name: data.graphic.name }).value())
list(ctx, true)
}
@ -45,11 +48,13 @@ export async function display(ctx, data) {
* Hide a specific graphic content
*/
export async function hide(ctx, data) {
let content = await Content.getSingle(data.name)
let playing = ctx.db.get('playing')
if (!content) return
let old = playing.find({ name: data.name }).value()
await content.destroy()
if (!old) return
await playing.removeById(old.id).write()
ctx.io.emit('client.hide', {
name: data.name,
@ -63,7 +68,7 @@ function generateDisplayText(item) {
// return `${item.data.text} - ${item.data.finished}`
// }
try {
return _.template(item.graphic.settings.main)(item.data)
return template(item.graphic.settings.main)(item.data)
} catch (e) {
return `Error creating display: ${e.message}`
}
@ -76,12 +81,14 @@ function generateDisplayText(item) {
* Send a name list of all active graphics
*/
export async function list(ctx, all) {
let allContent = await Content.getAll()
let allContent = ctx.db.get('playing').value()
let payload = await Promise.all(allContent.map(item => ({
name: item.get('name'),
display: generateDisplayText(item.toJSON()),
})))
let payload = await Promise.all(allContent.map(function(item) {
return {
name: item.name,
display: generateDisplayText(item),
}
}))
getSocket(ctx, all).emit('content.list', payload)
}
@ -93,7 +100,7 @@ export async function list(ctx, all) {
* Send actual graphics of all active graphics
*/
export async function reset(ctx) {
let allContent = await Content.getAll()
let allContent = ctx.db.get('playing').value()
ctx.socket.emit('client.reset', allContent.toJSON())
ctx.socket.emit('client.reset', allContent)
}

162
api/db.mjs Normal file
View file

@ -0,0 +1,162 @@
import lowdb from 'lowdb'
import FileAsync from 'lowdb/adapters/FileAsync.js'
import log from './log.mjs'
let lastId = -1
// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js
// from package lodash-id
const lodashId = {
// Empties properties
__empty: function (doc) {
this.forEach(doc, function (value, key) {
delete doc[key]
})
},
// Copies properties from an object to another
__update: function (dest, src) {
this.forEach(src, function (value, key) {
dest[key] = value
})
},
// Removes an item from an array
__remove: function (array, item) {
var index = this.indexOf(array, item)
if (index !== -1) array.splice(index, 1)
},
__id: function () {
var id = this.id || 'id'
return id
},
getById: function (collection, id) {
var self = this
return this.find(collection, function (doc) {
if (self.has(doc, self.__id())) {
return doc[self.__id()] === id
}
})
},
createId: function (collection, doc) {
let next = new Date().getTime()
if (next <= lastId) {
next = lastId + 1
}
lastId = next
return next
},
insert: function (collection, doc) {
doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc)
var d = this.getById(collection, doc[this.__id()])
if (d) throw new Error('Insert failed, duplicate id')
collection.push(doc)
return doc
},
upsert: function (collection, doc) {
if (doc[this.__id()]) {
// id is set
var d = this.getById(collection, doc[this.__id()])
if (d) {
// replace properties of existing object
this.__empty(d)
this.assign(d, doc)
} else {
// push new object
collection.push(doc)
}
} else {
// create id and push new object
doc[this.__id()] = this.createId(collection, doc)
collection.push(doc)
}
return doc
},
updateById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
this.assign(doc, attrs, {id: doc.id})
}
return doc
},
updateWhere: function (collection, predicate, attrs) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.assign(doc, attrs, {id: doc.id})
})
return docs
},
replaceById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
var docId = doc.id
this.__empty(doc)
this.assign(doc, attrs, {id: docId})
}
return doc
},
removeById: function (collection, id) {
var doc = this.getById(collection, id)
this.__remove(collection, doc)
return doc
},
removeWhere: function (collection, predicate) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.__remove(collection, doc)
})
return docs
}
}
const adapter = new FileAsync('db.json')
export default function GetDB() {
return lowdb(adapter)
.then(function(db) {
db._.mixin(lodashId)
db.defaults({
graphics: [],
presets: [],
playing: [],
schedule: [],
settings: {
casparplayhost: 'localhost:3000',
casparhost: 'host.docker.internal',
},
version: 1,
trash: [],
})
.write()
.then(
function() { },
function(e) { log.error(e, 'Error writing defaults to lowdb') }
)
return db
})
}

34
api/defaults.mjs Normal file
View file

@ -0,0 +1,34 @@
// taken from isobject npm library
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false
}
export default function defaults(options, def) {
let out = { }
if (options) {
Object.keys(options || {}).forEach(key => {
out[key] = options[key]
if (Array.isArray(out[key])) {
out[key] = out[key].map(item => {
if (isObject(item)) return defaults(item)
return item
})
} else if (out[key] && typeof out[key] === 'object') {
out[key] = defaults(options[key], def && def[key])
}
})
}
if (def) {
Object.keys(def).forEach(function(key) {
if (typeof out[key] === 'undefined') {
out[key] = def[key]
}
})
}
return out
}

View file

@ -1,4 +1,4 @@
import bookshelf from '../bookshelf'
import bookshelf from '../bookshelf.mjs'
/* Graphic model:
{

View file

@ -1,14 +1,12 @@
import Graphic from './model'
/*
* Event: 'graphic.all'
*
* Request all graphics in store
*/
export async function all(ctx) {
let data = await Graphic.getAll()
let data = ctx.db.get('graphics').value()
ctx.socket.emit('graphic.all', data.toJSON())
ctx.socket.emit('graphic.all', data)
}
/*
@ -22,9 +20,9 @@ export async function single(ctx, data) {
return
}
let graphic = await Graphic.getSingle(data.id)
let graphic = ctx.db.get('graphics').getById(Number(data.id)).value()
ctx.socket.emit('graphic.single', graphic.toJSON())
ctx.socket.emit('graphic.single', graphic)
}
/*
@ -37,16 +35,17 @@ export async function single(ctx, data) {
*/
export async function create(ctx, data) {
data.settings = {}
data.is_deleted = false
if (data.engine === 'countdown') {
data.settings.html = `<span id="${data.name}-countdown-timer">countdown appears here</span>`
data.settings.main = '<%- text %> - <%- finished %>'
}
let graphic = await Graphic.create(data)
ctx.io.emit('graphic.single', graphic.toJSON())
let graphics = ctx.db.get('graphics').insert(data)
let graphic = graphics.last().value()
await graphics.write()
ctx.io.emit('graphic.single', graphic)
}
/*
@ -62,12 +61,16 @@ export async function remove(ctx, data) {
return
}
let graphic = await Graphic.getSingle(data.id)
graphic.set({ is_deleted: true })
await graphic.save()
let graphics = ctx.db.get('graphics')
let graphic = graphics.removeById(Number(data.id)).value()
await graphics.write()
let output = await Graphic.getAll()
ctx.io.emit('graphic.all', output.toJSON())
graphic.deleted_at = new Date().getTime()
graphic.type = 'graphic'
await ctx.db.get('trash').insert(graphic).write()
ctx.io.emit('graphic.all', graphics)
}
/*
@ -86,11 +89,9 @@ export async function update(ctx, data) {
return
}
let graphic = await Graphic.getSingle(data.id)
await ctx.db.get('graphics').updateById(Number(data.id), data).write()
graphic.set(data)
let graphic = ctx.db.get('graphics').getById(Number(data.id)).value()
await graphic.save()
ctx.io.emit('graphic.single', graphic.toJSON())
ctx.io.emit('graphic.single', graphic)
}

View file

@ -1,7 +1,5 @@
import _ from 'lodash'
export function register(ctx, name, method) {
if (_.isPlainObject(method)) {
if (typeof(method) === 'object') {
Object.keys(method).forEach(key => {
register(ctx, [name, key].join('.'), method[key])
})

View file

@ -1,10 +1,10 @@
import _ from 'lodash'
import bunyan from 'bunyan'
import config from './config'
import defaults from './defaults.mjs'
import config from './config.mjs'
// Clone the settings as we will be touching
// on them slightly.
let settings = _.cloneDeep(config.get('bunyan'))
let settings = defaults(config.get('bunyan'), null)
// Replace any instance of 'process.stdout' with the
// actual reference to the process.stdout.

View file

@ -1,4 +1,4 @@
import bookshelf from '../bookshelf'
import bookshelf from '../bookshelf.mjs'
/* Preset model:
{

View file

@ -1,50 +0,0 @@
import Preset from './model'
export async function all(ctx, payload) {
let id = Number(payload.graphic_id || payload.id)
let data = await Preset.getAll({ graphic_id: id }, [], 'sort')
ctx.io.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)
}
export async function patch(ctx, payload) {
await Promise.all(payload.map(async item => {
let preset = await Preset.getSingle(item.id)
preset.set({ sort: item.sort })
await preset.save()
}))
await all(ctx, payload[0])
}
export async function remove(ctx, payload) {
let preset = await Preset.getSingle(payload.id)
preset.set('is_deleted', true)
await preset.save()
await all(ctx, payload)
}

50
api/preset/routes.mjs Normal file
View file

@ -0,0 +1,50 @@
export async function all(ctx, payload) {
let id = Number(payload.graphic_id || payload.id)
let data = ctx.db.get('presets').filter({ graphic_id: id }).value()
ctx.io.emit(`preset.all:${id}`, data || [])
}
export async function add(ctx, payload) {
payload.sort = 1
let presets = ctx.db.get('presets')
let last = presets.sortBy('sort').last().value()
if (last) {
payload.sort = last.sort + 1
}
payload.graphic_id = Number(payload.graphic_id)
await presets.insert(payload).write()
await all(ctx, payload)
}
export async function patch(ctx, payload) {
let presets = ctx.db.get('presets')
payload.forEach(function(item) {
presets.updateById(Number(item.id), { sort: item.sort })
})
await presets.write()
await all(ctx, payload[0])
}
export async function remove(ctx, payload) {
let presets = ctx.db.get('presets')
let preset = presets.removeById(Number(payload.id)).value()
await presets.write()
preset.deleted_at = new Date().getTime()
preset.type = 'preset'
await ctx.db.get('trash').insert(preset).write()
await all(ctx, payload)
}

View file

@ -1,33 +0,0 @@
import logger from '../log'
import { register } from './io/helper'
import { contentConnection } from './content/connection'
import { casparConnection } from './casparcg/connection'
import * as content from './content/routes'
import * as engine from './engine/routes'
import * as graphic from './graphic/routes'
import * as preset from './preset/routes'
import * as settings from './settings/routes'
import * as schedule from './schedule/routes'
function onConnection(server, data) {
const io = server.socket
const socket = data.socket
const log = logger.child({
id: socket.id,
})
let ctx = { io, socket, log }
contentConnection(ctx)
casparConnection(ctx)
register(ctx, 'content', content)
register(ctx, 'engine', engine)
register(ctx, 'graphic', graphic)
register(ctx, 'preset', preset)
register(ctx, 'settings', settings)
register(ctx, 'schedule', schedule)
}
export default onConnection

33
api/routerio.mjs Normal file
View file

@ -0,0 +1,33 @@
import logger from './log.mjs'
import { register } from './io/helper.mjs'
import { contentConnection } from './content/connection.mjs'
import { casparConnection } from './casparcg/connection.mjs'
import * as content from './content/routes.mjs'
import * as engine from './engine/routes.mjs'
import * as graphic from './graphic/routes.mjs'
import * as preset from './preset/routes.mjs'
import * as settings from './settings/routes.mjs'
import * as schedule from './schedule/routes.mjs'
function onConnection(server, db, data) {
const io = server.socket
const socket = data.socket
const log = logger.child({
id: socket.id,
})
let ctx = { io, socket, log, db }
contentConnection(ctx)
casparConnection(ctx)
register(ctx, 'content', content)
register(ctx, 'engine', engine)
register(ctx, 'graphic', graphic)
register(ctx, 'preset', preset)
register(ctx, 'settings', settings)
register(ctx, 'schedule', schedule)
}
export default onConnection

View file

@ -1,5 +1,5 @@
import bookshelf from '../bookshelf'
import Graphic from '../graphic/model'
import bookshelf from '../bookshelf.mjs'
import Graphic from '../graphic/model.mjs'
/* Schedule model:
{

View file

@ -1,32 +1,34 @@
import Schedule from './model'
import Schedule from './model.mjs'
export async function all(ctx) {
let data = await Schedule.getAll({ }, ['graphic'], 'sort')
let graphics = ctx.db.get('graphics')
let data = ctx.db.get('schedule').forEach(function(s) {
s.graphic = graphics.getById(s.graphic_id).value()
}).sortBy('sort').value()
// let data = await Schedule.getAll({ }, ['graphic'], 'sort')
ctx.io.emit('schedule.all', data.toJSON())
ctx.io.emit('schedule.all', data)
total(ctx)
}
export async function total(ctx) {
let data = await Schedule.getAll({ }, ['graphic'], 'sort')
let data = ctx.db.get('schedule').size()
ctx.io.emit('schedule.total', { total: data.length })
ctx.io.emit('schedule.total', { total: data })
}
export async function add(ctx, payload) {
payload.is_deleted = false
payload.sort = 1
let last = await Schedule.query(q => {
q.orderBy('sort', 'desc')
q.limit(1)
}).fetch({ require: false })
let schedule = ctx.db.get('schedule')
let last = schedule.sortBy('sort').last().value()
if (last) {
payload.sort = last.get('sort') + 1
payload.sort = last.sort + 1
}
await Schedule.create(payload)
await schedule.insert(payload).write()
await all(ctx)
}

View file

@ -1,35 +0,0 @@
import Koa from 'koa'
import serve from 'koa-better-serve'
import socket from 'koa-socket'
import * as casparcg from './casparcg/client'
import config from '../config'
import log from '../log'
import onConnection from './routerio'
import { bunyanLogger, errorHandler } from './middlewares'
const app = new Koa()
const io = new socket()
io.attach(app)
io.on('connection', onConnection.bind(this, io))
casparcg.initialise(log, io).catch(e => {
log.error(e, 'Critical error initialising casparcg')
})
app.use(bunyanLogger(log))
app.use(errorHandler())
app.use(async (ctx, next) => {
if (ctx.url === '/') {
return ctx.redirect('/index.html')
}
await next()
})
app.use(serve('./public', ''))
app.listen(config.get('server:port'), err => {
if (err) return log.critical(err)
log.info(`Server is listening on ${config.get('server:port')}`)
})

44
api/server.mjs Normal file
View file

@ -0,0 +1,44 @@
import Koa from 'koa'
import serve from 'koa-better-serve'
import socket from 'koa-socket'
import * as casparcg from './casparcg/client.mjs'
import lowdb from './db.mjs'
import config from './config.mjs'
import log from './log.mjs'
import onConnection from './routerio.mjs'
import { bunyanLogger, errorHandler } from './middlewares.mjs'
log.info('Server: Opening database db.json')
lowdb().then(function(db) {
const app = new Koa()
const io = new socket()
io.attach(app)
io.on('connection', onConnection.bind(this, io, db))
casparcg.initialise(log, db, io)
app.use(bunyanLogger(log))
app.use(errorHandler())
app.use(async (ctx, next) => {
if (ctx.url === '/') {
return ctx.redirect('/index.html')
}
await next()
})
app.use(serve('./public', ''))
app.listen(config.get('server:port'), err => {
if (err) return log.fatal(err)
log.info(`Server is listening on ${config.get('server:port')}`)
})
}, function(e) {
log.fatal(e, 'Critical error loading database')
process.exit(1)
}).catch(function(e) {
log.fatal(e, 'Critical error starting server')
process.exit(1)
})

View file

@ -1,51 +0,0 @@
import bookshelf from '../bookshelf'
/* Settings model:
{
id,
name,
value,
}
*/
const Settings = bookshelf.createModel({
tableName: 'settings',
}, {
getValue(name) {
return this.query({ where: { name: name } })
.fetch({ require: false })
.then(item => item && item.get('value') || item)
},
setValue(name, value) {
return this.query({ where: { name } })
.fetch({ require: false })
.then(item => {
if (item) {
item.set({ value })
return item.save()
}
return this.create({
name,
value,
is_deleted: false,
})
})
},
getSettings() {
return this.query({ where: { }})
.fetchAll({ })
.then(data => {
let out = { }
data.forEach(item => {
out[item.get('name')] = item.get('value')
})
return out
})
},
})
export default Settings

View file

@ -1,5 +1,4 @@
import Settings from './model'
import { connect } from '../casparcg/client'
import { connect } from '../casparcg/client.mjs'
/*
* Event: 'settings.all'
@ -7,7 +6,7 @@ import { connect } from '../casparcg/client'
* Request all settings in store
*/
export async function all(ctx) {
let data = await Settings.getSettings()
let data = ctx.db.get('settings').value()
ctx.socket.emit('settings.all', data)
}

27
api/setup.mjs Normal file
View file

@ -0,0 +1,27 @@
import knex from 'knex'
import defaults from './defaults.mjs'
import config from './config.mjs'
import log from './log.mjs'
// This is important for setup to run cleanly.
let knexConfig = defaults(config.get('knex'), null) // Clone
knexConfig.pool = { min: 1, max: 1 }
let knexSetup = knex(knexConfig)
export default function setup() {
log.info(knexConfig, 'Running database integrity scan.')
return knexSetup.migrate.latest({
directory: './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 knexSetup.destroy()
})
}

View file

@ -1,4 +1,4 @@
var socket = require('../socket')
var socket = require('../shared/socket')
var engines = {
text: require('./text'),

View file

@ -1,7 +1,7 @@
const m = require('mithril')
const createModule = require('../common/module')
const components = require('../common/components')
const socket = require('../../socket')
const socket = require('../../shared/socket')
const store = require('../store')
const Add = createModule({

View file

@ -1,7 +1,7 @@
const m = require('mithril')
const _ = require('lodash')
const store = require('../store')
const socket = require('../../socket')
const socket = require('../../shared/socket')
const dragula = require('dragula')
function createModule(component, view) {

View file

@ -1,7 +1,7 @@
const _ = require('lodash')
const m = require('mithril')
const createModule = require('../common/module')
const socket = require('../../socket')
const socket = require('../../shared/socket')
const Dagskra = createModule({
init: function() {

View file

@ -1,7 +1,7 @@
const _ = require('lodash')
const m = require('mithril')
const createModule = require('../common/module')
const socket = require('../../socket')
const socket = require('../../shared/socket')
const view = require('./view')
const dragula = require('dragula')

View file

@ -1,6 +1,6 @@
const m = require('mithril')
const createModule = require('./common/module')
const socket = require('../socket')
const socket = require('../shared/socket')
const Header = createModule({
init: function() {

View file

@ -12,7 +12,7 @@
//in the console.
window.components = {}
require('../socket')
require('../shared/socket')
require('./store')
const m = require('mithril')

View file

@ -1,6 +1,6 @@
const m = require('mithril')
const createModule = require('./common/module')
const socket = require('../socket')
const socket = require('../shared/socket')
const Menu = createModule({
init: function() {
@ -22,7 +22,7 @@ const Menu = createModule({
saveNewHost() {
socket.emit('settings.update', {
name: 'casparcg',
name: 'casparhost',
value: this.newHost,
})
@ -53,7 +53,7 @@ const Menu = createModule({
m('h5.header.header--space', 'CasparCG Status'),
m('input[type=text]', {
placeholder: 'Host IP',
value: this.newHost || this.settings.casparcg || '',
value: this.newHost || this.settings.casparhost || '',
oninput: control => this.setHost(control.target.value),
}),
this.enableEdit && m('button', {

View file

@ -1,5 +1,5 @@
const _ = require('lodash')
const socket = require('../socket')
const socket = require('../shared/socket')
const storage = {}
const events = {}

View file

@ -1,4 +1,4 @@
const socket = require('../socket')
const socket = require('../shared/socket')
const m = require('mithril')
const Status = {

View file

@ -1,47 +0,0 @@
* {
-webkit-box-sizing: border-box;
box-sizing:border-box; /* This sets all elements to be the actual set dimensions, disregarding padding and borders */
/* -webkit-backface-visibility: hidden; */ /* Hide the backface of elements - useful for 3d effects */
-webkit-transition: translate3d(0,0,0); /* Turns on hardware acceleration - not known to be of benefit in CCG, but won't hurt */
}
html, body {
width:1920px; /* Set to your channel's resolution */
height:1080px; /* Set to your channel's resolution */
margin:0; /* Use all available space */
padding:0; /* Use all available space */
background:transparent; /* The HTML consumer actually makes your background transparent by default, unless a color or image is specified - but this might be usefull when debugging in browsers */
overflow:hidden; /* Hide any overflowing elements - to disable scollbars */
-webkit-font-smoothing:antialiased !important; /* Set aliasing of fonts - possible options: none, antialiased, subpixel-antialiased */
}
body {
font-family: Calibri,Arial;
font-size: 40px;
color: #FFFFFF;
/* -webkit-text-stroke-width: 0.5px;
-webkit-text-stroke-color: #888888;
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;
}
body > div
{
position: absolute;
}
.root-element {
opacity: 0;
transition: opacity 1s;
}
.root-element-display {
opacity: 1;
transition: opacity 1s;
}

View file

@ -1,566 +0,0 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input {
font-size: 16px;
-webkit-appearance: none;
border-radius: 0;
}
button {
outline: none;
border: none;
cursor: pointer;
}
a {
text-decoration: none;
}
body {
background: #3f3f41;
color: #f1f1f1;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
h4 {
margin-bottom: 2rem;
}
/* Components */
button {
border: none;
color: #f1f1f1;
background: #2199e8;
font-size: 0.6em;
height: 3em;
&.green {
background: #3adb78;
}
&.red {
background: #ec5840;
}
}
.error-box {
margin: 1rem 0rem 2rem 0;
padding: 1rem;
background: #FF0000;
color: white;
font-size: 0.7em;
line-height: 1em;
}
$header-size = 0.8em;
$header-color = #777777;
/* Container */
.container {
display: flex;
align-items: stretch;
flex-grow: 2;
}
/* Header */
section.current {
padding: 0 13px;
background: black;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 10;
h4 {
color: $header-color;
font-size: 0.7em;
padding: 0.2em;
margin: 0;
}
h3 {
font-size: 1em;
line-height: 2em;
color: #eb6e00;
flex-grow: 2;
height: 2em;
padding-right: 0.5em;
overflow: hidden;
word-break: break-all;
}
button {
width: 80px;
flex-shrink: 0;
}
.item {
display: flex;
margin-bottom: 5px;
}
}
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
font-size: 1em;
display: flex;
justify-content: center;
align-items: center;
}
/* Menu */
nav {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px;
background: #2d2d30;
text-align: center;
.header {
color: $header-color;
font-size: $header-size;
margin-bottom: 10px;
&--space {
margin-top: 2em;
}
}
a {
font-size: $header-size;
line-height: 2.6em;
display: block;
border: 4px solid #2d2d30;
background: #007acc;
color: white;
&.active {
background: transparent;
border: 4px solid #007acc;
}
&:hover {
border: 4px solid #007acc;
}
}
input[type=text] {
text-align: center;
}
.status {
padding: 5px 20px;
font-size: 0.8em;
color: $header-color;
text-align: left;
position: relative;
margin-left: 1.8em;
&::after {
position: absolute;
left: 0;
top: calc(50% - 5px);
content: '';
border: 6px solid #ec5840;
}
&.green::after {
border-color: #008000;
}
}
}
/* Main */
main {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px 1em;
flex-grow: 2;
width: 300px;
.header {
color: $header-color;
font-size: $header-size;
margin-bottom: 10px;
}
}
/* Inputs */
label {
margin-top: 0.6em;
color: #f1f1f1;
font-size: 0.7em;
& a,
& a:hover,
& a:visited {
color: #aaa;
text-decoration: underline;
}
}
input[type='text'],
textarea,
select {
font-size: 0.6em;
padding: 0.5em;
margin: 0.5em 0;
background: #333337;
border: 1px solid #2d2d30;
color: #999999;
transition-property: none !important;
outline: none;
&:hover {
color: #f1f1f1;
border-color: #007acc;
}
&:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
}
textarea#graphic-html {
min-height: 150px;
}
textarea#graphic-css {
min-height: 400px;
}
input[type=submit] {
margin-top: 0.6em;
border: none;
color: #f1f1f1;
background: #2199e8;
font-size: 0.6em;
line-height: 3em;
}
input[readonly],
input[readonly]:hover {
background: #2d2d30 !important;
border-color: #3f3f3f;
}
select {
height: 2.5em;
-webkit-appearance: none;
border-radius: 0;
background-position: right center;
background-size: 9px 6px;
background-origin: content-box;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28138, 138, 138%29'></polygon></svg>")
}
select:hover {
color: #f1f1f1;
border-color: #007acc;
}
select:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
a.button {
margin: 0 1rem 0 0;
width: 7rem;
}
/* Graphic */
header {
display: flex;
h3 {
font-size: 1em;
flex-grow: 2;
border-bottom: 1px solid #2d2d30;
padding-top: 10px;
margin-right: 30px;
}
button {
border: 0;
width: 100px;
}
}
.graphic {
&-presetlist {
display: flex;
flex-direction: column;
align-items: stretch;
}
&-presetadd {
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid #2d2d30;
margin: 30px 0 10px;
padding: 20px;
position: relative;
&-header {
background: #3f3f41;
position: absolute;
top: -1.3em;
left: 10px;
font-size: 0.8em;
padding: 0.8em 10px;
}
&-buttons {
display: flex;
margin-top: 10px;
& button {
margin-right: 10px;
width: 150px;
}
}
}
&-presetremove {
align-self: center;
margin-top: 50px;
width: 150px;
}
&-empty {
font-size: 0.7em;
color: #999;
text-align: center;
margin: 20px 0;
}
&-delete {
align-self: center;
margin-top: 30px;
width: 150px;
}
&-label {
margin-top: 30px;
padding-bottom: 0.5em;
}
&-helper {
font-size: 0.7em;
color: #999;
margin: 5px 0 0;
&.bottom {
margin: 0;
}
}
&-property,
&-preset {
display: flex;
align-items: stretch;
&-reorder {
width: 62px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODQ5NDcyMzY3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODQ5NDcyMzc3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4NDk0NzIzNDcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4NDk0NzIzNTcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pla8e2wAAABFUExURXd3d////3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0brzhMAAAAWdFJOUwAAN0NEVXJzdHV2d3h/iImKmaq73e5w9SzPAAAAlElEQVRIx+XT2wqDMBRE0Witt2piW53//9SCoETMwJkXkWY/CgtNBp0jDa/086JgwgNeIwEghpFVAMFOJoCaNNkFMNlIJBImRcrPodJ8ybQ/I285B7mLSCWX+/qL3EVTznK5T/mUu++P7OVyX/9U9T30sLyliUeobR8WmcZ6lnYTrf34HRX8xlbTaZfcA726yziyXX5Joj0NgTfsUQAAAABJRU5ErkJggg==') no-repeat transparent;
background-size: 25px;
background-position: center;
touch-action: none;
}
& input {
flex-grow: 2;
margin: 0;
}
& button {
width: 100px;
border: 1px solid #3f3f41;
border-left: none;
}
}
&-preset {
& input {
padding-top: 1.5em;
padding-bottom: 1.5em;
}
& button {
height: auto;
}
}
}
.schedule {
&-empty {
margin-top: 2em;
font-size: 1em;
text-align: center;
}
}
.settings {
&-empty {
text-align: center;
margin: 50px 0 30px;
font-size: 0.8em;
color: #999;
&-button {
align-self: center;
width: 200px;
}
}
}
/* Dragula */
@css {
#dragcontainer {
position: fixed;
top: 0;
left: 0;
}
.gu-mirror {
position: absolute !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
}
.gu-hide {
display: none !important;
}
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}
}
/* Media queries */
body {
font-size: 1.5rem;
}
@media only screen and (max-device-width: 600px) {
#container {
flex-direction: column;
}
nav {
width: auto;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
.header {
width: 100%;
}
a {
width: calc(50% - 8px);
}
input[type=text] {
width: 100%;
}
.status {
align-self: center;
width: 120px;
}
}
#content {
width: auto;
}
}

View file

@ -1,168 +0,0 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input {
font-size: 16px;
-webkit-appearance: none;
border-radius: 0;
}
button {
outline: none;
border: none;
cursor: pointer;
}
a {
text-decoration: none;
}
body {
background: #3f3f41;
color: #eb6e00;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
$header-size = 2.3em;
#container {
display: flex;
align-items: stretch;
flex-direction: column;
flex-grow: 2;
h3 {
background: black;
font-size: $header-size;
line-height: ($header-size);
color: #eb6e00;
height: ($header-size);
overflow: hidden;
padding: 0 0.3em;
flex-grow: 2;
border-radius: 5px 0 0 5px;
word-break: break-all;
}
button {
border: none;
color: black;
background: #eb6e00;
font-size: 2em;
width: 80px;
flex-shrink: 0;
border-radius: 0 5px 5px 0;
}
.item {
display: flex;
margin: 5px;
}
.empty {
flex-grow: 2;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
font-size: 2em;
}
}
header {
display: flex;
margin-bottom: 5px;
color: #ccc;
h2 {
font-size: 3em;
line-height: 64px;
padding: 0 0.3em;
flex-grow: 2;
}
.status {
padding: 0 20px 0 30px;
line-height: 64px;
font-size: 2em;
color: $header-color;
text-align: left;
position: relative;
&::after {
position: absolute;
left: 0;
top: calc(50% - 7px);
content: '';
border: 10px solid #ec5840;
}
&.green::after {
border-color: #00FF00;
}
}
}
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
font-size: 3em;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
}

187
db.json Normal file
View file

@ -0,0 +1,187 @@
{
"graphics": [
{
"name": "Nidurteljari",
"engine": "countdown",
"settings": {
"html": "<%- text %><br><span id=\"Nidurteljari-countdown-timer\">countdown appears here</span>",
"main": "text",
"text": "Sunnudagssamkoman hefst klukkan 11:00",
"countdown": "2018-07-01 11:00",
"finished": "Skamma stund",
"css": "#Nidurteljari {\n position: absolute;\n font: 2em \"Berthold Akzidenz Grotesk BE\";\n top: 830px;\n left: 230px;\n width: 1450px;\n padding-top: 20px;\n border-top: 1px solid #095376;\n text-align: center;\n color: #095376;\n font-size: 60pt;\n line-height: 70pt;\n font-weight: bold;\n}\n#Nidurteljari-countdown-timer {\n font-size: 80pt;\n}"
},
"id": 1586112780891
},
{
"name": "Nafn",
"engine": "text",
"settings": {
"properties": [
"nafn",
"titill"
],
"main": "<%- nafn %>",
"html": "<div class=\"outer\">\n<div class=\"inside\">\n<h2><%- nafn %></h2>\n<h4><%- titill %></h4>\n</div>\n</div>",
"css": "#Nafn {\nposition: absolute;\nbottom: 50px;\nleft: 0px;\nwidth: 100%;\nright: 0;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\ndisplay: flex;\njustify-content: center;\n}\n\n#Nafn .outer {\nborder: 4px solid white;\npadding: 6px;\n}\n\n#Nafn .inside {\npadding: 8px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Nafn .inside h2 {\nfont-weight: bold;\nfont-size: 40px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\nline-height: 120%;\n}\n\n#Nafn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\ntext-transform: uppercase;\n}"
},
"id": 1586112780892
},
{
"name": "Minnisvers",
"engine": "text",
"settings": {
"properties": [
"bok",
"vers"
],
"main": "bok",
"css": "#Minnisvers {\n position: absolute;\n font: 2em \"Berthold Akzidenz Grotesk BE\";\n top: 850px;\n right: 0px;\n}\n.bok-holder {\n background: rgba(0,68,105,0.6);\n line-height: 60px;\n font-size: 60px;\n color: white;\n padding: 10px 60px 0px 30px;\n}\n.vers-holder {\n background: rgba(33,29,29,0.6);\n font-size: 30px;\n line-height: 30px;\n color: white;\n padding: 10px 60px 0px 40px;\n}",
"html": "<div class=\"bok-holder\"><%- bok %></div>\n<div class=\"vers-holder\"><%- vers %></div>"
},
"id": 1586112780893
},
{
"name": "Forn",
"engine": "text",
"settings": {
"properties": [
"Titill",
"Text"
],
"main": "<%- Titill %> - <%- Text %>",
"css": "#Forn {\nposition: absolute;\ntop: 50px;\nleft: 95px;\nborder: 4px solid white;\npadding: 6px;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\n}\n\n#Forn .inside {\npadding: 6px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Forn .inside h2 {\nfont-weight: bold;\nfont-size: 24px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\n}\n\n#Forn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\n}",
"html": "<div class=\"inside\">\n<h2><%- Titill %></h2>\n<h4><%= Text.replace('\\n', '<br>') %></h4>\n</div>"
},
"id": 1586112780894
},
{
"name": "Lög",
"engine": "text",
"settings": {
"properties": [
"nafn"
],
"html": "<div class=\"box-before\"></div>\n<div class=\"box-logo\"></div>\n<div class=\"box-after\"></div>\n<div class=\"box-name\">\n <div class=\"holder\">\n <div class=\"name-holder\"><%- nafn %>\n <div class=\"name-triangle\"></div><div class=\"titill-triangle\"></div>\n </div>\n </div>\n</div>",
"css": ".box-before {\n margin-bottom: 15px;\n width: 160px;\n height: 112px;\n margin-right: -37px;\n /* ---------- THEME COLOR ---------- */\n background-image: -webkit-radial-gradient(200px 50%, circle, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 75px, rgba(0,68,105,1) 77px);\n /* ---------- END THEME COLOR ---------- */\n}\n.box-after {\n /* ---------- THEME COLOR ---------- */\n background-image: -webkit-radial-gradient(-40px 50%, circle, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 75px, rgba(0,68,105,1) 77px);\n /* ---------- END THEME COLOR ---------- */\n margin-bottom: 15px;\n width: 100px;\n height: 112px;\n margin-left: -37px;\n}\n\n#Lög {\n position: absolute;\n color: white;\n font: 2em \"Berthold Akzidenz Grotesk BE\";\n top: 100px;\n left: 0px;\n}\n\n#Lög > div { display: inline-block; }\n\n.box-logo {\n width: 141px;\n height: 141px;\n background: url('uploads/logo.png') 0px 0px no-repeat;\n background-size: contain;\n}\n\n.box-name {\n margin-bottom: 15px;\n height: 112px;\n position: relative;\n margin-left: -6px;\n width: 700px;\n}\n.holder { position: absolute; }\n.name-holder {\n position: relative;\n background: linear-gradient(to right, rgba(207,209,210,0.6) 0%,rgba(235,235,237,0.6) 50%,rgba(183,185,185,0.6) 100%);\n line-height: 102px;\n font-size: 60px;\n color: rgba(0,68,105,1);\n padding: 10px 30px 0px 20px;\n}\n.name-triangle {\n position: absolute;\n right: -112px;\n border-width: 0 112px 112px 0px;\n top: 0;\n border-style: solid;\n border-color: transparent transparent rgba(183,185,185,0.6) transparent;\n}",
"main": "nafn"
},
"id": 1586112780895
},
{
"name": "Dagskra",
"engine": "schedule",
"settings": {
"properties": [
"temp"
],
"textfields": [
"item1"
],
"main": "temp"
},
"id": 1586112780896
}
],
"presets": [
{
"graphic_id": 1586112780892,
"values": {
"nafn": "Bla",
"titill": "test"
},
"sort": 1,
"id": 1586205588587
},
{
"graphic_id": 1586112780892,
"values": {
"nafn": "Bla 2",
"titill": "test 2"
},
"sort": 2,
"id": 1586205644890
}
],
"playing": [
{
"graphic": {
"name": "Nafn",
"engine": "text",
"settings": {
"properties": [
"nafn",
"titill"
],
"main": "<%- nafn %>",
"html": "<div class=\"outer\">\n<div class=\"inside\">\n<h2><%- nafn %></h2>\n<h4><%- titill %></h4>\n</div>\n</div>",
"css": "#Nafn {\nposition: absolute;\nbottom: 50px;\nleft: 0px;\nwidth: 100%;\nright: 0;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\ndisplay: flex;\njustify-content: center;\n}\n\n#Nafn .outer {\nborder: 4px solid white;\npadding: 6px;\n}\n\n#Nafn .inside {\npadding: 8px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Nafn .inside h2 {\nfont-weight: bold;\nfont-size: 40px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\nline-height: 120%;\n}\n\n#Nafn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\ntext-transform: uppercase;\n}"
},
"id": 1586112780892
},
"name": "Nafn",
"html": "<div class=\"outer\">\n<div class=\"inside\">\n<h2>Bla 2</h2>\n<h4>test 2</h4>\n</div>\n</div>",
"css": "#Nafn {\nposition: absolute;\nbottom: 50px;\nleft: 0px;\nwidth: 100%;\nright: 0;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\ndisplay: flex;\njustify-content: center;\n}\n\n#Nafn .outer {\nborder: 4px solid white;\npadding: 6px;\n}\n\n#Nafn .inside {\npadding: 8px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Nafn .inside h2 {\nfont-weight: bold;\nfont-size: 40px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\nline-height: 120%;\n}\n\n#Nafn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\ntext-transform: uppercase;\n}",
"data": {
"nafn": "Bla 2",
"titill": "test 2"
},
"is_deleted": false,
"id": 1586213076185
}
],
"schedule": [
{
"graphic_id": 1586112780892,
"values": {
"nafn": "Bla 2",
"titill": "test 2"
},
"sort": 1,
"id": 1586213073361,
"graphic": {
"name": "Nafn",
"engine": "text",
"settings": {
"properties": [
"nafn",
"titill"
],
"main": "<%- nafn %>",
"html": "<div class=\"outer\">\n<div class=\"inside\">\n<h2><%- nafn %></h2>\n<h4><%- titill %></h4>\n</div>\n</div>",
"css": "#Nafn {\nposition: absolute;\nbottom: 50px;\nleft: 0px;\nwidth: 100%;\nright: 0;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\ndisplay: flex;\njustify-content: center;\n}\n\n#Nafn .outer {\nborder: 4px solid white;\npadding: 6px;\n}\n\n#Nafn .inside {\npadding: 8px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Nafn .inside h2 {\nfont-weight: bold;\nfont-size: 40px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\nline-height: 120%;\n}\n\n#Nafn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\ntext-transform: uppercase;\n}"
},
"id": 1586112780892
}
},
{
"graphic_id": 1586112780892,
"values": {
"nafn": "Bla",
"titill": "test"
},
"sort": 2,
"id": 1586213074043,
"graphic": {
"name": "Nafn",
"engine": "text",
"settings": {
"properties": [
"nafn",
"titill"
],
"main": "<%- nafn %>",
"html": "<div class=\"outer\">\n<div class=\"inside\">\n<h2><%- nafn %></h2>\n<h4><%- titill %></h4>\n</div>\n</div>",
"css": "#Nafn {\nposition: absolute;\nbottom: 50px;\nleft: 0px;\nwidth: 100%;\nright: 0;\nbackground: transparent;\ncolor: black;\nfont-family: Raleway;\ndisplay: flex;\njustify-content: center;\n}\n\n#Nafn .outer {\nborder: 4px solid white;\npadding: 6px;\n}\n\n#Nafn .inside {\npadding: 8px 24px;\nbackground: white;\ndisplay: flex;\nflex-direction: column;\n}\n\n#Nafn .inside h2 {\nfont-weight: bold;\nfont-size: 40px;\ntext-align: center;\ntext-transform: uppercase;\npadding: 0;\nmargin: 0;\nline-height: 120%;\n}\n\n#Nafn .inside h4 {\nfont-weight: normal;\nfont-size: 18px;\npadding: 0;\nmargin: 0;\ntext-align: center;\nline-height: 125%;\ntext-transform: uppercase;\n}"
},
"id": 1586112780892
}
}
],
"settings": {
"casparplayhost": "localhost:3000",
"casparhost": "host.docker.internal"
},
"version": 1,
"trash": []
}

View file

@ -1,14 +1,11 @@
'use strict'
require('babel-register')
let log = require('./log').default
import log from './api/log.mjs'
import setup from './api/setup.mjs'
// Run the database script automatically.
log.info('Running database integrity scan.')
let setup = require('./script/setup')
setup().then(() => {
require('./api/server')
import('./api/server.mjs')
}).catch((error) => {
log.error(error, 'Error while preparing database')
process.exit(1)

View file

@ -1,6 +1,3 @@
'use strict'
require('babel-register')
const _ = require('lodash')
const config = require('./config')

4082
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,17 +3,17 @@
"version": "1.0.0",
"description": "CasparCG superimposed graphics project",
"main": "index.js",
"type": "module",
"scripts": {
"build:styl": "stylus -m app/styl/main.styl --out public && stylus -m app/styl/client.styl --out public && stylus -m app/styl/status.styl --out public",
"watch:styl": "stylus -w -m app/styl/main.styl app/styl/client.styl app/styl/status.styl --out public",
"build:js": "asbundle app/main/index.js ../../../public/main.js && asbundle app/client/index.js public/client.js && asbundle app/status/index.js ../../public/status.js",
"watch:js": "nodemon --watch app --exec \"npm run build:js\"",
"watch:server": "nodemon --watch api index.js | bunyan -o short",
"start": "node index.js | bunyan -o short",
"start:win": "node index.js | bunyan -o short",
"dev": "run-p watch:styl watch:js watch:server",
"prod-run": "npm run build:js && npm run build-client:js && npm run build-status:js && npm run build:styl && npm run build-client:styl && npm run build-status:styl && npm start",
"build": "npm run build:js && npm run build:styl",
"js:build:main": "asbundle app/main/index.js public/main.js",
"js:build:client": "asbundle app/client/index.js public/client.js",
"js:build:status": "asbundle app/status/index.js public/status.js",
"js:watch": "nodemon --watch app --exec \"npm run build\"",
"start:watch": "nodemon --experimental-modules --watch api index.mjs | bunyan -o short",
"start": "node --experimental-modules index.mjs | bunyan -o short",
"start:win": "node --experimental-modules index.mjs | bunyan -o short",
"dev": "run-p js:watch start:watch",
"build": "run-p js:build:main js:build:client js:build:status",
"docker": "docker run -it --rm --name my-running-script -p 3000:3000 -v \"%cd%\":/usr/src/app -w /usr/src/app node",
"docker:install": "npm run docker -- npm install",
"docker:dev": "npm run docker -- npm run dev"
@ -34,8 +34,6 @@
},
"homepage": "https://github.com/nfp-projects/caspar-sup#readme",
"dependencies": {
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-register": "^6.26.0",
"bookshelf": "^0.11.1",
"bunyan": "^1.8.12",
"casparcg-connection": "4.9.0",
@ -44,18 +42,19 @@
"koa": "^2.4.1",
"koa-better-serve": "^2.0.7",
"koa-socket": "^4.4.0",
"lodash.template": "^4.5.0",
"lowdb": "^1.0.0",
"nconf": "^0.9.1",
"socket.io": "^2.3.0",
"sqlite3": "^4.1.1",
"tslib": "^1.11.1"
},
"devDependencies": {
"asbundle": "^2.6.0",
"asbundle": "TheThing/asbundle",
"dragula": "^3.7.2",
"mithril": "^1.1.5",
"nodemon": "^2.0.2",
"npm-run-all": "^4.1.2",
"run-p": "0.0.0",
"stylus": "^0.54.7"
"run-p": "0.0.0"
}
}

View file

@ -1,54 +1,45 @@
* {
html, body {
width:1920px; /* Set to your channel's resolution */
height:1080px; /* Set to your channel's resolution */
margin:0; /* Use all available space */
padding:0; /* Use all available space */
background:transparent; /* The HTML consumer actually makes your background transparent by default, unless a color or image is specified - but this might be usefull when debugging in browsers */
overflow:hidden; /* Hide any overflowing elements - to disable scollbars */
-webkit-font-smoothing:antialiased !important; /* Set aliasing of fonts - possible options: none, antialiased, subpixel-antialiased */
}
html {
overflow: auto;
-webkit-box-sizing: border-box;
box-sizing: border-box;
/* This sets all elements to be the actual set dimensions, disregarding padding and borders */
/* -webkit-backface-visibility: hidden; */
/* Hide the backface of elements - useful for 3d effects */
-webkit-transition: translate3d(0, 0, 0); /* Turns on hardware acceleration - not known to be of benefit in CCG, but won't hurt */
box-sizing:border-box;
}
html,
body {
width: 1920px;
/* Set to your channel's resolution */
height: 1080px;
/* Set to your channel's resolution */
margin: 0;
/* Use all available space */
padding: 0;
/* Use all available space */
background: transparent;
/* The HTML consumer actually makes your background transparent by default, unless a color or image is specified - but this might be usefull when debugging in browsers */
overflow: hidden;
/* Hide any overflowing elements - to disable scollbars */
-webkit-font-smoothing: antialiased !important;
/* Set aliasing of fonts - possible options: none, antialiased, subpixel-antialiased */
*, *:before, *:after {
box-sizing: inherit;
}
body {
font-family: Calibri, Arial;
font-family: Arial;
font-size: 40px;
color: #fff;
/* -webkit-text-stroke-width: 0.5px;
color: #FFFFFF;
font-weight: normal;
font-size: 22pt;
/* -webkit-text-stroke-width: 0.5px;
-webkit-text-stroke-color: #888888;
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;
}
body > div {
body > div
{
position: absolute;
}
.root-element {
opacity: 0;
transition: opacity 1s;
}
.root-element-display {
opacity: 1;
transition: opacity 1s;
}
/*# sourceMappingURL=client.css.map */

View file

@ -1 +0,0 @@
{"version":3,"sources":["../app/styl/client.styl"],"names":[],"mappings":"AAAA;EACE,oBAAoB,WAApB;EACA,YAAW,WAAX;AAA4C;AAC7C;AAAiD;EAChD,oBAAoB,qBAApB;AAA4C;;AAE9C;AAAM;EACJ,OAAM,OAAN;AAAyD;EACzD,QAAO,OAAP;AAAyD;EACzD,QAAO,EAAP;AAAyD;EACzD,SAAQ,EAAR;AAAyD;EACzD,YAAW,YAAX;AAAyD;EACzD,UAAS,OAAT;AAAyD;EACzD,wBAAuB,uBAAvB;AAAyD;;AAE3D;EACE,aAAoB,eAApB;EACA,WAAW,KAAX;EACA,OAAO,KAAP;AACA;;;;AAKF;EACE,aAAa,MAAb;EACA,aAAa,OAAb;AACA;EACA,WAAW,KAAX;;AAEF;EACE,UAAU,KAAV;;AAEF;EAEE,UAAU,SAAV;;AAGF;EACE,SAAS,EAAT;EACA,YAAY,WAAZ;;AAGF;EACE,SAAS,EAAT;EACA,YAAY,WAAZ","file":"client.css"}

437
public/main.css Normal file
View file

@ -0,0 +1,437 @@
html {
box-sizing: border-box;
font-size: 16px;
height: 100%;
}
*, *:before, *:after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p {
margin: 0;
padding: 0;
font-weight: normal;
}
body {
background: #3f3f41;
color: #f1f1f1;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
h4 {
margin-bottom: 2rem;
}
/* Components */
button {
border: none;
color: #f1f1f1;
background: #2199e8;
font-size: 0.6em;
height: 3em;
}
button.green {
background: #3adb78;
}
button.red {
background: #ec5840;
}
.error-box {
margin: 1rem 0rem 2rem 0;
padding: 1rem;
background: #f00;
color: #fff;
font-size: 0.7em;
line-height: 1em;
}
/* Container */
.container {
display: flex;
align-items: stretch;
flex-grow: 2;
}
/* Header */
section.current {
padding: 0 13px;
background: #000;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 10;
}
section.current h4 {
color: #777;
font-size: 0.7em;
padding: 0.2em;
margin: 0;
}
section.current h3 {
font-size: 1em;
line-height: 2em;
color: #eb6e00;
flex-grow: 2;
height: 2em;
padding-right: 0.5em;
overflow: hidden;
word-break: break-all;
}
section.current button {
width: 80px;
flex-shrink: 0;
}
section.current .item {
display: flex;
margin-bottom: 5px;
}
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: #fff;
font-size: 1em;
display: flex;
justify-content: center;
align-items: center;
}
/* Menu */
nav {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px;
background: #2d2d30;
text-align: center;
}
nav .header {
color: #777;
font-size: 0.8em;
margin-bottom: 10px;
}
nav .header--space {
margin-top: 2em;
}
nav a {
font-size: 0.8em;
line-height: 2.6em;
display: block;
border: 4px solid #2d2d30;
background: #007acc;
color: #fff;
}
nav a.active {
background: transparent;
border: 4px solid #007acc;
}
nav a:hover {
border: 4px solid #007acc;
}
nav input[type=text] {
text-align: center;
}
nav .status {
padding: 5px 20px;
font-size: 0.8em;
color: #777;
text-align: left;
position: relative;
margin-left: 1.8em;
}
nav .status::after {
position: absolute;
left: 0;
top: calc(50% - 5px);
content: '';
border: 6px solid #ec5840;
}
nav .status.green::after {
border-color: #008000;
}
/* Main */
main {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px 1em;
flex-grow: 2;
width: 300px;
}
main .header {
color: #777;
font-size: 0.8em;
margin-bottom: 10px;
}
/* Inputs */
label {
margin-top: 0.6em;
color: #f1f1f1;
font-size: 0.7em;
}
label a,
label a:hover,
label a:visited {
color: #aaa;
text-decoration: underline;
}
input[type='text'],
textarea,
select {
font-size: 0.6em;
padding: 0.5em;
margin: 0.5em 0;
background: #333337;
border: 1px solid #2d2d30;
color: #999;
transition-property: none !important;
outline: none;
}
input[type='text']:hover,
textarea:hover,
select:hover {
color: #f1f1f1;
border-color: #007acc;
}
input[type='text']:focus,
textarea:focus,
select:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
textarea#graphic-html {
min-height: 150px;
}
textarea#graphic-css {
min-height: 400px;
}
input[type=submit] {
margin-top: 0.6em;
border: none;
color: #f1f1f1;
background: #2199e8;
font-size: 0.6em;
line-height: 3em;
}
input[readonly],
input[readonly]:hover {
background: #2d2d30 !important;
border-color: #3f3f3f;
}
select {
height: 2.5em;
-webkit-appearance: none;
border-radius: 0;
background-position: right center;
background-size: 9px 6px;
background-origin: content-box;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28138, 138, 138%29'></polygon></svg>");
}
select:hover {
color: #f1f1f1;
border-color: #007acc;
}
select:focus {
background: #333337;
color: #f1f1f1;
border-color: #007acc;
box-shadow: none;
}
a.button {
margin: 0 1rem 0 0;
width: 7rem;
}
/* Graphic */
header {
display: flex;
}
header h3 {
font-size: 1em;
flex-grow: 2;
border-bottom: 1px solid #2d2d30;
padding-top: 10px;
margin-right: 30px;
}
header button {
border: 0;
width: 100px;
}
.graphic-presetlist {
display: flex;
flex-direction: column;
align-items: stretch;
}
.graphic-presetadd {
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid #2d2d30;
margin: 30px 0 10px;
padding: 20px;
position: relative;
}
.graphic-presetadd-header {
background: #3f3f41;
position: absolute;
top: -1.3em;
left: 10px;
font-size: 0.8em;
padding: 0.8em 10px;
}
.graphic-presetadd-buttons {
display: flex;
margin-top: 10px;
}
.graphic-presetadd-buttons button {
margin-right: 10px;
width: 150px;
}
.graphic-presetremove {
align-self: center;
margin-top: 50px;
width: 150px;
}
.graphic-empty {
font-size: 0.7em;
color: #999;
text-align: center;
margin: 20px 0;
}
.graphic-delete {
align-self: center;
margin-top: 30px;
width: 150px;
}
.graphic-label {
margin-top: 30px;
padding-bottom: 0.5em;
}
.graphic-helper {
font-size: 0.7em;
color: #999;
margin: 5px 0 0;
}
.graphic-helper.bottom {
margin: 0;
}
.graphic-property,
.graphic-preset {
display: flex;
align-items: stretch;
}
.graphic-property-reorder,
.graphic-preset-reorder {
width: 62px;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODQ5NDcyMzY3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODQ5NDcyMzc3MTU5MTFFOEEwQjVFQUFCMEYzRDE2QjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4NDk0NzIzNDcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4NDk0NzIzNTcxNTkxMUU4QTBCNUVBQUIwRjNEMTZCNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pla8e2wAAABFUExURXd3d////3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0brzhMAAAAWdFJOUwAAN0NEVXJzdHV2d3h/iImKmaq73e5w9SzPAAAAlElEQVRIx+XT2wqDMBRE0Witt2piW53//9SCoETMwJkXkWY/CgtNBp0jDa/086JgwgNeIwEghpFVAMFOJoCaNNkFMNlIJBImRcrPodJ8ybQ/I285B7mLSCWX+/qL3EVTznK5T/mUu++P7OVyX/9U9T30sLyliUeobR8WmcZ6lnYTrf34HRX8xlbTaZfcA726yziyXX5Joj0NgTfsUQAAAABJRU5ErkJggg==") no-repeat transparent;
background-size: 25px;
background-position: center;
touch-action: none;
}
.graphic-property input,
.graphic-preset input {
flex-grow: 2;
margin: 0;
}
.graphic-property button,
.graphic-preset button {
width: 100px;
border: 1px solid #3f3f41;
border-left: none;
}
.graphic-preset input {
padding-top: 1.5em;
padding-bottom: 1.5em;
}
.graphic-preset button {
height: auto;
}
.schedule-empty {
margin-top: 2em;
font-size: 1em;
text-align: center;
}
.settings-empty {
text-align: center;
margin: 50px 0 30px;
font-size: 0.8em;
color: #999;
}
.settings-empty-button {
align-self: center;
width: 200px;
}
/* Dragula */
#dragcontainer {
position: fixed;
top: 0;
left: 0;
}
.gu-mirror {
position: absolute !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
}
.gu-hide {
display: none !important;
}
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}
/* Media queries */
body {
font-size: 1.5rem;
}
@media only screen and (max-device-width: 600px) {
#container {
flex-direction: column;
}
nav {
width: auto;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
nav .header {
width: 100%;
}
nav a {
width: calc(50% - 8px);
}
nav input[type=text] {
width: 100%;
}
nav .status {
align-self: center;
width: 120px;
}
#content {
width: auto;
}
}
/*# sourceMappingURL=main.css.map */

116
public/status.css Normal file
View file

@ -0,0 +1,116 @@
html {
box-sizing: border-box;
font-size: 16px;
height: 100%;
}
*, *:before, *:after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p {
margin: 0;
padding: 0;
font-weight: normal;
}
body {
background: #3f3f41;
color: #eb6e00;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif, Arial;
}
#container {
display: flex;
align-items: stretch;
flex-direction: column;
flex-grow: 2;
}
#container h3 {
background: black;
font-size: 2.3em;
line-height: (2.3em);
color: #eb6e00;
height: (2.3em);
overflow: hidden;
padding: 0 0.3em;
flex-grow: 2;
border-radius: 5px 0 0 5px;
word-break: break-all;
}
#container button {
border: none;
color: black;
background: #eb6e00;
font-size: 2em;
width: 80px;
flex-shrink: 0;
border-radius: 0 5px 5px 0;
}
#container .item {
display: flex;
margin: 5px;
}
#container .empty {
flex-grow: 2;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
font-size: 2em;
}
header {
display: flex;
margin-bottom: 5px;
color: #ccc;
}
header h2 {
font-size: 3em;
line-height: 64px;
padding: 0 0.3em;
flex-grow: 2;
}
header .status {
padding: 0 20px 0 30px;
line-height: 64px;
font-size: 2em;
text-align: left;
position: relative;
}
header .status::after {
position: absolute;
left: 0;
top: calc(50% - 7px);
content: '';
border: 10px solid #ec5840;
}
header .status.green::after {
border-color: #00FF00;
}
.disconnected {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
font-size: 3em;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
}

View file

@ -1,44 +0,0 @@
#!/usr/bin/env node
/* eslint-disable no-console */
'use strict'
const _ = require('lodash')
const config = require('../config')
let log = require('../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: './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)
})
}