Compare commits

..

3 Commits

Author SHA1 Message Date
Carlos Valente c6eccec30e refactor(settings): show new app indicator 2026-08-09 16:48:20 +02:00
Carlos Valente 5220c2c374 fix(settings): prevent loader overflow 2026-08-09 16:48:20 +02:00
Carlos Valente 4eeeb294f7 chore: update electron navigation 2026-08-09 16:48:20 +02:00
14 changed files with 94 additions and 270 deletions
@@ -200,8 +200,7 @@ $card-padding: 2rem;
.overlay {
position: absolute;
z-index: $zindex-backdrop;
width: 100%;
height: 100%;
inset: 0;
backdrop-filter: blur(2px);
display: grid;
place-content: center;
@@ -0,0 +1,7 @@
.updateIndicator {
width: 0.5em;
height: 0.5em;
flex: 0 0 auto;
border-radius: 99px;
background-color: $red-400;
}
@@ -3,6 +3,8 @@ import useAppVersion from '../../../../common/hooks-query/useAppVersion';
import { appVersion, isOntimeCloud, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './AppVersion.module.scss';
export default function AppVersion() {
const { data, isError } = useAppVersion();
@@ -18,7 +20,12 @@ export default function AppVersion() {
return (
<Panel.ListItem>
<Panel.Field
title={`Ontime ${appVersion}`}
title={
<>
<span className={style.updateIndicator} aria-hidden='true' />
{`Ontime ${appVersion}`}
</>
}
description={
isOntimeCloud
? `Version ${data.version} is available. Restart your stage to update.`
@@ -26,7 +33,7 @@ export default function AppVersion() {
}
/>
{!isOntimeCloud && (
<ExternalLink href={websiteUrl}>Visit Ontime's page to download the latest version.</ExternalLink>
<ExternalLink href={websiteUrl}>Download the latest version from Ontime's page</ExternalLink>
)}
</Panel.ListItem>
);
@@ -85,10 +85,10 @@ export default function ServerPortSettings() {
</Button>
</Panel.InlineElements>
</Panel.SubHeader>
<Panel.Loader isLoading={status === 'pending'} />
{rootError && <Panel.Error>{rootError}</Panel.Error>}
<Panel.Divider />
<Panel.Section>
<Panel.Loader isLoading={status === 'pending'} />
{data.pendingRestart && (
<Info type='warning'>A port change is pending and will happen on the next restart.</Info>
)}
+13 -1
View File
@@ -100,7 +100,7 @@ function makeFileMenu(askToQuit, serverUrl, redirectWindow, showDialog, download
submenu: [
{
label: 'New project...',
click: () => redirectWindow('/editor?settings=project__manage&new=true'),
click: () => redirectWindow('/editor?settings=project__create'),
},
{
label: 'Load...',
@@ -202,6 +202,18 @@ function makeSettingsMenu(redirectWindow) {
label: 'View settings',
click: () => redirectWindow('/editor?settings=settings__view'),
},
{
label: 'Custom views',
click: () => redirectWindow('/editor?settings=settings__custom-views'),
},
{
label: 'MCP Server',
click: () => redirectWindow('/editor?settings=settings__mcp'),
},
{
label: 'Server port',
click: () => redirectWindow('/editor?settings=settings__port'),
},
],
},
{
@@ -1,4 +1,4 @@
import { EndAction, Instant, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
import type { RuntimeState } from '../../stores/runtimeState.js';
@@ -16,13 +16,6 @@ import {
const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeState['clock'];
/**
* Instants are epoch based, we anchor them to a fixed day to keep the tests deterministic
*/
const midnight = new Date(2026, 0, 1).getTime();
const instantToday = (timeOfDay: number): Instant => (midnight + timeOfDay) as Instant;
const instantYesterday = (timeOfDay: number): Instant => (midnight - dayInMs + timeOfDay) as Instant;
describe('getElapsed()', () => {
it('returns active elapsed time from startedAt without add-time adjustments', () => {
const state = {
@@ -60,12 +53,11 @@ describe('getElapsed()', () => {
it('uses the current pause start while paused', () => {
const state = {
clock: 10 * MILLIS_PER_MINUTE,
_now: instantToday(10 * MILLIS_PER_MINUTE),
timer: {
startedAt: 2 * MILLIS_PER_MINUTE,
},
_timer: {
pausedAt: instantToday(7 * MILLIS_PER_MINUTE),
pausedAt: 7 * MILLIS_PER_MINUTE,
pausedDuration: 1 * MILLIS_PER_MINUTE,
},
} as RuntimeState;
@@ -152,28 +144,6 @@ describe('getExpectedFinish()', () => {
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(31);
});
it('adds the time of an ongoing pause which spans midnight', () => {
const state = {
eventNow: {
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
timerType: TimerType.CountDown,
},
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
_now: instantToday(3 * MILLIS_PER_MINUTE),
timer: {
addedTime: 0,
duration: 2 * MILLIS_PER_HOUR,
startedAt: 23 * MILLIS_PER_HOUR, // 23:00
},
_timer: {
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
hasFinished: false,
},
} as RuntimeState;
// the 5 minute pause pushes the finish from 01:00 to 01:05
expect(getExpectedFinish(state)).toBe(1 * MILLIS_PER_HOUR + 5 * MILLIS_PER_MINUTE);
});
it('added time could be negative', () => {
const state = {
eventNow: {
@@ -430,51 +400,6 @@ describe('getCurrent()', () => {
expect(current).toBe(35);
});
it('is frozen while paused', () => {
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
clock: 5,
_now: instantToday(5),
timer: {
addedTime: 0,
duration: 10,
startedAt: 0,
},
_timer: {
pausedAt: instantToday(2), // we have been paused for 3ms (see clock)
hasFinished: false,
},
} as RuntimeState;
// the timer holds the value it had when we paused
expect(getCurrent(state)).toBe(8);
});
it('is frozen while paused, even if the pause spans midnight', () => {
const state = {
eventNow: {
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
timerType: TimerType.CountDown,
},
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
_now: instantToday(3 * MILLIS_PER_MINUTE),
timer: {
addedTime: 0,
duration: 2 * MILLIS_PER_HOUR,
startedAt: 23 * MILLIS_PER_HOUR, // 23:00
},
_timer: {
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
hasFinished: false,
},
} as RuntimeState;
// we ran for 58 minutes before pausing, so the timer holds at 1h02
expect(getCurrent(state)).toBe(1 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE);
});
describe('on timers of type count-to-end', () => {
it('current time is the time to end even if it hasnt started, this is weird, but by design', () => {
const state = {
@@ -1030,14 +955,13 @@ describe('getRuntimeOffset()', () => {
dayOffset: 0,
},
clock: 150,
_now: instantToday(150),
timer: {
startedAt: 100, // started on time
current: 25, // are 25ms into it
addedTime: 0,
},
_timer: {
pausedAt: instantToday(125), // we have been paused for 25ms (see clock)
pausedAt: 125, // we have been paused for 25ms (see clock)
},
rundown: {
actualStart: 100,
@@ -1051,41 +975,6 @@ describe('getRuntimeOffset()', () => {
expect(absolute).toBe(25);
});
it('paused time is delayed time when the pause spans midnight', () => {
const state = {
eventNow: {
id: '1',
timeStart: 23 * MILLIS_PER_HOUR, // 23:00
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
dayOffset: 0,
},
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
_now: instantToday(3 * MILLIS_PER_MINUTE),
timer: {
startedAt: 23 * MILLIS_PER_HOUR, // started on time at 23:00
current: 25, // still counting down
addedTime: 0,
},
_timer: {
pausedAt: instantYesterday(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE), // 23:58, before midnight
pausedDuration: 0,
},
rundown: {
actualStart: 23 * MILLIS_PER_HOUR,
plannedStart: 23 * MILLIS_PER_HOUR,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
// paused from 23:58 to 00:03 -> 5 minutes, regardless of the midnight wrap
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(5 * MILLIS_PER_MINUTE);
// and the pause is not active time, elapsed is still the 58 minutes we ran before pausing
expect(getElapsed(state)).toBe(58 * MILLIS_PER_MINUTE);
});
it('offset doesnt exist if we havent started', () => {
const state = {
clock: 78480789,
@@ -13,7 +13,7 @@ describe('isRestorePoint()', () => {
selectedEventId: '123',
startedAt: 1,
addedTime: 2,
pausedAt: asInstant(1754745600000),
pausedAt: 3,
firstStart: 1,
startEpoch: asInstant(1),
currentDay: 0,
@@ -64,22 +64,6 @@ describe('isRestorePoint()', () => {
expect(isRestorePoint({ ...restorePoint, pausedDuration: '3000' })).toBe(false);
});
it('rejects a pausedAt which is not an instant', () => {
const restorePoint = {
playback: Playback.Pause,
selectedEventId: '123',
startedAt: 1,
addedTime: 2,
// restore points from before pausedAt became an instant contain a time of day
pausedAt: 3,
firstStart: 1,
startEpoch: 1,
currentDay: 0,
};
expect(isRestorePoint(restorePoint)).toBe(false);
});
describe('rejects a badly formatted file', () => {
it('with invalid playback value', () => {
const restorePoint = {
@@ -16,7 +16,7 @@ describe('restoreService', () => {
selectedEventId: 'da5b4',
startedAt: 1234,
addedTime: 5678,
pausedAt: asInstant(1754745609087),
pausedAt: 9087,
firstStart: 1234,
startEpoch: asInstant(1234),
currentDay: 0,
@@ -55,7 +55,7 @@ describe('restoreService', () => {
selectedEventId: 'da5b4',
startedAt: 1234,
addedTime: 1234,
pausedAt: asInstant(1754745601234),
pausedAt: 1234,
groupStartAt: 10,
};
@@ -83,7 +83,7 @@ describe('restoreService', () => {
selectedEventId: '1234',
startedAt: 1234,
addedTime: 1234,
pausedAt: asInstant(1754745601234),
pausedAt: 1234,
firstStart: 1234,
startEpoch: asInstant(1234),
currentDay: 0,
@@ -101,7 +101,7 @@ describe('restoreService', () => {
selectedEventId: '2345',
startedAt: 2345,
addedTime: 2345,
pausedAt: asInstant(1754745602345),
pausedAt: 2345,
firstStart: 2345,
startEpoch: asInstant(2345),
currentDay: 0,
@@ -112,7 +112,7 @@ describe('restoreService', () => {
selectedEventId: '3456',
startedAt: 3456,
addedTime: 3456,
pausedAt: asInstant(1754745603456),
pausedAt: 3456,
firstStart: 3456,
startEpoch: asInstant(3456),
currentDay: 1,
@@ -134,7 +134,7 @@ describe('restoreService', () => {
selectedEventId: '5678',
startedAt: 5678,
addedTime: 5678,
pausedAt: asInstant(1754745605678),
pausedAt: 5678,
firstStart: 5678,
startEpoch: asInstant(5678),
currentDay: 0,
@@ -1,5 +1,4 @@
import { Playback } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { is } from '../../utils/is.js';
import type { RestorePoint } from './restore.type.js';
@@ -43,9 +42,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false;
}
// pausedAt is an instant, restore points made before this change contained a time of day
// we reject those to avoid resuming with a corrupt pause duration
if (restorePoint.pausedAt !== null && (!is.number(restorePoint.pausedAt) || restorePoint.pausedAt < dayInMs)) {
if (!is.number(restorePoint.pausedAt) && restorePoint.pausedAt !== null) {
return false;
}
@@ -5,8 +5,7 @@ export type RestorePoint = {
selectedEventId: MaybeString;
startedAt: MaybeNumber;
addedTime: number;
/** instant the playback was paused at */
pausedAt: Maybe<Instant>;
pausedAt: MaybeNumber;
pausedDuration?: number;
firstStart: MaybeNumber;
startEpoch: Maybe<Instant>;
+30 -34
View File
@@ -1,7 +1,6 @@
import { Day, Duration, Maybe, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
import * as timeCore from '../lib/time-core/timeCore.js';
import type { RuntimeState } from '../stores/runtimeState.js';
/**
@@ -32,12 +31,14 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
}
const { countToEnd, timeEnd } = state.eventNow;
const { pausedAt } = state._timer;
const { clock } = state;
if (startedAt === null) {
return null;
}
const pausedTime = getOngoingPauseDuration(state);
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
if (countToEnd) {
return timeEnd + addedTime + pausedTime;
@@ -56,11 +57,11 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
/**
* Calculates running countdown
* The result is a duration (time left in the timer), never a point in time
* @param {RuntimeState} state runtime state
* @returns {Duration} time remaining in the running timer
* @returns {number} current time for timer
*/
export function getCurrent(state: RuntimeState): Duration {
export function getCurrent(state: RuntimeState): number {
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
if (state.eventNow === null || state.timer.duration === null) {
@@ -69,58 +70,53 @@ export function getCurrent(state: RuntimeState): Duration {
}
const { startedAt, duration, addedTime } = state.timer;
const { countToEnd, timeStart, timeEnd } = state.eventNow;
const { pausedAt } = state._timer;
const { clock } = state;
if (countToEnd) {
const isEventOverMidnight = timeStart > timeEnd;
const correctDay = isEventOverMidnight ? dayInMs : 0;
return (correctDay - clock + timeEnd + addedTime) as Duration;
return correctDay - clock + timeEnd + addedTime;
}
if (startedAt === null) {
return duration as Duration;
return duration;
}
// an ongoing pause freezes the timer, so we discount the time spent in it
const pausedTime = getOngoingPauseDuration(state);
const elapsedSinceStart = timeCore.elapsedTime(clock, startedAt as TimeOfDay);
if (pausedAt != null) {
return startedAt + duration + addedTime - pausedAt;
}
return (duration + addedTime - elapsedSinceStart + pausedTime) as Duration;
const hasPassedMidnight = startedAt > clock;
const correctDay = hasPassedMidnight ? dayInMs : 0;
return startedAt + duration + addedTime - clock - correctDay;
}
/**
* Calculates active time elapsed since the timer started
* Time spent paused is not active time, so it is discounted
* Calculates active time elapsed since the timer started.
*/
export function getElapsed(state: RuntimeState): Maybe<Duration> {
export function getElapsed(state: RuntimeState): MaybeNumber {
const { clock } = state;
const { startedAt } = state.timer;
const { pausedDuration } = state._timer;
const { pausedAt, pausedDuration } = state._timer;
if (startedAt === null) {
return null;
}
const elapsedSinceStart = timeCore.elapsedTime(clock, startedAt as TimeOfDay);
const activeElapsed = elapsedSinceStart - pausedDuration - getOngoingPauseDuration(state);
const referenceClock = pausedAt ?? clock;
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
const activeElapsed = elapsedSinceStart - pausedDuration;
return Math.max(0, activeElapsed) as Duration;
return Math.max(0, activeElapsed);
}
/**
* Calculates how long the current pause has been going on for
* The pause is tracked as an instant, which makes the calculation
* immune to the clock wrapping around midnight
* @returns 0 if the playback is not paused
*/
function getOngoingPauseDuration(state: RuntimeState): Duration {
const { pausedAt } = state._timer;
if (pausedAt == null) {
return 0 as Duration;
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
if (clock < startedAt) {
return clock + dayInMs - startedAt;
}
return timeCore.timeSince(state._now, pausedAt);
return clock - startedAt;
}
/**
@@ -156,7 +152,7 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
* Negative offset is under time / ahead of schedule
*/
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
const { eventNow, _startDayOffset } = state;
const { eventNow, clock, _startDayOffset } = state;
const { addedTime, current, startedAt } = state.timer;
// nothing to calculate if there are no loaded events or if we havent started
if (eventNow === null || startedAt === null || _startDayOffset === null) {
@@ -180,8 +176,8 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
const overtime = Math.abs(Math.min(current, 0));
// time the playback was paused, the difference from now to when we paused is added to the offset
const pausedTime = getOngoingPauseDuration(state);
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
// absolute offset is difference between schedule and playback time
// in case of count to end, the absolute offset is overtime and added time
@@ -1,11 +1,10 @@
import { Instant, OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
import { deepmerge } from 'ontime-utils';
import type { RuntimeState } from '../runtimeState.js';
const baseState: RuntimeState = {
clock: 0 as TimeOfDay,
_now: 0 as Instant,
eventNow: null,
eventNext: null,
eventFlag: null,
@@ -135,7 +135,7 @@ describe('mutation on runtimeState', () => {
playback: Playback.Pause,
addedTime: 0,
});
expect(newState._timer.pausedAt).toEqual(newState._now);
expect(newState._timer.pausedAt).toEqual(newState.clock);
success = pause();
expect(success).toBe(false);
@@ -248,63 +248,6 @@ describe('mutation on runtimeState', () => {
state = getState();
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
});
test('elapsed excludes a pause that spans midnight', async () => {
clearState();
// an event that runs over midnight (23:00 -> 01:00)
const event = {
...mockEvent,
id: 'elapsed-pause-midnight',
timeStart: 23 * MILLIS_PER_HOUR,
timeEnd: 1 * MILLIS_PER_HOUR,
duration: 2 * MILLIS_PER_HOUR,
};
const mockRundown = makeRundown({
entries: { [event.id]: event },
order: [event.id],
});
await initRundown(mockRundown, {});
vi.runAllTimers();
const { metadata, rundown } = rundownCache.get();
// start before midnight
vi.setSystemTime('jan 1 23:50');
load(event, rundown, metadata);
start();
// 8 minutes of active running before we pause
vi.setSystemTime('jan 1 23:58');
update();
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
pause();
// elapsed is active time since start, so it must not advance while paused,
// not even when the pause itself crosses midnight
vi.setSystemTime('jan 2 00:01');
update();
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
// the timer is frozen at the value it had when we paused
expect(getState().timer.current).toBe(2 * MILLIS_PER_HOUR - 8 * MILLIS_PER_MINUTE);
// we started 50 minutes late and have been paused for 3 minutes
expect(getState().offset.absolute).toBe(53 * MILLIS_PER_MINUTE);
// resume 5 minutes after pausing, having crossed midnight (23:58 -> 00:03)
vi.setSystemTime('jan 2 00:03');
start();
let state = getState();
// the accumulated pause count is 5 minutes, regardless of the midnight wrap
expect(state._timer.pausedDuration).toBe(5 * MILLIS_PER_MINUTE);
// and elapsed still reflects only the 8 active minutes
expect(state.timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
// 2 more active minutes after resume -> 10 minutes elapsed
vi.setSystemTime('jan 2 00:05');
update();
state = getState();
expect(state.timer.elapsed).toBe(10 * MILLIS_PER_MINUTE);
});
});
test('runtime offset', async () => {
+19 -27
View File
@@ -63,11 +63,7 @@ export type RuntimeState = {
// private properties of the timer calculations
_timer: {
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
/** Instant the playback was paused at, an instant so that pause durations survive midnight */
pausedAt: Maybe<Instant>;
/** Accumulated duration of past pauses, does not include an ongoing pause */
pausedAt: Maybe<TimeOfDay>;
pausedDuration: number;
secondaryTarget: Maybe<TimeOfDay>;
hasFinished: boolean;
@@ -80,14 +76,10 @@ export type RuntimeState = {
_end: ExpectedMetadata;
_startEpoch: Maybe<Instant>;
_startDayOffset: Maybe<Day>;
/** Current instant, kept in parity with the clock (see setClock) */
_now: Instant;
};
const runtimeState: RuntimeState = {
clock: timeCore.timeOfDayNow(),
_now: timeCore.now(),
groupNow: null,
eventNow: null,
eventNext: null,
@@ -112,12 +104,6 @@ const runtimeState: RuntimeState = {
_startDayOffset: null,
};
/** set the current clock to ensure parity between _now and clock */
function setClock(state: RuntimeState) {
state._now = timeCore.now();
state.clock = timeCore.toTimeOfDay(state._now);
}
export function getState(): Readonly<RuntimeState> {
// create a shallow copy of the state
return {
@@ -150,7 +136,7 @@ export function clearEventData() {
runtimeState.rundown.selectedEventIndex = null;
runtimeState.timer.playback = Playback.Stop;
setClock(runtimeState);
runtimeState.clock = timeCore.timeOfDayNow();
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown
@@ -183,7 +169,7 @@ export function clearState() {
runtimeState._end = null;
runtimeState.timer.playback = Playback.Stop;
setClock(runtimeState);
runtimeState.clock = timeCore.timeOfDayNow();
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown
@@ -436,12 +422,15 @@ export function start(state: RuntimeState = runtimeState): boolean {
return false;
}
setClock(state);
const epoch = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
state.clock = now;
state.timer.secondaryTimer = null;
// add paused time if it exists
if (state._timer.pausedAt) {
const timeToAdd = state._now - state._timer.pausedAt;
const timeToAdd = state.clock - state._timer.pausedAt;
state.timer.addedTime += timeToAdd;
state._timer.pausedDuration += timeToAdd;
state._timer.pausedAt = null;
@@ -458,7 +447,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
if (state.rundown.actualStart === null) {
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
state.rundown.currentDay = state._startDayOffset;
state._startEpoch = state._now;
state._startEpoch = epoch;
state.rundown.actualStart = state.clock;
}
@@ -492,8 +481,8 @@ export function pause(state: RuntimeState = runtimeState): boolean {
}
state.timer.playback = Playback.Pause;
setClock(state);
state._timer.pausedAt = state._now;
state.clock = timeCore.timeOfDayNow();
state._timer.pausedAt = state.clock;
return true;
}
@@ -558,7 +547,9 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
setClock(runtimeState); // we update the clock on every update call
const epoch = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
runtimeState.clock = now; // we update the clock on every update call
// 1. is playback idle?
if (!isPlaybackActive(runtimeState.timer.playback)) {
@@ -567,13 +558,13 @@ export function update(): UpdateResult {
// calculate currentDay from epoch (days elapsed since playback was started)
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
}
// 2. are we waiting to roll?
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, runtimeState.clock);
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
return updateIfWaitingToRoll(clockHasCrossedMidnight);
}
@@ -659,8 +650,9 @@ export function roll(
}
// we will need to do some calculations, update the time first
setClock(runtimeState);
const epoch = runtimeState._now;
const epoch = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
runtimeState.clock = now;
// 2. if there is an event armed, we use it
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {