From 8780eb228e01460d3dee088c292bc682bf09c76a Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Tue, 26 Jun 2018 18:35:12 +0000 Subject: [PATCH] Major update and redesign --- .eslintrc | 85 +- api/bookshelf.js | 3 +- api/casparcg/client.js | 120 ++ api/casparcg/connection.js | 5 + api/content/model.js | 41 + api/content/routes.js | 84 +- api/engine/routes.js | 7 +- api/graphic/routes.js | 50 +- api/preset/model.js | 2 +- api/preset/routes.js | 14 +- api/routerio.js | 6 + api/schedule/model.js | 35 + api/schedule/routes.js | 52 + api/server.js | 13 +- api/settings/model.js | 51 + api/settings/routes.js | 37 + app/client/countdown.js | 100 +- app/client/index.js | 22 +- app/client/schedule.js | 6 +- app/client/text.js | 6 +- app/main/add/index.js | 52 - app/main/add/module.js | 57 + app/main/common/components.js | 60 + app/main/common/module.js | 98 + app/main/controller/components.js | 80 - app/main/controller/module.js | 51 - app/main/dagskra/module.js | 83 + app/main/graphic/controller.js | 158 -- app/main/graphic/engine/countdown.js | 115 +- app/main/graphic/engine/schedule.js | 239 +-- app/main/graphic/engine/text.js | 186 +- app/main/graphic/module.js | 260 +++ app/main/graphic/view.js | 34 +- app/main/header.js | 57 +- app/main/index.js | 9 +- app/main/menu.js | 80 +- app/main/store.js | 36 +- app/status/index.js | 106 + app/styl/main.styl | 631 ++++-- app/styl/status.styl | 168 ++ install_run.bat | 8 + migrations/20180625133103_settings.js | 24 + migrations/20180626143319_content.js | 22 + migrations/20180626154925_schedule.js | 20 + package.json | 21 +- public/foundation.css | 2555 ------------------------- public/index.html | 15 +- public/main.css | 190 -- public/main.css.map | 1 - public/status.html | 11 + 50 files changed, 2465 insertions(+), 3701 deletions(-) create mode 100644 api/casparcg/client.js create mode 100644 api/casparcg/connection.js create mode 100644 api/content/model.js create mode 100644 api/schedule/model.js create mode 100644 api/schedule/routes.js create mode 100644 api/settings/model.js create mode 100644 api/settings/routes.js delete mode 100644 app/main/add/index.js create mode 100644 app/main/add/module.js create mode 100644 app/main/common/components.js create mode 100644 app/main/common/module.js delete mode 100644 app/main/controller/components.js delete mode 100644 app/main/controller/module.js create mode 100644 app/main/dagskra/module.js delete mode 100644 app/main/graphic/controller.js create mode 100644 app/main/graphic/module.js create mode 100644 app/status/index.js create mode 100644 app/styl/status.styl create mode 100644 install_run.bat create mode 100644 migrations/20180625133103_settings.js create mode 100644 migrations/20180626143319_content.js create mode 100644 migrations/20180626154925_schedule.js delete mode 100644 public/foundation.css delete mode 100644 public/main.css delete mode 100644 public/main.css.map create mode 100644 public/status.html diff --git a/.eslintrc b/.eslintrc index b87d399..0e3f92e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,33 +1,88 @@ { - "parser": "babel-eslint", - "extends": "airbnb-base", - "ecmaFeatures": { - "modules": false + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "ecmaFeatures": { + "impliedStrict": true + } }, + "extends": "eslint:recommended", "plugins": [ "mocha" ], + "env": { + "mocha": true, + "node": true, + "es6": true + }, "rules": { "mocha/no-exclusive-tests": 2, + "require-await": 0, + "no-invalid-this": 0, + + "array-callback-return": 2, + "block-scoped-var": 2, + "complexity": ["error", 20], + "eqeqeq": [2, "smart"], + "no-else-return": ["error", { "allowElseIf": false }], + "no-extra-bind": 2, + "no-implicit-coercion": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-new": 2, + "no-param-reassign": [2, {"props": false}], + "no-return-assign": 2, + "no-return-await": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-unmodified-loop-condition": 2, + "no-useless-call": 2, + "no-useless-concat": 2, + "no-useless-return": 2, + "no-void": 2, + "no-warning-comments": 2, + "prefer-promise-reject-errors": 2, + "no-catch-shadow": 2, + "no-shadow": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-use-before-define": 0, + "no-new-require": 2, + "no-sync": 2, + "array-bracket-newline": [2, "consistent"], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs"], + "comma-dangle": [2, "always-multiline"], + "comma-spacing": 2, + "comma-style": 2, + "computed-property-spacing": 2, + "eol-last": 2, + "func-call-spacing": 2, + "key-spacing": 2, + "keyword-spacing": 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" + "args": "after-used", + "argsIgnorePattern": "next|res|req" } - ] + ], + "generator-star-spacing": 0, + "global-require": 0, + "import/prefer-default-export": 0, + "no-underscore-dangle": 0, + "strict": 0, + "require-yield": 0 }, - "globals": { - "describe": false, - "it": false, - "before": false, - "beforeEach": false, - "after": false, - "afterEach": false + globals: { + "window": true, + "document": true } } diff --git a/api/bookshelf.js b/api/bookshelf.js index c202fa5..7ff37e2 100644 --- a/api/bookshelf.js +++ b/api/bookshelf.js @@ -73,10 +73,11 @@ shelf.createModel = (attr, opts) => { .fetch({ require, withRelated }) }, - getAll(where = {}, withRelated = []) { + getAll(where = {}, withRelated = [], orderBy = 'id') { where.is_deleted = false return this.query({ where }) + .orderBy(orderBy, 'ASC') .fetchAll({ withRelated }) }, }) diff --git a/api/casparcg/client.js b/api/casparcg/client.js new file mode 100644 index 0000000..1e10b97 --- /dev/null +++ b/api/casparcg/client.js @@ -0,0 +1,120 @@ +import Settings from '../settings/model' +import { address } from 'ip' +import { CasparCG, AMCP } from 'casparcg-connection' + +const timeoutDuration = 60000 + +let io +let logger + +let connection +let casparIsPlaying +let casparIsConnected +let checkTimeout +let currentHost + +export async function initialise(log, socket) { + io = socket.socket + logger = log + + return connect() +} + +export async function connect() { + currentHost = await Settings.getValue('casparcg') + casparIsPlaying = false + casparIsConnected = false + logger.info('CasparCG: Connectiong to', currentHost + ':' + 5250) + + if (connection) { + await connection.close() + } + + connection = new CasparCG({ + host: currentHost, + port: 5250, + queueMode: 2, + autoReconnectInterval: timeoutDuration, + onError: err => { + logger.error('CasparCG: Error', err.message) + }, + onConnectionStatus: data => { + casparIsConnected = data.connected + + if (!data.connected) { + casparIsPlaying = false + logger.warn('CasparCG: connection closed, retrying in 60 seconds', connection.connected) + io.emit('casparcg.status', currentStatus()) + if (checkTimeout) clearInterval(checkTimeout) + checkTimeout = null + } + }, + onConnected: async connected => { + logger.info('CasparCG: connected', connected) + io.emit('casparcg.status', currentStatus()) + checkClientPlaying(false, true) + + // Run our check on hourly interval + if (checkTimeout) clearInterval(checkTimeout) + checkTimeout = setInterval(() => checkClientPlaying(), timeoutDuration * 60) + }, + }) +} + +export function currentStatus(e) { + return { + connected: casparIsConnected, + playing: casparIsPlaying, + error: e, + } +} + +export async function checkClientPlaying(starting = false, first = false) { + let ip + if (currentHost === 'localhost' || currentHost === '127.0.0.1') { + ip = 'localhost' + } else { + ip = address() + } + + // Check if we lost connection while attempting to start playing + if (!connection.connected) { + logger.error('CasparCG: Attempted to play but connection was lost') + } + + try { + // Check if we're already playing + let output = await connection.info(1, 100) + + if (output.response.data.status !== 'playing') { + casparIsPlaying = false + + if (starting) { + // We are not playing, check to see if we've already attempted + // to issue a play command and delay trying for a minute + await new Promise(res => { + logger.warn('CasparCG: Play did not start playing, retrying in 60 seconds') + setTimeout(res, timeoutDuration) + }) + } + + // Send a play command and retry checking again + logger.info(`CasparCG: Sending play command for ${ip}:3000`) + await connection.do(new AMCP.CustomCommand(`PLAY 1-100 [HTML] "http://${ip}:3000/client.html" CUT 1 LINEAR RIGHT`)) + return checkClientPlaying(true) + } + + casparIsPlaying = true + + // We are playing, notify all clients + io.emit('casparcg.status', currentStatus()) + if (starting || first) { + logger.info('CasparCG: client is up and playing') + } + } catch (e) { + // Unknown error occured + casparIsPlaying = true + logger.error(e, 'CasparCG: Error starting play on client') + io.emit('casparcg.status', currentStatus(e)) + } +} diff --git a/api/casparcg/connection.js b/api/casparcg/connection.js new file mode 100644 index 0000000..2a93491 --- /dev/null +++ b/api/casparcg/connection.js @@ -0,0 +1,5 @@ +import { currentStatus } from './client' + +export async function casparConnection(ctx) { + ctx.socket.emit('casparcg.status', currentStatus()) +} diff --git a/api/content/model.js b/api/content/model.js new file mode 100644 index 0000000..f4c1fd8 --- /dev/null +++ b/api/content/model.js @@ -0,0 +1,41 @@ +import bookshelf from '../bookshelf' + +/* Content model: +{ + id, + name, + graphic, + html, + css, + data, +} +*/ + +const Content = bookshelf.createModel({ + tableName: 'content', + + format(attributes) { + attributes.graphic = JSON.stringify(attributes.graphic) + attributes.data = JSON.stringify(attributes.data) + return attributes + }, + + parse(attributes) { + if (attributes.graphic) { + attributes.graphic = JSON.parse(attributes.graphic) + } + if (attributes.data) { + attributes.data = JSON.parse(attributes.data) + } + return attributes + }, +}, { + getSingle(name, withRelated = [], require = false) { + let where = { name } + + return this.query({ where }) + .fetch({ require, withRelated }) + }, +}) + +export default Content diff --git a/api/content/routes.js b/api/content/routes.js index d9daf7f..1b447ee 100644 --- a/api/content/routes.js +++ b/api/content/routes.js @@ -1,4 +1,5 @@ import _ from 'lodash' +import Content from './model' export const active = { } @@ -7,25 +8,48 @@ function getSocket(ctx, all) { return ctx.socket } -export function display(ctx, data) { +/* + * Event: 'content.display' + * + * Display a specific graphic content + */ +export async function display(ctx, data) { let compiled = _.template(data.graphic.settings.html) let html = compiled(data.data) - let payload = { - graphic: data.graphic, - html, - css: data.graphic.settings.css, - data: data.data, + let old = await Content.getSingle(data.graphic.name) + + if (old) { + await old.destroy() } - active[data.graphic.name] = payload - ctx.io.emit('client.display', payload) + let payload = { + graphic: data.graphic, + name: data.graphic.name, + html: html || '', + css: data.graphic.settings.css || '', + data: data.data, + is_deleted: false, + } + + let content = await Content.create(payload) + + ctx.io.emit('client.display', content.toJSON()) list(ctx, true) } -export function hide(ctx, data) { - delete active[data.name] +/* + * Event: 'content.hide' + * + * Hide a specific graphic content + */ +export async function hide(ctx, data) { + let content = await Content.getSingle(data.name) + + if (!content) return + + await content.destroy() ctx.io.emit('client.hide', { name: data.name, @@ -35,21 +59,41 @@ export function hide(ctx, data) { } function generateDisplayText(item) { - if (item.graphic.engine === 'countdown') { - return `${item.data[item.graphic.settings.main]} - ${item.data.countdown}` + // if (item.graphic.engine === 'countdown') { + // return `${item.data.text} - ${item.data.finished}` + // } + try { + return _.template(item.graphic.settings.main)(item.data) + } catch (e) { + return `Error creating display: ${e.message}` } - 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]), - })) +/* + * Event: 'content.list' + * Runs on start of every new connection + * + * Send a name list of all active graphics + */ +export async function list(ctx, all) { + let allContent = await Content.getAll() + + let payload = await Promise.all(allContent.map(item => ({ + name: item.get('name'), + display: generateDisplayText(item.toJSON()), + }))) getSocket(ctx, all).emit('content.list', payload) } -export function reset(ctx) { - ctx.socket.emit('client.reset', _.values(active)) +/* + * Event: 'content.list' + * Runs on start of every new connection + * + * Send actual graphics of all active graphics + */ +export async function reset(ctx) { + let allContent = await Content.getAll() + + ctx.socket.emit('client.reset', allContent.toJSON()) } diff --git a/api/engine/routes.js b/api/engine/routes.js index 2adaaaf..ee943b5 100644 --- a/api/engine/routes.js +++ b/api/engine/routes.js @@ -1,4 +1,9 @@ +/* + * Event: 'engine.all' + * + * Return all supported graphic engines. + */ export function all(ctx) { - ctx.socket.emit('engine.all', ['text', 'countdown', 'schedule']) + ctx.socket.emit('engine.all', ['text', 'countdown']) } diff --git a/api/graphic/routes.js b/api/graphic/routes.js index b3b9e29..fb0008e 100644 --- a/api/graphic/routes.js +++ b/api/graphic/routes.js @@ -1,11 +1,21 @@ import Graphic from './model' +/* + * Event: 'graphic.all' + * + * Request all graphics in store + */ export async function all(ctx) { let data = await Graphic.getAll() - ctx.io.emit('graphic.all', data.toJSON()) + ctx.socket.emit('graphic.all', data.toJSON()) } +/* + * Event: 'graphic.single' + * + * Request a single graphic + */ export async function single(ctx, data) { if (!data || !data.id) { ctx.log.warn('called graphic get single but no id specified') @@ -14,23 +24,38 @@ export async function single(ctx, data) { let graphic = await Graphic.getSingle(data.id) - ctx.io.emit('graphic.single', graphic.toJSON()) + ctx.socket.emit('graphic.single', graphic.toJSON()) } +/* + * Event: 'graphic.create' + * + * Create a single graphic and emit to all clients. + * + * @body {string} engine - Engine for the graphic + * @body {string} name - Name of graphic + */ 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' + data.settings.main = '<%- text %> - <%- finished %>' } - await Graphic.create(data) + let graphic = await Graphic.create(data) - await all(ctx) + ctx.io.emit('graphic.single', graphic.toJSON()) } +/* + * Event: 'graphic.remove' + * + * Remove a single graphic + * + * @body {int} id - Id of the graphic to remove + */ export async function remove(ctx, data) { if (!data || !data.id) { ctx.log.warn('called graphic get single but no id specified') @@ -41,9 +66,20 @@ export async function remove(ctx, data) { graphic.set({ is_deleted: true }) await graphic.save() - await all(ctx) + let output = await Graphic.getAll() + ctx.io.emit('graphic.all', output.toJSON()) } +/* + * Event: 'graphic.update' + * + * Update a single graphic + * + * @body {int} id - Id of the graphic to update + * @body {string} [name] - Name of the graphic + * @body {string} [engine] - Engine for the graphic + * @body {object} [settings] - Settings for the graphic, JSON object + */ export async function update(ctx, data) { if (!data || !data.id) { ctx.log.warn('called graphic update but no id specified') @@ -56,5 +92,5 @@ export async function update(ctx, data) { await graphic.save() - await single(ctx, data) + ctx.io.emit('graphic.single', graphic.toJSON()) } diff --git a/api/preset/model.js b/api/preset/model.js index f4badd0..df6f800 100644 --- a/api/preset/model.js +++ b/api/preset/model.js @@ -23,7 +23,7 @@ const Preset = bookshelf.createModel({ attributes.values = JSON.parse(attributes.values) } return attributes - } + }, }, { }) diff --git a/api/preset/routes.js b/api/preset/routes.js index 72ef031..bf3d9ee 100644 --- a/api/preset/routes.js +++ b/api/preset/routes.js @@ -3,7 +3,7 @@ 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 }) + let data = await Preset.getAll({ graphic_id: id }, [], 'sort') ctx.io.emit(`preset.all:${id}`, data.toJSON()) } @@ -27,6 +27,18 @@ export async function add(ctx, 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) diff --git a/api/routerio.js b/api/routerio.js index 0e14f2f..99f84da 100644 --- a/api/routerio.js +++ b/api/routerio.js @@ -1,11 +1,14 @@ 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 @@ -17,11 +20,14 @@ function onConnection(server, data) { 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 diff --git a/api/schedule/model.js b/api/schedule/model.js new file mode 100644 index 0000000..b4ed2ea --- /dev/null +++ b/api/schedule/model.js @@ -0,0 +1,35 @@ +import bookshelf from '../bookshelf' +import Graphic from '../graphic/model' + +/* Schedule model: +{ + id, + graphic_id, + values, + sort, + is_deleted, +} +*/ + +const Schedule = bookshelf.createModel({ + tableName: 'schedule', + + graphic() { + return this.belongsTo(Graphic, 'graphic_id') + }, + + 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 Schedule diff --git a/api/schedule/routes.js b/api/schedule/routes.js new file mode 100644 index 0000000..755d7d0 --- /dev/null +++ b/api/schedule/routes.js @@ -0,0 +1,52 @@ +import Schedule from './model' + +export async function all(ctx) { + let data = await Schedule.getAll({ }, ['graphic'], 'sort') + + ctx.io.emit('schedule.all', data.toJSON()) + total(ctx) +} + +export async function total(ctx) { + let data = await Schedule.getAll({ }, ['graphic'], 'sort') + + ctx.io.emit('schedule.total', { total: data.length }) +} + +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 }) + + if (last) { + payload.sort = last.get('sort') + 1 + } + + await Schedule.create(payload) + + await all(ctx) +} + +export async function patch(ctx, payload) { + await Promise.all(payload.map(async item => { + let scheduleItem = await Schedule.getSingle(item.id) + + scheduleItem.set({ sort: item.sort }) + + await scheduleItem.save() + })) + + await all(ctx) +} + +export async function remove(ctx, payload) { + let scheduleItem = await Schedule.getSingle(payload.id) + + await scheduleItem.destroy() + + await all(ctx) +} diff --git a/api/server.js b/api/server.js index 681a27b..a8884e0 100644 --- a/api/server.js +++ b/api/server.js @@ -1,6 +1,7 @@ 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' @@ -14,9 +15,19 @@ 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(serve('./public', '/public')) +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) diff --git a/api/settings/model.js b/api/settings/model.js new file mode 100644 index 0000000..84866e3 --- /dev/null +++ b/api/settings/model.js @@ -0,0 +1,51 @@ +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 diff --git a/api/settings/routes.js b/api/settings/routes.js new file mode 100644 index 0000000..9edc69f --- /dev/null +++ b/api/settings/routes.js @@ -0,0 +1,37 @@ +import Settings from './model' +import { connect } from '../casparcg/client' + +/* + * Event: 'settings.all' + * + * Request all settings in store + */ +export async function all(ctx) { + let data = await Settings.getSettings() + + ctx.socket.emit('settings.all', data) +} + +/* + * Event: 'settings.update' + * + * Update a single setting value + * + * @body {string} [name] - Name of the settings + * @body {string} [value] - Value of the settings + */ +export async function update(ctx, data) { + if (!data || data.name == null || data.value == null) { + ctx.log.warn(data, 'called settings update but no name or value specified, body was:') + return + } + + await Settings.setValue(data.name, data.value) + + let output = await Settings.getSettings() + ctx.io.emit('settings.all', output) + + if (data.name === 'casparcg') { + connect() + } +} diff --git a/app/client/countdown.js b/app/client/countdown.js index 308d2d1..db3dd48 100644 --- a/app/client/countdown.js +++ b/app/client/countdown.js @@ -1,5 +1,48 @@ -module.exports = function(data) { +var currentActiveTimer = null + +function pad(n) { return (n < 10) ? ('0' + n) : n } + +function timer(name) { + var days = 0 + var hours = 0 + var mins = 0 + var secs = 0 + + var now = new Date() + + var timeElement = document.getElementById(name + '-countdown-timer') + + if (!timeElement) { + clearInterval(currentActiveTimer) + currentActiveTimer = null + return + } + + var data = timeElement.tag + var time = data.time + var difference = (time - now) + + if (difference <= 0) { + clearInterval(currentActiveTimer) + currentActiveTimer = null + timeElement.innerHTML = data.data.finished || '' + 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 +} + +module.exports.init = function(data) { var element = document.createElement('div') element.innerHTML = data.html element.id = data.graphic.name @@ -18,50 +61,19 @@ module.exports = function(data) { element.classList.add('root-element-display') }, 100) + module.exports.update(data) +} + +module.exports.update = function(data) { 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 - } - + data.time = new Date(data.data.countdown.replace(' ', 'T')) + if (timeElement) { - timeElement.tag = time - timer() - data.timer = setInterval(timer, 1000) + timeElement.tag = data + timer(data.graphic.name) + if (currentActiveTimer) { + clearInterval(currentActiveTimer) + } + currentActiveTimer = setInterval(timer.bind(null, data.graphic.name), 1000) } } diff --git a/app/client/index.js b/app/client/index.js index 1475879..46f675e 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -6,23 +6,21 @@ var engines = { schedule: require('./schedule'), } -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 engine = data.graphic.engine + if (exists) { + exists.innerHtml = data.html + exists.tag.innerHtml = data.css + + engines[engine].update(data) + return + } + if (engines[engine]) { - engines[engine](data) + engines[engine].init(data) } } @@ -32,8 +30,6 @@ socket.on('client.hide', function (data) { var exists = document.getElementById(data.name) if (exists) { - current.splice(current.indexOf(data.name), 1) - exists.classList.remove('root-element-display') window.setTimeout(function () { diff --git a/app/client/schedule.js b/app/client/schedule.js index 0fb2e92..e478e9f 100644 --- a/app/client/schedule.js +++ b/app/client/schedule.js @@ -1,5 +1,5 @@ -module.exports = function(data) { +module.exports.init = function(data) { var element = document.createElement('div') element.innerHTML = data.html element.id = data.graphic.name @@ -18,3 +18,7 @@ module.exports = function(data) { element.classList.add('root-element-display') }, 100) } + +module.exports.update = function() { + +} diff --git a/app/client/text.js b/app/client/text.js index 0fb2e92..e478e9f 100644 --- a/app/client/text.js +++ b/app/client/text.js @@ -1,5 +1,5 @@ -module.exports = function(data) { +module.exports.init = function(data) { var element = document.createElement('div') element.innerHTML = data.html element.id = data.graphic.name @@ -18,3 +18,7 @@ module.exports = function(data) { element.classList.add('root-element-display') }, 100) } + +module.exports.update = function() { + +} diff --git a/app/main/add/index.js b/app/main/add/index.js deleted file mode 100644 index cb39b18..0000000 --- a/app/main/add/index.js +++ /dev/null @@ -1,52 +0,0 @@ -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/main/add/module.js b/app/main/add/module.js new file mode 100644 index 0000000..1854d5c --- /dev/null +++ b/app/main/add/module.js @@ -0,0 +1,57 @@ +const m = require('mithril') +const createModule = require('../common/module') +const components = require('../common/components') +const socket = require('../../socket') +const store = require('../store') + +const Add = createModule({ + init: function() { + this.monitor('engines', 'engine.all', []) + store.listen('graphic.single', data => { + if (data.name === this.graphic.name) { + m.route.set(`/graphic/${data.id}`) + } + }) + this.graphic = { } + }, + + updated: function(name, control) { + this.graphic[name] = control.target.value + }, + + create: function() { + if (!this.graphic.engine) { + this.graphic.engine = this.engines[0] + } + if (!this.graphic.name) { + this.error = 'Name cannot be empty' + return + } + + socket.emit('graphic.create', this.graphic) + }, + + removing: function() { + store.unlisten('graphic.single') + }, +}, function() { + return [ + m('h4.header', 'Add graphic'), + components.error(this.error), + m('label', { for: 'create-name' }, 'Name'), + m('input#create-name[type=text]', { + oninput: (control) => this.updated('name', control), + }), + m('label', { for: 'create-engine' }, 'Engine'), + m('select', { + onchange: (control) => this.updated('engine', control), + }, this.engines.map(engine => + m('option', { key: engine, value: engine }, engine) + )), + m('input[type=submit]', { + value: 'Create', + onclick: () => this.create(), + }), + ] +}) +module.exports = Add diff --git a/app/main/common/components.js b/app/main/common/components.js new file mode 100644 index 0000000..ef2b4eb --- /dev/null +++ b/app/main/common/components.js @@ -0,0 +1,60 @@ +const m = require('mithril') + +exports.error = function(error) { + if (!error) return null + + return m('div.error-box', error) +} + +exports.presetOnlyList = function(module, graphic, title, color = 'green', button = 'Display now', schedule = 'Schedule') { + return [ + m('label.graphic-label', { key: 'first' }, title), + m('div.graphic-presetlist', { + key: `second-${graphic.id}`, + oncreate: control => module.presetlistInit(control), + }, + module.presets.map(item => + m('div.graphic-preset', { + key: `preset-${graphic.id}-${item.id}`, + data: item.id, + }, [ + m('div.graphic-preset-reorder'), + m('input[type=text]', { + readonly: true, + value: module.mainTemplate(item.values), + }), + schedule && m(`button`, { + onclick: module.schedulePreset.bind(module, item), + }, schedule) || null, + m(`button.${color}`, { + onclick: module.displayPreset.bind(module, item), + }, button), + module.displayRemove && m('button.red', { + onclick: module.removePreset.bind(module, item), + }, 'Remove') || null, + ]), + ), + ), + module.presets.length && + m('button.red.graphic-presetremove', { + key: 'third', + onclick: () => (module.displayRemove = !module.displayRemove), + }, 'Remove entries') || null, + ] +} + +exports.presetButtons = function(module, green, blue) { + return [ + m('div.graphic-presetadd-buttons', [ + green && m('button.green', { + onclick: module.displayCurrent.bind(module), + }, green) || null, + blue && m('button', { + onclick: module.addPreset.bind(module), + }, blue) || null, + m('button', { + onclick: module.scheduleCurrent.bind(module), + }, 'Add to schedule'), + ]), + ] +} diff --git a/app/main/common/module.js b/app/main/common/module.js new file mode 100644 index 0000000..a2788a1 --- /dev/null +++ b/app/main/common/module.js @@ -0,0 +1,98 @@ +const m = require('mithril') +const _ = require('lodash') +const store = require('../store') +const socket = require('../../socket') +const dragula = require('dragula') + +function createModule(component, view) { + let newModule = { } + + newModule = _.defaults(component, { + oninit: function(vnode) { + this.error = null + this.listening = [] + this.init(vnode) + }, + + _storeUpdated: function(key, name, id, cb) { + this[key] = store.get(name, id) + if (cb) cb(store.get(name, id)) + m.redraw() + }, + + init: function() { }, + + removing: function() { }, + + monitor: function(key, name, fallback, id, cb) { + this[key] = store.get(name, id) || fallback || { } + + this.listening.push(store.getId(name, id)) + + store.listen(name, this._storeUpdated.bind(this, key, name, id, cb), id) + + socket.emit(name, { id: id }) + }, + + unmonitor: function(name, id) { + store.unlisten(store.getId(name, id)) + this.listening.splice(this.listening.indexOf(store.getId(name, id)), 1) + }, + + initDragula: function(control, cb) { + let dragContainer = document.getElementById('dragcontainer') + let out = dragula([control.dom], { + mirrorContainer: dragContainer, + invalid: el => el.className !== 'graphic-preset-reorder' + && el.className !== 'graphic-preset', + }) + out.on('dragend', () => { + if (is_touch_device()) { + document.body.style.cssText = '' + window.scroll(0, document.body.data) + } + }) + out.on('drag', () => { + if (is_touch_device()) { + document.body.data = window.scrollY + document.body.style.cssText = `position: fixed; left: 0; right: 0; overflow: hidden; top: -${window.scrollY}px;` + dragContainer.style.marginTop = `${document.body.data}px` + } + }) + out.on('drop', (a, b, c, d) => { + cb(a, d) + }) + }, + + onremove: function() { + this.listening.forEach((item) => { + store.unlisten(item) + }) + this.removing() + }, + + view: view, + }) + + return newModule +} + +// https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886 +// LOL +function is_touch_device() { + var prefixes = ' -webkit- -moz- -o- -ms- '.split(' ') + var mq = function(query) { + return window.matchMedia(query).matches + } + + if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + return true + } + + // include the 'heartz' as a way to have a non matching MQ to help terminate the join + // https://git.io/vznFH + var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('') + return mq(query) +} + +module.exports = createModule diff --git a/app/main/controller/components.js b/app/main/controller/components.js deleted file mode 100644 index eeb5e80..0000000 --- a/app/main/controller/components.js +++ /dev/null @@ -1,80 +0,0 @@ -const m = require('mithril') - -exports.error = function(error) { - if (!error) return null - - return m('div.error-box', error) -} - -exports.presetOnlyList = function(vm) { - return [ - 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-4 columns' }, - [ - m('a.panel-graphic-preset-remove.button.success', { - onclick: vm.displayPreset.bind(vm, item), - }, 'Display'), - m('a.panel-graphic-preset-remove.button.alert', { - onclick: vm.removePreset.bind(vm, item), - }, 'X') - ] - ), - ]) - ]) - )) - ] -} - -exports.presetButtons = function(vm) { - return [ - m('a.panel-graphic-preset-add.button', { - onclick: vm.addPreset.bind(vm), - }, 'Save Preset'), - m('a.panel-graphic-display.success.button', { - onclick: vm.displayCurrent.bind(vm), - }, 'Display Live'), - ] -} - -exports.presetList = function(vm) { - return [ - m('a.panel-graphic-preset-add.button', { - onclick: vm.addPreset.bind(vm), - }, 'Save Preset'), - m('a.panel-graphic-display.success.button', { - onclick: vm.displayCurrent.bind(vm), - }, 'Display Live'), - 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-4 columns' }, - [ - m('a.panel-graphic-preset-remove.button.success', { - onclick: vm.displayPreset.bind(vm, item), - }, 'Display'), - m('a.panel-graphic-preset-remove.button.alert', { - onclick: vm.removePreset.bind(vm, item), - }, 'X') - ] - ), - ]) - ]) - )) - ] -} diff --git a/app/main/controller/module.js b/app/main/controller/module.js deleted file mode 100644 index b9e9aef..0000000 --- a/app/main/controller/module.js +++ /dev/null @@ -1,51 +0,0 @@ -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/main/dagskra/module.js b/app/main/dagskra/module.js new file mode 100644 index 0000000..dee6442 --- /dev/null +++ b/app/main/dagskra/module.js @@ -0,0 +1,83 @@ +const _ = require('lodash') +const m = require('mithril') +const createModule = require('../common/module') +const socket = require('../../socket') + +const Dagskra = createModule({ + init: function() { + this.error = '' + this.displayRemove = false + this.monitor('schedule', 'schedule.all', []) + }, + + scheduleListInit: function(control) { + this.initDragula(control, (source, target) => { + let dragOldIndex = _.findIndex(this.schedule, { id: Number(source.getAttribute('data')) }) + let targetOldIndex = this.schedule.length - 1 + if (target) { + targetOldIndex = _.findIndex(this.schedule, { id: Number(target.getAttribute('data')) }) + } + + this.schedule.splice(targetOldIndex, 0, this.schedule.splice(dragOldIndex, 1)[0]) + + this.schedule.forEach((item, i) => { + item.sort = i + 1 + }) + + socket.emit('schedule.patch', this.schedule) + }) + }, + + displaySchedule: function(item) { + socket.emit('content.display', { + graphic: item.graphic, + data: item.values, + }) + }, + + removeSchedule: function(item) { + socket.emit('schedule.remove', item) + }, +}, function() { + this.schedule.forEach(item => { + if (!item.cachedDisplay) { + try { + item.cachedDisplay = _.template(item.graphic.settings.main || '')(item.values) + } catch (e) { + item.cachedDisplay = `ERROR WITH TEMPLATE: ${e.message}` + } + item.cachedDisplay = `[${item.graphic.name}] ${item.cachedDisplay}` + } + }) + return [ + m('h4.header', 'Schedule'), + m('div.graphic-presetlist', { + oncreate: control => this.scheduleListInit(control), + }, + this.schedule.map(item => + m('div.graphic-preset', { + key: `preset-${item.id}`, + data: item.id, + }, [ + m('div.graphic-preset-reorder'), + m('input[type=text]', { + readonly: true, + value: item.cachedDisplay, + }), + m(`button.green`, { + onclick: () => this.displaySchedule(item), + }, 'Display'), + this.displayRemove && m('button.red', { + onclick: () => this.removeSchedule(item), + }, 'Remove') || null, + ]), + ), + ), + this.schedule.length + && m('button.red.graphic-presetremove', { + onclick: () => (this.displayRemove = !this.displayRemove), + }, 'Remove entries') + || m('div.schedule-empty', 'Schedule is empty'), + ] +}) +module.exports = Dagskra diff --git a/app/main/graphic/controller.js b/app/main/graphic/controller.js deleted file mode 100644 index ebaf9ad..0000000 --- a/app/main/graphic/controller.js +++ /dev/null @@ -1,158 +0,0 @@ -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('') - this.newTextField = 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) - } - }, - - addDataField: function(type, name) { - if (!name) { - return 'Please type in proper name' - } - - if (this.graphic.settings[type].includes(name)) { - return 'A property with that name already exists' - } - - this.graphic.settings[type].push(name) - - socket.emit('graphic.update', this.graphic) - - return null - }, - - addProperty: function() { - this.error = this.addDataField('properties', this.newProperty()) - - if (!this.error) { - this.newProperty('') - - if (!this.graphic.settings.main) { - this.graphic.settings.main = this.graphic.settings.properties[0] - socket.emit('graphic.update', this.graphic) - } - } - }, - - addTextField: function() { - this.error = this.addDataField('textfields', this.newTextField()) - - if (!this.error) { - this.newTextField('') - } - }, - - removeDataField: function(type, name) { - this.graphic.settings[type].splice( - this.graphic.settings[type].indexOf(name), 1) - socket.emit('graphic.update', this.graphic) - }, - - removeProperty: function(prop) { - this.removeDataField('properties', prop) - }, - - 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, - }) - }, - - 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/main/graphic/engine/countdown.js b/app/main/graphic/engine/countdown.js index 96691a5..63485b1 100644 --- a/app/main/graphic/engine/countdown.js +++ b/app/main/graphic/engine/countdown.js @@ -1,65 +1,82 @@ const m = require('mithril') -const components = require('../../components') +const components = require('../../common/components') -exports.view = function(ctlr, graphic, vm) { +exports.view = function(module, graphic) { return [ - m('label', [ - 'Text', - m('input[type=text]', { - value: vm.graphic.settings.text || '', - oninput: vm.updated.bind(vm, 'settings.text'), + m('div.graphic-presetadd', [ + m('h3.graphic-presetadd-header', 'Start countdown'), + + m('label', { for: `countdown-text` }, 'Text'), + m(`input#countdown-text[type=text]`, { + value: graphic.settings.text || '', + oninput: module.updated.bind(module, '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', { for: `countdown-countdown` }, 'Count to (format: "YYYY-MM-DD hh:mm")'), + m(`input#countdown-countdown[type=text]`, { + value: graphic.settings.countdown || '', + oninput: module.updated.bind(module, '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'), + + m('label', { for: `countdown-finished` }, 'Finished (gets displayed in the countdown upon reaching 0)'), + m(`input#countdown-finished[type=text]`, { + value: graphic.settings.finished || '', + oninput: module.updated.bind(module, 'settings.finished'), }), + components.presetButtons(module, 'Display live now', 'Add to template'), ]), - components.presetList(vm), + components.presetOnlyList(module, graphic, 'Templates', '', 'Fill top', ''), ] } -exports.settings = function(cltr, graphic, vm) { +exports.settings = function(module, graphic) { return [ - m('label', [ - 'Name', - m('input[type=text]', { - value: graphic.name, - oninput: vm.updated.bind(vm, 'name'), - }), + // Name + m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'), + m('input#graphic-name[type=text]', { + value: graphic.name, + oninput: module.updated.bind(module, 'name'), + }), + + // HTML + m('label.graphic-label', { for: 'graphic-html' }, [ + 'Graphic HTML (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: <%- text %>)', ]), - 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('p.graphic-helper', `
`), + m('textarea#graphic-html', { + rows: '4', + oninput: module.updated.bind(module, 'settings.html'), + value: graphic.settings.html || '', + }), + m('p.graphic-helper.bottom', `
`), + + // CSS + m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'), + m('p.graphic-helper', ''), + + // Main display template + m('label.graphic-label', { for: 'graphic-main' }, [ + 'Graphic control display template (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: <%- text %>, <%- countdown %>, <%- finished %>)', ]), - 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), + m('input#graphic-main[type=text]', { + value: graphic.settings.main, + oninput: module.updated.bind(module, 'settings.main'), + }), + components.error(module.mainTemplateError), + + // Remove + m('button.red.graphic-delete', { + onclick: module.remove.bind(module), }, 'Delete graphic'), ] } diff --git a/app/main/graphic/engine/schedule.js b/app/main/graphic/engine/schedule.js index d0dc583..263d7c9 100644 --- a/app/main/graphic/engine/schedule.js +++ b/app/main/graphic/engine/schedule.js @@ -1,162 +1,121 @@ const m = require('mithril') -const components = require('../../components') +const components = require('../../common/components') -exports.view = function(ctlr, graphic, vm) { +exports.view = function(module, graphic) { if (!graphic.settings.properties) { graphic.settings.properties = [] } - if (!graphic.settings.textfields) { - graphic.settings.textfields = [] - } 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.'), + m('p.settings-empty', ` + No properties have been defined. + This graphic needs properties to be defined before usage. + Click the settings button to define the properties for this graphic. + `), + m('button.settings-empty-button', { + onclick: () => module.switchView(), + }, module.changeViewTitle()), ] } return [ - components.presetOnlyList(vm), - 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'), + m('div.graphic-presetadd', [ + m('h3.graphic-presetadd-header', 'Create preset/display graphic'), + graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [ + m('label', { for: `preset-add-${index}` }, prop), + m(`input#preset-add-${index}[type=text]`, { + value: module.current[prop] || '', + oninput: module.updated.bind(module, prop, 'current'), }), - ]) - ), - graphic.settings.textfields.map((prop, index) => - m('label', { key: index }, [ - prop, - m('textarea', { - rows: '6', - oninput: vm.updated.bind(vm, prop, 'current'), - value: vm.current[prop] || '', - }), - ]) - ), - components.presetButtons(vm), + ])), + components.presetButtons(module, 'Display live now', 'Add to preset list'), + ]), + components.presetOnlyList(module, graphic, 'Presets'), ] } -exports.settings = function(cltr, graphic, vm) { +exports.settings = function(module, graphic) { return [ - m('label', [ - 'Name', - m('input[type=text]', { - value: graphic.name, - oninput: vm.updated.bind(vm, 'name'), - }), + // Name + m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'), + m('input#graphic-name[type=text]', { + value: graphic.name, + oninput: module.updated.bind(module, 'name'), + }), + + // HTML + m('label.graphic-label', { for: 'graphic-html' }, [ + 'Graphic HTML (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: ', + graphic.settings.properties.map(prop => + `<%- ${prop} %>` + ).join(', '), + ')', ]), - m('label', [ - 'HTML (', - m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), - ' available: ', - graphic.settings.properties.map(prop => - `<%- ${prop} %>` - ).join(', '), - ', ', - graphic.settings.textfields.map(prop => - `<%- ${prop} %>` - ).join(', '), - ')', - m('p', `
`), - m('textarea', { - rows: '4', - oninput: vm.updated.bind(null, 'settings.html'), - value: graphic.settings.html || '', - }), - m('p', `
`), + m('p.graphic-helper', `
`), + m('textarea#graphic-html', { + rows: '4', + oninput: module.updated.bind(null, 'settings.html'), + value: graphic.settings.html || '', + }), + m('p.graphic-helper.bottom', `
`), + + // CSS + m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'), + m('p.graphic-helper', ''), + + // Main display template + m('label.graphic-label', { for: 'graphic-main' }, [ + 'Graphic control display template (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: ', + graphic.settings.properties.map(prop => + `<%- ${prop} %>` + ).join(', '), + ')', ]), - m('label', [ - 'CSS', - m('textarea', { - rows: '4', - oninput: vm.updated.bind(null, 'settings.css'), - value: graphic.settings.css || '', - }) - ]), - /* -------- Simple Properties -------- */ - m('label', 'Simple Properties'), - m('label', [ - 'Main', - m('select', { - onchange: vm.updated.bind(vm, 'settings.main'), - }, graphic.settings.properties.map((prop, index) => - m('option', { - key: 'prop-list-' + index, + m('input#graphic-main[type=text]', { + value: graphic.settings.main, + oninput: module.updated.bind(module, 'settings.main'), + }), + + // Property list + m('label.graphic-label', 'Properties'), + graphic.settings.properties.map((prop, index) => + m('div.graphic-property', { key: `prop-${index}` }, [ + m('input[type=text]', { + readonly: true, value: prop, - selected: prop === graphic.settings.main, - }, prop) - )) + }), + m('button.red', { + onclick: module.removeProperty.bind(module, prop), + }, 'Remove'), + ]), + ), + graphic.settings.properties.length === 0 && m('p.graphic-empty', 'No properties exist yet.') || [], + + // Add a new property + m('label.graphic-label', { for: 'graphic-newproperty' }, 'Add new graphic property'), + m('div.graphic-property', [ + m('input#graphic-newproperty[type=text]', { + value: module.newProperty, + oninput: m.withAttr('value', val => (module.newProperty = val)), + }), + m('button', { + onclick: module.addProperty.bind(module), + }, 'Add'), ]), - /* -------- Simple Properties List -------- */ - m('label', 'List'), - 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') - ), - ]), - /* -------- Text Properties -------- */ - m('label', 'Text Fields'), - m('div', [ - graphic.settings.textfields.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.removeDataField.bind(vm, 'textfields', prop), - }, 'Remove') - ) - ]) - ), - ]), - m('.row', [ - m('div', { class: 'small-10 columns panel-graphic-property-item' }, - m('input[type=text]', { - value: vm.newTextField(), - oninput: m.withAttr('value', vm.newTextField), - }) - ), - m('div', { class: 'small-2 columns' }, - m('a.panel-graphic-property-add.button', { - onclick: vm.addTextField.bind(vm), - }, 'Add') - ), - ]), - /* -------- Delete -------- */ - m('a.panel-graphic-delete.button.alert', { - onclick: vm.remove.bind(vm), + components.error(module.mainTemplateError), + + // Remove + m('button.red.graphic-delete', { + onclick: module.remove.bind(module), }, 'Delete graphic'), ] } diff --git a/app/main/graphic/engine/text.js b/app/main/graphic/engine/text.js index 29e0e0d..263d7c9 100644 --- a/app/main/graphic/engine/text.js +++ b/app/main/graphic/engine/text.js @@ -1,109 +1,121 @@ const m = require('mithril') -const components = require('../../components') +const components = require('../../common/components') -exports.view = function(ctlr, graphic, vm) { +exports.view = function(module, graphic) { 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.'), + m('p.settings-empty', ` + No properties have been defined. + This graphic needs properties to be defined before usage. + Click the settings button to define the properties for this graphic. + `), + m('button.settings-empty-button', { + onclick: () => module.switchView(), + }, module.changeViewTitle()), ] } return [ - components.presetOnlyList(vm), - 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'), + m('div.graphic-presetadd', [ + m('h3.graphic-presetadd-header', 'Create preset/display graphic'), + graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [ + m('label', { for: `preset-add-${index}` }, prop), + m(`input#preset-add-${index}[type=text]`, { + value: module.current[prop] || '', + oninput: module.updated.bind(module, prop, 'current'), }), - ]) - ), - components.presetButtons(vm), + ])), + components.presetButtons(module, 'Display live now', 'Add to preset list'), + ]), + components.presetOnlyList(module, graphic, 'Presets'), ] } -exports.settings = function(cltr, graphic, vm) { +exports.settings = function(module, graphic) { return [ - m('label', [ - 'Name', - m('input[type=text]', { - value: graphic.name, - oninput: vm.updated.bind(vm, 'name'), - }), + // Name + m('label.graphic-label', { for: 'graphic-name' }, 'Graphic ID'), + m('input#graphic-name[type=text]', { + value: graphic.name, + oninput: module.updated.bind(module, 'name'), + }), + + // HTML + m('label.graphic-label', { for: 'graphic-html' }, [ + 'Graphic HTML (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: ', + graphic.settings.properties.map(prop => + `<%- ${prop} %>` + ).join(', '), + ')', ]), - 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('p.graphic-helper', `
`), + m('textarea#graphic-html', { + rows: '4', + oninput: module.updated.bind(null, 'settings.html'), + value: graphic.settings.html || '', + }), + m('p.graphic-helper.bottom', `
`), + + // CSS + m('label.graphic-label', { for: 'graphic-css' }, 'Graphic CSS'), + m('p.graphic-helper', ''), + + // Main display template + m('label.graphic-label', { for: 'graphic-main' }, [ + 'Graphic control display template (', + m('a', { href: 'https://lodash.com/docs#template', target: '_blank' }, 'variables'), + ' available: ', + graphic.settings.properties.map(prop => + `<%- ${prop} %>` + ).join(', '), + ')', ]), - 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#graphic-main[type=text]', { + value: graphic.settings.main, + oninput: module.updated.bind(module, 'settings.main'), + }), + + // Property list + m('label.graphic-label', 'Properties'), + graphic.settings.properties.map((prop, index) => + m('div.graphic-property', { key: `prop-${index}` }, [ 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') - ), + readonly: true, + value: prop, + }), + m('button.red', { + onclick: module.removeProperty.bind(module, prop), + }, 'Remove'), + ]), + ), + graphic.settings.properties.length === 0 && m('p.graphic-empty', 'No properties exist yet.') || [], + + // Add a new property + m('label.graphic-label', { for: 'graphic-newproperty' }, 'Add new graphic property'), + m('div.graphic-property', [ + m('input#graphic-newproperty[type=text]', { + value: module.newProperty, + oninput: m.withAttr('value', val => (module.newProperty = val)), + }), + m('button', { + onclick: module.addProperty.bind(module), + }, 'Add'), ]), - m('a.panel-graphic-delete.button.alert', { - onclick: vm.remove.bind(vm), + components.error(module.mainTemplateError), + + // Remove + m('button.red.graphic-delete', { + onclick: module.remove.bind(module), }, 'Delete graphic'), ] } diff --git a/app/main/graphic/module.js b/app/main/graphic/module.js new file mode 100644 index 0000000..b307a54 --- /dev/null +++ b/app/main/graphic/module.js @@ -0,0 +1,260 @@ +const _ = require('lodash') +const m = require('mithril') +const createModule = require('../common/module') +const socket = require('../../socket') +const view = require('./view') +const dragula = require('dragula') + +const Graphic = createModule({ + init: function(vnode) { + this.fetchData(vnode) + }, + + onupdate: function(vnode) { + this.fetchData(vnode) + }, + + fetchData: function(vnode) { + if (this.currentId === vnode.attrs.id) return + + if (this.currentId && this.currentId !== vnode.attrs.id) { + this.unmonitor('graphic.single', this.currentId) + this.unmonitor('preset.all', this.currentId) + } + + this.currentId = vnode.attrs.id + + this.monitor( + 'graphic', + 'graphic.single', + {}, + vnode.attrs.id, + () => this.recheckTemplate() + ) + this.monitor('presets', 'preset.all', [], vnode.attrs.id) + + this.currentView = 'view' + this.current = {} + this.displayRemove = false + this.newProperty = '' + this.newTextField = '' + this.mainTemplateString = '' + this.mainTemplateError = '' + this.mainTemplate = _.template('') + }, + + recheckTemplate: function() { + if (this.graphic.settings.main !== this.mainTemplateString) { + this.mainTemplateError = '' + this.mainTemplateString = this.graphic.settings.main + + try { + this.mainTemplate = _.template(this.mainTemplateString) + } catch (e) { + this.mainTemplateError = `Invalid template: ${e.message}` + } + } + }, + + updated: function(name, variable, cont) { + let target = variable + let control = cont + + if (!control) { + control = variable + target = 'graphic' + } + _.set(this[target], name, control.target.value) + + this.recheckTemplate() + + if (target === 'graphic') { + socket.emit('graphic.update', this.graphic) + } + }, + + addDataField: function(type, name) { + if (!name) { + return 'Please type in proper name' + } + + if (this.graphic.settings[type].includes(name)) { + return 'A property with that name already exists' + } + + this.graphic.settings[type].push(name) + + socket.emit('graphic.update', this.graphic) + + return null + }, + + addProperty: function() { + this.error = this.addDataField('properties', this.newProperty) + + if (!this.error) { + this.newProperty = '' + + if (!this.graphic.settings.main) { + this.graphic.settings.main = `<%- ${ this.graphic.settings.properties[0] } %>` + this.recheckTemplate() + socket.emit('graphic.update', this.graphic) + } + } + }, + + addTextField: function() { + this.error = this.addDataField('textfields', this.newTextField) + + if (!this.error) { + this.newTextField = '' + } + }, + + removeDataField: function(type, name) { + this.graphic.settings[type].splice( + this.graphic.settings[type].indexOf(name), 1) + + if (type === 'properties' && this.graphic.settings.properties.length === 0) { + this.graphic.settings.main = '' + this.recheckTemplate() + } + + socket.emit('graphic.update', this.graphic) + }, + + removeProperty: function(prop) { + this.removeDataField('properties', prop) + }, + + presetlistInit: function(control) { + this.initDragula(control, (source, target) => { + let dragOldIndex = _.findIndex(this.presets, { id: Number(source.getAttribute('data')) }) + let targetOldIndex = this.presets.length - 1 + if (target) { + targetOldIndex = _.findIndex(this.presets, { id: Number(target.getAttribute('data')) }) + } + + this.presets.splice(targetOldIndex, 0, this.presets.splice(dragOldIndex, 1)[0]) + + this.presets.forEach((item, i) => { + item.sort = i + 1 + }) + + socket.emit('preset.patch', this.presets) + }) + }, + + 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" needs 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] = '' + } + }) + } + }, + + addPreset: function() { + this.error = '' + + this.cleanCurrent() + + if (this.error) return + + if (this.graphic.engine === 'countdown') { + this.current.countdown = null + } + + 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.set('/') + }, + + displayPreset: function(preset) { + if (this.graphic.engine === 'countdown') { + this.graphic.settings.text = preset.values.text + this.graphic.settings.finished = preset.values.finished + socket.emit('graphic.update', this.graphic) + return + } + + socket.emit('content.display', { + graphic: this.graphic, + data: preset.values, + }) + }, + + schedulePreset: function(preset) { + socket.emit('schedule.add', { + graphic_id: this.graphic.id, + values: preset.values, + }) + }, + + scheduleCurrent: function() { + this.error = '' + + this.cleanCurrent() + + if (this.error) return + + socket.emit('schedule.add', { + graphic_id: this.graphic.id, + values: this.current, + }) + }, + + displayCurrent: function() { + this.error = '' + + this.cleanCurrent() + + if (this.error) return + + socket.emit('content.display', { + graphic: this.graphic, + data: this.current, + }) + }, + + switchView: function() { + if (this.currentView === 'view') { + this.currentView = 'settings' + } else { + this.currentView = 'view' + } + }, + + changeViewTitle: function() { + if (this.currentView === 'view') { + return 'Settings' + } + return 'Control' + }, +}, view) + +module.exports = Graphic diff --git a/app/main/graphic/view.js b/app/main/graphic/view.js index dae9e69..56b2a55 100644 --- a/app/main/graphic/view.js +++ b/app/main/graphic/view.js @@ -1,6 +1,5 @@ const m = require('mithril') -const Graphic = require('./controller') -const components = require('../components') +const components = require('../common/components') const engines = { text: require('./engine/text'), @@ -8,21 +7,20 @@ const engines = { schedule: require('./engine/schedule'), } -Graphic.view = function(ctrl) { - graphic = Graphic.vm.graphic +module.exports = function() { + let graphic = this.graphic + let currentView = graphic.engine && engines[graphic.engine][this.currentView] || null - 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), - ] - ), - ]) + return [ + m('h4.header', 'Graphic'), + m('header', [ + m('h3', graphic.name), + m('button', { + onclick: () => this.switchView(), + }, this.changeViewTitle()), + ]), + components.error(this.error), + !currentView && m('p', 'Loading...') + || currentView(this, graphic), + ] } diff --git a/app/main/header.js b/app/main/header.js index 555d21b..2b04646 100644 --- a/app/main/header.js +++ b/app/main/header.js @@ -1,10 +1,22 @@ const m = require('mithril') -const createModule = require('./module') +const createModule = require('./common/module') const socket = require('../socket') const Header = createModule({ init: function() { - this.monitor('list', 'content.list', []) + this.currentLength = 0 + this.updateMargin = false + this.connected = socket.connected + this.monitor('list', 'content.list', [], null, () => this.checkChanged()) + + socket.on('connect', () => { + this.connected = true + m.redraw() + }) + socket.on('disconnect', () => { + this.connected = false + m.redraw() + }) }, hide: function(item) { @@ -12,19 +24,38 @@ const Header = createModule({ 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), + + onupdate: function() { + if (!this.updateMargin) return + this.updateMargin = false + + let header = document.getElementById('header') + let container = document.getElementById('container') + + container.style.marginTop = `${ header.clientHeight - 1}px` + }, + + checkChanged: function() { + if (this.currentLength === this.list.length) return + this.currentLength = this.list.length + this.updateMargin = true + }, +}, function() { + return [ + this.list.length > 0 && [ + m('h4', 'Active graphics'), + this.list.map(item => + m('div.item', { key: `header-${item.id}` }, [ + m('h3', `${item.name} - ${item.display}`), + m('button.red', { + onclick: () => this.hide(item), }, 'Hide'), - m('div.header-item-display', `${item.name} - ${item.display}`), ]) ), - ]), - ] || '') + ] || null, + !this.connected && m('div.disconnected', ` + Lost connection with server, Attempting to reconnect + `) || null, + ] }) module.exports = Header diff --git a/app/main/index.js b/app/main/index.js index b7a94f7..6fb520b 100644 --- a/app/main/index.js +++ b/app/main/index.js @@ -19,14 +19,15 @@ const m = require('mithril') const Header = require('./header') const Menu = require('./menu') -const Add = require('./add') -const Graphic = require('./graphic') +const Add = require('./add/module') +const Graphic = require('./graphic/module') +const Dagskra = require('./dagskra/module') m.mount(document.getElementById('header'), Header) m.mount(document.getElementById('menu'), Menu) m.route(document.getElementById('content'), '/', { - '/': {}, + '/': Dagskra, '/add': Add, '/graphic/:id': Graphic, -}); +}) diff --git a/app/main/menu.js b/app/main/menu.js index 333b05b..7128c45 100644 --- a/app/main/menu.js +++ b/app/main/menu.js @@ -1,24 +1,70 @@ const m = require('mithril') -const createModule = require('./module') +const createModule = require('./common/module') +const socket = require('../socket') 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' ), - ]), - ]) + this.monitor('settings', 'settings.all', {}) + this.monitor('schedule', 'schedule.total', { total: 0 }) + this.monitor('status', 'casparcg.status', { + connected: false, + playing: false, + }) + this.newHost = '' + this.enableEdit = false + }, + + setHost(value) { + this.newHost = value + this.enableEdit = true + }, + + saveNewHost() { + socket.emit('settings.update', { + name: 'casparcg', + value: this.newHost, + }) + + this.newHost = '' + this.enableEdit = false + }, +}, function() { + return [ + m('a', { + href: '/', + oncreate: m.route.link, + class: m.route.get() === '/' && 'active' || '', + }, `Schedule (${this.schedule.total})` ), + m('h4.header.header--space', 'Graphics'), + this.list.map((item) => + m('a', { + href: `/graphic/${item.id}`, + oncreate: m.route.link, + class: m.route.get() === `/graphic/${item.id}` && 'active' || '', + }, item.name) + ), + m('h5.header.header--space', 'Other'), + m('a', { + href: '/add', + oncreate: m.route.link, + class: m.route.get() === '/add' && 'active' || '', + }, 'Add graphic' ), + m('h5.header.header--space', 'CasparCG Status'), + m('input[type=text]', { + placeholder: 'Host IP', + value: this.newHost || this.settings.casparcg || '', + oninput: control => this.setHost(control.target.value), + }), + this.enableEdit && m('button', { + onclick: () => this.saveNewHost(), + }, 'Connect'), + m('div.status', { + class: this.status.connected && 'green', + }, 'connected'), + m('div.status', { + class: this.status.playing && 'green', + }, 'playing'), + ] }) module.exports = Menu diff --git a/app/main/store.js b/app/main/store.js index dcb862c..b580c59 100644 --- a/app/main/store.js +++ b/app/main/store.js @@ -5,6 +5,7 @@ const events = {} // Listen on all events let onevent = socket.onevent +let disconnected = false socket.onevent = function(packet) { let args = packet.data || [] @@ -13,6 +14,25 @@ socket.onevent = function(packet) { onevent.call(this, packet) } +socket.on('disconnect', () => { + disconnected = true +}) + +socket.on('connect', () => { + if (disconnected) { + Object.keys(events).forEach(event => { + let name = event + let id = null + if (event.indexOf(':') > 0) { + name = event.split(':')[0] + id = Number(event.split(':')[1]) + } + socket.emit(name, { id: id }) + }) + } + disconnected = false +}) + function genId(name, id) { if (id) { return `${name}:${id}` @@ -21,6 +41,10 @@ function genId(name, id) { } const store = { + getId: function(name, id) { + return genId(name, id) + }, + get: function(name, id) { return storage[genId(name, id)] }, @@ -39,18 +63,24 @@ const store = { } socket.on('*', (event, data) => { - let name = genId(event, data && data.id) + let id = data && data.id + + let name = genId(event, id) + if (events[name]) { storage[name] = data events[name]() } if (event.indexOf('single') >= 0) { let check = event.replace('single', 'all') - if (events[name]) { + if (events[check]) { let index = _.findIndex(storage[check], { id: data.id }) if (index > -1) { storage[check][index] = data - events[name]() + events[check]() + } else { + storage[check].push(data) + events[check]() } } } diff --git a/app/status/index.js b/app/status/index.js new file mode 100644 index 0000000..4cf6d19 --- /dev/null +++ b/app/status/index.js @@ -0,0 +1,106 @@ +const socket = require('../socket') +const m = require('mithril') + +const Status = { + oninit: function() { + this.current = [] + this.connected = socket.connected + this.status = { + connected: false, + playing: false, + } + + socket.on('casparcg.status', data => { + this.status = data + m.redraw() + }) + socket.on('content.list', data => { + this.current = data + m.redraw() + }) + socket.on('connect', () => this.updateConnected(true)) + socket.on('disconnect', () => this.updateConnected(false)) + }, + + updateConnected: function(connected) { + this.connected = connected + m.redraw() + }, + + hide: function(item) { + socket.emit('content.hide', { + name: item.name, + }) + }, + + view: function() { + return [ + m('header', [ + m('h2', 'Active graphics:'), + m('div.status', { + class: this.status.connected && 'green', + }, 'connected'), + m('div.status', { + class: this.status.playing && 'green', + }, 'playing'), + ]), + this.current.map(item => + m('div.item', { key: `header-${item.id}` }, [ + m('h3', `${item.name} - ${item.display}`), + m('button', { + onclick: () => this.hide(item), + }, 'Hide'), + ]) + ), + this.current.length === 0 && m('div.empty', 'No active graphics') || null, + !this.connected && m('div.disconnected', ` + Lost connection with server, Attempting to reconnect + `) || null, + ] + }, +} + +m.mount(document.getElementById('container'), Status) + +// var engines = { +// text: require('./text'), +// countdown: require('./countdown'), +// schedule: require('./schedule'), +// } + +// function display(data) { +// var exists = document.getElementById(data.graphic.name) + +// var engine = data.graphic.engine + +// if (exists) { +// exists.innerHtml = data.html +// exists.tag.innerHtml = data.css + +// engines[engine].update(data) +// return +// } + +// if (engines[engine]) { +// engines[engine].init(data) +// } +// } + +// socket.on('client.display', display) + +// socket.on('client.hide', function (data) { +// var exists = document.getElementById(data.name) + +// if (exists) { +// exists.classList.remove('root-element-display') + +// window.setTimeout(function () { +// exists.tag.remove() +// exists.remove() +// }, 1500) +// } +// }) + +// socket.on('client.reset', function(data) { +// data.forEach(display) +// }) diff --git a/app/styl/main.styl b/app/styl/main.styl index 8c0ed40..a07d072 100644 --- a/app/styl/main.styl +++ b/app/styl/main.styl @@ -1,156 +1,306 @@ +/* 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 { - 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; + display: flex; + align-items: stretch; + flex-grow: 2; } /* Header */ - - .header-list { - list-style-type: none; - margin: 0; + 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; + } } - - .header-item { - margin-bottom: 1rem; - } - - .header-item-hide { - float: right; - width: 5rem; - border-radius: 6px; - margin: 0; - } - - .header-item-display { - background: #070707; - color: #eb6e00; - border-radius: 6px; - padding: 0.5rem 1rem; - margin-right: 5.5rem; + + .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 */ - .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 { + 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 */ - .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; + 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 */ - .panel-graphic-property-item input { - display: inline-block; - } - 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 { + input[type='text'], + textarea, + select { + font-size: 0.6em; + padding: 0.5em; + margin: 0.5em 0; background: #333337; - border-color: #3f3f3f; + 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; + } } - - input[type="text"]:hover, - textarea:hover { + + input[type=submit] { + margin-top: 0.6em; + border: none; color: #f1f1f1; - border-color: #007acc; - } - - input[type="text"]:focus, - textarea:focus { - background: #333337; - color: #f1f1f1; - border-color: #007acc; - box-shadow: none; + background: #2199e8; + font-size: 0.6em; + line-height: 3em; } input[readonly], @@ -160,14 +310,14 @@ h4 { } select { - background: #333337; - border-color: #3f3f3f; - color: #999999; + 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,') + background-image: url("data:image/svg+xml;utf8,") } select:hover { @@ -187,55 +337,210 @@ h4 { 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; + + &-reorder { + width: 30px; + 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: left center; + touch-action: none; + } + + & input { + flex-grow: 2; + margin: 0; + } + + & button { + width: 100px; + border: 1px solid #3f3f41; + border-left: none; + } + } + } + + .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: 1280px) { - .header-item-hide { - width: 9rem; - line-height: 0rem; + @media only screen and (max-device-width: 600px) { + #container { + flex-direction: column; } - - a.button { - font-size: 2rem; - line-height: 0; + + 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; } - - .panel-graphic-preset { - margin: 0; - } - - .panel-graphic-display, - .panel-graphic-preset-add { - margin-bottom: 3rem !important; - } - - .panel-graphic-preset-remove { - padding-right: 0.5rem; - padding-left: 0.5rem; - } - - .panel-graphic-preset-remove.alert { - padding-right: 1rem; - padding-left: 1rem; - } - - .panel-graphic-settings { - font-size: 1.3rem !important; - } - - .header-item-display { - font-size: 2rem; - margin-right: 12.5rem; - padding: 0.2rem 1rem; - } - - .panel-graphic-property-item input { - font-size: 2rem; - height: 3.5rem; - } } diff --git a/app/styl/status.styl b/app/styl/status.styl new file mode 100644 index 0000000..20de087 --- /dev/null +++ b/app/styl/status.styl @@ -0,0 +1,168 @@ +/* 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; +} diff --git a/install_run.bat b/install_run.bat new file mode 100644 index 0000000..9bcf17e --- /dev/null +++ b/install_run.bat @@ -0,0 +1,8 @@ +git pull +npm install +npm run build +npm start | bunyan +echo. +echo EXITED +echo. +PAUSE diff --git a/migrations/20180625133103_settings.js b/migrations/20180625133103_settings.js new file mode 100644 index 0000000..59446d6 --- /dev/null +++ b/migrations/20180625133103_settings.js @@ -0,0 +1,24 @@ +/* eslint-disable */ +'use strict'; + +exports.up = function(knex, Promise) { + return Promise.all([ + knex.schema.createTable('settings', function(table) { + table.increments() + table.text('name') + table.text('value') + table.boolean('is_deleted') + }).then(() => { + return knex('settings').insert({ + name: 'casparcg', + value: '' + }) + }), + ]); +}; + +exports.down = function(knex, Promise) { + return Promise.all([ + knex.schema.dropTable('settings'), + ]); +}; diff --git a/migrations/20180626143319_content.js b/migrations/20180626143319_content.js new file mode 100644 index 0000000..4350238 --- /dev/null +++ b/migrations/20180626143319_content.js @@ -0,0 +1,22 @@ +/* eslint-disable */ +'use strict'; + +exports.up = function(knex, Promise) { + return Promise.all([ + knex.schema.createTable('content', function(table) { + table.increments() + table.text('graphic') + table.text('name') + table.text('html') + table.text('css') + table.text('data') + table.boolean('is_deleted') + }), + ]); +}; + +exports.down = function(knex, Promise) { + return Promise.all([ + knex.schema.dropTable('content'), + ]); +}; diff --git a/migrations/20180626154925_schedule.js b/migrations/20180626154925_schedule.js new file mode 100644 index 0000000..2ce674f --- /dev/null +++ b/migrations/20180626154925_schedule.js @@ -0,0 +1,20 @@ +/* eslint-disable */ +'use strict'; + +exports.up = function(knex, Promise) { + return Promise.all([ + knex.schema.createTable('schedule', 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('schedule'), + ]); +}; diff --git a/package.json b/package.json index 2a97b12..2f9c267 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,18 @@ "watch:styl": "stylus -w -m app/styl/main.styl --out public", "build-client:styl": "stylus -m app/styl/client.styl --out public", "watch-client:styl": "stylus -w -m app/styl/client.styl --out public", - "watch:js": "watchify -t babelify app/main.js -o public/main.js --debug", - "build:js": "browserify app/main.js -o public/main.js --debug -t [ babelify ]", - "watch-client:js": "watchify -t babelify app/client.js -o public/client.js --debug", - "build-client:js": "browserify app/client.js -o public/client.js --debug -t [ babelify ]", + "build-status:styl": "stylus -m app/styl/status.styl --out public", + "watch-status:styl": "stylus -w -m app/styl/status.styl --out public", + "watch:js": "watchify -t babelify app/main/index.js -o public/main.js --debug", + "build:js": "browserify app/main/index.js -o public/main.js --debug -t [ babelify ]", + "watch-client:js": "watchify -t babelify app/client/index.js -o public/client.js --debug", + "build-client:js": "browserify app/client/index.js -o public/client.js --debug -t [ babelify ]", + "watch-status:js": "watchify -t babelify app/status/index.js -o public/status.js --debug", + "build-status:js": "browserify app/status/index.js -o public/status.js --debug -t [ babelify ]", "watch:server": "nodemon index.js", "start": "node index.js", - "dev-run": "run-p watch:js watch-client:js watch:server watch:styl watch-client:styl", - "prod-run": "npm run build:js && npm run build-client:js && npm run build:styl && npm run build-client:styl && npm start", + "dev-run": "run-p watch:js watch-client:js watch-status:js watch:server watch:styl watch-client:styl watch-status:styl", + "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", "test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot", "docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine", "docker:test": "npm run docker -- npm install && npm run test", @@ -43,6 +47,9 @@ "babel-register": "^6.26.0", "bookshelf": "^0.11.1", "bunyan": "^1.8.12", + "casparcg-connection": "^4.1.0", + "dragula": "^3.7.2", + "ip": "^1.1.5", "knex": "^0.14.2", "koa": "^2.4.1", "koa-better-serve": "^2.0.7", @@ -54,6 +61,8 @@ "sqlite3": "^3.1.13" }, "devDependencies": { + "eslint": "^4.16.0", + "eslint-plugin-mocha": "^4.11.0", "babelify": "^8.0.0", "mocha": "^4.0.1", "nodemon": "^1.12.1", diff --git a/public/foundation.css b/public/foundation.css deleted file mode 100644 index 09f17ef..0000000 --- a/public/foundation.css +++ /dev/null @@ -1,2555 +0,0 @@ -/** - * Foundation for Sites by ZURB - * Version 6.2.1 - * foundation.zurb.com - * Licensed under MIT Open Source - */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS and IE text size adjust after device orientation change, - * without disabling user zoom. - */ -html { - font-family: sans-serif; - /* 1 */ - -ms-text-size-adjust: 100%; - /* 2 */ - -webkit-text-size-adjust: 100%; - /* 2 */ } - -/** - * Remove default margin. - */ -body { - margin: 0; } - -/* HTML5 display definitions - ========================================================================== */ -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; } - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ -audio, -canvas, -progress, -video { - display: inline-block; - /* 1 */ - vertical-align: baseline; - /* 2 */ } - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ -audio:not([controls]) { - display: none; - height: 0; } - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. - */ -[hidden], -template { - display: none; } - -/* Links - ========================================================================== */ -/** - * Remove the gray background color from active links in IE 10. - */ -a { - background-color: transparent; } - -/** - * Improve readability of focused elements when they are also in an - * active/hover state. - */ -a:active, -a:hover { - outline: 0; } - -/* Text-level semantics - ========================================================================== */ -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ -abbr[title] { - border-bottom: 1px dotted; } - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ -b, -strong { - font-weight: bold; } - -/** - * Address styling not present in Safari and Chrome. - */ -dfn { - font-style: italic; } - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ -h1 { - font-size: 2em; - margin: 0.67em 0; } - -/** - * Address styling not present in IE 8/9. - */ -mark { - background: #ff0; - color: #000; } - -/** - * Address inconsistent and variable font size in all browsers. - */ -small { - font-size: 80%; } - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; } - -sup { - top: -0.5em; } - -sub { - bottom: -0.25em; } - -/* Embedded content - ========================================================================== */ -/** - * Remove border when inside `a` element in IE 8/9/10. - */ -img { - border: 0; } - -/** - * Correct overflow not hidden in IE 9/10/11. - */ -svg:not(:root) { - overflow: hidden; } - -/* Grouping content - ========================================================================== */ -/** - * Address margin not present in IE 8/9 and Safari. - */ -figure { - margin: 1em 40px; } - -/** - * Address differences between Firefox and other browsers. - */ -hr { - box-sizing: content-box; - height: 0; } - -/** - * Contain overflow in all browsers. - */ -pre { - overflow: auto; } - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; } - -/* Forms - ========================================================================== */ -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ -button, -input, -optgroup, -select, -textarea { - color: inherit; - /* 1 */ - font: inherit; - /* 2 */ - margin: 0; - /* 3 */ } - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ -button { - overflow: visible; } - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ -button, -select { - text-transform: none; } - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - /* 2 */ - cursor: pointer; - /* 3 */ } - -/** - * Re-set default cursor for disabled elements. - */ -button[disabled], -html input[disabled] { - cursor: not-allowed; } - -/** - * Remove inner padding and border in Firefox 4+. - */ -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; } - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ -input { - line-height: normal; } - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; - /* 1 */ - padding: 0; - /* 2 */ } - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; } - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. - */ -input[type="search"] { - -webkit-appearance: textfield; - /* 1 */ - box-sizing: content-box; - /* 2 */ } - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; } - -/** - * Define consistent border, margin, and padding. - * [NOTE] We don't enable this ruleset in Foundation, because we want the
element to have plain styling. - */ -/* fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; - } */ -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ -legend { - border: 0; - /* 1 */ - padding: 0; - /* 2 */ } - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ -textarea { - overflow: auto; } - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ -optgroup { - font-weight: bold; } - -/* Tables - ========================================================================== */ -/** - * Remove most spacing between table cells. - */ -table { - border-collapse: collapse; - border-spacing: 0; } - -td, -th { - padding: 0; } - -.foundation-mq { - font-family: "small=0em&medium=40em&large=64em&xlarge=75em&xxlarge=90em"; } - -html { - font-size: 100%; - box-sizing: border-box; } - -*, -*::before, -*::after { - box-sizing: inherit; } - -body { - padding: 0; - margin: 0; - font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; - font-weight: normal; - line-height: 1.5; - color: #0a0a0a; - background: #fefefe; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } - -img { - max-width: 100%; - height: auto; - -ms-interpolation-mode: bicubic; - display: inline-block; - vertical-align: middle; } - -textarea { - height: auto; - min-height: 50px; - border-radius: 0; } - -select { - width: 100%; - border-radius: 0; } - -#map_canvas img, -#map_canvas embed, -#map_canvas object, -.map_canvas img, -.map_canvas embed, -.map_canvas object, -.mqa-display img, -.mqa-display embed, -.mqa-display object { - max-width: none !important; } - -button { - -webkit-appearance: none; - -moz-appearance: none; - background: transparent; - padding: 0; - border: 0; - border-radius: 0; - line-height: 1; } - [data-whatinput='mouse'] button { - outline: 0; } - -.is-visible { - display: block !important; } - -.is-hidden { - display: none !important; } - -div, -dl, -dt, -dd, -ul, -ol, -li, -h1, -h2, -h3, -h4, -h5, -h6, -pre, -form, -p, -blockquote, -th, -td { - margin: 0; - padding: 0; } - -p { - font-size: inherit; - line-height: 1.6; - margin-bottom: 1rem; - text-rendering: optimizeLegibility; } - -em, -i { - font-style: italic; - line-height: inherit; } - -strong, -b { - font-weight: bold; - line-height: inherit; } - -small { - font-size: 80%; - line-height: inherit; } - -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; - font-weight: normal; - font-style: normal; - color: inherit; - text-rendering: optimizeLegibility; - margin-top: 0; - margin-bottom: 0.5rem; - line-height: 1.4; } - h1 small, - h2 small, - h3 small, - h4 small, - h5 small, - h6 small { - color: #cacaca; - line-height: 0; } - -h1 { - font-size: 1.5rem; } - -h2 { - font-size: 1.25rem; } - -h3 { - font-size: 1.1875rem; } - -h4 { - font-size: 1.125rem; } - -h5 { - font-size: 1.0625rem; } - -h6 { - font-size: 1rem; } - -@media screen and (min-width: 40em) { - h1 { - font-size: 3rem; } - h2 { - font-size: 2.5rem; } - h3 { - font-size: 1.9375rem; } - h4 { - font-size: 1.5625rem; } - h5 { - font-size: 1.25rem; } - h6 { - font-size: 1rem; } } - -a { - color: #2199e8; - text-decoration: none; - line-height: inherit; - cursor: pointer; } - a:hover, a:focus { - color: #1585cf; } - a img { - border: 0; } - -hr { - max-width: 75rem; - height: 0; - border-right: 0; - border-top: 0; - border-bottom: 1px solid #cacaca; - border-left: 0; - margin: 1.25rem auto; - clear: both; } - -ul, -ol, -dl { - line-height: 1.6; - margin-bottom: 1rem; - list-style-position: outside; } - -li { - font-size: inherit; } - -ul { - list-style-type: disc; - margin-left: 1.25rem; } - -ol { - margin-left: 1.25rem; } - -ul ul, ol ul, ul ol, ol ol { - margin-left: 1.25rem; - margin-bottom: 0; } - -dl { - margin-bottom: 1rem; } - dl dt { - margin-bottom: 0.3rem; - font-weight: bold; } - -blockquote { - margin: 0 0 1rem; - padding: 0.5625rem 1.25rem 0 1.1875rem; - border-left: 1px solid #cacaca; } - blockquote, blockquote p { - line-height: 1.6; - color: #8a8a8a; } - -cite { - display: block; - font-size: 0.8125rem; - color: #8a8a8a; } - cite:before { - content: '\2014 \0020'; } - -abbr { - color: #0a0a0a; - cursor: help; - border-bottom: 1px dotted #0a0a0a; } - -code { - font-family: Consolas, "Liberation Mono", Courier, monospace; - font-weight: normal; - color: #0a0a0a; - background-color: #e6e6e6; - border: 1px solid #cacaca; - padding: 0.125rem 0.3125rem 0.0625rem; } - -kbd { - padding: 0.125rem 0.25rem 0; - margin: 0; - background-color: #e6e6e6; - color: #0a0a0a; - font-family: Consolas, "Liberation Mono", Courier, monospace; } - -.subheader { - margin-top: 0.2rem; - margin-bottom: 0.5rem; - font-weight: normal; - line-height: 1.4; - color: #8a8a8a; } - -.lead { - font-size: 125%; - line-height: 1.6; } - -.stat { - font-size: 2.5rem; - line-height: 1; } - p + .stat { - margin-top: -1rem; } - -.no-bullet { - margin-left: 0; - list-style: none; } - -.text-left { - text-align: left; } - -.text-right { - text-align: right; } - -.text-center { - text-align: center; } - -.text-justify { - text-align: justify; } - -@media screen and (min-width: 40em) { - .medium-text-left { - text-align: left; } - .medium-text-right { - text-align: right; } - .medium-text-center { - text-align: center; } - .medium-text-justify { - text-align: justify; } } - -@media screen and (min-width: 64em) { - .large-text-left { - text-align: left; } - .large-text-right { - text-align: right; } - .large-text-center { - text-align: center; } - .large-text-justify { - text-align: justify; } } - -.show-for-print { - display: none !important; } - -@media print { - * { - background: transparent !important; - color: black !important; - box-shadow: none !important; - text-shadow: none !important; } - .show-for-print { - display: block !important; } - .hide-for-print { - display: none !important; } - table.show-for-print { - display: table !important; } - thead.show-for-print { - display: table-header-group !important; } - tbody.show-for-print { - display: table-row-group !important; } - tr.show-for-print { - display: table-row !important; } - td.show-for-print { - display: table-cell !important; } - th.show-for-print { - display: table-cell !important; } - a, - a:visited { - text-decoration: underline; } - a[href]:after { - content: " (" attr(href) ")"; } - .ir a:after, - a[href^='javascript:']:after, - a[href^='#']:after { - content: ''; } - abbr[title]:after { - content: " (" attr(title) ")"; } - pre, - blockquote { - border: 1px solid #8a8a8a; - page-break-inside: avoid; } - thead { - display: table-header-group; } - tr, - img { - page-break-inside: avoid; } - img { - max-width: 100% !important; } - @page { - margin: 0.5cm; } - p, - h2, - h3 { - orphans: 3; - widows: 3; } - h2, - h3 { - page-break-after: avoid; } } - -.row { - max-width: 75rem; - margin-left: auto; - margin-right: auto; } - .row::before, .row::after { - content: ' '; - display: table; } - .row::after { - clear: both; } - .row.collapse > .column, .row.collapse > .columns { - padding-left: 0; - padding-right: 0; } - .row .row { - max-width: none; - margin-left: -0.625rem; - margin-right: -0.625rem; - max-width: none; } - @media screen and (min-width: 40em) { - .row .row { - margin-left: -0.9375rem; - margin-right: -0.9375rem; } } - .row .row.collapse { - margin-left: 0; - margin-right: 0; } - .row.expanded { - max-width: none; } - .row.expanded .row { - margin-left: auto; - margin-right: auto; } - -.column, .columns { - width: 100%; - float: left; - padding-left: 0.625rem; - padding-right: 0.625rem; } - @media screen and (min-width: 40em) { - .column, .columns { - padding-left: 0.9375rem; - padding-right: 0.9375rem; } } - .column:last-child:not(:first-child), .columns:last-child:not(:first-child) { - float: right; } - .column.end:last-child:last-child, .end.columns:last-child:last-child { - float: left; } - -.column.row.row, .row.row.columns { - float: none; } - .row .column.row.row, .row .row.row.columns { - padding-left: 0; - padding-right: 0; - margin-left: 0; - margin-right: 0; } - -.small-1 { - width: 8.33333%; } - -.small-push-1 { - position: relative; - left: 8.33333%; } - -.small-pull-1 { - position: relative; - left: -8.33333%; } - -.small-offset-0 { - margin-left: 0%; } - -.small-2 { - width: 16.66667%; } - -.small-push-2 { - position: relative; - left: 16.66667%; } - -.small-pull-2 { - position: relative; - left: -16.66667%; } - -.small-offset-1 { - margin-left: 8.33333%; } - -.small-3 { - width: 25%; } - -.small-push-3 { - position: relative; - left: 25%; } - -.small-pull-3 { - position: relative; - left: -25%; } - -.small-offset-2 { - margin-left: 16.66667%; } - -.small-4 { - width: 33.33333%; } - -.small-push-4 { - position: relative; - left: 33.33333%; } - -.small-pull-4 { - position: relative; - left: -33.33333%; } - -.small-offset-3 { - margin-left: 25%; } - -.small-5 { - width: 41.66667%; } - -.small-push-5 { - position: relative; - left: 41.66667%; } - -.small-pull-5 { - position: relative; - left: -41.66667%; } - -.small-offset-4 { - margin-left: 33.33333%; } - -.small-6 { - width: 50%; } - -.small-push-6 { - position: relative; - left: 50%; } - -.small-pull-6 { - position: relative; - left: -50%; } - -.small-offset-5 { - margin-left: 41.66667%; } - -.small-7 { - width: 58.33333%; } - -.small-push-7 { - position: relative; - left: 58.33333%; } - -.small-pull-7 { - position: relative; - left: -58.33333%; } - -.small-offset-6 { - margin-left: 50%; } - -.small-8 { - width: 66.66667%; } - -.small-push-8 { - position: relative; - left: 66.66667%; } - -.small-pull-8 { - position: relative; - left: -66.66667%; } - -.small-offset-7 { - margin-left: 58.33333%; } - -.small-9 { - width: 75%; } - -.small-push-9 { - position: relative; - left: 75%; } - -.small-pull-9 { - position: relative; - left: -75%; } - -.small-offset-8 { - margin-left: 66.66667%; } - -.small-10 { - width: 83.33333%; } - -.small-push-10 { - position: relative; - left: 83.33333%; } - -.small-pull-10 { - position: relative; - left: -83.33333%; } - -.small-offset-9 { - margin-left: 75%; } - -.small-11 { - width: 91.66667%; } - -.small-push-11 { - position: relative; - left: 91.66667%; } - -.small-pull-11 { - position: relative; - left: -91.66667%; } - -.small-offset-10 { - margin-left: 83.33333%; } - -.small-12 { - width: 100%; } - -.small-offset-11 { - margin-left: 91.66667%; } - -.small-up-1 > .column, .small-up-1 > .columns { - width: 100%; - float: left; } - .small-up-1 > .column:nth-of-type(1n), .small-up-1 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-1 > .column:nth-of-type(1n+1), .small-up-1 > .columns:nth-of-type(1n+1) { - clear: both; } - .small-up-1 > .column:last-child, .small-up-1 > .columns:last-child { - float: left; } - -.small-up-2 > .column, .small-up-2 > .columns { - width: 50%; - float: left; } - .small-up-2 > .column:nth-of-type(1n), .small-up-2 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-2 > .column:nth-of-type(2n+1), .small-up-2 > .columns:nth-of-type(2n+1) { - clear: both; } - .small-up-2 > .column:last-child, .small-up-2 > .columns:last-child { - float: left; } - -.small-up-3 > .column, .small-up-3 > .columns { - width: 33.33333%; - float: left; } - .small-up-3 > .column:nth-of-type(1n), .small-up-3 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-3 > .column:nth-of-type(3n+1), .small-up-3 > .columns:nth-of-type(3n+1) { - clear: both; } - .small-up-3 > .column:last-child, .small-up-3 > .columns:last-child { - float: left; } - -.small-up-4 > .column, .small-up-4 > .columns { - width: 25%; - float: left; } - .small-up-4 > .column:nth-of-type(1n), .small-up-4 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-4 > .column:nth-of-type(4n+1), .small-up-4 > .columns:nth-of-type(4n+1) { - clear: both; } - .small-up-4 > .column:last-child, .small-up-4 > .columns:last-child { - float: left; } - -.small-up-5 > .column, .small-up-5 > .columns { - width: 20%; - float: left; } - .small-up-5 > .column:nth-of-type(1n), .small-up-5 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-5 > .column:nth-of-type(5n+1), .small-up-5 > .columns:nth-of-type(5n+1) { - clear: both; } - .small-up-5 > .column:last-child, .small-up-5 > .columns:last-child { - float: left; } - -.small-up-6 > .column, .small-up-6 > .columns { - width: 16.66667%; - float: left; } - .small-up-6 > .column:nth-of-type(1n), .small-up-6 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-6 > .column:nth-of-type(6n+1), .small-up-6 > .columns:nth-of-type(6n+1) { - clear: both; } - .small-up-6 > .column:last-child, .small-up-6 > .columns:last-child { - float: left; } - -.small-up-7 > .column, .small-up-7 > .columns { - width: 14.28571%; - float: left; } - .small-up-7 > .column:nth-of-type(1n), .small-up-7 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-7 > .column:nth-of-type(7n+1), .small-up-7 > .columns:nth-of-type(7n+1) { - clear: both; } - .small-up-7 > .column:last-child, .small-up-7 > .columns:last-child { - float: left; } - -.small-up-8 > .column, .small-up-8 > .columns { - width: 12.5%; - float: left; } - .small-up-8 > .column:nth-of-type(1n), .small-up-8 > .columns:nth-of-type(1n) { - clear: none; } - .small-up-8 > .column:nth-of-type(8n+1), .small-up-8 > .columns:nth-of-type(8n+1) { - clear: both; } - .small-up-8 > .column:last-child, .small-up-8 > .columns:last-child { - float: left; } - -.small-collapse > .column, .small-collapse > .columns { - padding-left: 0; - padding-right: 0; } - -.small-collapse .row { - margin-left: 0; - margin-right: 0; } - -.small-uncollapse > .column, .small-uncollapse > .columns { - padding-left: 0.625rem; - padding-right: 0.625rem; } - -.small-centered { - float: none; - margin-left: auto; - margin-right: auto; } - -.small-uncentered, -.small-push-0, -.small-pull-0 { - position: static; - margin-left: 0; - margin-right: 0; - float: left; } - -@media screen and (min-width: 40em) { - .medium-1 { - width: 8.33333%; } - .medium-push-1 { - position: relative; - left: 8.33333%; } - .medium-pull-1 { - position: relative; - left: -8.33333%; } - .medium-offset-0 { - margin-left: 0%; } - .medium-2 { - width: 16.66667%; } - .medium-push-2 { - position: relative; - left: 16.66667%; } - .medium-pull-2 { - position: relative; - left: -16.66667%; } - .medium-offset-1 { - margin-left: 8.33333%; } - .medium-3 { - width: 25%; } - .medium-push-3 { - position: relative; - left: 25%; } - .medium-pull-3 { - position: relative; - left: -25%; } - .medium-offset-2 { - margin-left: 16.66667%; } - .medium-4 { - width: 33.33333%; } - .medium-push-4 { - position: relative; - left: 33.33333%; } - .medium-pull-4 { - position: relative; - left: -33.33333%; } - .medium-offset-3 { - margin-left: 25%; } - .medium-5 { - width: 41.66667%; } - .medium-push-5 { - position: relative; - left: 41.66667%; } - .medium-pull-5 { - position: relative; - left: -41.66667%; } - .medium-offset-4 { - margin-left: 33.33333%; } - .medium-6 { - width: 50%; } - .medium-push-6 { - position: relative; - left: 50%; } - .medium-pull-6 { - position: relative; - left: -50%; } - .medium-offset-5 { - margin-left: 41.66667%; } - .medium-7 { - width: 58.33333%; } - .medium-push-7 { - position: relative; - left: 58.33333%; } - .medium-pull-7 { - position: relative; - left: -58.33333%; } - .medium-offset-6 { - margin-left: 50%; } - .medium-8 { - width: 66.66667%; } - .medium-push-8 { - position: relative; - left: 66.66667%; } - .medium-pull-8 { - position: relative; - left: -66.66667%; } - .medium-offset-7 { - margin-left: 58.33333%; } - .medium-9 { - width: 75%; } - .medium-push-9 { - position: relative; - left: 75%; } - .medium-pull-9 { - position: relative; - left: -75%; } - .medium-offset-8 { - margin-left: 66.66667%; } - .medium-10 { - width: 83.33333%; } - .medium-push-10 { - position: relative; - left: 83.33333%; } - .medium-pull-10 { - position: relative; - left: -83.33333%; } - .medium-offset-9 { - margin-left: 75%; } - .medium-11 { - width: 91.66667%; } - .medium-push-11 { - position: relative; - left: 91.66667%; } - .medium-pull-11 { - position: relative; - left: -91.66667%; } - .medium-offset-10 { - margin-left: 83.33333%; } - .medium-12 { - width: 100%; } - .medium-offset-11 { - margin-left: 91.66667%; } - .medium-up-1 > .column, .medium-up-1 > .columns { - width: 100%; - float: left; } - .medium-up-1 > .column:nth-of-type(1n), .medium-up-1 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-1 > .column:nth-of-type(1n+1), .medium-up-1 > .columns:nth-of-type(1n+1) { - clear: both; } - .medium-up-1 > .column:last-child, .medium-up-1 > .columns:last-child { - float: left; } - .medium-up-2 > .column, .medium-up-2 > .columns { - width: 50%; - float: left; } - .medium-up-2 > .column:nth-of-type(1n), .medium-up-2 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-2 > .column:nth-of-type(2n+1), .medium-up-2 > .columns:nth-of-type(2n+1) { - clear: both; } - .medium-up-2 > .column:last-child, .medium-up-2 > .columns:last-child { - float: left; } - .medium-up-3 > .column, .medium-up-3 > .columns { - width: 33.33333%; - float: left; } - .medium-up-3 > .column:nth-of-type(1n), .medium-up-3 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-3 > .column:nth-of-type(3n+1), .medium-up-3 > .columns:nth-of-type(3n+1) { - clear: both; } - .medium-up-3 > .column:last-child, .medium-up-3 > .columns:last-child { - float: left; } - .medium-up-4 > .column, .medium-up-4 > .columns { - width: 25%; - float: left; } - .medium-up-4 > .column:nth-of-type(1n), .medium-up-4 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-4 > .column:nth-of-type(4n+1), .medium-up-4 > .columns:nth-of-type(4n+1) { - clear: both; } - .medium-up-4 > .column:last-child, .medium-up-4 > .columns:last-child { - float: left; } - .medium-up-5 > .column, .medium-up-5 > .columns { - width: 20%; - float: left; } - .medium-up-5 > .column:nth-of-type(1n), .medium-up-5 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-5 > .column:nth-of-type(5n+1), .medium-up-5 > .columns:nth-of-type(5n+1) { - clear: both; } - .medium-up-5 > .column:last-child, .medium-up-5 > .columns:last-child { - float: left; } - .medium-up-6 > .column, .medium-up-6 > .columns { - width: 16.66667%; - float: left; } - .medium-up-6 > .column:nth-of-type(1n), .medium-up-6 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-6 > .column:nth-of-type(6n+1), .medium-up-6 > .columns:nth-of-type(6n+1) { - clear: both; } - .medium-up-6 > .column:last-child, .medium-up-6 > .columns:last-child { - float: left; } - .medium-up-7 > .column, .medium-up-7 > .columns { - width: 14.28571%; - float: left; } - .medium-up-7 > .column:nth-of-type(1n), .medium-up-7 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-7 > .column:nth-of-type(7n+1), .medium-up-7 > .columns:nth-of-type(7n+1) { - clear: both; } - .medium-up-7 > .column:last-child, .medium-up-7 > .columns:last-child { - float: left; } - .medium-up-8 > .column, .medium-up-8 > .columns { - width: 12.5%; - float: left; } - .medium-up-8 > .column:nth-of-type(1n), .medium-up-8 > .columns:nth-of-type(1n) { - clear: none; } - .medium-up-8 > .column:nth-of-type(8n+1), .medium-up-8 > .columns:nth-of-type(8n+1) { - clear: both; } - .medium-up-8 > .column:last-child, .medium-up-8 > .columns:last-child { - float: left; } - .medium-collapse > .column, .medium-collapse > .columns { - padding-left: 0; - padding-right: 0; } - .medium-collapse .row { - margin-left: 0; - margin-right: 0; } - .medium-uncollapse > .column, .medium-uncollapse > .columns { - padding-left: 0.9375rem; - padding-right: 0.9375rem; } - .medium-centered { - float: none; - margin-left: auto; - margin-right: auto; } - .medium-uncentered, - .medium-push-0, - .medium-pull-0 { - position: static; - margin-left: 0; - margin-right: 0; - float: left; } } - -@media screen and (min-width: 64em) { - .large-1 { - width: 8.33333%; } - .large-push-1 { - position: relative; - left: 8.33333%; } - .large-pull-1 { - position: relative; - left: -8.33333%; } - .large-offset-0 { - margin-left: 0%; } - .large-2 { - width: 16.66667%; } - .large-push-2 { - position: relative; - left: 16.66667%; } - .large-pull-2 { - position: relative; - left: -16.66667%; } - .large-offset-1 { - margin-left: 8.33333%; } - .large-3 { - width: 25%; } - .large-push-3 { - position: relative; - left: 25%; } - .large-pull-3 { - position: relative; - left: -25%; } - .large-offset-2 { - margin-left: 16.66667%; } - .large-4 { - width: 33.33333%; } - .large-push-4 { - position: relative; - left: 33.33333%; } - .large-pull-4 { - position: relative; - left: -33.33333%; } - .large-offset-3 { - margin-left: 25%; } - .large-5 { - width: 41.66667%; } - .large-push-5 { - position: relative; - left: 41.66667%; } - .large-pull-5 { - position: relative; - left: -41.66667%; } - .large-offset-4 { - margin-left: 33.33333%; } - .large-6 { - width: 50%; } - .large-push-6 { - position: relative; - left: 50%; } - .large-pull-6 { - position: relative; - left: -50%; } - .large-offset-5 { - margin-left: 41.66667%; } - .large-7 { - width: 58.33333%; } - .large-push-7 { - position: relative; - left: 58.33333%; } - .large-pull-7 { - position: relative; - left: -58.33333%; } - .large-offset-6 { - margin-left: 50%; } - .large-8 { - width: 66.66667%; } - .large-push-8 { - position: relative; - left: 66.66667%; } - .large-pull-8 { - position: relative; - left: -66.66667%; } - .large-offset-7 { - margin-left: 58.33333%; } - .large-9 { - width: 75%; } - .large-push-9 { - position: relative; - left: 75%; } - .large-pull-9 { - position: relative; - left: -75%; } - .large-offset-8 { - margin-left: 66.66667%; } - .large-10 { - width: 83.33333%; } - .large-push-10 { - position: relative; - left: 83.33333%; } - .large-pull-10 { - position: relative; - left: -83.33333%; } - .large-offset-9 { - margin-left: 75%; } - .large-11 { - width: 91.66667%; } - .large-push-11 { - position: relative; - left: 91.66667%; } - .large-pull-11 { - position: relative; - left: -91.66667%; } - .large-offset-10 { - margin-left: 83.33333%; } - .large-12 { - width: 100%; } - .large-offset-11 { - margin-left: 91.66667%; } - .large-up-1 > .column, .large-up-1 > .columns { - width: 100%; - float: left; } - .large-up-1 > .column:nth-of-type(1n), .large-up-1 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-1 > .column:nth-of-type(1n+1), .large-up-1 > .columns:nth-of-type(1n+1) { - clear: both; } - .large-up-1 > .column:last-child, .large-up-1 > .columns:last-child { - float: left; } - .large-up-2 > .column, .large-up-2 > .columns { - width: 50%; - float: left; } - .large-up-2 > .column:nth-of-type(1n), .large-up-2 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-2 > .column:nth-of-type(2n+1), .large-up-2 > .columns:nth-of-type(2n+1) { - clear: both; } - .large-up-2 > .column:last-child, .large-up-2 > .columns:last-child { - float: left; } - .large-up-3 > .column, .large-up-3 > .columns { - width: 33.33333%; - float: left; } - .large-up-3 > .column:nth-of-type(1n), .large-up-3 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-3 > .column:nth-of-type(3n+1), .large-up-3 > .columns:nth-of-type(3n+1) { - clear: both; } - .large-up-3 > .column:last-child, .large-up-3 > .columns:last-child { - float: left; } - .large-up-4 > .column, .large-up-4 > .columns { - width: 25%; - float: left; } - .large-up-4 > .column:nth-of-type(1n), .large-up-4 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-4 > .column:nth-of-type(4n+1), .large-up-4 > .columns:nth-of-type(4n+1) { - clear: both; } - .large-up-4 > .column:last-child, .large-up-4 > .columns:last-child { - float: left; } - .large-up-5 > .column, .large-up-5 > .columns { - width: 20%; - float: left; } - .large-up-5 > .column:nth-of-type(1n), .large-up-5 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-5 > .column:nth-of-type(5n+1), .large-up-5 > .columns:nth-of-type(5n+1) { - clear: both; } - .large-up-5 > .column:last-child, .large-up-5 > .columns:last-child { - float: left; } - .large-up-6 > .column, .large-up-6 > .columns { - width: 16.66667%; - float: left; } - .large-up-6 > .column:nth-of-type(1n), .large-up-6 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-6 > .column:nth-of-type(6n+1), .large-up-6 > .columns:nth-of-type(6n+1) { - clear: both; } - .large-up-6 > .column:last-child, .large-up-6 > .columns:last-child { - float: left; } - .large-up-7 > .column, .large-up-7 > .columns { - width: 14.28571%; - float: left; } - .large-up-7 > .column:nth-of-type(1n), .large-up-7 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-7 > .column:nth-of-type(7n+1), .large-up-7 > .columns:nth-of-type(7n+1) { - clear: both; } - .large-up-7 > .column:last-child, .large-up-7 > .columns:last-child { - float: left; } - .large-up-8 > .column, .large-up-8 > .columns { - width: 12.5%; - float: left; } - .large-up-8 > .column:nth-of-type(1n), .large-up-8 > .columns:nth-of-type(1n) { - clear: none; } - .large-up-8 > .column:nth-of-type(8n+1), .large-up-8 > .columns:nth-of-type(8n+1) { - clear: both; } - .large-up-8 > .column:last-child, .large-up-8 > .columns:last-child { - float: left; } - .large-collapse > .column, .large-collapse > .columns { - padding-left: 0; - padding-right: 0; } - .large-collapse .row { - margin-left: 0; - margin-right: 0; } - .large-uncollapse > .column, .large-uncollapse > .columns { - padding-left: 0.9375rem; - padding-right: 0.9375rem; } - .large-centered { - float: none; - margin-left: auto; - margin-right: auto; } - .large-uncentered, - .large-push-0, - .large-pull-0 { - position: static; - margin-left: 0; - margin-right: 0; - float: left; } } - -[type='text'], [type='password'], [type='date'], [type='datetime'], [type='datetime-local'], [type='month'], [type='week'], [type='email'], [type='number'], [type='search'], [type='tel'], [type='time'], [type='url'], [type='color'], -textarea { - display: block; - box-sizing: border-box; - width: 100%; - height: 2.4375rem; - padding: 0.5rem; - border: 1px solid #cacaca; - margin: 0 0 1rem; - font-family: inherit; - font-size: 1rem; - color: #0a0a0a; - background-color: #fefefe; - box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); - border-radius: 0; - transition: box-shadow 0.5s, border-color 0.25s ease-in-out; - -webkit-appearance: none; - -moz-appearance: none; } - [type='text']:focus, [type='password']:focus, [type='date']:focus, [type='datetime']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='week']:focus, [type='email']:focus, [type='number']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='url']:focus, [type='color']:focus, - textarea:focus { - border: 1px solid #8a8a8a; - background-color: #fefefe; - outline: none; - box-shadow: 0 0 5px #cacaca; - transition: box-shadow 0.5s, border-color 0.25s ease-in-out; } - -textarea { - max-width: 100%; } - textarea[rows] { - height: auto; } - -input::-webkit-input-placeholder, -textarea::-webkit-input-placeholder { - color: #cacaca; } - -input::-moz-placeholder, -textarea::-moz-placeholder { - color: #cacaca; } - -input:-ms-input-placeholder, -textarea:-ms-input-placeholder { - color: #cacaca; } - -input::placeholder, -textarea::placeholder { - color: #cacaca; } - -input:disabled, input[readonly], -textarea:disabled, -textarea[readonly] { - background-color: #e6e6e6; - cursor: not-allowed; } - -[type='submit'], -[type='button'] { - border-radius: 0; - -webkit-appearance: none; - -moz-appearance: none; } - -input[type='search'] { - box-sizing: border-box; } - -[type='file'], -[type='checkbox'], -[type='radio'] { - margin: 0 0 1rem; } - -[type='checkbox'] + label, -[type='radio'] + label { - display: inline-block; - margin-left: 0.5rem; - margin-right: 1rem; - margin-bottom: 0; - vertical-align: baseline; } - [type='checkbox'] + label[for], - [type='radio'] + label[for] { - cursor: pointer; } - -label > [type='checkbox'], -label > [type='radio'] { - margin-right: 0.5rem; } - -[type='file'] { - width: 100%; } - -label { - display: block; - margin: 0; - font-size: 0.875rem; - font-weight: normal; - line-height: 1.8; - color: #0a0a0a; } - label.middle { - margin: 0 0 1rem; - padding: 0.5625rem 0; } - -.help-text { - margin-top: -0.5rem; - font-size: 0.8125rem; - font-style: italic; - color: #0a0a0a; } - -.input-group { - display: table; - width: 100%; - margin-bottom: 1rem; } - .input-group > :first-child { - border-radius: 0 0 0 0; } - .input-group > :last-child > * { - border-radius: 0 0 0 0; } - -.input-group-label, .input-group-field, .input-group-button { - margin: 0; - display: table-cell; - vertical-align: middle; } - -.input-group-label { - text-align: center; - padding: 0 1rem; - background: #e6e6e6; - color: #0a0a0a; - border: 1px solid #cacaca; - white-space: nowrap; - width: 1%; - height: 100%; } - .input-group-label:first-child { - border-right: 0; } - .input-group-label:last-child { - border-left: 0; } - -.input-group-field { - border-radius: 0; - height: 2.5rem; } - -.input-group-button { - padding-top: 0; - padding-bottom: 0; - text-align: center; - height: 100%; - width: 1%; } - .input-group-button a, - .input-group-button input, - .input-group-button button { - margin: 0; } - -.input-group .input-group-button { - display: table-cell; } - -fieldset { - border: 0; - padding: 0; - margin: 0; } - -legend { - margin-bottom: 0.5rem; - max-width: 100%; } - -.fieldset { - border: 1px solid #cacaca; - padding: 1.25rem; - margin: 1.125rem 0; } - .fieldset legend { - background: #fefefe; - padding: 0 0.1875rem; - margin: 0; - margin-left: -0.1875rem; } - -select { - height: 2.4375rem; - padding: 0.5rem; - border: 1px solid #cacaca; - margin: 0 0 1rem; - font-size: 1rem; - font-family: inherit; - line-height: normal; - color: #0a0a0a; - background-color: #fefefe; - border-radius: 0; - -webkit-appearance: none; - -moz-appearance: none; - background-image: url('data:image/svg+xml;utf8,'); - background-size: 9px 6px; - background-position: right center; - background-origin: content-box; - background-repeat: no-repeat; } - @media screen and (min-width: 0\0) { - select { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIpJREFUeNrEkckNgDAMBBfRkEt0ObRBBdsGXUDgmQfK4XhH2m8czQAAy27R3tsw4Qfe2x8uOO6oYLb6GlOor3GF+swURAOmUJ+RwtEJs9WvTGEYxBXqI1MQAZhCfUQKRzDMVj+TwrAIV6jvSUEkYAr1LSkcyTBb/V+KYfX7xAeusq3sLDtGH3kEGACPWIflNZfhRQAAAABJRU5ErkJggg=="); } } - select:disabled { - background-color: #e6e6e6; - cursor: not-allowed; } - select::-ms-expand { - display: none; } - select[multiple] { - height: auto; - background-image: none; } - -.is-invalid-input:not(:focus) { - background-color: rgba(236, 88, 64, 0.1); - border-color: #ec5840; } - -.is-invalid-label { - color: #ec5840; } - -.form-error { - display: none; - margin-top: -0.5rem; - margin-bottom: 1rem; - font-size: 0.75rem; - font-weight: bold; - color: #ec5840; } - .form-error.is-visible { - display: block; } - -.button { - display: inline-block; - text-align: center; - line-height: 1; - cursor: pointer; - -webkit-appearance: none; - transition: background-color 0.25s ease-out, color 0.25s ease-out; - vertical-align: middle; - border: 1px solid transparent; - border-radius: 0; - padding: 0.85em 1em; - margin: 0 0 1rem 0; - font-size: 0.9rem; - background-color: #2199e8; - color: #fefefe; } - [data-whatinput='mouse'] .button { - outline: 0; } - .button:hover, .button:focus { - background-color: #1583cc; - color: #fefefe; } - .button.tiny { - font-size: 0.6rem; } - .button.small { - font-size: 0.75rem; } - .button.large { - font-size: 1.25rem; } - .button.expanded { - display: block; - width: 100%; - margin-left: 0; - margin-right: 0; } - .button.primary { - background-color: #2199e8; - color: #fefefe; } - .button.primary:hover, .button.primary:focus { - background-color: #147cc0; - color: #fefefe; } - .button.secondary { - background-color: #777; - color: #fefefe; } - .button.secondary:hover, .button.secondary:focus { - background-color: #5f5f5f; - color: #fefefe; } - .button.success { - background-color: #3adb76; - color: #fefefe; } - .button.success:hover, .button.success:focus { - background-color: #22bb5b; - color: #fefefe; } - .button.warning { - background-color: #ffae00; - color: #fefefe; } - .button.warning:hover, .button.warning:focus { - background-color: #cc8b00; - color: #fefefe; } - .button.alert { - background-color: #ec5840; - color: #fefefe; } - .button.alert:hover, .button.alert:focus { - background-color: #da3116; - color: #fefefe; } - .button.hollow { - border: 1px solid #2199e8; - color: #2199e8; } - .button.hollow, .button.hollow:hover, .button.hollow:focus { - background-color: transparent; } - .button.hollow:hover, .button.hollow:focus { - border-color: #0c4d78; - color: #0c4d78; } - .button.hollow.primary { - border: 1px solid #2199e8; - color: #2199e8; } - .button.hollow.primary:hover, .button.hollow.primary:focus { - border-color: #0c4d78; - color: #0c4d78; } - .button.hollow.secondary { - border: 1px solid #777; - color: #777; } - .button.hollow.secondary:hover, .button.hollow.secondary:focus { - border-color: #3c3c3c; - color: #3c3c3c; } - .button.hollow.success { - border: 1px solid #3adb76; - color: #3adb76; } - .button.hollow.success:hover, .button.hollow.success:focus { - border-color: #157539; - color: #157539; } - .button.hollow.warning { - border: 1px solid #ffae00; - color: #ffae00; } - .button.hollow.warning:hover, .button.hollow.warning:focus { - border-color: #805700; - color: #805700; } - .button.hollow.alert { - border: 1px solid #ec5840; - color: #ec5840; } - .button.hollow.alert:hover, .button.hollow.alert:focus { - border-color: #881f0e; - color: #881f0e; } - .button.disabled, .button[disabled] { - opacity: 0.25; - cursor: not-allowed; } - .button.dropdown::after { - content: ''; - display: block; - width: 0; - height: 0; - border: inset 0.4em; - border-color: #fefefe transparent transparent; - border-top-style: solid; - border-bottom-width: 0; - position: relative; - top: 0.4em; - float: right; - margin-left: 1em; - display: inline-block; } - .button.arrow-only::after { - margin-left: 0; - float: none; - top: -0.1em; } - -.callout { - margin: 0 0 1rem 0; - padding: 1rem; - border: 1px solid rgba(10, 10, 10, 0.25); - border-radius: 0; - position: relative; - color: #0a0a0a; - background-color: white; } - .callout > :first-child { - margin-top: 0; } - .callout > :last-child { - margin-bottom: 0; } - .callout.primary { - background-color: #def0fc; } - .callout.secondary { - background-color: #ebebeb; } - .callout.success { - background-color: #e1faea; } - .callout.warning { - background-color: #fff3d9; } - .callout.alert { - background-color: #fce6e2; } - .callout.small { - padding-top: 0.5rem; - padding-right: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 0.5rem; } - .callout.large { - padding-top: 3rem; - padding-right: 3rem; - padding-bottom: 3rem; - padding-left: 3rem; } - -body.is-reveal-open { - overflow: hidden; } - -.reveal-overlay { - display: none; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1005; - background-color: rgba(10, 10, 10, 0.45); - overflow-y: scroll; } - -.reveal { - display: none; - z-index: 1006; - padding: 1rem; - border: 1px solid #cacaca; - background-color: #fefefe; - border-radius: 0; - position: relative; - top: 100px; - margin-left: auto; - margin-right: auto; - overflow-y: auto; } - [data-whatinput='mouse'] .reveal { - outline: 0; } - @media screen and (min-width: 40em) { - .reveal { - min-height: 0; } } - .reveal .column, .reveal .columns, - .reveal .columns { - min-width: 0; } - .reveal > :last-child { - margin-bottom: 0; } - @media screen and (min-width: 40em) { - .reveal { - width: 600px; - max-width: 75rem; } } - @media screen and (min-width: 40em) { - .reveal .reveal { - left: auto; - right: auto; - margin: 0 auto; } } - .reveal.collapse { - padding: 0; } - @media screen and (min-width: 40em) { - .reveal.tiny { - width: 30%; - max-width: 75rem; } } - @media screen and (min-width: 40em) { - .reveal.small { - width: 50%; - max-width: 75rem; } } - @media screen and (min-width: 40em) { - .reveal.large { - width: 90%; - max-width: 75rem; } } - .reveal.full { - top: 0; - left: 0; - width: 100%; - height: 100%; - height: 100vh; - min-height: 100vh; - max-width: none; - margin-left: 0; - border: 0; - border-radius: 0; } - @media screen and (max-width: 39.9375em) { - .reveal { - top: 0; - left: 0; - width: 100%; - height: 100%; - height: 100vh; - min-height: 100vh; - max-width: none; - margin-left: 0; - border: 0; - border-radius: 0; } } - .reveal.without-overlay { - position: fixed; } - -.slide-in-down.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateY(-100%); - -ms-transform: translateY(-100%); - transform: translateY(-100%); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-in-down.mui-enter.mui-enter-active { - -webkit-transform: translateY(0); - -ms-transform: translateY(0); - transform: translateY(0); } - -.slide-in-left.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateX(-100%); - -ms-transform: translateX(-100%); - transform: translateX(-100%); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-in-left.mui-enter.mui-enter-active { - -webkit-transform: translateX(0); - -ms-transform: translateX(0); - transform: translateX(0); } - -.slide-in-up.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateY(100%); - -ms-transform: translateY(100%); - transform: translateY(100%); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-in-up.mui-enter.mui-enter-active { - -webkit-transform: translateY(0); - -ms-transform: translateY(0); - transform: translateY(0); } - -.slide-in-right.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateX(100%); - -ms-transform: translateX(100%); - transform: translateX(100%); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-in-right.mui-enter.mui-enter-active { - -webkit-transform: translateX(0); - -ms-transform: translateX(0); - transform: translateX(0); } - -.slide-out-down.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateY(0); - -ms-transform: translateY(0); - transform: translateY(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-out-down.mui-leave.mui-leave-active { - -webkit-transform: translateY(100%); - -ms-transform: translateY(100%); - transform: translateY(100%); } - -.slide-out-right.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateX(0); - -ms-transform: translateX(0); - transform: translateX(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-out-right.mui-leave.mui-leave-active { - -webkit-transform: translateX(100%); - -ms-transform: translateX(100%); - transform: translateX(100%); } - -.slide-out-up.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateY(0); - -ms-transform: translateY(0); - transform: translateY(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-out-up.mui-leave.mui-leave-active { - -webkit-transform: translateY(-100%); - -ms-transform: translateY(-100%); - transform: translateY(-100%); } - -.slide-out-left.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: translateX(0); - -ms-transform: translateX(0); - transform: translateX(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; } - -.slide-out-left.mui-leave.mui-leave-active { - -webkit-transform: translateX(-100%); - -ms-transform: translateX(-100%); - transform: translateX(-100%); } - -.fade-in.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - opacity: 0; - transition-property: opacity; } - -.fade-in.mui-enter.mui-enter-active { - opacity: 1; } - -.fade-out.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - opacity: 1; - transition-property: opacity; } - -.fade-out.mui-leave.mui-leave-active { - opacity: 0; } - -.hinge-in-from-top.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateX(-90deg); - transform: perspective(2000px) rotateX(-90deg); - -webkit-transform-origin: top; - -ms-transform-origin: top; - transform-origin: top; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-top.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-in-from-right.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateY(-90deg); - transform: perspective(2000px) rotateY(-90deg); - -webkit-transform-origin: right; - -ms-transform-origin: right; - transform-origin: right; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-right.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-in-from-bottom.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateX(90deg); - transform: perspective(2000px) rotateX(90deg); - -webkit-transform-origin: bottom; - -ms-transform-origin: bottom; - transform-origin: bottom; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-bottom.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-in-from-left.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateY(90deg); - transform: perspective(2000px) rotateY(90deg); - -webkit-transform-origin: left; - -ms-transform-origin: left; - transform-origin: left; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-left.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-in-from-middle-x.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateX(-90deg); - transform: perspective(2000px) rotateX(-90deg); - -webkit-transform-origin: center; - -ms-transform-origin: center; - transform-origin: center; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-middle-x.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-in-from-middle-y.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotateY(-90deg); - transform: perspective(2000px) rotateY(-90deg); - -webkit-transform-origin: center; - -ms-transform-origin: center; - transform-origin: center; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.hinge-in-from-middle-y.mui-enter.mui-enter-active { - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - opacity: 1; } - -.hinge-out-from-top.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: top; - -ms-transform-origin: top; - transform-origin: top; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-top.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateX(-90deg); - transform: perspective(2000px) rotateX(-90deg); - opacity: 0; } - -.hinge-out-from-right.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: right; - -ms-transform-origin: right; - transform-origin: right; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-right.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateY(-90deg); - transform: perspective(2000px) rotateY(-90deg); - opacity: 0; } - -.hinge-out-from-bottom.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: bottom; - -ms-transform-origin: bottom; - transform-origin: bottom; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-bottom.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateX(90deg); - transform: perspective(2000px) rotateX(90deg); - opacity: 0; } - -.hinge-out-from-left.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: left; - -ms-transform-origin: left; - transform-origin: left; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-left.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateY(90deg); - transform: perspective(2000px) rotateY(90deg); - opacity: 0; } - -.hinge-out-from-middle-x.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: center; - -ms-transform-origin: center; - transform-origin: center; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-middle-x.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateX(-90deg); - transform: perspective(2000px) rotateX(-90deg); - opacity: 0; } - -.hinge-out-from-middle-y.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: perspective(2000px) rotate(0deg); - transform: perspective(2000px) rotate(0deg); - -webkit-transform-origin: center; - -ms-transform-origin: center; - transform-origin: center; - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.hinge-out-from-middle-y.mui-leave.mui-leave-active { - -webkit-transform: perspective(2000px) rotateY(-90deg); - transform: perspective(2000px) rotateY(-90deg); - opacity: 0; } - -.scale-in-up.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.scale-in-up.mui-enter.mui-enter-active { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - opacity: 1; } - -.scale-in-down.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: scale(1.5); - -ms-transform: scale(1.5); - transform: scale(1.5); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.scale-in-down.mui-enter.mui-enter-active { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - opacity: 1; } - -.scale-out-up.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.scale-out-up.mui-leave.mui-leave-active { - -webkit-transform: scale(1.5); - -ms-transform: scale(1.5); - transform: scale(1.5); - opacity: 0; } - -.scale-out-down.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.scale-out-down.mui-leave.mui-leave-active { - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; } - -.spin-in.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: rotate(-0.75turn); - -ms-transform: rotate(-0.75turn); - transform: rotate(-0.75turn); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.spin-in.mui-enter.mui-enter-active { - -webkit-transform: rotate(0); - -ms-transform: rotate(0); - transform: rotate(0); - opacity: 1; } - -.spin-out.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: rotate(0); - -ms-transform: rotate(0); - transform: rotate(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.spin-out.mui-leave.mui-leave-active { - -webkit-transform: rotate(0.75turn); - -ms-transform: rotate(0.75turn); - transform: rotate(0.75turn); - opacity: 0; } - -.spin-in-ccw.mui-enter { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: rotate(0.75turn); - -ms-transform: rotate(0.75turn); - transform: rotate(0.75turn); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 0; } - -.spin-in-ccw.mui-enter.mui-enter-active { - -webkit-transform: rotate(0); - -ms-transform: rotate(0); - transform: rotate(0); - opacity: 1; } - -.spin-out-ccw.mui-leave { - transition-duration: 500ms; - transition-timing-function: linear; - -webkit-transform: rotate(0); - -ms-transform: rotate(0); - transform: rotate(0); - transition-property: -webkit-transform, opacity; - transition-property: transform, opacity; - opacity: 1; } - -.spin-out-ccw.mui-leave.mui-leave-active { - -webkit-transform: rotate(-0.75turn); - -ms-transform: rotate(-0.75turn); - transform: rotate(-0.75turn); - opacity: 0; } - -.slow { - transition-duration: 750ms !important; } - -.fast { - transition-duration: 250ms !important; } - -.linear { - transition-timing-function: linear !important; } - -.ease { - transition-timing-function: ease !important; } - -.ease-in { - transition-timing-function: ease-in !important; } - -.ease-out { - transition-timing-function: ease-out !important; } - -.ease-in-out { - transition-timing-function: ease-in-out !important; } - -.bounce-in { - transition-timing-function: cubic-bezier(0.485, 0.155, 0.24, 1.245) !important; } - -.bounce-out { - transition-timing-function: cubic-bezier(0.485, 0.155, 0.515, 0.845) !important; } - -.bounce-in-out { - transition-timing-function: cubic-bezier(0.76, -0.245, 0.24, 1.245) !important; } - -.short-delay { - transition-delay: 300ms !important; } - -.long-delay { - transition-delay: 700ms !important; } - -.shake { - -webkit-animation-name: shake-7; - animation-name: shake-7; } - -@-webkit-keyframes shake-7 { - 0%, 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90% { - -webkit-transform: translateX(7%); - transform: translateX(7%); } - 5%, 15%, 25%, 35%, 45%, 55%, 65%, 75%, 85%, 95% { - -webkit-transform: translateX(-7%); - transform: translateX(-7%); } } - -@keyframes shake-7 { - 0%, 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90% { - -webkit-transform: translateX(7%); - transform: translateX(7%); } - 5%, 15%, 25%, 35%, 45%, 55%, 65%, 75%, 85%, 95% { - -webkit-transform: translateX(-7%); - transform: translateX(-7%); } } - -.spin-cw { - -webkit-animation-name: spin-cw-1turn; - animation-name: spin-cw-1turn; } - -@-webkit-keyframes spin-cw-1turn { - 0% { - -webkit-transform: rotate(-1turn); - transform: rotate(-1turn); } - 100% { - -webkit-transform: rotate(0); - transform: rotate(0); } } - -@keyframes spin-cw-1turn { - 0% { - -webkit-transform: rotate(-1turn); - transform: rotate(-1turn); } - 100% { - -webkit-transform: rotate(0); - transform: rotate(0); } } - -.spin-ccw { - -webkit-animation-name: spin-cw-1turn; - animation-name: spin-cw-1turn; } - -@keyframes spin-cw-1turn { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); } - 100% { - -webkit-transform: rotate(1turn); - transform: rotate(1turn); } } - -.wiggle { - -webkit-animation-name: wiggle-7deg; - animation-name: wiggle-7deg; } - -@-webkit-keyframes wiggle-7deg { - 40%, 50%, 60% { - -webkit-transform: rotate(7deg); - transform: rotate(7deg); } - 35%, 45%, 55%, 65% { - -webkit-transform: rotate(-7deg); - transform: rotate(-7deg); } - 0%, 30%, 70%, 100% { - -webkit-transform: rotate(0); - transform: rotate(0); } } - -@keyframes wiggle-7deg { - 40%, 50%, 60% { - -webkit-transform: rotate(7deg); - transform: rotate(7deg); } - 35%, 45%, 55%, 65% { - -webkit-transform: rotate(-7deg); - transform: rotate(-7deg); } - 0%, 30%, 70%, 100% { - -webkit-transform: rotate(0); - transform: rotate(0); } } - -.shake, -.spin-cw, -.spin-ccw, -.wiggle { - -webkit-animation-duration: 500ms; - animation-duration: 500ms; } - -.infinite { - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; } - -.slow { - -webkit-animation-duration: 750ms !important; - animation-duration: 750ms !important; } - -.fast { - -webkit-animation-duration: 250ms !important; - animation-duration: 250ms !important; } - -.linear { - -webkit-animation-timing-function: linear !important; - animation-timing-function: linear !important; } - -.ease { - -webkit-animation-timing-function: ease !important; - animation-timing-function: ease !important; } - -.ease-in { - -webkit-animation-timing-function: ease-in !important; - animation-timing-function: ease-in !important; } - -.ease-out { - -webkit-animation-timing-function: ease-out !important; - animation-timing-function: ease-out !important; } - -.ease-in-out { - -webkit-animation-timing-function: ease-in-out !important; - animation-timing-function: ease-in-out !important; } - -.bounce-in { - -webkit-animation-timing-function: cubic-bezier(0.485, 0.155, 0.24, 1.245) !important; - animation-timing-function: cubic-bezier(0.485, 0.155, 0.24, 1.245) !important; } - -.bounce-out { - -webkit-animation-timing-function: cubic-bezier(0.485, 0.155, 0.515, 0.845) !important; - animation-timing-function: cubic-bezier(0.485, 0.155, 0.515, 0.845) !important; } - -.bounce-in-out { - -webkit-animation-timing-function: cubic-bezier(0.76, -0.245, 0.24, 1.245) !important; - animation-timing-function: cubic-bezier(0.76, -0.245, 0.24, 1.245) !important; } - -.short-delay { - -webkit-animation-delay: 300ms !important; - animation-delay: 300ms !important; } - -.long-delay { - -webkit-animation-delay: 700ms !important; - animation-delay: 700ms !important; } diff --git a/public/index.html b/public/index.html index 3d4d842..e57f6b6 100644 --- a/public/index.html +++ b/public/index.html @@ -3,16 +3,15 @@ CasparCG Controller - + -
- + +
+ +
-
- -
-
- +
+ diff --git a/public/main.css b/public/main.css deleted file mode 100644 index 7e632d5..0000000 --- a/public/main.css +++ /dev/null @@ -1,190 +0,0 @@ -body { - background: #3f3f41; - color: #f1f1f1; -} -h4 { - margin-bottom: 2rem; -} -/* Container */ -.container { - padding: 1rem; -} -.container-header { - font-size: 1.5rem; - margin-left: 1rem; - color: #777; -} -.container-panel { - border: 1px solid #3f3f3f; - background: #2d2d30; - padding: 1rem; - border-radius: 5px; -} -/* Header */ -.header-list { - list-style-type: none; - margin: 0; -} -.header-item { - margin-bottom: 1rem; -} -.header-item-hide { - float: right; - width: 5rem; - border-radius: 6px; - margin: 0; -} -.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-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: #f00; -} -/* Inputs */ -.panel-graphic-property-item input { - display: inline-block; -} -label { - color: #f1f1f1; -} -input[type="text"], -textarea { - background: #333337; - border-color: #3f3f3f; - color: #999; - 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: #999; - 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; -} -a.button { - margin: 0 1rem 0 0; - width: 7rem; -} -/* Media queries */ -body { - font-size: 1.5rem; -} -@media only screen and (max-device-width: 1280px) { - .header-item-hide { - width: 9rem; - line-height: 0rem; - } - a.button { - font-size: 2rem; - line-height: 0; - width: auto; - } - .panel-graphic-preset { - margin: 0; - } - .panel-graphic-display, - .panel-graphic-preset-add { - margin-bottom: 3rem !important; - } - .panel-graphic-preset-remove { - padding-right: 0.5rem; - padding-left: 0.5rem; - } - .panel-graphic-preset-remove.alert { - padding-right: 1rem; - padding-left: 1rem; - } - .panel-graphic-settings { - font-size: 1.3rem !important; - } - .header-item-display { - font-size: 2rem; - margin-right: 12.5rem; - padding: 0.2rem 1rem; - } - .panel-graphic-property-item input { - font-size: 2rem; - height: 3.5rem; - } -} -/*# sourceMappingURL=main.css.map */ \ No newline at end of file diff --git a/public/main.css.map b/public/main.css.map deleted file mode 100644 index 8a7d6eb..0000000 --- a/public/main.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../app/styl/main.styl"],"names":[],"mappings":"AAAA;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;;AAGF;EACE,eAAe,KAAf;;AAGF;AAEE;EACE,SAAS,KAAT;;AAGF;EACE,WAAW,OAAX;EACA,aAAa,KAAb;EACA,OAAO,KAAP;;AAGF;EACE,QAAQ,kBAAR;EACA,YAAY,QAAZ;EACA,SAAS,KAAT;EACA,eAAe,IAAf;;AAGJ;AAEE;EACE,iBAAiB,KAAjB;EACA,QAAQ,EAAR;;AAGF;EACE,eAAe,KAAf;;AAGF;EACE,OAAO,MAAP;EACA,OAAO,KAAP;EACA,eAAe,IAAf;EACA,QAAQ,EAAR;;AAGF;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,eAAe,IAAf;EACA,SAAS,YAAT;EACA,cAAc,OAAd;;AAGJ;AAEE;EACE,iBAAiB,KAAjB;EACA,QAAQ,EAAR;;AAGF;EACE,OAAO,QAAP;EACA,SAAS,MAAT;EACA,QAAQ,kBAAR;EACA,SAAS,cAAT;;AAGF;EACE,OAAO,QAAP;EACA,QAAQ,kBAAR;;AAGF;EACE,YAAY,KAAZ;;AAGJ;AAEE;EACE,SAAS,KAAT;;AAGF;AAA4B;EAE1B,OAAO,KAAP;;AAGJ;AAME;EACE,OAAO,MAAP;EACA,cAAc,KAAd;;AAGF;EACE,cAAc,EAAd;;AAGF;EACE,cAAc,KAAd;;AAGF;EACE,YAAY,KAAZ;EACA,iBAAiB,KAAjB;;AAGF;EACE,OAAO,KAAP;;AAGJ;AAEE;EACE,QAAQ,iBAAR;EACA,OAAO,KAAP;;AAGJ;AAEE;EACE,SAAS,aAAT;;AAGF;EACE,OAAO,QAAP;;AAGF;AAAmB;EAEjB,YAAY,QAAZ;EACA,cAAc,QAAd;EACA,OAAO,KAAP;EACA,qBAAqB,gBAArB;;AAGF;AAAyB;EAEvB,OAAO,QAAP;EACA,cAAc,QAAd;;AAGF;AAAyB;EAEvB,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,cAAc,QAAd;EACA,YAAY,KAAZ;;AAGF;AAAgB;EAEd,YAAY,mBAAZ;EACA,cAAc,QAAd;;AAGF;EACE,YAAY,QAAZ;EACA,cAAc,QAAd;EACA,OAAO,KAAP;EACA,qBAAqB,aAArB;EACA,iBAAiB,QAAjB;EACA,mBAAmB,YAAnB;EACA,mBAAmB,UAAnB;EACA,kBAAuO,uNAAvO;;AAGF;EACE,OAAO,QAAP;EACA,cAAc,QAAd;;AAGF;EACE,YAAY,QAAZ;EACA,OAAO,QAAP;EACA,cAAc,QAAd;EACA,YAAY,KAAZ;;AAGF;EACE,QAAQ,WAAR;EACA,OAAO,KAAP;;AAGJ;AAEE;EACE,WAAW,OAAX;;AAG8C;AAC9C;IACE,OAAO,KAAP;IACA,aAAa,KAAb;;AAGF;IACE,WAAW,KAAX;IACA,aAAa,EAAb;IACA,OAAO,KAAP;;AAGF;IACE,QAAQ,EAAR;;AAGF;AAAuB;IAErB,eAAe,gBAAf;;AAGF;IACE,eAAe,OAAf;IACA,cAAc,OAAd;;AAGF;IACE,eAAe,KAAf;IACA,cAAc,KAAd;;AAGF;IACE,WAAW,kBAAX;;AAGF;IACE,WAAW,KAAX;IACA,cAAc,QAAd;IACA,SAAS,YAAT;;AAGF;IACE,WAAW,KAAX;IACA,QAAQ,OAAR","file":"main.css"} \ No newline at end of file diff --git a/public/status.html b/public/status.html new file mode 100644 index 0000000..fc41077 --- /dev/null +++ b/public/status.html @@ -0,0 +1,11 @@ + + + + CasparCG Client + + + +
+ + +