mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
Relative mode (#1764)
* feat: add group start time to state * refactor: calculate expected times when offset mode changes * feat: show relative data in ui * fixup! feat: add group start time to state * fixup! refactor: calculate expected times when offset mode changes * fixup! feat: show relative data in ui * try to handle multiday * show planed/expected rundown end day offset values * refactor: style tweaks to timers * refactor: cleanup data use * day offset * update tests * test getExpectedStart multiday * write start epoch to restore file * current day in relative mode * remove todo * move find day offset to utils --------- Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
@@ -164,24 +164,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
|
|||||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
|
|
||||||
plannedStart: state.rundown.plannedStart,
|
|
||||||
actualStart: state.rundown.actualStart,
|
|
||||||
plannedEnd: state.rundown.plannedEnd,
|
|
||||||
expectedEnd: state.offset.expectedRundownEnd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) => ({
|
|
||||||
playback: state.timer.playback,
|
|
||||||
clock: state.clock,
|
|
||||||
|
|
||||||
numEvents: state.rundown.numEvents,
|
|
||||||
selectedEventIndex: state.rundown.selectedEventIndex,
|
|
||||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
|
||||||
|
|
||||||
groupExpectedEnd: state.offset.expectedGroupEnd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
offset: state.offset.absolute,
|
offset: state.offset.absolute,
|
||||||
@@ -190,7 +172,7 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
|||||||
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
||||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
mode: state.offset.mode,
|
mode: state.offset.mode,
|
||||||
currentDay: state.eventNow?.dayOffset ?? 0,
|
currentDay: state.rundown.currentDay ?? 0,
|
||||||
actualStart: state.rundown.actualStart,
|
actualStart: state.rundown.actualStart,
|
||||||
plannedStart: state.rundown.plannedStart,
|
plannedStart: state.rundown.plannedStart,
|
||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
@@ -227,6 +209,46 @@ export const usePlayback = () => {
|
|||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ======================= Overview data subscriptions ======================= */
|
||||||
|
|
||||||
|
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
plannedStart: state.rundown.plannedStart,
|
||||||
|
actualStart: state.rundown.actualStart,
|
||||||
|
plannedEnd: state.rundown.plannedEnd,
|
||||||
|
expectedEnd: state.offset.expectedRundownEnd,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useProgressOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
numEvents: state.rundown.numEvents,
|
||||||
|
selectedEventIndex: state.rundown.selectedEventIndex,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useOffsetOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useGroupTimerOverView = createSelector((state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
|
mode: state.offset.mode,
|
||||||
|
groupExpectedEnd: state.offset.expectedGroupEnd,
|
||||||
|
// we can force these numbers to 0 fo this use case to avoid null checks
|
||||||
|
actualGroupStart: state.rundown.actualGroupStart ?? 0,
|
||||||
|
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useFlagTimerOverView = createSelector((state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
mode: state.offset.mode,
|
||||||
|
// we can force these numbers to 0 fo this use case to avoid null checks
|
||||||
|
actualStart: state.rundown.actualStart ?? 0,
|
||||||
|
plannedStart: state.rundown.plannedStart ?? 0,
|
||||||
|
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
/* ======================= View specific subscriptions ======================= */
|
/* ======================= View specific subscriptions ======================= */
|
||||||
|
|
||||||
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
|
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
|
||||||
|
|||||||
@@ -47,9 +47,10 @@
|
|||||||
|
|
||||||
.daySpan {
|
.daySpan {
|
||||||
&::after {
|
&::after {
|
||||||
content: '*';
|
content: "+"attr(data-day-offset);
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
font-size: 0.75em;
|
font-size: 0.6em;
|
||||||
|
letter-spacing: 0;
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,3 +68,10 @@
|
|||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dueTime {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: $playback-over;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,23 +8,26 @@ import {
|
|||||||
TbFolderPin,
|
TbFolderPin,
|
||||||
TbFolderStar,
|
TbFolderStar,
|
||||||
} from 'react-icons/tb';
|
} from 'react-icons/tb';
|
||||||
import { OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
|
import { OffsetMode, OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
import { dayInMs, isPlaybackActive, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
import {
|
import {
|
||||||
useClock,
|
useClock,
|
||||||
useCurrentGroupId,
|
useCurrentGroupId,
|
||||||
|
useFlagTimerOverView,
|
||||||
|
useGroupTimerOverView,
|
||||||
useNextFlag,
|
useNextFlag,
|
||||||
|
useOffsetOverview,
|
||||||
|
useProgressOverview,
|
||||||
useRundownOverview,
|
useRundownOverview,
|
||||||
useRuntimePlaybackOverview,
|
|
||||||
useTimer,
|
useTimer,
|
||||||
} from '../../../common/hooks/useSocket';
|
} from '../../../common/hooks/useSocket';
|
||||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||||
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
||||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
|
import { calculateEndAndDaySpan, formatDueTime, formattedTime } from '../overview.utils';
|
||||||
|
|
||||||
import { OverUnder, TimeColumn } from './TimeLayout';
|
import { OverUnder, TimeColumn } from './TimeLayout';
|
||||||
|
|
||||||
@@ -58,8 +61,8 @@ export function StartTimes() {
|
|||||||
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
|
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
|
||||||
{maybePlannedDaySpan > 0 ? (
|
{maybePlannedDaySpan > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
|
text={`Rundown spans over ${maybePlannedDaySpan + 1} days`}
|
||||||
render={<span className={cx([style.time, style.daySpan])} />}
|
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybePlannedDaySpan} />}
|
||||||
>
|
>
|
||||||
{plannedEndText}
|
{plannedEndText}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -71,8 +74,8 @@ export function StartTimes() {
|
|||||||
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
|
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
|
||||||
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
|
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
|
text={`Rundown spans over ${maybeExpectedDaySpan + 1} days`}
|
||||||
render={<span className={cx([style.time, style.daySpan])} />}
|
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybeExpectedDaySpan} />}
|
||||||
>
|
>
|
||||||
{formattedTime(maybeExpectedEnd)}
|
{formattedTime(maybeExpectedEnd)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -96,61 +99,111 @@ export function MetadataTimes() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: there a some things here we still need to think about, mainly what to do whit the planed group duration in relation to the events
|
|
||||||
function GroupTimes() {
|
function GroupTimes() {
|
||||||
const { clock, groupExpectedEnd } = useRuntimePlaybackOverview();
|
const { clock, groupExpectedEnd, actualGroupStart, mode, playback, currentDay } = useGroupTimerOverView();
|
||||||
const { currentGroupId } = useCurrentGroupId();
|
const { currentGroupId } = useCurrentGroupId();
|
||||||
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
||||||
|
|
||||||
// the group end time dose not encode any day offsets
|
const active = isPlaybackActive(playback);
|
||||||
const plannedGroupEnd = group && group.timeStart !== null ? group.timeStart + group.duration - clock : null;
|
|
||||||
const plannedTimeUntilGroupEnd = formattedTime(plannedGroupEnd, 3, TimerType.CountDown);
|
// the group end time dose not encode any day offsets so it is calculated with group start time and duration
|
||||||
|
const plannedGroupEnd = (() => {
|
||||||
|
if (!active) return null;
|
||||||
|
if (!group || group.timeStart === null) return null;
|
||||||
|
const normalizedClock = clock + currentDay * dayInMs;
|
||||||
|
return mode === OffsetMode.Absolute
|
||||||
|
? group.timeStart + group.duration - normalizedClock
|
||||||
|
: actualGroupStart + group.duration - normalizedClock;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const plannedTimeUntilGroupEnd = formatDueTime(plannedGroupEnd, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
|
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
|
||||||
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
|
const expectedTimeUntilGroupEnd = formatDueTime(expectedGroupEnd, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const groupTitle = group?.title ?? null;
|
const groupTitle = group?.title ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle ? groupTitle : 'Group'} `}</span>
|
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle || 'Group'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
|
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
|
||||||
<span className={cx([style.time, !group && style.muted])}>{plannedTimeUntilGroupEnd}</span>
|
<span
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
(!group || !active) && style.muted,
|
||||||
|
plannedTimeUntilGroupEnd === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{plannedTimeUntilGroupEnd}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
||||||
<span className={cx([style.time, groupExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
|
<span
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
!groupExpectedEnd && style.muted,
|
||||||
|
expectedTimeUntilGroupEnd === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{expectedTimeUntilGroupEnd}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlagTimes() {
|
function FlagTimes() {
|
||||||
const { clock } = useClock();
|
const { clock, mode, actualStart, plannedStart, playback, currentDay } = useFlagTimerOverView();
|
||||||
const { id, expectedStart } = useNextFlag();
|
const { id, expectedStart } = useNextFlag();
|
||||||
const entry = useEntry(id) as OntimeEvent | null;
|
const entry = useEntry(id) as OntimeEvent | null;
|
||||||
|
|
||||||
const plannedFlagStart = entry ? entry.timeStart - clock : null;
|
const active = isPlaybackActive(playback);
|
||||||
const plannedTimeUntilDisplay = formattedTime(plannedFlagStart, 3, TimerType.CountDown);
|
|
||||||
|
const plannedFlagStart = (() => {
|
||||||
|
if (!active) return null;
|
||||||
|
if (!entry) return null;
|
||||||
|
const normalizedTimeStart = entry.timeStart + entry.dayOffset * dayInMs;
|
||||||
|
const normalizedClock = clock + currentDay * dayInMs;
|
||||||
|
return mode === OffsetMode.Absolute
|
||||||
|
? normalizedTimeStart - normalizedClock
|
||||||
|
: normalizedTimeStart + actualStart - plannedStart - normalizedClock;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const plannedTimeUntilDisplay = formatDueTime(plannedFlagStart, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
|
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
|
||||||
const expectedTimeUntilDisplay = formattedTime(expectedTimeUntil, 3, TimerType.CountDown);
|
const expectedTimeUntilDisplay = formatDueTime(expectedTimeUntil, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const title = entry?.title ?? null;
|
const title = entry?.title ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={title ? style.labelTitle : style.label}>{`${title ? title : 'Flag'} `}</span>
|
<span className={title ? style.labelTitle : style.label}>{`${title || 'Flag'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
|
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
|
||||||
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>
|
<span
|
||||||
|
data-testid='flag-plannedStart'
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
(!entry || !active) && style.muted,
|
||||||
|
plannedTimeUntilDisplay === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{plannedTimeUntilDisplay}
|
{plannedTimeUntilDisplay}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
||||||
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>
|
<span
|
||||||
|
data-testid='flag-expectedStart'
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
expectedTimeUntil === null && style.muted,
|
||||||
|
expectedTimeUntilDisplay === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{expectedTimeUntilDisplay}
|
{expectedTimeUntilDisplay}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +212,7 @@ function FlagTimes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProgressOverview() {
|
export function ProgressOverview() {
|
||||||
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
|
const { numEvents, selectedEventIndex } = useProgressOverview();
|
||||||
|
|
||||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||||
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
||||||
@@ -168,7 +221,7 @@ export function ProgressOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OffsetOverview() {
|
export function OffsetOverview() {
|
||||||
const { offset, playback } = useRuntimePlaybackOverview();
|
const { offset, playback } = useOffsetOverview();
|
||||||
|
|
||||||
const isPlaying = isPlaybackActive(playback);
|
const isPlaying = isPlaybackActive(playback);
|
||||||
const offsetState = getOffsetState(isPlaying ? offset : null);
|
const offsetState = getOffsetState(isPlaying ? offset : null);
|
||||||
|
|||||||
@@ -3,6 +3,23 @@ import { dayInMs, millisToString } from 'ontime-utils';
|
|||||||
|
|
||||||
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composition to stop negative timers from being formatted
|
||||||
|
* They should show a due string instead
|
||||||
|
*
|
||||||
|
* This is used for cases when a negative timer is unwanted
|
||||||
|
* eg: count down to a milestone
|
||||||
|
*/
|
||||||
|
export function formatDueTime(
|
||||||
|
time: MaybeNumber,
|
||||||
|
segments: number = 3,
|
||||||
|
direction?: TimerType.CountDown | TimerType.CountUp,
|
||||||
|
dueString = 'due',
|
||||||
|
): string {
|
||||||
|
if (time !== null && time <= 0) return dueString;
|
||||||
|
return formattedTime(time, segments, direction);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates the logic for formatting time in overview
|
* Encapsulates the logic for formatting time in overview
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ interface EventUntilProps {
|
|||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventUntil(props: EventUntilProps) {
|
function EventUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: EventUntilProps) {
|
||||||
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
|
|
||||||
|
|
||||||
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
||||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ $header-font-size: clamp(24px, 2.5vw, 48px);
|
|||||||
|
|
||||||
// General styling
|
// General styling
|
||||||
$accent-color: $red-500; // --accent-color-override
|
$accent-color: $red-500; // --accent-color-override
|
||||||
$delay-color: $ontime-delay-text;
|
|
||||||
$viewer-label-color: rgba(white, 25%);
|
$viewer-label-color: rgba(white, 25%);
|
||||||
|
|
||||||
// Main Properties of a viewer
|
// Main Properties of a viewer
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ $item-height: 3.5rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub__schedule--delayed {
|
.sub__schedule--delayed {
|
||||||
color: $delay-color;
|
color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub__schedule--strike {
|
.sub__schedule--strike {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
|
|||||||
}
|
}
|
||||||
|
|
||||||
.delay {
|
.delay {
|
||||||
color: $delay-color;
|
color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeOverview {
|
.timeOverview {
|
||||||
@@ -117,7 +117,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
|
|||||||
.cross {
|
.cross {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
text-decoration-color: $delay-color;
|
text-decoration-color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separeLeft {
|
.separeLeft {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { dayInMs, millisToString } from 'ontime-utils';
|
import { dayInMs, MILLIS_PER_HOUR, millisToString } from 'ontime-utils';
|
||||||
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
findDayOffset,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
@@ -726,6 +727,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 100,
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 150,
|
startedAt: 150,
|
||||||
@@ -738,7 +740,10 @@ describe('getRuntimeOffset()', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 150,
|
actualStart: 150,
|
||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
clock: 150,
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
@@ -750,6 +755,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 100,
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 150, // we started 50ms delayed
|
startedAt: 150, // we started 50ms delayed
|
||||||
@@ -762,7 +768,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 150,
|
actualStart: 150,
|
||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
@@ -775,6 +783,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 100,
|
timeStart: 100,
|
||||||
timeEnd: 140,
|
timeEnd: 140,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 100, // we started ontime
|
startedAt: 100, // we started ontime
|
||||||
@@ -787,7 +796,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 100,
|
actualStart: 100,
|
||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
@@ -800,6 +811,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 100,
|
timeStart: 100,
|
||||||
timeEnd: 150,
|
timeEnd: 150,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
clock: 150,
|
clock: 150,
|
||||||
timer: {
|
timer: {
|
||||||
@@ -813,7 +825,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 100,
|
actualStart: 100,
|
||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
@@ -830,6 +844,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
duration: 3600000,
|
duration: 3600000,
|
||||||
timeStrategy: 'lock-duration',
|
timeStrategy: 'lock-duration',
|
||||||
linkStart: false,
|
linkStart: false,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
selectedEventIndex: 0,
|
selectedEventIndex: 0,
|
||||||
@@ -837,6 +852,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 77400000,
|
plannedStart: 77400000,
|
||||||
plannedEnd: 84600000,
|
plannedEnd: 84600000,
|
||||||
actualStart: null,
|
actualStart: null,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: -77400000,
|
absolute: -77400000,
|
||||||
@@ -852,6 +868,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
@@ -874,6 +891,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
|
dayOffset: 0,
|
||||||
skip: false,
|
skip: false,
|
||||||
note: '',
|
note: '',
|
||||||
colour: '',
|
colour: '',
|
||||||
@@ -890,6 +908,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 77400000, // 21:30:00
|
plannedStart: 77400000, // 21:30:00
|
||||||
plannedEnd: 81000000, // 22:30:00
|
plannedEnd: 81000000, // 22:30:00
|
||||||
actualStart: 78000000, // 21:40:00
|
actualStart: 78000000, // 21:40:00
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: 0,
|
absolute: 0,
|
||||||
@@ -905,6 +924,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: 78000000,
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
@@ -922,6 +942,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
timeEnd: 81000000, // 22:30:00
|
timeEnd: 81000000, // 22:30:00
|
||||||
duration: 3600000, // 01:00:00
|
duration: 3600000, // 01:00:00
|
||||||
|
dayOffset: 0,
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
timeStrategy: TimeStrategy.LockEnd,
|
||||||
linkStart: false,
|
linkStart: false,
|
||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
@@ -943,6 +964,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 77400000, // 21:30:00
|
plannedStart: 77400000, // 21:30:00
|
||||||
plannedEnd: 81000000, // 22:30:00
|
plannedEnd: 81000000, // 22:30:00
|
||||||
actualStart: 78000000, // 21:40:00
|
actualStart: 78000000, // 21:40:00
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: 0,
|
absolute: 0,
|
||||||
@@ -958,6 +980,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: 78000000,
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
@@ -978,6 +1001,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
endAction: EndAction.None,
|
endAction: EndAction.None,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
selectedEventIndex: 0,
|
selectedEventIndex: 0,
|
||||||
@@ -985,6 +1009,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 77400000, // 21:30:00
|
plannedStart: 77400000, // 21:30:00
|
||||||
plannedEnd: 81000000, // 22:30:00
|
plannedEnd: 81000000, // 22:30:00
|
||||||
actualStart: 82000000, // 22:46:40 <--- started now
|
actualStart: 82000000, // 22:46:40 <--- started now
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: 0,
|
absolute: 0,
|
||||||
@@ -1000,6 +1025,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: 82000000, // <--- started now
|
startedAt: 82000000, // <--- started now
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
@@ -1017,6 +1043,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 150,
|
timeStart: 150,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 150,
|
startedAt: 150,
|
||||||
@@ -1029,7 +1056,9 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 150,
|
actualStart: 150,
|
||||||
plannedStart: 150,
|
plannedStart: 150,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
@@ -1041,6 +1070,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 100,
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 150,
|
startedAt: 150,
|
||||||
@@ -1053,7 +1083,9 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 150,
|
actualStart: 150,
|
||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
@@ -1065,6 +1097,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
eventNow: {
|
eventNow: {
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: 150,
|
timeStart: 150,
|
||||||
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 100,
|
startedAt: 100,
|
||||||
@@ -1077,7 +1110,9 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 100,
|
actualStart: 100,
|
||||||
plannedStart: 150,
|
plannedStart: 150,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
@@ -1258,3 +1293,21 @@ describe('getTimerPhase()', () => {
|
|||||||
expect(phase).toBe(TimerPhase.Pending);
|
expect(phase).toBe(TimerPhase.Pending);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findDay()', () => {
|
||||||
|
test('finds dayOffset', () => {
|
||||||
|
//both have 1 hour offset but the clock are on different days
|
||||||
|
expect(findDayOffset(0, 23 * MILLIS_PER_HOUR)).toBe(-1); // -> 23
|
||||||
|
expect(findDayOffset(0, 13 * MILLIS_PER_HOUR)).toBe(-1); // -> 13
|
||||||
|
expect(findDayOffset(0, 12 * MILLIS_PER_HOUR)).toBe(-1); // -> 12
|
||||||
|
expect(findDayOffset(0, 11 * MILLIS_PER_HOUR)).toBe(0); // -> 11
|
||||||
|
expect(findDayOffset(1 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -1
|
||||||
|
|
||||||
|
//both have 1 hour offset but the clock are on different days
|
||||||
|
expect(findDayOffset(23 * MILLIS_PER_HOUR, 0)).toBe(1); // -> -23
|
||||||
|
expect(findDayOffset(13 * MILLIS_PER_HOUR, 0)).toBe(1); // -> -13
|
||||||
|
expect(findDayOffset(12 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -12
|
||||||
|
expect(findDayOffset(11 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -11
|
||||||
|
expect(findDayOffset(22 * MILLIS_PER_HOUR, 23 * MILLIS_PER_HOUR)).toBe(0); // -> 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe('isRestorePoint()', () => {
|
|||||||
addedTime: 2,
|
addedTime: 2,
|
||||||
pausedAt: 3,
|
pausedAt: 3,
|
||||||
firstStart: 1,
|
firstStart: 1,
|
||||||
|
startEpoch: 1,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ describe('isRestorePoint()', () => {
|
|||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
firstStart: 1,
|
firstStart: 1,
|
||||||
|
startEpoch: 1,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -35,6 +37,7 @@ describe('isRestorePoint()', () => {
|
|||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
groupStartAt: 10,
|
groupStartAt: 10,
|
||||||
|
startEpoch: 1,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -56,6 +59,7 @@ describe('isRestorePoint()', () => {
|
|||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
groupStartAt: 10,
|
groupStartAt: 10,
|
||||||
|
startEpoch: 1,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe('restoreService', () => {
|
|||||||
addedTime: 5678,
|
addedTime: 5678,
|
||||||
pausedAt: 9087,
|
pausedAt: 9087,
|
||||||
firstStart: 1234,
|
firstStart: 1234,
|
||||||
|
startEpoch: 1234,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRead = vi.fn().mockResolvedValue(expected);
|
const mockRead = vi.fn().mockResolvedValue(expected);
|
||||||
@@ -33,6 +34,7 @@ describe('restoreService', () => {
|
|||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
firstStart: 1234,
|
firstStart: 1234,
|
||||||
|
startEpoch: 1234,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRead = vi.fn().mockResolvedValue(expected);
|
const mockRead = vi.fn().mockResolvedValue(expected);
|
||||||
@@ -79,6 +81,7 @@ describe('restoreService', () => {
|
|||||||
addedTime: 1234,
|
addedTime: 1234,
|
||||||
pausedAt: 1234,
|
pausedAt: 1234,
|
||||||
firstStart: 1234,
|
firstStart: 1234,
|
||||||
|
startEpoch: 1234,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockWrite = vi.fn().mockResolvedValue(undefined);
|
const mockWrite = vi.fn().mockResolvedValue(undefined);
|
||||||
@@ -95,6 +98,7 @@ describe('restoreService', () => {
|
|||||||
addedTime: 5678,
|
addedTime: 5678,
|
||||||
pausedAt: 5678,
|
pausedAt: 5678,
|
||||||
firstStart: 5678,
|
firstStart: 5678,
|
||||||
|
startEpoch: 5678,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockWrite = vi.fn().mockRejectedValue(new Error('Write failed'));
|
const mockWrite = vi.fn().mockRejectedValue(new Error('Write failed'));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
|
|||||||
'addedTime',
|
'addedTime',
|
||||||
'pausedAt',
|
'pausedAt',
|
||||||
'firstStart',
|
'firstStart',
|
||||||
|
'startEpoch',
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -49,5 +50,9 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export type RestorePoint = {
|
|||||||
addedTime: number;
|
addedTime: number;
|
||||||
pausedAt: MaybeNumber;
|
pausedAt: MaybeNumber;
|
||||||
firstStart: MaybeNumber;
|
firstStart: MaybeNumber;
|
||||||
|
startEpoch: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -743,6 +743,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
addedTime: state.timer.addedTime,
|
addedTime: state.timer.addedTime,
|
||||||
pausedAt: state._timer.pausedAt,
|
pausedAt: state._timer.pausedAt,
|
||||||
firstStart: state.rundown.actualStart,
|
firstStart: state.rundown.actualStart,
|
||||||
|
startEpoch: state._startEpoch,
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch((_e) => {
|
||||||
//we don't do anything with the error here
|
//we don't do anything with the error here
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
||||||
import { dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { dayInMs, isPlaybackActive, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
import type { RuntimeState } from '../stores/runtimeState.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,14 +113,14 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
|||||||
* Negative offset is under time / ahead of schedule
|
* Negative offset is under time / ahead of schedule
|
||||||
*/
|
*/
|
||||||
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
|
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
|
||||||
const { eventNow, clock } = state;
|
const { eventNow, clock, _startDayOffset } = state;
|
||||||
const { addedTime, current, startedAt } = state.timer;
|
const { addedTime, current, startedAt } = state.timer;
|
||||||
// nothing to calculate if there are no loaded events or if we havent started
|
// nothing to calculate if there are no loaded events or if we havent started
|
||||||
if (eventNow === null || startedAt === null) {
|
if (eventNow === null || startedAt === null || _startDayOffset === null) {
|
||||||
return { absolute: 0, relative: 0 };
|
return { absolute: 0, relative: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countToEnd, timeStart } = eventNow;
|
const { countToEnd, timeStart, dayOffset } = eventNow;
|
||||||
const { plannedStart, actualStart } = state.rundown;
|
const { plannedStart, actualStart } = state.rundown;
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
@@ -131,8 +131,8 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
|
|||||||
if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set');
|
if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// difference between planned event start and actual event start (will be positive if we stared behind )
|
// difference between planned event start and actual event start (will be positive if we started behind )
|
||||||
const eventStartOffset = startedAt - timeStart;
|
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
|
||||||
|
|
||||||
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
|
// 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));
|
const overtime = Math.abs(Math.min(current, 0));
|
||||||
@@ -142,8 +142,8 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
|
|||||||
|
|
||||||
const absolute = eventStartOffset + overtime + pausedTime + addedTime;
|
const absolute = eventStartOffset + overtime + pausedTime + addedTime;
|
||||||
|
|
||||||
// the relative offset i the same as the absolute offset but adjusted relative to the actual start time
|
// the relative offset is the same as the absolute offset but adjusted relative to the actual start time
|
||||||
const relative = absolute + plannedStart - actualStart;
|
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs;
|
||||||
|
|
||||||
// in case of count to end, the absolute offset is just the overtime
|
// in case of count to end, the absolute offset is just the overtime
|
||||||
return countToEnd ? { absolute: overtime, relative } : { absolute, relative };
|
return countToEnd ? { absolute: overtime, relative } : { absolute, relative };
|
||||||
@@ -180,3 +180,14 @@ export function getTimerPhase(state: RuntimeState): TimerPhase {
|
|||||||
|
|
||||||
return TimerPhase.Default;
|
return TimerPhase.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the day offset relative to an event start
|
||||||
|
* used byt the runtimeState on first start to get correct offsets
|
||||||
|
*/
|
||||||
|
export function findDayOffset(plannedStart: number, clock: number): number {
|
||||||
|
const distance = clock - plannedStart;
|
||||||
|
if (distance >= 12 * MILLIS_PER_HOUR) return -1;
|
||||||
|
if (distance < -12 * MILLIS_PER_HOUR) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const baseState: RuntimeState = {
|
|||||||
plannedStart: 0,
|
plannedStart: 0,
|
||||||
plannedEnd: 0,
|
plannedEnd: 0,
|
||||||
actualStart: null,
|
actualStart: null,
|
||||||
|
actualGroupStart: null,
|
||||||
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: 0,
|
absolute: 0,
|
||||||
@@ -46,6 +48,8 @@ const baseState: RuntimeState = {
|
|||||||
_group: null,
|
_group: null,
|
||||||
_end: null,
|
_end: null,
|
||||||
_flag: null,
|
_flag: null,
|
||||||
|
_startDayOffset: null,
|
||||||
|
_startEpoch: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[11],
|
eventNow: rundown.entries[11],
|
||||||
|
rundown: { actualGroupStart: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
||||||
@@ -420,6 +421,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[22],
|
eventNow: rundown.entries[22],
|
||||||
|
rundown: { actualGroupStart: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
@@ -447,6 +449,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
|
rundown: { actualGroupStart: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
@@ -471,6 +474,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
|
rundown: { actualGroupStart: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|||||||
@@ -23,9 +23,15 @@ import {
|
|||||||
isPlaybackActive,
|
isPlaybackActive,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { timeNow } from '../utils/time.js';
|
import { getTimeObject, timeNow } from '../utils/time.js';
|
||||||
import type { RestorePoint } from '../services/restore-service/restore.type.js';
|
import type { RestorePoint } from '../services/restore-service/restore.type.js';
|
||||||
import { getCurrent, getExpectedFinish, getRuntimeOffset, getTimerPhase } from '../services/timerUtils.js';
|
import {
|
||||||
|
findDayOffset,
|
||||||
|
getCurrent,
|
||||||
|
getExpectedFinish,
|
||||||
|
getRuntimeOffset,
|
||||||
|
getTimerPhase,
|
||||||
|
} from '../services/timerUtils.js';
|
||||||
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||||
import { timerConfig } from '../setup/config.js';
|
import { timerConfig } from '../setup/config.js';
|
||||||
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
||||||
@@ -55,6 +61,8 @@ export type RuntimeState = {
|
|||||||
_group: ExpectedMetadata;
|
_group: ExpectedMetadata;
|
||||||
_flag: ExpectedMetadata;
|
_flag: ExpectedMetadata;
|
||||||
_end: ExpectedMetadata;
|
_end: ExpectedMetadata;
|
||||||
|
_startEpoch: MaybeNumber;
|
||||||
|
_startDayOffset: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: RuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
@@ -78,6 +86,8 @@ const runtimeState: RuntimeState = {
|
|||||||
_group: null,
|
_group: null,
|
||||||
_flag: null,
|
_flag: null,
|
||||||
_end: null,
|
_end: null,
|
||||||
|
_startEpoch: null,
|
||||||
|
_startDayOffset: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
@@ -152,6 +162,10 @@ export function clearState() {
|
|||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
runtimeState._timer.secondaryTarget = null;
|
runtimeState._timer.secondaryTarget = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
|
|
||||||
|
runtimeState._startEpoch = null;
|
||||||
|
runtimeState._startDayOffset = null;
|
||||||
|
runtimeState.rundown.currentDay = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,7 +204,8 @@ export function updateRundownData(rundownData: {
|
|||||||
runtimeState.rundown.plannedStart = rundownData.firstStart;
|
runtimeState.rundown.plannedStart = rundownData.firstStart;
|
||||||
runtimeState.rundown.plannedEnd =
|
runtimeState.rundown.plannedEnd =
|
||||||
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
||||||
getExpectedTimes();
|
|
||||||
|
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,9 +244,14 @@ export function load(
|
|||||||
// patch with potential provided data
|
// patch with potential provided data
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
patchTimer(initialData);
|
patchTimer(initialData);
|
||||||
|
const startEpoch = initialData?.startEpoch;
|
||||||
const firstStart = initialData?.firstStart;
|
const firstStart = initialData?.firstStart;
|
||||||
if (firstStart === null || typeof firstStart === 'number') {
|
if (
|
||||||
|
(firstStart === null || typeof firstStart === 'number') &&
|
||||||
|
(startEpoch === null || typeof startEpoch === 'number')
|
||||||
|
) {
|
||||||
runtimeState.rundown.actualStart = firstStart;
|
runtimeState.rundown.actualStart = firstStart;
|
||||||
|
runtimeState._startEpoch = startEpoch;
|
||||||
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.offset.absolute = absolute;
|
runtimeState.offset.absolute = absolute;
|
||||||
runtimeState.offset.relative = relative;
|
runtimeState.offset.relative = relative;
|
||||||
@@ -367,7 +387,8 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.clock = timeNow();
|
const [epoch, now] = getTimeObject();
|
||||||
|
state.clock = now;
|
||||||
state.timer.secondaryTimer = null;
|
state.timer.secondaryTimer = null;
|
||||||
|
|
||||||
// add paused time if it exists
|
// add paused time if it exists
|
||||||
@@ -386,9 +407,16 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
state.timer.elapsed = 0;
|
state.timer.elapsed = 0;
|
||||||
|
|
||||||
if (state.rundown.actualStart === null) {
|
if (state.rundown.actualStart === null) {
|
||||||
|
state._startDayOffset = findDayOffset(state.eventNow.timeStart, state.clock);
|
||||||
|
state.rundown.currentDay = state._startDayOffset;
|
||||||
|
state._startEpoch = epoch;
|
||||||
state.rundown.actualStart = state.clock;
|
state.rundown.actualStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
|
||||||
|
state.rundown.actualGroupStart = state.clock;
|
||||||
|
}
|
||||||
|
|
||||||
// update timer phase
|
// update timer phase
|
||||||
runtimeState.timer.phase = getTimerPhase(runtimeState);
|
runtimeState.timer.phase = getTimerPhase(runtimeState);
|
||||||
|
|
||||||
@@ -480,13 +508,20 @@ export type UpdateResult = {
|
|||||||
export function update(): UpdateResult {
|
export function update(): UpdateResult {
|
||||||
// 0. there are some things we always do
|
// 0. there are some things we always do
|
||||||
const previousClock = runtimeState.clock;
|
const previousClock = runtimeState.clock;
|
||||||
runtimeState.clock = timeNow(); // we update the clock on every update call
|
const [epoch, fromMidnight] = getTimeObject();
|
||||||
|
runtimeState.clock = fromMidnight; // we update the clock on every update call
|
||||||
|
|
||||||
// 1. is playback idle?
|
// 1. is playback idle?
|
||||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||||
return updateIfIdle();
|
return updateIfIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we are playing and playback changes. we tick the current runtime day
|
||||||
|
if (runtimeState._startDayOffset !== null && runtimeState._startEpoch) {
|
||||||
|
runtimeState.rundown.currentDay =
|
||||||
|
runtimeState._startDayOffset + Math.floor((epoch - runtimeState._startEpoch) / dayInMs);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. are we waiting to roll?
|
// 2. are we waiting to roll?
|
||||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||||
const hasCrossedMidnight = previousClock > runtimeState.clock;
|
const hasCrossedMidnight = previousClock > runtimeState.clock;
|
||||||
@@ -704,7 +739,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
if (_group !== null) {
|
if (_group !== null) {
|
||||||
const { event: lastEvent, accumulatedGap, isLinkedToLoaded } = _group;
|
const { event: lastEvent, accumulatedGap, isLinkedToLoaded } = _group;
|
||||||
const lastEventExpectedStart = getExpectedStart(lastEvent, {
|
const lastEventExpectedStart = getExpectedStart(lastEvent, {
|
||||||
currentDay: eventNow.dayOffset,
|
currentDay: state.rundown.currentDay!,
|
||||||
totalGap: accumulatedGap,
|
totalGap: accumulatedGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
mode: offset.mode,
|
mode: offset.mode,
|
||||||
@@ -721,7 +756,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
if (_flag) {
|
if (_flag) {
|
||||||
const { event, accumulatedGap, isLinkedToLoaded } = _flag;
|
const { event, accumulatedGap, isLinkedToLoaded } = _flag;
|
||||||
const expectedStart = getExpectedStart(event, {
|
const expectedStart = getExpectedStart(event, {
|
||||||
currentDay: eventNow.dayOffset,
|
currentDay: state.rundown.currentDay!,
|
||||||
totalGap: accumulatedGap,
|
totalGap: accumulatedGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
mode: offset.mode,
|
mode: offset.mode,
|
||||||
@@ -736,7 +771,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
if (state._end) {
|
if (state._end) {
|
||||||
const { event, accumulatedGap, isLinkedToLoaded } = state._end;
|
const { event, accumulatedGap, isLinkedToLoaded } = state._end;
|
||||||
const expectedStart = getExpectedStart(event, {
|
const expectedStart = getExpectedStart(event, {
|
||||||
currentDay: eventNow.dayOffset,
|
currentDay: state.rundown.currentDay!,
|
||||||
totalGap: accumulatedGap,
|
totalGap: accumulatedGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
mode: offset.mode,
|
mode: offset.mode,
|
||||||
@@ -752,16 +787,19 @@ export function loadGroupFlagAndEnd(
|
|||||||
rundown: Rundown,
|
rundown: Rundown,
|
||||||
metadata: RundownMetadata,
|
metadata: RundownMetadata,
|
||||||
currentIndex: MaybeNumber,
|
currentIndex: MaybeNumber,
|
||||||
state = runtimeState,
|
state = runtimeState, // used for testing
|
||||||
) {
|
) {
|
||||||
|
const previousGroup = state.groupNow?.id;
|
||||||
state.groupNow = null;
|
state.groupNow = null;
|
||||||
state._group = null;
|
state._group = null;
|
||||||
state.eventFlag = null;
|
state.eventFlag = null;
|
||||||
state._flag = null;
|
state._flag = null;
|
||||||
state._end = null;
|
state._end = null;
|
||||||
|
|
||||||
if (currentIndex == null) return;
|
if (currentIndex === null || state.eventNow === null) {
|
||||||
if (state.eventNow === null) return;
|
state.rundown.actualGroupStart = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentGroupId = state.eventNow.parent;
|
const currentGroupId = state.eventNow.parent;
|
||||||
const flagsPresent = metadata.flags.length !== 0;
|
const flagsPresent = metadata.flags.length !== 0;
|
||||||
@@ -773,6 +811,10 @@ export function loadGroupFlagAndEnd(
|
|||||||
state.groupNow = currentGroupId ? (entries[currentGroupId] as OntimeGroup) : null;
|
state.groupNow = currentGroupId ? (entries[currentGroupId] as OntimeGroup) : null;
|
||||||
const lastEventInGroup = orderInGroup ? getLastEventNormal(rundown.entries, orderInGroup).lastEvent : null;
|
const lastEventInGroup = orderInGroup ? getLastEventNormal(rundown.entries, orderInGroup).lastEvent : null;
|
||||||
|
|
||||||
|
if (previousGroup !== currentGroupId) {
|
||||||
|
state.rundown.actualGroupStart = null;
|
||||||
|
}
|
||||||
|
|
||||||
// if we don't have a any flags in the rundown then no need to look for it
|
// if we don't have a any flags in the rundown then no need to look for it
|
||||||
let foundFlag = !flagsPresent;
|
let foundFlag = !flagsPresent;
|
||||||
// if we don't have a last event for the group there is no need to find its end time
|
// if we don't have a last event for the group there is no need to find its end time
|
||||||
@@ -814,4 +856,5 @@ export function loadGroupFlagAndEnd(
|
|||||||
|
|
||||||
export function setOffsetMode(mode: OffsetMode) {
|
export function setOffsetMode(mode: OffsetMode) {
|
||||||
runtimeState.offset.mode = mode;
|
runtimeState.offset.mode = mode;
|
||||||
|
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,3 +74,18 @@ export function timeNow() {
|
|||||||
elapsed += now.getMilliseconds();
|
elapsed += now.getMilliseconds();
|
||||||
return elapsed;
|
return elapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current time from system
|
||||||
|
* @returns [number, number] - [epoch time, milliseconds since midnight]
|
||||||
|
*/
|
||||||
|
export function getTimeObject(): [number, number] {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// extract milliseconds since midnight
|
||||||
|
let elapsed = now.getHours() * 3600000;
|
||||||
|
elapsed += now.getMinutes() * 60000;
|
||||||
|
elapsed += now.getSeconds() * 1000;
|
||||||
|
elapsed += now.getMilliseconds();
|
||||||
|
return [now.getTime(), elapsed];
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,4 +72,6 @@ test('time until relative', async ({ page }) => {
|
|||||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
||||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
||||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Absolute' }).click();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type RundownState = {
|
|||||||
selectedEventIndex: MaybeNumber;
|
selectedEventIndex: MaybeNumber;
|
||||||
numEvents: number;
|
numEvents: number;
|
||||||
plannedStart: MaybeNumber;
|
plannedStart: MaybeNumber;
|
||||||
actualStart: MaybeNumber;
|
|
||||||
plannedEnd: MaybeNumber;
|
plannedEnd: MaybeNumber;
|
||||||
|
actualStart: MaybeNumber;
|
||||||
|
currentDay: MaybeNumber;
|
||||||
|
actualGroupStart: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
|
|||||||
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
|
||||||
|
actualGroupStart: null, // maybe set once we start the timer
|
||||||
|
currentDay: null,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
absolute: 0, // changes at runtime
|
absolute: 0, // changes at runtime
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OffsetMode } from 'ontime-types';
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
import { dayInMs } from './conversionUtils';
|
import { dayInMs, MILLIS_PER_HOUR } from './conversionUtils';
|
||||||
import { getExpectedStart } from './getExpectedStart';
|
import { getExpectedStart } from './getExpectedStart';
|
||||||
|
|
||||||
describe('getExpectedStart()', () => {
|
describe('getExpectedStart()', () => {
|
||||||
@@ -277,4 +277,41 @@ describe('getExpectedStart()', () => {
|
|||||||
// the overlap will be pushed out to the expected available time
|
// the overlap will be pushed out to the expected available time
|
||||||
expect(getExpectedStart(testEvent, { ...testState, totalGap: -5 })).toBe(110);
|
expect(getExpectedStart(testEvent, { ...testState, totalGap: -5 })).toBe(110);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('we started on the day before', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 5,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: -1,
|
||||||
|
actualStart: 23 * MILLIS_PER_HOUR,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: -1 * MILLIS_PER_HOUR,
|
||||||
|
mode: OffsetMode.Absolute,
|
||||||
|
isLinkedToLoaded: true,
|
||||||
|
totalGap: 0,
|
||||||
|
};
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState })).toBe(23 * MILLIS_PER_HOUR + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next day in multi-day rundown', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 5,
|
||||||
|
dayOffset: 1,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: -1,
|
||||||
|
actualStart: 23 * MILLIS_PER_HOUR,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: -1 * MILLIS_PER_HOUR,
|
||||||
|
mode: OffsetMode.Absolute,
|
||||||
|
isLinkedToLoaded: true,
|
||||||
|
totalGap: 0,
|
||||||
|
};
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState })).toBe(23 * MILLIS_PER_HOUR + 5 + dayInMs);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, currentDay: 0 })).toBe(23 * MILLIS_PER_HOUR + 5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { dayInMs } from './conversionUtils.js';
|
|||||||
export function getExpectedStart(
|
export function getExpectedStart(
|
||||||
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'>,
|
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'>,
|
||||||
state: {
|
state: {
|
||||||
currentDay: number;
|
currentDay: number; // the current day from the rundown
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
offset: number;
|
offset: number;
|
||||||
@@ -38,7 +38,7 @@ export function getExpectedStart(
|
|||||||
let relativeStartOffset = 0;
|
let relativeStartOffset = 0;
|
||||||
|
|
||||||
if (mode === OffsetMode.Relative) {
|
if (mode === OffsetMode.Relative) {
|
||||||
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
relativeStartOffset = (actualStart ?? 0) + currentDay * dayInMs - (plannedStart ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
|
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
|
||||||
|
|||||||
Reference in New Issue
Block a user