diff --git a/client/src/common/components/countdown/Countdown.jsx b/client/src/common/components/countdown/Countdown.jsx index 740eae82c..33ca67aef 100644 --- a/client/src/common/components/countdown/Countdown.jsx +++ b/client/src/common/components/countdown/Countdown.jsx @@ -1,15 +1,16 @@ import { memo } from 'react'; import { formatDisplay } from 'common/utils/dateConfig'; +import PropTypes from 'prop-types'; import styles from './Countdown.module.css'; -const Countdown = ({ time, small, negative, hideZeroHours }) => { +const Countdown = ({ time, small, isNegative, hideZeroHours }) => { // prepare display string const display = time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --'; - const colour = negative ? '#ff7597' : '#fffffa'; + const colour = isNegative ? '#ff7597' : '#fffffa'; return (
{ }; export default memo(Countdown); + +Countdown.propTypes = { + time: PropTypes.number.isRequired, + small: PropTypes.bool, + isNegative: PropTypes.bool, + hideZeroHour: PropTypes.bool, +}; diff --git a/client/src/common/utils/__tests__/dateConfig.test.js b/client/src/common/utils/__tests__/dateConfig.test.js index 757896b76..9d78d9fe4 100644 --- a/client/src/common/utils/__tests__/dateConfig.test.js +++ b/client/src/common/utils/__tests__/dateConfig.test.js @@ -6,6 +6,7 @@ import { forgivingStringToMillis, timeStringToMillis, } from '../dateConfig'; +import { stringFromMillis } from 'ontime-utils/time'; describe('test string from formatDisplay function', () => { it('test with null values', () => { @@ -49,6 +50,13 @@ describe('test string from formatDisplay function', () => { }); }); +describe('test formatDisplay handles partial secs', () => { + it('test with 1795829', () => { + const t = { val: 1795829, result: '00:29:55' }; + expect(stringFromMillis(t.val)).toBe(t.result); + }); +}); + describe('test string from formatDisplay function with hidezero', () => { it('test with null values', () => { const t = { val: null, result: '00:00' }; @@ -94,22 +102,22 @@ describe('test string from formatDisplay function with hidezero', () => { describe('test millisToSeconds function', () => { it('test with null values', () => { const t = { val: null, result: 0 }; - expect(millisToSeconds(t.val, false)).toBe(t.result); + expect(millisToSeconds(t.val)).toBe(t.result); }); it('test with valid millis', () => { const t = { val: 3600000, result: 3600 }; - expect(millisToSeconds(t.val, false)).toBe(t.result); + expect(millisToSeconds(t.val)).toBe(t.result); }); it('test with negative millis', () => { const t = { val: -3600000, result: -3600 }; - expect(millisToSeconds(t.val, false)).toBe(t.result); + expect(millisToSeconds(t.val)).toBe(t.result); }); it('test with 0', () => { const t = { val: 0, result: 0 }; - expect(millisToSeconds(t.val, false)).toBe(t.result); + expect(millisToSeconds(t.val)).toBe(t.result); }); it('test with -0', () => { diff --git a/client/src/features/control/PlaybackTimer.jsx b/client/src/features/control/PlaybackTimer.jsx index a30e036c1..12b381c64 100644 --- a/client/src/features/control/PlaybackTimer.jsx +++ b/client/src/features/control/PlaybackTimer.jsx @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; const areEqual = (prevProps, nextProps) => { return ( prevProps.timer.running === nextProps.timer.running && + prevProps.timer.isNegative === nextProps.timer.isNegative && prevProps.timer.expectedFinish === nextProps.timer.expectedFinish && prevProps.timer.startedAt === nextProps.timer.startedAt && prevProps.playback === nextProps.playback && @@ -21,7 +22,6 @@ const PlaybackTimer = (props) => { const { timer, playback, handleIncrement, selectedId } = props; const started = stringFromMillis(timer.startedAt, true); const finish = stringFromMillis(timer.expectedFinish, true); - const isNegative = timer.running < 0; const isRolling = playback === 'roll'; const isWaiting = timer.secondary > 0 && timer.running == null; const disableButtons = selectedId == null || isRolling; @@ -42,15 +42,15 @@ const PlaybackTimer = (props) => {
{isWaiting ? ( diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 7f47cfd77..3aa5bb9f5 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -30,6 +30,7 @@ const withSocket = (Component) => { const [timer, setTimer] = useState({ clock: null, running: null, + isNegative: null, startedAt: null, expectedFinish: null, }); @@ -226,7 +227,7 @@ const withSocket = (Component) => { // get clock string const timeManager = { ...timer, - finished: playback === 'start' && timer.running <= 0 && timer.startedAt, + finished: playback === 'start' && timer.isNegative && timer.startedAt, clock: stringFromMillis(timer.clock), clockNoSeconds: stringFromMillis(timer.clock, false), playstate: playback, diff --git a/client/src/features/viewers/backstage/StageManager.jsx b/client/src/features/viewers/backstage/StageManager.jsx index 6751bfc08..bed3a627c 100644 --- a/client/src/features/viewers/backstage/StageManager.jsx +++ b/client/src/features/viewers/backstage/StageManager.jsx @@ -27,7 +27,6 @@ export default function StageManager(props) { }, [backstageEvents]); // Format messages - const showPubl = publ.text !== '' && publ.visible; let stageTimer; @@ -35,7 +34,7 @@ export default function StageManager(props) { stageTimer = '- - : - -'; } else { stageTimer = formatDisplay(Math.abs(time.running), true); - if (time.running < 0) stageTimer = `-${stageTimer}`; + if (time.isNegative) stageTimer = `-${stageTimer}`; } // motion diff --git a/client/src/features/viewers/production/Pip.jsx b/client/src/features/viewers/production/Pip.jsx index 1142511a2..4dd7e912c 100644 --- a/client/src/features/viewers/production/Pip.jsx +++ b/client/src/features/viewers/production/Pip.jsx @@ -52,7 +52,7 @@ export default function Pip(props) { const showInfo = general.backstageInfo !== '' && general.backstageInfo != null; let stageTimer = formatDisplay(Math.abs(time.running), true); - if (time.running < 0) stageTimer = `-${stageTimer}`; + if (time.isNegative) stageTimer = `-${stageTimer}`; return (
diff --git a/client/src/features/viewers/studio/StudioClock.jsx b/client/src/features/viewers/studio/StudioClock.jsx index 3b947b713..eba12c0c6 100644 --- a/client/src/features/viewers/studio/StudioClock.jsx +++ b/client/src/features/viewers/studio/StudioClock.jsx @@ -49,7 +49,7 @@ export default function StudioClock(props) { > {title.titleNext}
-
0 ? style.nextCountdown : style.nextCountdown__overtime}> +
{selectedId != null && formatDisplay(time.running)}
diff --git a/client/src/features/viewers/timer/MinimalTimer.jsx b/client/src/features/viewers/timer/MinimalTimer.jsx index 30d32a4f1..e7a36d2f4 100644 --- a/client/src/features/viewers/timer/MinimalTimer.jsx +++ b/client/src/features/viewers/timer/MinimalTimer.jsx @@ -18,11 +18,7 @@ export default function MinimalTimer(props) { return (
-
+
{pres.text}
@@ -30,7 +26,7 @@ export default function MinimalTimer(props) { style={{ fontSize: `${89 / (clean.length - 1)}vw` }} className={isPlaying ? style.timer : style.timerPaused} > - {time.running < 0 ? `-${timer}` : timer} + {time.isNegative ? `-${timer}` : timer}
); diff --git a/client/src/features/viewers/timer/Timer.jsx b/client/src/features/viewers/timer/Timer.jsx index fbf92b04f..1672394be 100644 --- a/client/src/features/viewers/timer/Timer.jsx +++ b/client/src/features/viewers/timer/Timer.jsx @@ -37,7 +37,11 @@ export default function Timer(props) { // show timer if end message is empty const endMessage = general.endMessage == null || general.endMessage === '' ? ( - + ) : ( general.endMessage ); diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js index 4684ed7ff..72701aa8a 100644 --- a/server/src/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -1,11 +1,6 @@ import { Timer } from './Timer.js'; import { Server } from 'socket.io'; -import { - DAY_TO_MS, - getSelectionByRoll, - replacePlaceholder, - updateRoll, -} from './classUtils.js'; +import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js'; import { OSCIntegration } from './integrations/Osc.js'; import { HTTPIntegration } from './integrations/Http.js'; import { cleanURL } from '../utils/url.js'; @@ -90,10 +85,7 @@ export class EventTimer extends Timer { }); // set recurrent emits - this._interval = setInterval( - () => this.runCycle(), - timerConfig?.refresh || 1000 - ); + this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000); // listen to new connections this._listenToConnections(); @@ -306,10 +298,7 @@ export class EventTimer extends Timer { // _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 - ); + this.sendOsc(this.osc.implemented.eventNumber, this.selectedEventIndex || 0); } // check integrations - http if (h?.onLoad?.enabled) { @@ -330,18 +319,9 @@ export class EventTimer extends Timer { 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 || '' - ); + 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 || ''); } } @@ -474,17 +454,13 @@ export class EventTimer extends Timer { selectedEventId: this.selectedEventId, current: this.current, // safeguard on midnight rollover - _finishAt: - this._finishAt >= this._startedAt - ? this._finishAt - : this._finishAt + DAY_TO_MS, + _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); + const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(u); this.current = updatedTimer; this.secondaryTimer = updatedSecondaryTimer; @@ -560,9 +536,7 @@ export class EventTimer extends Timer { // keep track of connections this._numClients++; this._clientNames[socket.id] = getRandomName(); - const m = `${this._numClients} Clients with new connection: ${ - this._clientNames[socket.id] - }`; + const m = `${this._numClients} Clients with new connection: ${this._clientNames[socket.id]}`; this.info('CLIENT', m); // send state @@ -579,9 +553,7 @@ export class EventTimer extends Timer { /********************************/ socket.on('disconnect', () => { this._numClients--; - const m = `${this._numClients} Clients with disconnection: ${ - this._clientNames[socket.id] - }`; + const m = `${this._numClients} Clients with disconnection: ${this._clientNames[socket.id]}`; delete this._clientNames[socket.id]; this.info('CLIENT', m); }); @@ -807,9 +779,7 @@ export class EventTimer extends Timer { } else if (this.selectedEventId != null) { // handle reload selected // Look for event (order might have changed) - const eventIndex = this._eventlist.findIndex( - (e) => e.id === this.selectedEventId - ); + const eventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId); // Maybe is missing if (eventIndex === -1) { @@ -849,10 +819,7 @@ export class EventTimer extends Timer { if (e.id === this.selectedEventId) { // handle reload selected // Reload data if running - let type = - this.selectedEventId === id && this._startedAt != null - ? 'reload' - : 'load'; + let type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load'; this.loadEvent(this.selectedEventIndex, type); } else if (e.id === this.nextEventId) { // roll needs to recalculate @@ -898,9 +865,7 @@ export class EventTimer extends Timer { } // update selected event index - this.selectedEventIndex = this._eventlist.findIndex( - (e) => e.id === this.selectedEventId - ); + this.selectedEventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId); // reload titles if necessary if (eventId === this.nextEventId || eventId === this.nextPublicEventId) { @@ -1005,10 +970,7 @@ export class EventTimer extends Timer { // iterate backwards to find it for (let i = this.selectedEventIndex; i >= 0; i--) { - if ( - this._eventlist[i].type === 'event' && - this._eventlist[i].isPublic - ) { + if (this._eventlist[i].type === 'event' && this._eventlist[i].isPublic) { this._loadThisTitles(this._eventlist[i], 'now-public'); break; } @@ -1221,15 +1183,8 @@ export class EventTimer extends Timer { this._resetSelection(); } - const { - nowIndex, - nowId, - publicIndex, - nextIndex, - publicNextIndex, - timers, - timeToNext, - } = getSelectionByRoll(this._eventlist, now); + const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } = + getSelectionByRoll(this._eventlist, now); // nothing to play, unload if (nowIndex === null && nextIndex === null) { @@ -1334,8 +1289,7 @@ export class EventTimer extends Timer { // change playstate this.pause(); - const gotoEvent = - this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0; + const gotoEvent = this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0; if (gotoEvent === this.selectedEventIndex) return; this.loadEvent(gotoEvent); diff --git a/server/src/classes/Timer.js b/server/src/classes/Timer.js index fd1d61d50..f8ee8f5b7 100644 --- a/server/src/classes/Timer.js +++ b/server/src/classes/Timer.js @@ -50,8 +50,7 @@ export class Timer { if (this._startedAt == null) this._startedAt = now; // update current timer - this.current = - this._startedAt + this.duration + this._pausedTotal - now; + this.current = this._startedAt + this.duration + this._pausedTotal - now; // enable flag checkFinish = true; @@ -93,8 +92,8 @@ export class Timer { // helpers static toSeconds(millis) { - if (millis == null) return null; - return Math.ceil(millis * 0.001); + if (millis == null) return 0; + return millis < 0 ? Math.ceil(millis * 0.001) : Math.floor(millis * 0.001); } // get current time in epoc @@ -151,6 +150,7 @@ export class Timer { getTimeObject() { return { clock: this.clock, + isNegative: this.current < 0, running: Timer.toSeconds(this.current), secondary: Timer.toSeconds(this.secondaryTimer), durationSeconds: Timer.toSeconds(this.duration), diff --git a/server/src/classes/__tests__/timer.test.js b/server/src/classes/__tests__/timer.test.js index 7d27f817c..7ff7ade44 100644 --- a/server/src/classes/__tests__/timer.test.js +++ b/server/src/classes/__tests__/timer.test.js @@ -1,4 +1,4 @@ -import {Timer} from "../Timer"; +import { Timer } from '../Timer'; test('object instantiates correctly', () => { const t = new Timer(); @@ -21,15 +21,15 @@ test('object instantiates correctly', () => { test('convert between mills and seconds correctly', () => { expect(Timer.toSeconds(10000)).toBe(10); - expect(Timer.toSeconds(9016)).toBe(10); - expect(Timer.toSeconds(8016)).toBe(9); - expect(Timer.toSeconds(7010)).toBe(8); - expect(Timer.toSeconds(6006)).toBe(7); - expect(Timer.toSeconds(4999)).toBe(5); - expect(Timer.toSeconds(2995)).toBe(3); - expect(Timer.toSeconds(1991)).toBe(2); - expect(Timer.toSeconds(992)).toBe(1); - expect(Timer.toSeconds(127)).toBe(1); + 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); @@ -41,4 +41,14 @@ test('convert between mills and seconds correctly', () => { expect(Timer.toSeconds(-7010)).toBe(-7); expect(Timer.toSeconds(-8016)).toBe(-8); expect(Timer.toSeconds(-10000)).toBe(-10); -}); \ No newline at end of file +}); + +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); +}); diff --git a/server/utils/__tests__/time.tests.js b/server/utils/__tests__/time.tests.js index 38b5735de..5f11bb933 100644 --- a/server/utils/__tests__/time.tests.js +++ b/server/utils/__tests__/time.tests.js @@ -57,6 +57,17 @@ describe('test string to millis function', () => { }); }); +describe('test stringFromMillis handles partial secs', () => { + it('test with 1795829', () => { + const t = { val: 1795829, result: '00:29:55' }; + expect(stringFromMillis(t.val)).toBe(t.result); + }); + it('test with 1797482', () => { + const t = { val: 1797482, result: '00:29:57' }; + expect(stringFromMillis(t.val)).toBe(t.result); + }); +}); + describe('test excel date parser', () => { it('handles an invalid date string', () => { const s = 'hello';