diff --git a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx index b568d5166..b3e634f00 100644 --- a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx @@ -130,6 +130,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { > + diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index 3ca8e4749..d15abe82f 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useEffect, useState } from 'react'; import { Tooltip } from '@chakra-ui/react'; +import { BiArrowToBottom } from '@react-icons/all-files/bi/BiArrowToBottom'; import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; @@ -196,5 +197,8 @@ function TimerIcon(props: { type: TimerType; className: string }) { if (type === TimerType.Clock) { return ; } + if (type === TimerType.TimeToEnd) { + return ; + } return ; } diff --git a/apps/client/src/features/viewers/common/viewerUtils.ts b/apps/client/src/features/viewers/common/viewerUtils.ts index 637146a70..103ce81e2 100644 --- a/apps/client/src/features/viewers/common/viewerUtils.ts +++ b/apps/client/src/features/viewers/common/viewerUtils.ts @@ -17,7 +17,7 @@ export function getTimerByType(timerObject?: TimerTypeParams): string | number | return timer; } - if (timerObject.timerType === TimerType.CountDown) { + if (timerObject.timerType === TimerType.CountDown || timerObject.timerType === TimerType.TimeToEnd) { timer = timerObject.current; } else if (timerObject.timerType === TimerType.CountUp) { timer = timerObject.elapsed; diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index 2cad60621..7154d3c99 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -1,4 +1,4 @@ -import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types'; +import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState, TimerType } from 'ontime-types'; import { calculateDuration, dayInMs } from 'ontime-utils'; import { eventStore } from '../stores/EventStore.js'; @@ -24,6 +24,9 @@ export class TimerService { timer: TimerState; loadedTimerId: string | null; + private loadedTimerStart: number | null; + private loadedTimerEnd: number | null; + private pausedTime: number; private pausedAt: number | null; private secondaryTarget: number | null; @@ -61,6 +64,9 @@ export class TimerService { endAction: null, }; this.loadedTimerId = null; + this.loadedTimerStart = null; + this.loadedTimerEnd = null; + this.pausedTime = 0; this.pausedAt = null; this.secondaryTarget = null; @@ -93,6 +99,8 @@ export class TimerService { this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd); this.timer.timerType = timer.timerType; this.timer.endAction = timer.endAction; + this.loadedTimerStart = timer.timeStart; + this.loadedTimerEnd = timer.timeEnd; // this might not be ideal this.timer.finishedAt = null; @@ -102,6 +110,8 @@ export class TimerService { this.timer.duration, this.pausedTime, this.timer.addedTime, + this.loadedTimerEnd, + this.timer.timerType, ); if (this.timer.startedAt === null) { this.timer.current = this.timer.duration; @@ -129,14 +139,22 @@ export class TimerService { this._clear(); this.loadedTimerId = timer.id; + this.loadedTimerStart = timer.timeStart; + this.loadedTimerEnd = timer.timeEnd; + this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd); - this.timer.current = this.timer.duration; this.playback = Playback.Armed; this.timer.timerType = timer.timerType; this.timer.endAction = timer.endAction; this.pausedTime = 0; this.pausedAt = 0; + this.timer.current = this.timer.duration; + if (this.timer.timerType === TimerType.TimeToEnd) { + const now = clock.timeNow(); + this.timer.current = getCurrent(now, this.timer.duration, 0, 0, now, timer.timeEnd, this.timer.timerType); + } + if (typeof initialData !== 'undefined') { this.timer = { ...this.timer, ...initialData }; } @@ -188,6 +206,8 @@ export class TimerService { this.timer.duration, this.pausedTime, this.timer.addedTime, + this.loadedTimerEnd, + this.timer.timerType, ); this._onStart(); } @@ -310,6 +330,8 @@ export class TimerService { this.timer.duration, this.pausedTime, this.timer.addedTime, + this.loadedTimerEnd, + this.timer.timerType, ); } this.timer.current = getCurrent( @@ -318,6 +340,8 @@ export class TimerService { this.timer.addedTime, this.pausedTime, this.timer.clock, + this.loadedTimerEnd, + this.timer.timerType, ); this.timer.elapsed = this.timer.duration - this.timer.current; } diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 1d26b9ba3..2a6e15c67 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1,4 +1,5 @@ import { dayInMs } from 'ontime-utils'; +import { TimerType } from 'ontime-types'; import { getCurrent, getExpectedFinish } from '../timerUtils.js'; @@ -9,7 +10,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 0; const addedTime = 0; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 10; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(null); }); it('is finishedAt if defined', () => { @@ -18,7 +29,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 0; const addedTime = 0; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 20; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(finishedAt); }); it('calculates the finish time', () => { @@ -27,7 +48,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 0; const addedTime = 0; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 11; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(11); }); it('adds paused and added times', () => { @@ -36,7 +67,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 10; const addedTime = 10; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 11; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(31); }); it('added time could be negative', () => { @@ -45,7 +86,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 10; const addedTime = -10; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 11; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(11); }); it('user could add enough time for it to be negative', () => { @@ -54,7 +105,17 @@ describe('getExpectedFinish()', () => { const duration = 10; const pausedTime = 0; const addedTime = -100; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 11; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(1); }); it('timer can have no duration', () => { @@ -63,7 +124,17 @@ describe('getExpectedFinish()', () => { const duration = 0; const pausedTime = 0; const addedTime = 0; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 0; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(1); }); it('finish can be the day after', () => { @@ -72,9 +143,60 @@ describe('getExpectedFinish()', () => { const duration = dayInMs; const pausedTime = 0; const addedTime = 0; - const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + const endTime = 10; + const timerType = TimerType.CountDown; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); expect(calculatedFinish).toBe(10); }); + describe('on timers of type time-to-end', () => { + it('finish time is as schedule + added time', () => { + const startedAt = 10; + const finishedAt = null; + const duration = dayInMs; + const pausedTime = 0; + const addedTime = 10; + const endTime = 30; + const timerType = TimerType.TimeToEnd; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); + expect(calculatedFinish).toBe(40); + }); + it('handles events that finish the day after', () => { + const startedAt = 79200000; // 22:00:00 + const finishedAt = null; + const duration = Infinity; // not relevant + const pausedTime = 0; + const addedTime = 0; + const endTime = 600000; // 00:10:00 + const timerType = TimerType.TimeToEnd; + const calculatedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); + // expected finish is not a duration but a timetag + expect(calculatedFinish).toBe(600000); + }); + }); }); describe('getCurrent()', () => { @@ -84,7 +206,9 @@ describe('getCurrent()', () => { const pausedTime = 0; const addedTime = 0; const clock = 0; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 0; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(null); }); it('is the remaining time in clock', () => { @@ -93,7 +217,9 @@ describe('getCurrent()', () => { const pausedTime = 0; const addedTime = 0; const clock = 1; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 10; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(9); }); it('accounts for added times', () => { @@ -102,7 +228,9 @@ describe('getCurrent()', () => { const pausedTime = 5; const addedTime = 5; const clock = 1; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 10; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(19); }); it('counts over midnight', () => { @@ -111,7 +239,9 @@ describe('getCurrent()', () => { const pausedTime = 0; const addedTime = 0; const clock = 10; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 20; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(dayInMs + 10); }); it('rolls over midnight', () => { @@ -120,7 +250,9 @@ describe('getCurrent()', () => { const pausedTime = 0; const addedTime = 0; const clock = 5; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 20; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(15); }); it('midnight holds delays', () => { @@ -129,9 +261,46 @@ describe('getCurrent()', () => { const pausedTime = 10; const addedTime = 10; const clock = 5; - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 20; + const timerType = TimerType.CountDown; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); expect(current).toBe(35); }); + describe('on timers of type time-to-end', () => { + it('current time is the time to end', () => { + const startedAt = 10; + const duration = 100; + const pausedTime = 0; + const addedTime = 0; + const clock = 30; + const endTime = 100; + const timerType = TimerType.TimeToEnd; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); + expect(current).toBe(70); + }); + it('current time is the time to end + added time', () => { + const startedAt = 10; + const duration = 100; + const pausedTime = 3; + const addedTime = 4; + const clock = 30; + const endTime = 100; + const timerType = TimerType.TimeToEnd; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); + expect(current).toBe(77); + }); + it('handles events that finish the day after', () => { + const startedAt = 79200000; // 22:00:00 + const duration = Infinity; // not relevant + const pausedTime = 0; + const addedTime = 0; + const clock = 79500000; // 22:05:00 + const endTime = 600000; // 00:10:00 + const timerType = TimerType.TimeToEnd; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); + expect(current).toBe(600000 + dayInMs - clock); + }); + }); }); describe('getExpectedFinish() and getCurrentTime() combined', () => { @@ -142,8 +311,18 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { const pausedTime = 0; const addedTime = 0; const clock = 0; - const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 10; + const timerType = TimerType.CountDown; + const expectedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); const elapsed = duration - current; expect(expectedFinish).toBe(10); expect(elapsed).toBe(0); @@ -157,8 +336,18 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { const pausedTime = 1; const addedTime = 2; const clock = 5; - const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); - const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + const endTime = 10; + const timerType = TimerType.CountDown; + const expectedFinish = getExpectedFinish( + startedAt, + finishedAt, + duration, + pausedTime, + addedTime, + endTime, + timerType, + ); + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType); const elapsed = duration - current; expect(expectedFinish).toBe(13); expect(elapsed).toBe(2); diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 6ee9993b9..8edad2a7f 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -1,4 +1,4 @@ -import { MaybeNumber } from 'ontime-types'; +import { MaybeNumber, TimerType } from 'ontime-types'; import { dayInMs } from 'ontime-utils'; /** @@ -10,6 +10,8 @@ export function getExpectedFinish( duration: number, pausedTime: number, addedTime: number, + timeEnd: number, + timerType: TimerType, ) { if (startedAt === null) { return null; @@ -19,6 +21,10 @@ export function getExpectedFinish( return finishedAt; } + if (timerType === TimerType.TimeToEnd) { + return timeEnd + addedTime + pausedTime; + } + // handle events that finish the day after const expectedFinish = startedAt + duration + pausedTime + addedTime; if (expectedFinish > dayInMs) { @@ -38,11 +44,22 @@ export function getCurrent( addedTime: number, pausedTime: number, clock: number, + timeEnd: number, + timerType: TimerType, ) { if (startedAt === null) { return null; } + + if (timerType === TimerType.TimeToEnd) { + if (startedAt > timeEnd) { + return timeEnd + addedTime + pausedTime + dayInMs - clock; + } + return timeEnd + addedTime + pausedTime - clock; + } + if (startedAt > clock) { + // we are the day after the event was started return startedAt + duration + addedTime + pausedTime - clock - dayInMs; } return startedAt + duration + addedTime + pausedTime - clock; diff --git a/packages/types/src/definitions/TimerType.type.ts b/packages/types/src/definitions/TimerType.type.ts index 25446c941..eb8022109 100644 --- a/packages/types/src/definitions/TimerType.type.ts +++ b/packages/types/src/definitions/TimerType.type.ts @@ -1,5 +1,6 @@ export enum TimerType { CountDown = 'count-down', CountUp = 'count-up', - Clock = 'clock' + TimeToEnd = 'time-to-end', + Clock = 'clock', }