mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 832457620e | |||
| 9feec76858 | |||
| 05e3c8a9f8 | |||
| 08c580f773 | |||
| c33cbbea7d | |||
| 7c642f3832 | |||
| 729c68e9e2 | |||
| 932bb8102c | |||
| ee39bc296f | |||
| 8cda9eac0a | |||
| 140a3310cb |
@@ -1,4 +1,4 @@
|
|||||||
import { RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
||||||
|
|
||||||
import { useRuntimeStore } from '../stores/runtime';
|
import { useRuntimeStore } from '../stores/runtime';
|
||||||
import { socketSendJson } from '../utils/socket';
|
import { socketSendJson } from '../utils/socket';
|
||||||
@@ -160,7 +160,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
|
|||||||
|
|
||||||
numEvents: state.runtime.numEvents,
|
numEvents: state.runtime.numEvents,
|
||||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
offset: state.runtime.offset,
|
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
||||||
|
|
||||||
currentBlock: state.currentBlock,
|
currentBlock: state.currentBlock,
|
||||||
}));
|
}));
|
||||||
@@ -172,8 +172,11 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
|||||||
|
|
||||||
export const useTimeUntilData = createSelector((state: RuntimeStore) => ({
|
export const useTimeUntilData = createSelector((state: RuntimeStore) => ({
|
||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
offset: state.runtime.offset,
|
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
||||||
|
offsetMode: state.runtime.offsetMode,
|
||||||
currentDay: state.eventNow?.dayOffset ?? 0, //The day of the currently running event
|
currentDay: state.eventNow?.dayOffset ?? 0, //The day of the currently running event
|
||||||
|
actualStart: state.runtime.actualStart,
|
||||||
|
plannedStart: state.runtime.plannedStart,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({
|
export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({
|
||||||
@@ -189,6 +192,12 @@ export const useIsOnline = createSelector((state: RuntimeStore) => ({
|
|||||||
isOnline: state.ping > 0,
|
isOnline: state.ping > 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useOffsetMode = createSelector((state: RuntimeStore) => ({
|
||||||
|
offsetMode: state.runtime.offsetMode,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const setOffsetMode = (payload: OffsetMode) => socketSendJson('offsetmode', payload);
|
||||||
|
|
||||||
export const usePlayback = () => {
|
export const usePlayback = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.timer.playback,
|
playback: state.timer.playback,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
import { calculateTimeUntilStart, formatTime, nowInMillis } from '../time';
|
import { calculateTimeUntilStart, formatTime, nowInMillis } from '../time';
|
||||||
|
|
||||||
describe('nowInMillis()', () => {
|
describe('nowInMillis()', () => {
|
||||||
@@ -40,82 +43,265 @@ describe('formatTime()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateTimeUntilStart()', () => {
|
describe('calculateTimeUntilStart()', () => {
|
||||||
test('ontime', () => {
|
describe('Absolute offset mode', () => {
|
||||||
|
test('ontime', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 90,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 90,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running ahead', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 80,
|
||||||
|
offset: 10,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with enough gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 20,
|
||||||
|
clock: 50,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with too little gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
clock: 50,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: 0,
|
||||||
|
plannedStart: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Relative offset mode', () => {
|
||||||
|
test('basic function', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0, // relative
|
||||||
|
// offset: -100, // absolute
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeStartEvent2 = 10;
|
||||||
|
const timeStartEvent3 = 20;
|
||||||
|
|
||||||
|
//event 1 is the currently running event
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||||
|
test.clock = 105;
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0, // relative
|
||||||
|
// offset: -100, // absolute
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||||
|
test.clock = 105;
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added/remove time', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0, // relative
|
||||||
|
// offset: -100, // absolute
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
test.offset = 5; // remove 5 with addtime - we are ahead of time
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
|
||||||
|
|
||||||
|
test.offset = -5; // add 5 with addtime - we are behind
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next day', () => {
|
||||||
|
const test = {
|
||||||
|
// timeStart: 10,
|
||||||
|
// dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
// totalGap: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0, // relative
|
||||||
|
// offset: -100, // absolute
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
// this event will start the current day
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 10,
|
||||||
|
dayOffset: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(10);
|
||||||
|
|
||||||
|
// this event will start the next day
|
||||||
|
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
||||||
|
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 1,
|
||||||
|
totalGap: dayInMs - 20,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(dayInMs);
|
||||||
|
|
||||||
|
// advancing 100ms
|
||||||
|
test.clock = 200;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 1,
|
||||||
|
totalGap: dayInMs - 20,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(dayInMs - 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlap with negative total gap', () => {
|
||||||
const test = {
|
const test = {
|
||||||
timeStart: 100,
|
// timeStart: 20,
|
||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
totalGap: 0,
|
// totalGap: 10,
|
||||||
clock: 90,
|
clock: 100,
|
||||||
offset: 0,
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0, // relative
|
||||||
|
// offset: -100, // absolute
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
// the overlap will be pushed out to the expected available time
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
|
||||||
|
|
||||||
|
test.clock = 105;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('running behind', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 90,
|
|
||||||
offset: -20,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running ahead', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 80,
|
|
||||||
offset: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with enough gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 20,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with to little gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 10,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
|
|
||||||
//TODO: more indepth testing,
|
|
||||||
// including day offset handling
|
|
||||||
// and more?
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
import { MaybeNumber, OffsetMode, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
||||||
import { dayInMs, formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
import { dayInMs, formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||||
|
|
||||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||||
@@ -140,8 +140,8 @@ export function useTimeUntilStart(
|
|||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
},
|
},
|
||||||
): number {
|
): number {
|
||||||
const { offset, clock, currentDay } = useTimeUntilData();
|
const { offset, clock, currentDay, offsetMode, actualStart, plannedStart } = useTimeUntilData();
|
||||||
return calculateTimeUntilStart({ ...data, currentDay, clock, offset });
|
return calculateTimeUntilStart({ ...data, currentDay, clock, offset, offsetMode, actualStart, plannedStart });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,9 +160,24 @@ export function calculateTimeUntilStart(
|
|||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
clock: number;
|
clock: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
offsetMode: OffsetMode;
|
||||||
|
actualStart: MaybeNumber;
|
||||||
|
plannedStart: MaybeNumber;
|
||||||
},
|
},
|
||||||
): number {
|
): number {
|
||||||
const { timeStart, dayOffset, currentDay, totalGap, isLinkedToLoaded, clock, offset, delay } = data;
|
const {
|
||||||
|
timeStart,
|
||||||
|
dayOffset,
|
||||||
|
currentDay,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
clock,
|
||||||
|
offset,
|
||||||
|
delay,
|
||||||
|
offsetMode,
|
||||||
|
actualStart,
|
||||||
|
plannedStart,
|
||||||
|
} = data;
|
||||||
|
|
||||||
//How many days from the currently running event to this one
|
//How many days from the currently running event to this one
|
||||||
const relativeDayOffset = dayOffset - currentDay;
|
const relativeDayOffset = dayOffset - currentDay;
|
||||||
@@ -172,20 +187,23 @@ export function calculateTimeUntilStart(
|
|||||||
//The normalised start time of this event relative to the currently running event
|
//The normalised start time of this event relative to the currently running event
|
||||||
const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs;
|
const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs;
|
||||||
|
|
||||||
const offsetTimestart = normalisedTimeStart - offset;
|
let relativeStartOffset = 0;
|
||||||
const offsetTimeUntil = offsetTimestart - clock;
|
|
||||||
|
if (offsetMode === OffsetMode.Relative) {
|
||||||
|
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
|
||||||
|
|
||||||
|
const offsetTimeUntil = scheduledTimeUntil - offset;
|
||||||
|
|
||||||
if (isLinkedToLoaded) {
|
if (isLinkedToLoaded) {
|
||||||
//if we are directly linked back to the loaded event we just follow the offset
|
//if we are directly linked back to the loaded event we just follow the offset
|
||||||
return offsetTimeUntil;
|
return offsetTimeUntil;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledTimeUntil = normalisedTimeStart - clock;
|
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
||||||
|
if (gapsCanCompensateForOffset) {
|
||||||
const isAheadOfSchedule = offset >= 0;
|
|
||||||
const gapsCanCompensadeForOffset = totalGap + offset >= 0;
|
|
||||||
|
|
||||||
if (isAheadOfSchedule || gapsCanCompensadeForOffset) {
|
|
||||||
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
||||||
return scheduledTimeUntil;
|
return scheduledTimeUntil;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ function RuntimeOverview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} />
|
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} testId='offset' />
|
||||||
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ interface TimeLayoutProps {
|
|||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
daySpan?: number;
|
daySpan?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimeColumn({ label, value, muted, className }: TimeLayoutProps) {
|
export function TimeColumn({ label, value, muted, className, testId }: TimeLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<span className={style.label}>{label}</span>
|
<span className={style.label}>{label}</span>
|
||||||
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
|
<span className={cx([style.clock, muted && style.muted, className])} data-testid={testId}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Button, ButtonGroup } from '@chakra-ui/react';
|
import { Button, ButtonGroup } from '@chakra-ui/react';
|
||||||
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
|
||||||
|
|
||||||
import RundownMenu from './RundownMenu';
|
import RundownMenu from './RundownMenu';
|
||||||
@@ -12,6 +14,8 @@ export default function RundownHeader() {
|
|||||||
const setRunMode = () => setAppMode(AppMode.Run);
|
const setRunMode = () => setAppMode(AppMode.Run);
|
||||||
const setEditMode = () => setAppMode(AppMode.Edit);
|
const setEditMode = () => setAppMode(AppMode.Edit);
|
||||||
|
|
||||||
|
const { offsetMode } = useOffsetMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.header}>
|
<div className={style.header}>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
@@ -22,6 +26,22 @@ export default function RundownHeader() {
|
|||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant={offsetMode === OffsetMode.Absolute ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
|
onClick={() => setOffsetMode(OffsetMode.Absolute)}
|
||||||
|
>
|
||||||
|
Absolute
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant={offsetMode === OffsetMode.Relative ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
|
onClick={() => setOffsetMode(OffsetMode.Relative)}
|
||||||
|
>
|
||||||
|
Relative
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
<RundownMenu />
|
<RundownMenu />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MessageState, OntimeEvent, SimpleDirection, SimplePlayback } from 'ontime-types';
|
import { MessageState, OffsetMode, OntimeEvent, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||||
|
|
||||||
import { DeepPartial } from 'ts-essentials';
|
import { DeepPartial } from 'ts-essentials';
|
||||||
@@ -17,6 +17,7 @@ import { throttle } from '../utils/throttle.js';
|
|||||||
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
||||||
|
|
||||||
import { handleLegacyMessageConversion } from './integration.legacy.js';
|
import { handleLegacyMessageConversion } from './integration.legacy.js';
|
||||||
|
import { coerceEnum } from '../utils/coerceType.js';
|
||||||
|
|
||||||
const throttledUpdateEvent = throttle(updateEvent, 20);
|
const throttledUpdateEvent = throttle(updateEvent, 20);
|
||||||
let lastRequest: Date | null = null;
|
let lastRequest: Date | null = null;
|
||||||
@@ -286,6 +287,11 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
|
|
||||||
throw new Error('No matching method provided');
|
throw new Error('No matching method provided');
|
||||||
},
|
},
|
||||||
|
offsetmode: (payload) => {
|
||||||
|
const mode = coerceEnum<OffsetMode>(payload, OffsetMode);
|
||||||
|
runtimeService.setOffsetMode(mode);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime
|
|||||||
import {
|
import {
|
||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
|
getRelativeOffset,
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
getTimerPhase,
|
getTimerPhase,
|
||||||
normaliseEndTime,
|
normaliseEndTime,
|
||||||
@@ -1046,6 +1047,84 @@ describe('getRuntimeOffset()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getRelativeOffset()', () => {
|
||||||
|
it('relative offset is 0 when starting at the planed time', () => {
|
||||||
|
const state = {
|
||||||
|
eventNow: {
|
||||||
|
id: '1',
|
||||||
|
timeStart: 150,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
startedAt: 150,
|
||||||
|
addedTime: 0,
|
||||||
|
current: 0,
|
||||||
|
},
|
||||||
|
_timer: {
|
||||||
|
pausedAt: null,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 150,
|
||||||
|
plannedStart: 150,
|
||||||
|
},
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
state.runtime.offset = getRuntimeOffset(state);
|
||||||
|
expect(state.runtime.offset).toBe(0);
|
||||||
|
const relativeOffsetoffset = getRelativeOffset(state);
|
||||||
|
expect(relativeOffsetoffset).toBe(0);
|
||||||
|
});
|
||||||
|
it('relative offset is 0 when starting after the planed time', () => {
|
||||||
|
const state = {
|
||||||
|
eventNow: {
|
||||||
|
id: '1',
|
||||||
|
timeStart: 100,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
startedAt: 150,
|
||||||
|
addedTime: 0,
|
||||||
|
current: 0,
|
||||||
|
},
|
||||||
|
_timer: {
|
||||||
|
pausedAt: null,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 150,
|
||||||
|
plannedStart: 100,
|
||||||
|
},
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
state.runtime.offset = getRuntimeOffset(state);
|
||||||
|
expect(state.runtime.offset).toBe(-50);
|
||||||
|
const relativeOffsetoffset = getRelativeOffset(state);
|
||||||
|
expect(relativeOffsetoffset).toBe(0);
|
||||||
|
});
|
||||||
|
it('relative offset is 0 when starting before the planed time', () => {
|
||||||
|
const state = {
|
||||||
|
eventNow: {
|
||||||
|
id: '1',
|
||||||
|
timeStart: 150,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
startedAt: 100,
|
||||||
|
addedTime: 0,
|
||||||
|
current: 0,
|
||||||
|
},
|
||||||
|
_timer: {
|
||||||
|
pausedAt: null,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 150,
|
||||||
|
},
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
state.runtime.offset = getRuntimeOffset(state);
|
||||||
|
expect(state.runtime.offset).toBe(50);
|
||||||
|
const relativeOffsetoffset = getRelativeOffset(state);
|
||||||
|
expect(relativeOffsetoffset).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getTimerPhase()', () => {
|
describe('getTimerPhase()', () => {
|
||||||
it('should be None if the timer is not running', () => {
|
it('should be None if the timer is not running', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -1169,9 +1248,11 @@ describe('getTimerPhase()', () => {
|
|||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: null,
|
forceFinish: null,
|
||||||
totalDelay: 0,
|
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
|
_rundown: {
|
||||||
|
totalDelay: 0,
|
||||||
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
@@ -1208,9 +1289,11 @@ describe('getTimerPhase()', () => {
|
|||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: null,
|
forceFinish: null,
|
||||||
totalDelay: 0,
|
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
|
_rundown: {
|
||||||
|
totalDelay: 0,
|
||||||
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
LogOrigin,
|
LogOrigin,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
|
OffsetMode,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
Playback,
|
Playback,
|
||||||
TimerLifeCycle,
|
TimerLifeCycle,
|
||||||
@@ -68,6 +69,11 @@ class RuntimeService {
|
|||||||
RuntimeService.previousState = {} as RuntimeState;
|
RuntimeService.previousState = {} as RuntimeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@broadcastResult
|
||||||
|
setOffsetMode(mode: OffsetMode) {
|
||||||
|
runtimeState.setOffsetMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks result of an update and notifies integrations as needed
|
* Checks result of an update and notifies integrations as needed
|
||||||
* This is the only exception of a private method that has broadcast result
|
* This is the only exception of a private method that has broadcast result
|
||||||
@@ -697,11 +703,14 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
// for the very fist run there will be nothing in the previousState so we force an update
|
// for the very fist run there will be nothing in the previousState so we force an update
|
||||||
const justStarted = !RuntimeService.previousState?.timer;
|
const justStarted = !RuntimeService.previousState?.timer;
|
||||||
|
|
||||||
|
// offset mode has been changed
|
||||||
|
const offsetModeChanged = RuntimeService.previousState?.runtime?.offsetMode !== state.runtime.offsetMode;
|
||||||
|
|
||||||
// if playback changes most things should update
|
// if playback changes most things should update
|
||||||
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
|
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
|
||||||
|
|
||||||
// combine all big changes
|
// combine all big changes
|
||||||
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback;
|
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback || offsetModeChanged;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timer should be updated if
|
* Timer should be updated if
|
||||||
|
|||||||
@@ -156,6 +156,23 @@ export function getRuntimeOffset(state: RuntimeState): number {
|
|||||||
return startOffset - addedTime - pausedTime + overtime;
|
return startOffset - addedTime - pausedTime + overtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates relative offset
|
||||||
|
* should always be calculated after the absolute offset
|
||||||
|
*/
|
||||||
|
export function getRelativeOffset(state: RuntimeState): number {
|
||||||
|
const { actualStart, plannedStart, offset } = state.runtime;
|
||||||
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
|
DEV: {
|
||||||
|
// we know actualStart and plannedStart exists as long as a timer is running
|
||||||
|
if (actualStart === null || plannedStart === null) {
|
||||||
|
throw new Error('timerUtils.calculate: actualStart and plannedStart must be set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const relativeStartOffset = actualStart - plannedStart;
|
||||||
|
return offset + relativeStartOffset;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the expected end of the rundown
|
* Calculates the expected end of the rundown
|
||||||
*/
|
*/
|
||||||
@@ -164,7 +181,7 @@ export function getExpectedEnd(state: RuntimeState): MaybeNumber {
|
|||||||
if (state.runtime.actualStart === null || state.runtime.plannedEnd === null) {
|
if (state.runtime.actualStart === null || state.runtime.plannedEnd === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return state.runtime.plannedEnd - state.runtime.offset + state._timer.totalDelay;
|
return state.runtime.plannedEnd - state.runtime.offset + state._rundown.totalDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TimerPhase, Playback } from 'ontime-types';
|
import { TimerPhase, Playback, OffsetMode } from 'ontime-types';
|
||||||
import { deepmerge } from 'ontime-utils';
|
import { deepmerge } from 'ontime-utils';
|
||||||
import type { RuntimeState } from '../runtimeState.js';
|
import type { RuntimeState } from '../runtimeState.js';
|
||||||
|
|
||||||
@@ -16,10 +16,12 @@ const baseState: RuntimeState = {
|
|||||||
selectedEventIndex: null,
|
selectedEventIndex: null,
|
||||||
numEvents: 0,
|
numEvents: 0,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
relativeOffset: 0,
|
||||||
plannedStart: 0,
|
plannedStart: 0,
|
||||||
plannedEnd: 0,
|
plannedEnd: 0,
|
||||||
actualStart: null,
|
actualStart: null,
|
||||||
expectedEnd: null,
|
expectedEnd: null,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
@@ -35,10 +37,12 @@ const baseState: RuntimeState = {
|
|||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: null,
|
forceFinish: null,
|
||||||
totalDelay: 0,
|
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
},
|
},
|
||||||
|
_rundown: {
|
||||||
|
totalDelay: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { deepmerge } from 'ontime-utils';
|
|||||||
import {
|
import {
|
||||||
type RuntimeState,
|
type RuntimeState,
|
||||||
addTime,
|
addTime,
|
||||||
clear,
|
hardClear,
|
||||||
getState,
|
getState,
|
||||||
load,
|
load,
|
||||||
loadBlock,
|
loadBlock,
|
||||||
@@ -71,7 +71,7 @@ beforeAll(() => {
|
|||||||
|
|
||||||
describe('mutation on runtimeState', () => {
|
describe('mutation on runtimeState', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clear();
|
hardClear();
|
||||||
|
|
||||||
vi.mock('../../services/rundown-service/RundownService.js', async (importOriginal) => {
|
vi.mock('../../services/rundown-service/RundownService.js', async (importOriginal) => {
|
||||||
const actual = (await importOriginal()) as object;
|
const actual = (await importOriginal()) as object;
|
||||||
@@ -246,7 +246,7 @@ describe('roll mode', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime('jan 1 00:00');
|
vi.setSystemTime('jan 1 00:00');
|
||||||
clear();
|
hardClear();
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
|
OffsetMode,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
PlayableEvent,
|
PlayableEvent,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedEnd,
|
getExpectedEnd,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
|
getRelativeOffset,
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
getTimerPhase,
|
getTimerPhase,
|
||||||
} from '../services/timerUtils.js';
|
} from '../services/timerUtils.js';
|
||||||
@@ -45,10 +47,12 @@ export type RuntimeState = {
|
|||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time
|
forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time
|
||||||
totalDelay: number; // this value comes from rundown service
|
|
||||||
pausedAt: MaybeNumber;
|
pausedAt: MaybeNumber;
|
||||||
secondaryTarget: MaybeNumber;
|
secondaryTarget: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
_rundown: {
|
||||||
|
totalDelay: number; // this value comes from rundown service
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: RuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
@@ -62,10 +66,12 @@ const runtimeState: RuntimeState = {
|
|||||||
timer: { ...runtimeStorePlaceholder.timer },
|
timer: { ...runtimeStorePlaceholder.timer },
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: null,
|
forceFinish: null,
|
||||||
totalDelay: 0,
|
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
},
|
},
|
||||||
|
_rundown: {
|
||||||
|
totalDelay: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
@@ -79,10 +85,36 @@ export function getState(): Readonly<RuntimeState> {
|
|||||||
runtime: { ...runtimeState.runtime },
|
runtime: { ...runtimeState.runtime },
|
||||||
timer: { ...runtimeState.timer },
|
timer: { ...runtimeState.timer },
|
||||||
_timer: { ...runtimeState._timer },
|
_timer: { ...runtimeState._timer },
|
||||||
|
_rundown: { totalDelay: 0 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clear() {
|
//TODO: find other things that dose not need clearing
|
||||||
|
export function softClear() {
|
||||||
|
runtimeState.eventNow = null;
|
||||||
|
runtimeState.publicEventNow = null;
|
||||||
|
runtimeState.eventNext = null;
|
||||||
|
runtimeState.publicEventNext = null;
|
||||||
|
|
||||||
|
// runtimeState.currentBlock.block = null;
|
||||||
|
// runtimeState.currentBlock.startedAt = null;
|
||||||
|
|
||||||
|
runtimeState.runtime.offset = 0;
|
||||||
|
runtimeState.runtime.relativeOffset = 0;
|
||||||
|
runtimeState.runtime.expectedEnd = null;
|
||||||
|
runtimeState.runtime.selectedEventIndex = null;
|
||||||
|
|
||||||
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
|
runtimeState.clock = clock.timeNow();
|
||||||
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
|
// when clearing, we maintain the total delay from the rundown
|
||||||
|
runtimeState._timer.forceFinish = null;
|
||||||
|
runtimeState._timer.pausedAt = null;
|
||||||
|
runtimeState._timer.secondaryTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hardClear() {
|
||||||
runtimeState.eventNow = null;
|
runtimeState.eventNow = null;
|
||||||
runtimeState.publicEventNow = null;
|
runtimeState.publicEventNow = null;
|
||||||
runtimeState.eventNext = null;
|
runtimeState.eventNext = null;
|
||||||
@@ -93,6 +125,7 @@ export function clear() {
|
|||||||
runtimeState.publicEventNext = null;
|
runtimeState.publicEventNext = null;
|
||||||
|
|
||||||
runtimeState.runtime.offset = 0;
|
runtimeState.runtime.offset = 0;
|
||||||
|
runtimeState.runtime.relativeOffset = 0;
|
||||||
runtimeState.runtime.actualStart = null;
|
runtimeState.runtime.actualStart = null;
|
||||||
runtimeState.runtime.expectedEnd = null;
|
runtimeState.runtime.expectedEnd = null;
|
||||||
runtimeState.runtime.selectedEventIndex = null;
|
runtimeState.runtime.selectedEventIndex = null;
|
||||||
@@ -137,7 +170,7 @@ type RundownData = {
|
|||||||
*/
|
*/
|
||||||
export function updateRundownData(rundownData: RundownData) {
|
export function updateRundownData(rundownData: RundownData) {
|
||||||
// we keep this in private state since there is no UI use case for it
|
// we keep this in private state since there is no UI use case for it
|
||||||
runtimeState._timer.totalDelay = rundownData.totalDelay;
|
runtimeState._rundown.totalDelay = rundownData.totalDelay;
|
||||||
|
|
||||||
runtimeState.runtime.numEvents = rundownData.numEvents;
|
runtimeState.runtime.numEvents = rundownData.numEvents;
|
||||||
runtimeState.runtime.plannedStart = rundownData.firstStart;
|
runtimeState.runtime.plannedStart = rundownData.firstStart;
|
||||||
@@ -154,10 +187,7 @@ export function load(
|
|||||||
rundown: OntimeRundown,
|
rundown: OntimeRundown,
|
||||||
initialData?: Partial<TimerState & RestorePoint>,
|
initialData?: Partial<TimerState & RestorePoint>,
|
||||||
): boolean {
|
): boolean {
|
||||||
// we need to persist the current block state across loads
|
softClear();
|
||||||
const prevCurrentBlock = { ...runtimeState.currentBlock };
|
|
||||||
clear();
|
|
||||||
runtimeState.currentBlock = prevCurrentBlock;
|
|
||||||
|
|
||||||
// filter rundown
|
// filter rundown
|
||||||
const timedEvents = filterTimedEvents(rundown);
|
const timedEvents = filterTimedEvents(rundown);
|
||||||
@@ -185,6 +215,7 @@ export function load(
|
|||||||
if (firstStart === null || typeof firstStart === 'number') {
|
if (firstStart === null || typeof firstStart === 'number') {
|
||||||
runtimeState.runtime.actualStart = firstStart;
|
runtimeState.runtime.actualStart = firstStart;
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
|
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
}
|
}
|
||||||
if (typeof initialData.blockStartAt === 'number') {
|
if (typeof initialData.blockStartAt === 'number') {
|
||||||
@@ -390,6 +421,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
|
|
||||||
// update offset
|
// update offset
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
state.runtime.offset = getRuntimeOffset(state);
|
||||||
|
state.runtime.relativeOffset = getRelativeOffset(state);
|
||||||
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -409,9 +441,7 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
|||||||
if (state.timer.playback === Playback.Stop) {
|
if (state.timer.playback === Playback.Stop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
clear();
|
hardClear();
|
||||||
runtimeState.runtime.actualStart = null;
|
|
||||||
runtimeState.runtime.expectedEnd = null;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,6 +483,7 @@ export function addTime(amount: number) {
|
|||||||
|
|
||||||
// update runtime delays: over - under
|
// update runtime delays: over - under
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
|
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -498,6 +529,7 @@ export function update(): UpdateResult {
|
|||||||
|
|
||||||
// update runtime, needs up-to-date timer state
|
// update runtime, needs up-to-date timer state
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
|
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
|
|
||||||
const finishedNow =
|
const finishedNow =
|
||||||
@@ -604,9 +636,7 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need to persist the current block state across loads
|
// we need to persist the current block state across loads
|
||||||
const prevCurrentBlock = { ...runtimeState.currentBlock };
|
softClear();
|
||||||
clear();
|
|
||||||
runtimeState.currentBlock = prevCurrentBlock;
|
|
||||||
|
|
||||||
//account for offset but we only keep it if passed to us
|
//account for offset but we only keep it if passed to us
|
||||||
runtimeState.runtime.offset = offset;
|
runtimeState.runtime.offset = offset;
|
||||||
@@ -696,3 +726,7 @@ export function loadBlock(rundown: OntimeRundown, state = runtimeState) {
|
|||||||
// update the block anyway
|
// update the block anyway
|
||||||
state.currentBlock.block = newCurrentBlock === null ? null : { ...newCurrentBlock };
|
state.currentBlock.block = newCurrentBlock === null ? null : { ...newCurrentBlock };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setOffsetMode(mode: OffsetMode) {
|
||||||
|
runtimeState.runtime.offsetMode = mode;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test('linked time until', async ({ page }) => {
|
test('time until absolute', async ({ page }) => {
|
||||||
await page.goto('http://localhost:4001/editor');
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Clear rundown' }).click();
|
await page.getByRole('button', { name: 'Clear rundown' }).click();
|
||||||
@@ -11,7 +11,46 @@ test('linked time until', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Absolute' }).click();
|
||||||
await page.getByTestId('entry-1').getByLabel('Start event').click();
|
await page.getByTestId('entry-1').getByLabel('Start event').click();
|
||||||
|
await expect(page.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h
|
||||||
|
await page.getByLabel('Pause event').click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('9m');
|
||||||
|
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('19m');
|
||||||
|
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('29m');
|
||||||
|
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('5h59m');
|
||||||
|
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('6h9m');
|
||||||
|
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('6h19m');
|
||||||
|
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
|
||||||
|
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('29s');
|
||||||
|
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('10m');
|
||||||
|
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('20m');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('time until relative', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Clear rundown' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Delete all' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Event' }).nth(4).click();
|
||||||
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Relative' }).click();
|
||||||
|
await page.getByTestId('entry-1').getByLabel('Start event').click();
|
||||||
|
await expect(page.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s
|
||||||
await page.getByLabel('Pause event').click();
|
await page.getByLabel('Pause event').click();
|
||||||
|
|
||||||
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('9m');
|
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('9m');
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import type { MaybeNumber } from '../../utils/utils.type.js';
|
import type { MaybeNumber } from '../../utils/utils.type.js';
|
||||||
|
|
||||||
|
export enum OffsetMode {
|
||||||
|
Absolute = 'absolute',
|
||||||
|
Relative = 'relative',
|
||||||
|
}
|
||||||
|
|
||||||
export type Runtime = {
|
export type Runtime = {
|
||||||
numEvents: number;
|
numEvents: number;
|
||||||
selectedEventIndex: MaybeNumber;
|
selectedEventIndex: MaybeNumber;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
relativeOffset: number;
|
||||||
plannedStart: MaybeNumber;
|
plannedStart: MaybeNumber;
|
||||||
actualStart: MaybeNumber;
|
actualStart: MaybeNumber;
|
||||||
plannedEnd: MaybeNumber;
|
plannedEnd: MaybeNumber;
|
||||||
expectedEnd: MaybeNumber;
|
expectedEnd: MaybeNumber;
|
||||||
|
offsetMode: OffsetMode;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { SimpleDirection, SimplePlayback } from './AuxTimer.type.js';
|
import { SimpleDirection, SimplePlayback } from './AuxTimer.type.js';
|
||||||
import { Playback } from './Playback.type.js';
|
import { Playback } from './Playback.type.js';
|
||||||
|
import { OffsetMode } from './Runtime.type.js';
|
||||||
import type { RuntimeStore } from './RuntimeStore.type.js';
|
import type { RuntimeStore } from './RuntimeStore.type.js';
|
||||||
import { TimerPhase } from './TimerState.type.js';
|
import { TimerPhase } from './TimerState.type.js';
|
||||||
|
|
||||||
export const runtimeStorePlaceholder: RuntimeStore = {
|
export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
|
||||||
clock: 0,
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
@@ -32,10 +33,12 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
|||||||
selectedEventIndex: null, // changes if rundown changes or we load a new event
|
selectedEventIndex: null, // changes if rundown changes or we load a new event
|
||||||
numEvents: 0, // change initiated by user
|
numEvents: 0, // change initiated by user
|
||||||
offset: 0, // changes at runtime
|
offset: 0, // changes at runtime
|
||||||
|
relativeOffset: 0, // changes at runtime
|
||||||
plannedStart: 0, // only changes if event changes
|
plannedStart: 0, // only changes if event changes
|
||||||
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
||||||
actualStart: null, // set once we start the timer
|
actualStart: null, // set once we start the timer
|
||||||
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
},
|
},
|
||||||
currentBlock: {
|
currentBlock: {
|
||||||
block: null,
|
block: null,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export { TimerLifeCycle, timerLifecycleValues } from './definitions/core/TimerLi
|
|||||||
export type { TimerMessage, MessageState, SecondarySource } from './definitions/runtime/MessageControl.type.js';
|
export type { TimerMessage, MessageState, SecondarySource } from './definitions/runtime/MessageControl.type.js';
|
||||||
|
|
||||||
export type { Runtime } from './definitions/runtime/Runtime.type.js';
|
export type { Runtime } from './definitions/runtime/Runtime.type.js';
|
||||||
|
export { OffsetMode } from './definitions/runtime/Runtime.type.js';
|
||||||
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||||
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
||||||
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user