mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 07:53:54 +00:00
v2: roll (#280)
* ux: delete flow * style: small tweaks in interface * refactor: gracefully quit on error * fix: logic around updating events * chore: cleanup dictionary * refactor: improve DX on creating aux files * fix: safe destructure function return * refactor: safe handling of falsy timer values * fix: improve ux on stopping roll mode * chore: upgrade deps * feat: roll mode * refactor: remove unused
This commit is contained in:
@@ -51,7 +51,7 @@ export default function Transport(props: TransportProps) {
|
||||
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
|
||||
<TapButton
|
||||
onClick={() => setPlayback.stop()}
|
||||
disabled={!selectedId}
|
||||
disabled={!selectedId && !isRolling}
|
||||
theme='stop'
|
||||
>
|
||||
<IoStop />
|
||||
|
||||
@@ -246,7 +246,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
showDelay
|
||||
showBlock
|
||||
showClone
|
||||
enableDelete
|
||||
enableDelete={!selected}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function EventBlockProgressBar(props: EventBlockProgressBarProps)
|
||||
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
|
||||
const progress = `${elapsed}%`;
|
||||
|
||||
if (timer?.current != null && timer?.current < 0) {
|
||||
if ((timer?.current ?? 0) < 0) {
|
||||
return (
|
||||
<div
|
||||
className={`${style.progressBar} ${style.overtime}`}
|
||||
|
||||
@@ -92,18 +92,18 @@ $red-1200: #520000;
|
||||
$red-1300: #440000;
|
||||
$red-1350: #360000;
|
||||
|
||||
$violet-50: #F9F7FE;
|
||||
$violet-100: #F3EFFC;
|
||||
$violet-200: #E7DFF6;
|
||||
$violet-300: #CEBFEC;
|
||||
$violet-400: #B8A0E3;
|
||||
$violet-500: #AB8DB8;
|
||||
$violet-600: #9771D3;
|
||||
$violet-700: #8B60CA;
|
||||
$violet-800: #8352C6;
|
||||
$violet-900: #7945c1;
|
||||
$violet-1000: #7248AD;
|
||||
$violet-1100: #573486;
|
||||
$violet-1200: #3C235F;
|
||||
$violet-1300: #301A4D;
|
||||
$violet-1350: #231339;
|
||||
$violet-50: #F6F6F6;
|
||||
$violet-100: #EBE5FF;
|
||||
$violet-200: #DFD5FF;
|
||||
$violet-300: #C7B8FD;
|
||||
$violet-400: #A790F5;
|
||||
$violet-500: #9379EC;
|
||||
$violet-600: #8064E1;
|
||||
$violet-700: #674ACB;
|
||||
$violet-800: #4E31B1;
|
||||
$violet-900: #3F249D;
|
||||
$violet-1000: #311887;
|
||||
$violet-1100: #1F0B62;
|
||||
$violet-1200: #140545;
|
||||
$violet-1300: #0F0336;
|
||||
$violet-1350: #0A0126;
|
||||
|
||||
@@ -22,11 +22,10 @@ export const ontimeButtonFilled = {
|
||||
export const ontimeButtonOutlined = {
|
||||
...commonStyles,
|
||||
backgroundColor: '#2d2d2d', // $gray-1100
|
||||
color: '#9d9d9d', // $blue-400
|
||||
color: '#e2e2e2', // $blue-400
|
||||
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
|
||||
_hover: {
|
||||
backgroundColor: '#404040', // $gray-1000
|
||||
color: '#e2e2e2', // $gray-200
|
||||
},
|
||||
_active: {
|
||||
backgroundColor: '#2d2d2d', // $gray-1100
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
"supertest": "^6.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "yarn install && yarn setdb && yarn addversion",
|
||||
"postinstall": "yarn setdb && yarn addversion",
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.js",
|
||||
"nodestart": "NODE_ENV=development node src/app.js",
|
||||
"setdb": "cp demo-db/db.json src/preloaded-db/db.json",
|
||||
|
||||
+18
-4
@@ -156,20 +156,34 @@ export const startIntegrations = async (overrideConfig = null) => {
|
||||
|
||||
/**
|
||||
* @description clean shutdown app services
|
||||
* @param {number} exitCode
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const shutdown = async () => {
|
||||
export const shutdown = async (exitCode) => {
|
||||
// shutdown express server
|
||||
server.close();
|
||||
|
||||
shutdownOSCServer();
|
||||
eventTimer.shutdown();
|
||||
socket.shutdown();
|
||||
process.exit(exitCode || 0);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', async (error, promise) => {
|
||||
console.error(error, 'Error: unhandled rejection', promise);
|
||||
socket.error('SERVER', 'Error: unhandled rejection');
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error, promise) => {
|
||||
console.error(error, 'Error: uncaught exception', promise);
|
||||
socket.error('SERVER', 'Error: uncaught exception');
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
// register shutdown signals
|
||||
process.once('SIGHUP', shutdown);
|
||||
process.once('SIGINT', shutdown);
|
||||
process.once('SIGTERM', shutdown);
|
||||
process.once('SIGHUP', async () => shutdown(0));
|
||||
process.once('SIGINT', async () => shutdown(0));
|
||||
process.once('SIGTERM', async () => shutdown(0));
|
||||
|
||||
export { server, app };
|
||||
|
||||
@@ -30,9 +30,10 @@ export class DataProvider {
|
||||
|
||||
static async updateEventById(eventId, newData) {
|
||||
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
|
||||
const e = data.rundown[eventIndex];
|
||||
data.rundown[eventIndex] = { ...e, ...newData };
|
||||
data.rundown[eventIndex].revision++;
|
||||
const persistedEvent = data.rundown[eventIndex];
|
||||
const newEvent = { ...persistedEvent, ...newData };
|
||||
newEvent.revision++;
|
||||
data.rundown[eventIndex] = newEvent;
|
||||
await this.persist();
|
||||
return data.rundown[eventIndex];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { getSelectionByRoll } from '../timer/classUtils.js';
|
||||
import { Timer } from '../timer/Timer.js';
|
||||
import { getRollTimers } from '../../services/rollUtils.js';
|
||||
|
||||
let instance;
|
||||
|
||||
@@ -136,12 +135,36 @@ export class EventLoader {
|
||||
|
||||
/**
|
||||
* finds next event within Roll context
|
||||
* @returns {{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}|{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}}
|
||||
* @param {number} timeNow - current time in ms
|
||||
*/
|
||||
findRoll() {
|
||||
findRoll(timeNow) {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
const millisNow = Timer.getCurrentTime();
|
||||
return getSelectionByRoll(timedEvents, millisNow);
|
||||
if (!timedEvents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
nowIndex,
|
||||
timers,
|
||||
timeToNext,
|
||||
nextEvent,
|
||||
nextPublicEvent,
|
||||
currentEvent,
|
||||
currentPublicEvent,
|
||||
} = getRollTimers(timedEvents, timeNow);
|
||||
|
||||
this.loadedEvent = currentEvent;
|
||||
this.selectedEventIndex = nowIndex;
|
||||
this.selectedEventId = currentEvent?.id || null;
|
||||
this.numEvents = timedEvents.length;
|
||||
|
||||
// titles
|
||||
this._loadThisTitles(currentEvent, 'now-private');
|
||||
this._loadThisTitles(currentPublicEvent, 'now-public');
|
||||
this._loadThisTitles(nextEvent, 'next-private');
|
||||
this._loadThisTitles(nextPublicEvent, 'next-public');
|
||||
|
||||
return { currentEvent, nextEvent, timeToNext, timers };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,6 +323,10 @@ export class EventLoader {
|
||||
* @private
|
||||
*/
|
||||
_loadThisTitles(event, type) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
// now, load to both public and private
|
||||
case 'now':
|
||||
@@ -364,7 +391,7 @@ export class EventLoader {
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
throw new Error(`Unhandled title type: ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Server } from 'socket.io';
|
||||
import getRandomName from '../../utils/getRandomName.js';
|
||||
import { generateId } from '../../utils/generate_id.js';
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
import { Timer } from '../timer/Timer.js';
|
||||
import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/playbackService.js';
|
||||
import { PlaybackService } from '../../services/PlaybackService.js';
|
||||
|
||||
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
|
||||
import { eventTimer } from '../../services/TimerService.js';
|
||||
import { eventTimer, TimerService } from '../../services/TimerService.js';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
|
||||
class SocketController {
|
||||
@@ -342,7 +341,7 @@ class SocketController {
|
||||
level,
|
||||
origin,
|
||||
text,
|
||||
time: stringFromMillis(Timer.getCurrentTime() || 0),
|
||||
time: stringFromMillis(TimerService.getCurrentTime() || 0),
|
||||
};
|
||||
|
||||
this.messageStack.unshift(logMessage);
|
||||
|
||||
@@ -1,773 +0,0 @@
|
||||
import { Timer } from './Timer.js';
|
||||
import { DAY_TO_MS, replacePlaceholder, updateRoll } from './classUtils.js';
|
||||
import { OSCIntegration } from './integrations/Osc.js';
|
||||
import { HTTPIntegration } from './integrations/Http.js';
|
||||
import { cleanURL } from '../../utils/url.js';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
|
||||
/*
|
||||
* Class EventTimer adds functions specific to APP
|
||||
* @extends Timer
|
||||
*/
|
||||
export class EventTimer extends Timer {
|
||||
/**
|
||||
* Instantiates an event timer object
|
||||
* @param {object} socket
|
||||
* @param {object} timerConfig
|
||||
* @param {object} [oscConfig]
|
||||
* @param {object} [httpConfig]
|
||||
*/
|
||||
constructor(socket, timerConfig, oscConfig, httpConfig) {
|
||||
// call super constructor
|
||||
super();
|
||||
|
||||
this.cycleState = {
|
||||
/* idle: before it is initialised */
|
||||
idle: 'idle',
|
||||
/* onLoad: when a new event is loaded */
|
||||
onLoad: 'onLoad',
|
||||
/* armed: when a new event is loaded but hasn't started */
|
||||
armed: 'armed',
|
||||
onStart: 'onStart',
|
||||
/* update: every update call cycle (1 x second) */
|
||||
onUpdate: 'onUpdate',
|
||||
onPause: 'onPause',
|
||||
onStop: 'onStop',
|
||||
onFinish: 'onFinish',
|
||||
};
|
||||
this.ontimeCycle = 'idle';
|
||||
this.prevCycle = null;
|
||||
|
||||
// Socket Object
|
||||
this.socket = socket;
|
||||
|
||||
// OSC Object
|
||||
this.osc = null;
|
||||
|
||||
// HTTP Client Object
|
||||
this.http = null;
|
||||
|
||||
// call general title reset
|
||||
this._resetSelection();
|
||||
|
||||
// set recurrent emits
|
||||
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
|
||||
|
||||
if (oscConfig != null) {
|
||||
this._initOscClient(oscConfig);
|
||||
}
|
||||
|
||||
if (httpConfig != null) {
|
||||
this._initHTTPClient(httpConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Shutdown process
|
||||
*/
|
||||
shutdown() {
|
||||
clearInterval(this._interval);
|
||||
if (this.osc != null) {
|
||||
this.socket.info('TX', '... Closing OSC Client');
|
||||
this.osc.shutdown();
|
||||
}
|
||||
if (this.http != null) {
|
||||
this.socket.info('TX', '... Closing HTTP Client');
|
||||
this.http.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises OSC Integration object
|
||||
* @param {object} oscConfig
|
||||
* @private
|
||||
*/
|
||||
_initOscClient(oscConfig) {
|
||||
this.osc = new OSCIntegration();
|
||||
const r = this.osc.init(oscConfig);
|
||||
r.success ? this.socket.info('TX', r.message) : this.socket.error('TX', r.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises HTTP Integration object
|
||||
* @param {object} httpConfig
|
||||
* @private
|
||||
*/
|
||||
_initHTTPClient(httpConfig) {
|
||||
this.socket.info('TX', `Initialise HTTP Client on port`);
|
||||
this.http = new HTTPIntegration();
|
||||
this.http.init(httpConfig);
|
||||
this.httpMessages = httpConfig.messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends time object over websockets
|
||||
*/
|
||||
broadcastTimer() {
|
||||
// through websockets
|
||||
this.socket.send('timer', this.getTimeObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Broadcast timer data
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeatureTimer() {
|
||||
const featureData = {
|
||||
clock: this.clock,
|
||||
current: this.current,
|
||||
secondaryTimer: this.secondaryTimer,
|
||||
duration: this.duration,
|
||||
startedAt: this._startedAt,
|
||||
expectedFinish: this._getExpectedFinish(),
|
||||
};
|
||||
this.socket.send('ontime-timer', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Broadcast data for Event List feature
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeatureRundown() {
|
||||
const featureData = {
|
||||
selectedEventId: this.selectedEventId,
|
||||
nextEventId: this.nextEventId,
|
||||
playback: this.state,
|
||||
};
|
||||
this.socket.send('feat-rundown', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Broadcast data for Playback Control feature
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeaturePlaybackControl() {
|
||||
const numEvents = EventLoader.getNumEvents();
|
||||
const featureData = {
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
numEvents: numEvents,
|
||||
};
|
||||
this.socket.send('feat-playbackcontrol', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Broadcast data for Info feature
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeatureInfo() {
|
||||
const numEvents = EventLoader.getNumEvents();
|
||||
const featureData = {
|
||||
titles: this.titles,
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
numEvents: numEvents,
|
||||
};
|
||||
this.socket.send('feat-info', featureData);
|
||||
}
|
||||
|
||||
_broadcastFeatureCuesheet() {
|
||||
const numEvents = EventLoader.getNumEvents();
|
||||
const featureData = {
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
numEvents: numEvents,
|
||||
titleNow: this.titles.titleNow,
|
||||
};
|
||||
this.socket.send('feat-cuesheet', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts complete object state
|
||||
*/
|
||||
broadcastState() {
|
||||
// feature sync
|
||||
this._broadcastFeatureRundown();
|
||||
this._broadcastFeaturePlaybackControl();
|
||||
this._broadcastFeatureInfo();
|
||||
this._broadcastFeatureCuesheet();
|
||||
this._broadcastFeatureTimer();
|
||||
|
||||
this.broadcastTimer();
|
||||
this.socket.send('playstate', this.state);
|
||||
this.socket.send('selected-id', this.selectedEventId);
|
||||
this.socket.send('next-id', this.nextEventId);
|
||||
this.socket.send('publicselected-id', this.selectedPublicEventId);
|
||||
this.socket.send('publicnext-id', this.nextPublicEventId);
|
||||
this.socket.send('titles', this.titles);
|
||||
this.socket.send('publictitles', this.titlesPublic);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description State machine checks what actions need to
|
||||
* happen at every app cycle
|
||||
*/
|
||||
runCycle() {
|
||||
const h = this.httpMessages?.messages;
|
||||
let httpMessage = null;
|
||||
|
||||
switch (this.ontimeCycle) {
|
||||
case 'idle':
|
||||
break;
|
||||
case 'armed':
|
||||
// if we come from roll, see if we can start
|
||||
if (this.state === 'roll') {
|
||||
this.update();
|
||||
}
|
||||
break;
|
||||
case 'onLoad':
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onLoad?.url !== '') {
|
||||
httpMessage = h?.onLoad?.url;
|
||||
}
|
||||
}
|
||||
|
||||
// update lifecycle: armed
|
||||
this.ontimeCycle = this.cycleState.armed;
|
||||
break;
|
||||
case 'onStart':
|
||||
// send OSC if there is something running
|
||||
// _finish at is only set when an event is loaded
|
||||
if (this._finishAt > 0) {
|
||||
this.sendOsc(this.osc.implemented.play);
|
||||
this.sendOsc(this.osc.implemented.eventNumber, this.selectedEventIndex || 0);
|
||||
}
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onStart?.url !== '') {
|
||||
httpMessage = h?.onStart?.url;
|
||||
}
|
||||
}
|
||||
|
||||
// update lifecycle: onUpdate
|
||||
this.ontimeCycle = this.cycleState.onUpdate;
|
||||
break;
|
||||
case 'onUpdate':
|
||||
// call update
|
||||
this.update();
|
||||
// through OSC, only if running
|
||||
if (this.state === 'start' || this.state === 'roll') {
|
||||
if (this.current != null && this.secondaryTimer == null) {
|
||||
this.sendOsc(this.osc.implemented.time, this.timeTag);
|
||||
this.sendOsc(this.osc.implemented.overtime, this.current > 0 ? 0 : 1);
|
||||
this.sendOsc(this.osc.implemented.title, this.titles?.titleNow || '');
|
||||
this.sendOsc(this.osc.implemented.presenter, this.titles?.presenterNow || '');
|
||||
}
|
||||
}
|
||||
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onUpdate?.url !== '') {
|
||||
httpMessage = h?.onUpdate?.url;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'onPause':
|
||||
// send OSC
|
||||
if (this.prevCycle === this.cycleState.onUpdate) {
|
||||
this.sendOsc(this.osc.implemented.pause);
|
||||
}
|
||||
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onPause?.url !== '') {
|
||||
httpMessage = h?.onPause?.url;
|
||||
}
|
||||
}
|
||||
|
||||
// update lifecycle: armed
|
||||
this.ontimeCycle = this.cycleState.armed;
|
||||
|
||||
break;
|
||||
case 'onStop':
|
||||
// send OSC if something was actually stopped
|
||||
if (this.prevCycle === this.cycleState.onUpdate) {
|
||||
this.sendOsc(this.osc.implemented.stop);
|
||||
}
|
||||
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onStop?.url !== '') {
|
||||
httpMessage = h?.onStop?.url;
|
||||
}
|
||||
}
|
||||
|
||||
// update lifecycle: idle
|
||||
this.ontimeCycle = this.cycleState.idle;
|
||||
break;
|
||||
case 'onFinish':
|
||||
// finished an event
|
||||
this.sendOsc(this.osc.implemented.finished);
|
||||
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onFinish?.url !== '') {
|
||||
httpMessage = h?.onFinish?.url;
|
||||
}
|
||||
}
|
||||
|
||||
// update lifecycle: onUpdate
|
||||
this.ontimeCycle = this.cycleState.onUpdate;
|
||||
break;
|
||||
default:
|
||||
this.socket.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
|
||||
}
|
||||
|
||||
// send http message if any
|
||||
if (httpMessage != null) {
|
||||
const v = {
|
||||
$timer: this.timeTag,
|
||||
$title: this.titles.titleNow,
|
||||
$presenter: this.titles.presenterNow,
|
||||
$subtitle: this.titles.subtitleNow,
|
||||
'$next-title': this.titles.titleNext,
|
||||
'$next-presenter': this.titles.presenterNext,
|
||||
'$next-subtitle': this.titles.subtitleNext,
|
||||
};
|
||||
const m = cleanURL(replacePlaceholder(httpMessage, v));
|
||||
this.http.send(m);
|
||||
}
|
||||
|
||||
// update
|
||||
this.update();
|
||||
this.broadcastState();
|
||||
|
||||
// reset cycle
|
||||
this.prevCycle = this.ontimeCycle;
|
||||
}
|
||||
|
||||
update() {
|
||||
// if there is nothing selected, update clock
|
||||
this.clock = Timer.getCurrentTime();
|
||||
this._broadcastFeatureTimer();
|
||||
this.broadcastTimer();
|
||||
|
||||
// if we are not updating, send the timers
|
||||
if (this.ontimeCycle !== this.cycleState.onUpdate) {
|
||||
this.socket.send('timer', this.getTimeObject());
|
||||
}
|
||||
|
||||
// Have we skipped onStart?
|
||||
if (this.state === 'start' || this.state === 'roll') {
|
||||
if (this.ontimeCycle === this.cycleState.armed) {
|
||||
// update lifecycle: onStart
|
||||
this.ontimeCycle = this.cycleState.onStart;
|
||||
this.runCycle();
|
||||
}
|
||||
}
|
||||
|
||||
// update default functions
|
||||
super.update();
|
||||
|
||||
if (this._finishedFlag) {
|
||||
// update lifecycle: onFinish and call cycle
|
||||
|
||||
this.ontimeCycle = this.cycleState.onFinish;
|
||||
this._finishedFlag = false;
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
// only implement roll here, rest implemented in super
|
||||
if (this.state === 'roll') {
|
||||
const u = {
|
||||
selectedEventId: this.selectedEventId,
|
||||
current: this.current,
|
||||
// safeguard on midnight rollover
|
||||
_finishAt: this._finishAt >= this._startedAt ? this._finishAt : this._finishAt + DAY_TO_MS,
|
||||
clock: this.clock,
|
||||
secondaryTimer: this.secondaryTimer,
|
||||
_secondaryTarget: this._secondaryTarget,
|
||||
};
|
||||
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(u);
|
||||
|
||||
this.current = updatedTimer;
|
||||
this.secondaryTimer = updatedSecondaryTimer;
|
||||
|
||||
if (isFinished) {
|
||||
// update lifecycle: onFinish
|
||||
this.ontimeCycle = this.cycleState.onFinish;
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
if (doRollLoad) {
|
||||
this.rollLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} eventId
|
||||
*/
|
||||
syncLoaded(eventId) {
|
||||
if (this.state === 'roll') {
|
||||
this.rollLoad();
|
||||
} else {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
this.loadEvent(event, 'reload');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a given event by index
|
||||
* @typedef ('load'|'reload') loadEventOptions
|
||||
* @param {object} event
|
||||
* @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time
|
||||
*/
|
||||
loadEvent(event, type = 'load') {
|
||||
const loadedData = eventLoader.loadById(event.id);
|
||||
if (!loadedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { loadedEvent, selectedEventIndex, selectedEventId, nextEventId, titles, titlesPublic } =
|
||||
loadedData;
|
||||
|
||||
const start = loadedEvent.timeStart || 0;
|
||||
let end = loadedEvent.timeEnd || 0;
|
||||
|
||||
// in case the end is earlier than start, we assume is the day after
|
||||
if (end < start) {
|
||||
end += DAY_TO_MS;
|
||||
}
|
||||
|
||||
this.duration = end - start;
|
||||
this.selectedEventIndex = selectedEventIndex;
|
||||
this.selectedEventId = selectedEventId;
|
||||
this.nextEventId = nextEventId;
|
||||
|
||||
if (type === 'load') {
|
||||
this._resetTimers();
|
||||
this.current = this.duration;
|
||||
} else {
|
||||
const now = Timer.getCurrentTime();
|
||||
const elapsed = this.getElapsed();
|
||||
this._finishAt = now + (this.duration - elapsed);
|
||||
}
|
||||
|
||||
this.titles = titles;
|
||||
this.titlesPublic = titlesPublic;
|
||||
|
||||
// update lifecycle: onLoad
|
||||
this.ontimeCycle = this.cycleState.onLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description resets selected event data
|
||||
* @private
|
||||
*/
|
||||
_resetSelection() {
|
||||
this.titles = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
noteNow: null,
|
||||
titleNext: null,
|
||||
subtitleNext: null,
|
||||
presenterNext: null,
|
||||
noteNext: null,
|
||||
};
|
||||
|
||||
this.titlesPublic = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
titleNext: null,
|
||||
subtitleNext: null,
|
||||
presenterNext: null,
|
||||
};
|
||||
|
||||
this.selectedEventIndex = null;
|
||||
this.selectedEventId = null;
|
||||
this.nextEventId = null;
|
||||
this.selectedPublicEventId = null;
|
||||
this.nextPublicEventId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description start timer
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
start() {
|
||||
// do we need to change
|
||||
if (this.state === 'start') return 'start';
|
||||
|
||||
// call super
|
||||
super.start();
|
||||
|
||||
// update lifecycle: onStart
|
||||
this.ontimeCycle = this.cycleState.onStart;
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description pause timer
|
||||
* @return {('start'|'pause'|'stop')} Playback state
|
||||
*/
|
||||
pause() {
|
||||
// do we need to change
|
||||
if (this.state === 'pause') return 'pause';
|
||||
|
||||
// call super
|
||||
super.pause();
|
||||
|
||||
// update lifecycle: onPause
|
||||
this.ontimeCycle = this.cycleState.onPause;
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description stop timer
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
stop() {
|
||||
// do we need to change
|
||||
if (this.state === 'stop') return 'stop';
|
||||
|
||||
// call super
|
||||
super.stop();
|
||||
this._resetTimers(true);
|
||||
this._resetSelection();
|
||||
|
||||
// update lifecycle: onStop
|
||||
this.ontimeCycle = this.cycleState.onStop;
|
||||
|
||||
// broadcast state
|
||||
this.broadcastState();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description increment timer by amount
|
||||
* @param amount
|
||||
*/
|
||||
increment(amount) {
|
||||
// call super
|
||||
super.increment(amount);
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Look for current event considering local clock
|
||||
*/
|
||||
rollLoad() {
|
||||
const prevLoaded = this.selectedEventId;
|
||||
|
||||
// maybe roll has already been loaded
|
||||
if (this.secondaryTimer === null) {
|
||||
this._resetTimers(true);
|
||||
this._resetSelection();
|
||||
}
|
||||
|
||||
const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } =
|
||||
eventLoader.findRoll();
|
||||
|
||||
// nothing to play, unload
|
||||
if (nowIndex === null && nextIndex === null) {
|
||||
this.stop();
|
||||
this.socket.warning('SERVER', 'Roll: no events found');
|
||||
return;
|
||||
}
|
||||
|
||||
// there is something running, load
|
||||
if (nowIndex !== null) {
|
||||
// clear secondary timers
|
||||
this.secondaryTimer = null;
|
||||
this._secondaryTarget = null;
|
||||
|
||||
// set timers
|
||||
this._startedAt = timers._startedAt;
|
||||
this._finishAt = timers._finishAt;
|
||||
this.duration = timers.duration;
|
||||
this.current = timers.current;
|
||||
|
||||
// set selection
|
||||
this.selectedEventId = nowId;
|
||||
this.selectedEventIndex = nowIndex;
|
||||
}
|
||||
|
||||
// found something to run next
|
||||
if (nextIndex != null) {
|
||||
const eventNext = EventLoader.getPlayableAtIndex(nextIndex);
|
||||
|
||||
// Set running timers
|
||||
if (nowIndex === null) {
|
||||
// only warn the first time
|
||||
if (this.secondaryTimer === null) {
|
||||
this.socket.info('SERVER', 'Roll: waiting for event start');
|
||||
}
|
||||
|
||||
// reset running timer
|
||||
// ??? should this not have been reset?
|
||||
this.current = null;
|
||||
|
||||
// timer counts to next event
|
||||
this.secondaryTimer = timeToNext;
|
||||
this._secondaryTarget = eventNext.timeStart;
|
||||
}
|
||||
|
||||
// TITLES: Load next private
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
if (eventNext) {
|
||||
this.titles.titleNext = eventNext.title;
|
||||
this.titles.subtitleNext = eventNext.subtitle;
|
||||
this.titles.presenterNext = eventNext.presenter;
|
||||
this.titles.noteNext = eventNext.note;
|
||||
this.nextEventId = eventNext.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load next public
|
||||
if (publicNextIndex !== null) {
|
||||
const eventNextPublic = EventLoader.getPlayableAtIndex(publicNextIndex);
|
||||
if (eventNextPublic) {
|
||||
this.titlesPublic.titleNext = eventNextPublic.title;
|
||||
this.titlesPublic.subtitleNext = eventNextPublic.subtitle;
|
||||
this.titlesPublic.presenterNext = eventNextPublic.presenter;
|
||||
this.titlesPublic.noteNext = eventNextPublic.note;
|
||||
this.nextPublicEventId = eventNextPublic.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load now private
|
||||
if (nowIndex !== null) {
|
||||
const eventNowPrivate = EventLoader.getPlayableAtIndex(nowIndex);
|
||||
if (eventNowPrivate) {
|
||||
this.titles.titleNow = eventNowPrivate.title;
|
||||
this.titles.subtitleNow = eventNowPrivate.subtitle;
|
||||
this.titles.presenterNow = eventNowPrivate.presenter;
|
||||
this.titles.noteNow = eventNowPrivate.note;
|
||||
this.selectedEventId = eventNowPrivate.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load now public
|
||||
if (publicIndex !== null) {
|
||||
const eventNowPublic = EventLoader.getPlayableAtIndex(nowIndex);
|
||||
if (eventNowPublic) {
|
||||
this.titlesPublic.titleNow = eventNowPublic.title;
|
||||
this.titlesPublic.subtitleNow = eventNowPublic.subtitle;
|
||||
this.titlesPublic.presenterNow = eventNowPublic.presenter;
|
||||
this.titlesPublic.noteNow = eventNowPublic.note;
|
||||
this.selectedPublicEventId = eventNowPublic.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevLoaded !== this.selectedEventId) {
|
||||
// update lifecycle: onLoad
|
||||
this.ontimeCycle = this.cycleState.onLoad;
|
||||
// ensure we go through onLoad cycle
|
||||
this.runCycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts roll mode
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
roll() {
|
||||
if (this.state === 'roll') {
|
||||
return 'roll';
|
||||
}
|
||||
|
||||
this.state = 'roll';
|
||||
|
||||
// update lifecycle: armed
|
||||
this.ontimeCycle = this.cycleState.armed;
|
||||
|
||||
// load into event
|
||||
this.rollLoad();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.sendOsc(this.osc.implemented.previous);
|
||||
this.pause();
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
next() {
|
||||
this.sendOsc(this.osc.implemented.next);
|
||||
this.pause();
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description reloads current event
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
reload() {
|
||||
if (!this.selectedEventId) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
// change playstate
|
||||
this.pause();
|
||||
|
||||
// send OSC
|
||||
this.sendOsc(this.osc.implemented.reload);
|
||||
|
||||
// reload data
|
||||
const event = EventLoader.getEventWithId(this.selectedEventId);
|
||||
this.loadEvent(event);
|
||||
|
||||
this.runCycle();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
|
||||
/**
|
||||
* Integrations
|
||||
* -------------
|
||||
*
|
||||
* Code related to integrations
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calls OSC send message and resolves reply to logger
|
||||
* @param {string} message
|
||||
* @param {any} [payload]
|
||||
*/
|
||||
async sendOsc(message, payload) {
|
||||
const reply = await this.osc.send(message, payload);
|
||||
if (!reply.success) {
|
||||
this.socket.error('TX', reply.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Builds sync object
|
||||
*/
|
||||
poll() {
|
||||
return {
|
||||
currentId: this.selectedEventId,
|
||||
timer: this.timeTag,
|
||||
clock: this.clock,
|
||||
playback: this.state,
|
||||
currentColour: null,
|
||||
title: this.titles.titleNow,
|
||||
presenter: this.titles.presenterNow,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
|
||||
/**
|
||||
* @description Implements simple countdown timer functions
|
||||
* @class
|
||||
*/
|
||||
export class Timer {
|
||||
constructor() {
|
||||
this.clock = null;
|
||||
this._resetTimers(true);
|
||||
this.state = 'stop';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description updates the running timer
|
||||
*/
|
||||
update() {
|
||||
// get current time
|
||||
const now = Timer.getCurrentTime();
|
||||
this.clock = now;
|
||||
let checkFinish = false;
|
||||
|
||||
// check playstate
|
||||
switch (this.state) {
|
||||
case 'start':
|
||||
// ensure we have a start time
|
||||
if (this._startedAt == null) this._startedAt = now;
|
||||
|
||||
// update current timer
|
||||
this.current = this._startedAt + this.duration + this._pausedTotal - now;
|
||||
|
||||
// enable flag
|
||||
checkFinish = true;
|
||||
break;
|
||||
case 'pause':
|
||||
// update paused time
|
||||
this._pausedInterval = now - this._pausedAt;
|
||||
|
||||
if (this._startedAt != null) {
|
||||
// update current timer
|
||||
this.current =
|
||||
this._startedAt + this.duration + this._pausedTotal + this._pausedInterval - now;
|
||||
}
|
||||
|
||||
// enable flag
|
||||
checkFinish = true;
|
||||
break;
|
||||
case 'stop':
|
||||
// nothing here yet
|
||||
break;
|
||||
}
|
||||
|
||||
if (checkFinish) {
|
||||
// is event finished?
|
||||
const isTimeOver = this.current <= 0;
|
||||
const isUpdating = this.state !== 'pause';
|
||||
|
||||
if (isTimeOver && isUpdating && this._finishedAt == null) {
|
||||
if (this._finishedAt === null) this._finishedAt = now;
|
||||
this._finishedFlag = true;
|
||||
}
|
||||
}
|
||||
this.timeTag = stringFromMillis(this.current);
|
||||
}
|
||||
|
||||
// helpers
|
||||
/**
|
||||
* @description converts a value in millis to seconds
|
||||
* @param millis
|
||||
* @return {number}
|
||||
*/
|
||||
static toSeconds(millis) {
|
||||
if (millis == null) return 0;
|
||||
return millis < 0 ? Math.ceil(millis * 0.001) : Math.floor(millis * 0.001);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get current time in ms from midnight
|
||||
* @return {number}
|
||||
*/
|
||||
static getCurrentTime() {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description when is timer finishing
|
||||
* @return {null|*|null|number}
|
||||
* @private
|
||||
*/
|
||||
_getExpectedFinish() {
|
||||
if (this._startedAt == null) return null;
|
||||
if (this._finishedAt) return this._finishedAt;
|
||||
|
||||
return Math.max(
|
||||
this._startedAt + this.duration + this._pausedInterval + this._pausedTotal,
|
||||
this._startedAt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description resets timer parameters
|
||||
* @param total
|
||||
* @private
|
||||
*/
|
||||
_resetTimers(total = false) {
|
||||
if (total) this.duration = null;
|
||||
this.current = this.duration;
|
||||
this.timeTag = null;
|
||||
this.running = null;
|
||||
this.secondaryTimer = null;
|
||||
this._secondaryTarget = null;
|
||||
this._finishAt = null;
|
||||
this._finishedAt = null;
|
||||
this._finishedFlag = false;
|
||||
this._startedAt = null;
|
||||
this._pausedAt = null;
|
||||
this._pausedInterval = null;
|
||||
this._pausedTotal = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get elapsed time
|
||||
* @return {number}
|
||||
*/
|
||||
getElapsed() {
|
||||
return this.duration - this.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Builds time object
|
||||
* @returns {{running: number, secondary: number, expectedFinish: number, durationSeconds: number, startedAt: null, clock: null}}
|
||||
*/
|
||||
getTimeObject() {
|
||||
return {
|
||||
clock: this.clock,
|
||||
isNegative: this.current < 0,
|
||||
running: Timer.toSeconds(this.current),
|
||||
secondary: Timer.toSeconds(this.secondaryTimer),
|
||||
durationSeconds: Timer.toSeconds(this.duration),
|
||||
expectedFinish: this._getExpectedFinish(),
|
||||
startedAt: this._startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// playback
|
||||
/**
|
||||
* @description start current time
|
||||
*/
|
||||
start() {
|
||||
// do we need to change
|
||||
if (this.state === 'start') return;
|
||||
else if (this._startedAt == null) {
|
||||
// it hasn't started yet
|
||||
const now = Timer.getCurrentTime();
|
||||
// set start time as now
|
||||
this._startedAt = now;
|
||||
// calculate expected finish time
|
||||
this._finishAt = now + this.duration;
|
||||
// reset pauses
|
||||
this._pausedTotal = null;
|
||||
this._pausedInterval = null;
|
||||
} else {
|
||||
// check if there is paused time
|
||||
if (this._pausedInterval) {
|
||||
this._pausedTotal += this._pausedInterval;
|
||||
this._pausedInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// change state
|
||||
this.state = 'start';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description pause current timer
|
||||
*/
|
||||
pause() {
|
||||
// do we need to change
|
||||
if (this.state === 'pause') return;
|
||||
|
||||
if (this._pausedInterval) {
|
||||
this._pausedTotal += this._pausedInterval;
|
||||
this._pausedInterval = null;
|
||||
}
|
||||
|
||||
// set pause time
|
||||
this._pausedAt = Timer.getCurrentTime();
|
||||
|
||||
// change state
|
||||
this.state = 'pause';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description stop current timer
|
||||
*/
|
||||
stop() {
|
||||
// do we need to change
|
||||
if (this.state === 'stop') return;
|
||||
|
||||
// clear all timers
|
||||
this._resetTimers();
|
||||
|
||||
// change state
|
||||
this.state = 'stop';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description increments a given amout to the timer
|
||||
* @param amount
|
||||
*/
|
||||
increment(amount) {
|
||||
this.duration += amount;
|
||||
|
||||
if (amount < 0 && Math.abs(amount) > this.current) {
|
||||
// if we will make the clock negative
|
||||
if (this._finishedAt == null) this._finishedAt = Timer.getCurrentTime();
|
||||
} else if (this.current < 0 && this.current + amount > 0) {
|
||||
// clock will go from negative to positive
|
||||
this._finishedAt = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { EventTimer } from '../EventTimer';
|
||||
import jest from 'jest-mock';
|
||||
|
||||
// necessary config
|
||||
const timerConfig = { refresh: 1000 };
|
||||
|
||||
const mockSocket = {
|
||||
error: jest.fn(),
|
||||
send: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
|
||||
test('object instantiates correctly', async () => {
|
||||
const t = new EventTimer(mockSocket, timerConfig);
|
||||
|
||||
// it contains everything from Timer
|
||||
expect(t.clock).toBeNull();
|
||||
expect(t.duration).toBeNull();
|
||||
expect(t.current).toBeNull();
|
||||
expect(t.timeTag).toBeNull();
|
||||
expect(t.secondaryTimer).toBeNull();
|
||||
expect(t._secondaryTarget).toBeNull();
|
||||
expect(t._finishAt).toBeNull();
|
||||
expect(t._finishedAt).toBeNull();
|
||||
expect(t._finishedFlag).toBeFalsy();
|
||||
expect(t._startedAt).toBeNull();
|
||||
expect(t._pausedAt).toBeNull();
|
||||
expect(t._pausedInterval).toBeNull();
|
||||
expect(t._pausedTotal).toBeNull();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
// and its own properties
|
||||
expect(t.ontimeCycle).toBe('idle');
|
||||
expect(t.prevCycle).toBeNull();
|
||||
expect(t.io).not.toBeNull();
|
||||
expect(t.osc).toBeNull();
|
||||
expect(t.http).toBeNull();
|
||||
expect(t._interval).not.toBeNull();
|
||||
|
||||
const expectTitlesPublic = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
titleNext: null,
|
||||
subtitleNext: null,
|
||||
presenterNext: null,
|
||||
};
|
||||
|
||||
const expectTitles = {
|
||||
...expectTitlesPublic,
|
||||
noteNow: null,
|
||||
noteNext: null,
|
||||
};
|
||||
|
||||
expect(t.titlesPublic).toStrictEqual(expectTitlesPublic);
|
||||
expect(t.titles).toStrictEqual(expectTitles);
|
||||
|
||||
expect(t.selectedEventIndex).toBeNull();
|
||||
expect(t.selectedEventId).toBeNull();
|
||||
expect(t.nextEventId).toBeNull();
|
||||
expect(t.selectedPublicEventId).toBeNull();
|
||||
expect(t.nextPublicEventId).toBeNull();
|
||||
|
||||
t.shutdown();
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Timer } from '../Timer';
|
||||
|
||||
test('object instantiates correctly', () => {
|
||||
const t = new Timer();
|
||||
|
||||
expect(t.clock).toBeNull;
|
||||
expect(t.duration).toBeNull;
|
||||
expect(t.current).toBeNull;
|
||||
expect(t.timeTag).toBeNull;
|
||||
expect(t.secondaryTimer).toBeNull;
|
||||
expect(t._secondaryTarget).toBeNull;
|
||||
expect(t._finishAt).toBeNull;
|
||||
expect(t._finishedAt).toBeNull;
|
||||
expect(t._finishedFlag).toBeFalsy;
|
||||
expect(t._startedAt).toBeNull;
|
||||
expect(t._pausedAt).toBeNull;
|
||||
expect(t._pausedInterval).toBeNull;
|
||||
expect(t._pausedTotal).toBeNull;
|
||||
expect(t.state).toBe('stop');
|
||||
});
|
||||
|
||||
test('convert between mills and seconds correctly', () => {
|
||||
expect(Timer.toSeconds(10000)).toBe(10);
|
||||
expect(Timer.toSeconds(9016)).toBe(9);
|
||||
expect(Timer.toSeconds(8016)).toBe(8);
|
||||
expect(Timer.toSeconds(7010)).toBe(7);
|
||||
expect(Timer.toSeconds(6006)).toBe(6);
|
||||
expect(Timer.toSeconds(4999)).toBe(4);
|
||||
expect(Timer.toSeconds(2995)).toBe(2);
|
||||
expect(Timer.toSeconds(1991)).toBe(1);
|
||||
expect(Timer.toSeconds(992)).toBe(0);
|
||||
expect(Timer.toSeconds(127)).toBe(0);
|
||||
expect(Timer.toSeconds(0)).toBe(0);
|
||||
expect(Timer.toSeconds(-0)).toBe(-0);
|
||||
expect(Timer.toSeconds(-127)).toBe(-0);
|
||||
expect(Timer.toSeconds(-992)).toBe(-0);
|
||||
expect(Timer.toSeconds(-1991)).toBe(-1);
|
||||
expect(Timer.toSeconds(-2995)).toBe(-2);
|
||||
expect(Timer.toSeconds(-4999)).toBe(-4);
|
||||
expect(Timer.toSeconds(-6006)).toBe(-6);
|
||||
expect(Timer.toSeconds(-7010)).toBe(-7);
|
||||
expect(Timer.toSeconds(-8016)).toBe(-8);
|
||||
expect(Timer.toSeconds(-10000)).toBe(-10);
|
||||
});
|
||||
|
||||
test('converting between millis to seconds handles partials correctly', () => {
|
||||
const finish = 82162001;
|
||||
const now = 80364519;
|
||||
const runningMs = finish - now;
|
||||
expect(Timer.toSeconds(runningMs)).toBe(1797);
|
||||
|
||||
expect(Timer.toSeconds(1800000)).toBe(1800);
|
||||
expect(Timer.toSeconds(1799761)).toBe(1799);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Server } from 'node-osc';
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { ADDRESS_MESSAGE_CONTROL } from '../classes/socket/socketConfig.js';
|
||||
@@ -22,7 +22,7 @@ export const initiateOSC = (config) => {
|
||||
|
||||
oscServer.on('error', console.error);
|
||||
|
||||
oscServer.on('message', function(msg) {
|
||||
oscServer.on('message', function (msg) {
|
||||
// message should look like /ontime/{path} {args} where
|
||||
// ontime: fixed message for app
|
||||
// path: command to be called
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveDbPath } from '../modules/loadDb.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
import { mergeObject } from '../utils/parserUtils.js';
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Create controller for GET request to '/playback'
|
||||
// Returns ACK message
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
|
||||
// Create controller for POST request to '/playback'
|
||||
// Returns playback state
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
deleteEvent,
|
||||
editEvent,
|
||||
reorderEvent,
|
||||
} from '../services/rundownService.js';
|
||||
} from '../services/RundownService.js';
|
||||
|
||||
// Create controller for GET request to '/eventlist'
|
||||
// Returns -
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { JSONFile, Low } from 'lowdb';
|
||||
import { Low } from 'lowdb';
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { dirname, join } from 'path';
|
||||
import { copyFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"express": "^4.18.1",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^6.14.2",
|
||||
"lowdb": "3.0.0",
|
||||
"lowdb": "^5.0.5",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^4.0.0",
|
||||
"node-osc": "^8.0.6",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { eventTimer, TimerService } from './TimerService.js';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
@@ -121,7 +121,7 @@ export class PlaybackService {
|
||||
if (eventLoader.selectedEventId) {
|
||||
eventTimer.start();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export class PlaybackService {
|
||||
if (eventLoader.selectedEventId) {
|
||||
eventTimer.pause();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class PlaybackService {
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
@@ -165,13 +165,29 @@ export class PlaybackService {
|
||||
* Sets playback to roll
|
||||
*/
|
||||
static roll() {
|
||||
if (EventLoader.getNumEvents() && eventTimer.playback !== 'roll') {
|
||||
eventTimer.roll();
|
||||
if (EventLoader.getPlayableEvents()) {
|
||||
const rollTimers = eventLoader.findRoll(TimerService.getCurrentTime());
|
||||
|
||||
// nothing to play
|
||||
if (rollTimers === null) {
|
||||
socketProvider.error('SERVER', 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentEvent, nextEvent, timers } = rollTimers;
|
||||
if (!currentEvent && !nextEvent) {
|
||||
socketProvider.error('SERVER', 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
eventTimer.roll(currentEvent, nextEvent, timers);
|
||||
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
socketProvider.send('playback', newState);
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,16 +86,15 @@ export function updateTimer(affectedIds) {
|
||||
|
||||
if (safeOption) {
|
||||
eventLoader.reset();
|
||||
const loadedEvent = eventLoader.loadById(runningEventId);
|
||||
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventInMemory) {
|
||||
const loadedEvent = eventLoader.loadById(runningEventId);
|
||||
eventLoader.reset();
|
||||
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
|
||||
if (!loadedEvent) {
|
||||
// event was deleted
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
} else {
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
@@ -104,7 +103,7 @@ export function updateTimer(affectedIds) {
|
||||
}
|
||||
|
||||
if (isNext) {
|
||||
const loadedEvent = eventLoader.loadById(runningEventId);
|
||||
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
return true;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
import { updateRoll } from './rollUtils.js';
|
||||
import { DAY_TO_MS } from '../utils/time.js';
|
||||
|
||||
class TimerService {
|
||||
export class TimerService {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} [timerConfig]
|
||||
@@ -62,10 +65,10 @@ class TimerService {
|
||||
finishedAt: null,
|
||||
secondaryTimer: null,
|
||||
};
|
||||
this.loadedTimer = null;
|
||||
this.loadedTimerId = null;
|
||||
this._pausedInterval = 0;
|
||||
this._pausedAt = null;
|
||||
this._secondaryTarget = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,16 +76,28 @@ class TimerService {
|
||||
* @param timer
|
||||
*/
|
||||
hotReload(timer) {
|
||||
if (timer?.id !== this.loadedTimerId) {
|
||||
if (typeof timer === 'undefined') {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer?.id !== this.loadedTimerId) {
|
||||
// event timer only concerns itself with current event
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer?.skip) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// TODO: check if any relevant information warrants update
|
||||
|
||||
// update relevant information and force update
|
||||
this.loadedTimer = timer;
|
||||
this.timer.duration = timer.duration;
|
||||
|
||||
// this might not be ideal
|
||||
this.timer.finishedAt = null;
|
||||
this.timer.expectedFinish = this._getExpectedFinish();
|
||||
if (this.timer.startedAt === null) {
|
||||
this.timer.current = timer.duration;
|
||||
}
|
||||
@@ -106,7 +121,6 @@ class TimerService {
|
||||
|
||||
this._clear();
|
||||
|
||||
this.loadedTimer = timer;
|
||||
this.loadedTimerId = timer.id;
|
||||
this.timer.duration = timer.duration;
|
||||
this.timer.current = timer.duration;
|
||||
@@ -221,27 +235,58 @@ class TimerService {
|
||||
update() {
|
||||
this.timer.clock = TimerService.getCurrentTime();
|
||||
|
||||
// we only update timer if a timer has been started
|
||||
if (this.timer.startedAt !== null) {
|
||||
if (this.playback === 'pause') {
|
||||
this._pausedInterval = this.timer.clock - this._pausedAt;
|
||||
}
|
||||
if (this.playback === 'roll') {
|
||||
const tempCurrentTimer = {
|
||||
selectedEventId: this.loadedTimerId,
|
||||
current: this.timer.current,
|
||||
// safeguard on midnight rollover
|
||||
_finishAt:
|
||||
this.timer.expectedFinish >= this.timer.startedAt
|
||||
? this.timer.expectedFinish
|
||||
: this.timer.expectedFinish + DAY_TO_MS,
|
||||
|
||||
this.timer.current =
|
||||
this.timer.startedAt +
|
||||
this.timer.duration +
|
||||
this.timer.addedTime +
|
||||
this._pausedInterval -
|
||||
this.timer.clock;
|
||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||
clock: this.timer.clock,
|
||||
secondaryTimer: this.timer.secondaryTimer,
|
||||
_secondaryTarget: this._secondaryTarget,
|
||||
};
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
|
||||
updateRoll(tempCurrentTimer);
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
this.timer.finishedAt = this.timer.clock;
|
||||
this.timer.current = updatedTimer;
|
||||
this.timer.secondaryTimer = updatedSecondaryTimer;
|
||||
|
||||
if (isFinished) {
|
||||
this.timer.selectedEventId = null;
|
||||
this.loadedTimerId = null;
|
||||
this._onFinish();
|
||||
} else {
|
||||
this.timer.finishedAt = null;
|
||||
}
|
||||
this.timer.expectedFinish = this._getExpectedFinish();
|
||||
|
||||
if (doRollLoad) {
|
||||
PlaybackService.roll();
|
||||
}
|
||||
} else {
|
||||
// we only update timer if a timer has been started
|
||||
if (this.timer.startedAt !== null) {
|
||||
if (this.playback === 'pause') {
|
||||
this._pausedInterval = this.timer.clock - this._pausedAt;
|
||||
}
|
||||
|
||||
this.timer.current =
|
||||
this.timer.startedAt +
|
||||
this.timer.duration +
|
||||
this.timer.addedTime +
|
||||
this._pausedInterval -
|
||||
this.timer.clock;
|
||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
this.timer.finishedAt = this.timer.clock;
|
||||
this._onFinish();
|
||||
} else {
|
||||
this.timer.finishedAt = null;
|
||||
}
|
||||
this.timer.expectedFinish = this._getExpectedFinish();
|
||||
}
|
||||
}
|
||||
this._onUpdate();
|
||||
}
|
||||
@@ -256,12 +301,33 @@ class TimerService {
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
roll() {
|
||||
roll(currentEvent, nextEvent, timers) {
|
||||
this._clear();
|
||||
this.timer.clock = TimerService.getCurrentTime();
|
||||
|
||||
if (currentEvent) {
|
||||
// there is something running, load
|
||||
this.timer.secondaryTimer = null;
|
||||
this._secondaryTarget = null;
|
||||
|
||||
this.loadedTimerId = currentEvent.id;
|
||||
this.timer.startedAt = currentEvent.timeStart;
|
||||
this.timer.expectedFinish = currentEvent.timeEnd;
|
||||
this.timer.duration = timers.duration;
|
||||
this.timer.current = timers.current;
|
||||
} else if (nextEvent) {
|
||||
// nothing now, but something coming up
|
||||
this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock;
|
||||
this._secondaryTarget = nextEvent.timeStart;
|
||||
}
|
||||
|
||||
this.playback = 'roll';
|
||||
this._onRoll();
|
||||
this.update();
|
||||
}
|
||||
|
||||
_onRoll() {
|
||||
throw new Error('Roll not implemented');
|
||||
this._onLoad();
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
|
||||
+41
-21
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
DAY_TO_MS,
|
||||
getSelectionByRoll,
|
||||
getRollTimers,
|
||||
normaliseEndTime,
|
||||
replacePlaceholder,
|
||||
sortArrayByProperty,
|
||||
updateRoll,
|
||||
} from '../classUtils.js';
|
||||
} from '../rollUtils.js';
|
||||
|
||||
// test sortArrayByProperty()
|
||||
describe('sort simple arrays of objects', () => {
|
||||
@@ -54,7 +54,7 @@ describe('sort simple arrays of objects', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// test getSelectionByRoll()
|
||||
// test getRollTimers()
|
||||
describe('test that roll loads selection in right order', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
@@ -119,7 +119,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 5,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 5,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 5,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -182,7 +182,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 10,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -203,7 +203,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 1,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: 7,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
timeToNext: DAY_TO_MS - now + eventlist[0].timeStart,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -284,12 +284,12 @@ describe('test that roll loads selection in right order', () => {
|
||||
timers: null,
|
||||
timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart,
|
||||
};
|
||||
const state = getSelectionByRoll(singleEventList, now);
|
||||
const state = getRollTimers(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// test getSelectionByRoll()
|
||||
// test getRollTimers()
|
||||
describe('test that roll behaviour with overlapping times', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
@@ -324,7 +324,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
timeToNext: 10,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -345,7 +345,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
timeToNext: 0,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -366,7 +366,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
timeToNext: -5,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -387,7 +387,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -408,7 +408,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -475,7 +475,7 @@ describe('test that it replaces data correctly', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// test getSelectionByRoll() on issue #58
|
||||
// test getRollTimers() on issue #58
|
||||
describe('test that roll behaviour multi day event edge cases', () => {
|
||||
it('if the start time is the day after end time, and start time is earlier than now', () => {
|
||||
const now = 66600000; // 19:30
|
||||
@@ -502,7 +502,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
timeToNext: eventlist[0].timeStart - now,
|
||||
};
|
||||
|
||||
const state = getSelectionByRoll(eventlist, now);
|
||||
const state = getRollTimers(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -646,4 +646,24 @@ describe('typical scenarios', () => {
|
||||
|
||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when a secondary timer is finished, it prompts for new event load', () => {
|
||||
const timers = {
|
||||
selectedEventId: null,
|
||||
current: null,
|
||||
_finishAt: null,
|
||||
clock: 15,
|
||||
secondaryTimer: 0,
|
||||
_secondaryTarget: 15,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
updatedTimer: null,
|
||||
updatedSecondaryTimer: timers._secondaryTarget - timers.clock,
|
||||
doRollLoad: true,
|
||||
isFinished: false,
|
||||
};
|
||||
|
||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -40,56 +40,43 @@ export const replacePlaceholder = (str, values) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Used in roll mode, returns selection variables from array
|
||||
* @param {array} arr - event list
|
||||
* @param {number} now - time now in millis
|
||||
* @returns {object} object with selection variables
|
||||
*
|
||||
* @param rundown
|
||||
* @param timeNow
|
||||
* @returns {{}}
|
||||
*/
|
||||
|
||||
export const getSelectionByRoll = (arr, now) => {
|
||||
// Events now
|
||||
export const getRollTimers = (rundown, timeNow) => {
|
||||
let nowIndex = null; // index of event now
|
||||
let nowId = null; // id of event now
|
||||
let publicIndex = null; // index of public event now
|
||||
let publicTime = -1; // counter:
|
||||
|
||||
// Events next
|
||||
let publicTime = -1;
|
||||
let nextIndex = null; // index of next event
|
||||
let publicNextIndex = null; // index of next public event
|
||||
let timeToNext = null; // counter: time for next event
|
||||
let publicTimeToNext = null; // counter: time for next public event
|
||||
|
||||
// current timer
|
||||
let timers = null;
|
||||
|
||||
// exit early if there are no events
|
||||
if (arr.length < 1) {
|
||||
return {
|
||||
nowIndex,
|
||||
nowId,
|
||||
publicIndex,
|
||||
nextIndex,
|
||||
publicNextIndex,
|
||||
timers,
|
||||
timeToNext,
|
||||
};
|
||||
}
|
||||
|
||||
// Order events by startTime
|
||||
const orderedEvents = sortArrayByProperty(arr, 'timeStart');
|
||||
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
|
||||
|
||||
// preload first if we are past events
|
||||
const lastEvent = orderedEvents[orderedEvents.length - 1];
|
||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||
|
||||
if (now > lastNormalEnd) {
|
||||
let nextEvent = null;
|
||||
let nextPublicEvent = null;
|
||||
let currentEvent = null;
|
||||
let currentPublicEvent = null;
|
||||
|
||||
if (timeNow > lastNormalEnd) {
|
||||
nextIndex = 0;
|
||||
timeToNext = orderedEvents[0].timeStart + DAY_TO_MS - now;
|
||||
timeToNext = orderedEvents[0].timeStart + DAY_TO_MS - timeNow;
|
||||
|
||||
// look for next public
|
||||
for (const e of orderedEvents) {
|
||||
if (e.isPublic) {
|
||||
publicNextIndex = arr.findIndex((a) => a.id === e.id);
|
||||
for (const event of orderedEvents) {
|
||||
if (event.isPublic) {
|
||||
nextPublicEvent = event;
|
||||
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -98,39 +85,42 @@ export const getSelectionByRoll = (arr, now) => {
|
||||
let nowFound = false;
|
||||
|
||||
// loop through events, look for where we should be
|
||||
for (const e of orderedEvents) {
|
||||
for (const event of orderedEvents) {
|
||||
// When does the event end (handle midnight)
|
||||
const normalEnd = normaliseEndTime(e.timeStart, e.timeEnd);
|
||||
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
|
||||
|
||||
if (normalEnd <= now) {
|
||||
if (normalEnd <= timeNow) {
|
||||
// event ran already
|
||||
|
||||
// public event might not be the one running
|
||||
if (e.isPublic && normalEnd > publicTime) {
|
||||
if (event.isPublic && normalEnd > publicTime) {
|
||||
publicTime = normalEnd;
|
||||
publicIndex = arr.findIndex((a) => a.id === e.id);
|
||||
currentPublicEvent = event;
|
||||
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (normalEnd > now && now >= e.timeStart && !nowFound) {
|
||||
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) {
|
||||
// event is running
|
||||
|
||||
// it could also be public
|
||||
if (e.isPublic) {
|
||||
if (event.isPublic) {
|
||||
publicTime = normalEnd;
|
||||
publicIndex = arr.findIndex((a) => a.id === e.id);
|
||||
currentPublicEvent = event;
|
||||
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
|
||||
nowIndex = arr.findIndex((a) => a.id === e.id);
|
||||
nowId = e.id;
|
||||
currentEvent = event;
|
||||
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
nowId = event.id;
|
||||
|
||||
// set timers
|
||||
timers = {
|
||||
_startedAt: e.timeStart,
|
||||
_finishAt: e.timeEnd,
|
||||
duration: normalEnd - e.timeStart,
|
||||
current: normalEnd - now,
|
||||
_startedAt: event.timeStart,
|
||||
_finishAt: event.timeEnd,
|
||||
duration: normalEnd - event.timeStart,
|
||||
current: normalEnd - timeNow,
|
||||
};
|
||||
nowFound = true;
|
||||
} else if (normalEnd > now) {
|
||||
} else if (normalEnd > timeNow) {
|
||||
// event will run
|
||||
|
||||
// no need to look after found first
|
||||
@@ -138,15 +128,17 @@ export const getSelectionByRoll = (arr, now) => {
|
||||
|
||||
// look for next events
|
||||
// check how far the start is from now
|
||||
const wait = e.timeStart - now;
|
||||
const wait = event.timeStart - timeNow;
|
||||
|
||||
if (nextIndex === null || wait < timeToNext) {
|
||||
timeToNext = wait;
|
||||
nextIndex = arr.findIndex((a) => a.id === e.id);
|
||||
nextEvent = event;
|
||||
nextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
if ((publicNextIndex === null || wait < publicTimeToNext) && e.isPublic) {
|
||||
if ((publicNextIndex === null || wait < publicTimeToNext) && event.isPublic) {
|
||||
publicTimeToNext = wait;
|
||||
publicNextIndex = arr.findIndex((a) => a.id === e.id);
|
||||
nextPublicEvent = event;
|
||||
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +152,10 @@ export const getSelectionByRoll = (arr, now) => {
|
||||
publicNextIndex,
|
||||
timers,
|
||||
timeToNext,
|
||||
nextEvent,
|
||||
nextPublicEvent,
|
||||
currentEvent,
|
||||
currentPublicEvent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -183,7 +179,7 @@ export const updateRoll = (currentTimers) => {
|
||||
let updatedSecondaryTimer = secondaryTimer;
|
||||
// whether rollLoad should be called
|
||||
let doRollLoad = false;
|
||||
// whether runCycle should be called
|
||||
// whether finished event should trigger
|
||||
let isFinished = false;
|
||||
|
||||
if (selectedEventId && current >= 0) {
|
||||
@@ -194,6 +190,7 @@ export const updateRoll = (currentTimers) => {
|
||||
updatedTimer = _finishAt - clock;
|
||||
if (updatedTimer < 0) {
|
||||
isFinished = true;
|
||||
updatedTimer = null;
|
||||
}
|
||||
} else if (secondaryTimer >= 0) {
|
||||
// if secondaryTimer is running we are in waiting to roll
|
||||
@@ -327,8 +327,6 @@ const adjective = [
|
||||
'far-flung',
|
||||
'far-off',
|
||||
'fast',
|
||||
'fat',
|
||||
'fatal',
|
||||
'fatherly',
|
||||
'favorable',
|
||||
'favorite',
|
||||
@@ -1647,7 +1645,6 @@ const object = [
|
||||
'hall',
|
||||
'historian',
|
||||
'hospital',
|
||||
'injury',
|
||||
'instruction',
|
||||
'maintenance',
|
||||
'manufacturer',
|
||||
|
||||
@@ -4,6 +4,7 @@ const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
export const DAY_TO_MS = 86400000;
|
||||
|
||||
/**
|
||||
* @description Validates a time string
|
||||
|
||||
@@ -952,12 +952,12 @@ lodash@^4.17.21:
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lowdb@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-3.0.0.tgz#c10ab4e7eb86f1cbe255e35e60ffb0c6f42049e0"
|
||||
integrity sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==
|
||||
lowdb@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-5.0.5.tgz#30315e5a42432df188dcd17f1e5a288de68848e8"
|
||||
integrity sha512-7EWKmIMhNKA8TXFhL8t0p6N2LC53l3ZqsWQGSksGhhjrcms9rbKlyrAh2PzSGK5v0KPJ2W5VItBnC3NDRzOnzQ==
|
||||
dependencies:
|
||||
steno "^2.1.0"
|
||||
steno "^3.0.0"
|
||||
|
||||
lru_map@^0.3.3:
|
||||
version "0.3.3"
|
||||
@@ -1453,10 +1453,10 @@ statuses@2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
steno@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/steno/-/steno-2.1.0.tgz#05a9c378ce42ed04f642cda6fcb41787a10e4e33"
|
||||
integrity sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==
|
||||
steno@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/steno/-/steno-3.0.0.tgz#212a11e8ef3646b610efc8953842f556fd0df28f"
|
||||
integrity sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw==
|
||||
|
||||
streamsearch@0.1.2:
|
||||
version "0.1.2"
|
||||
|
||||
Reference in New Issue
Block a user