mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
@@ -17,7 +17,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||
if (typeof delayValue === 'number') {
|
||||
if (delayValue < 0) {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||
<span className={style.delaySymbol}>
|
||||
<IoChevronDown />
|
||||
</span>
|
||||
@@ -27,7 +27,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||
|
||||
if (delayValue > 0) {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||
<span className={style.delaySymbol}>
|
||||
<IoChevronUp />
|
||||
</span>
|
||||
|
||||
@@ -165,10 +165,23 @@ export const setClientName = (newName: string) => socketSendJson('set-client-nam
|
||||
|
||||
export const useRuntimeOverview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
clock: state.clock,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
plannedStart: state.runtime.plannedStart,
|
||||
actualStart: state.runtime.actualStart,
|
||||
plannedEnd: state.runtime.plannedEnd,
|
||||
expectedEnd: state.runtime.expectedEnd,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useRuntimePlaybackOverview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
clock: state.clock,
|
||||
|
||||
numEvents: state.runtime.numEvents,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
offset: state.runtime.offset,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
|
||||
@@ -37,8 +37,13 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
numEvents: 0,
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
offset: 0,
|
||||
plannedStart: 0,
|
||||
plannedEnd: 0,
|
||||
actualStart: null,
|
||||
expectedEnd: null,
|
||||
},
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
|
||||
@@ -260,17 +260,17 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('millisToDelayString()', () => {
|
||||
it('returns null for null values', () => {
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
expect(millisToDelayString(null)).toBe('');
|
||||
});
|
||||
it('returns null 0', () => {
|
||||
expect(millisToDelayString(0)).toBeNull();
|
||||
expect(millisToDelayString(0)).toBe('');
|
||||
});
|
||||
describe('converts values in seconds', () => {
|
||||
it('shows a simple string with value in seconds', () => {
|
||||
expect(millisToDelayString(10000, true)).toBe('+10 sec');
|
||||
expect(millisToDelayString(10000)).toBe('+10 sec');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-10000, true)).toBe('-10 sec');
|
||||
expect(millisToDelayString(-10000)).toBe('-10 sec');
|
||||
});
|
||||
|
||||
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
|
||||
@@ -279,37 +279,36 @@ describe('millisToDelayString()', () => {
|
||||
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
|
||||
});
|
||||
});
|
||||
expect(millisToDelayString(null)).toBeNull();
|
||||
});
|
||||
|
||||
describe('converts values in minutes', () => {
|
||||
it('shows a simple string with value in minutes', () => {
|
||||
expect(millisToDelayString(720000, true)).toBe('+12 min');
|
||||
expect(millisToDelayString(720000)).toBe('+12 min');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-720000, true)).toBe('-12 min');
|
||||
expect(millisToDelayString(-720000)).toBe('-12 min');
|
||||
});
|
||||
it('shows a simple string with value in minutes and seconds', () => {
|
||||
expect(millisToDelayString(630000, true)).toBe('+00:10:30');
|
||||
expect(millisToDelayString(630000)).toBe('+00:10:30');
|
||||
});
|
||||
it('... and its negative counterpart', () => {
|
||||
expect(millisToDelayString(-630000, true)).toBe('-00:10:30');
|
||||
expect(millisToDelayString(-630000)).toBe('-00:10:30');
|
||||
});
|
||||
|
||||
const underAnHour = [60000, 360000, 720000];
|
||||
underAnHour.forEach((value) => {
|
||||
it(`handles ${value}`, () => {
|
||||
expect(millisToDelayString(value, true)?.endsWith('min')).toBe(true);
|
||||
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('converts values with full time string', () => {
|
||||
it('positive added time', () => {
|
||||
expect(millisToDelayString(45015000, true)).toBe('+12:30:15');
|
||||
expect(millisToDelayString(45015000)).toBe('+12:30:15');
|
||||
});
|
||||
it('negative added time', () => {
|
||||
expect(millisToDelayString(-45015000, true)).toBe('-12:30:15');
|
||||
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,13 +16,13 @@ describe('nowInMillis()', () => {
|
||||
describe('formatTime()', () => {
|
||||
it('parses 24h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const time = formatTime(ms, {format12: "hh:mm:ss", format24: "HH:mm:ss" }, (_format12, format24) => format24);
|
||||
const time = formatTime(ms, { format12: 'hh:mm:ss', format24: 'HH:mm:ss' }, (_format12, format24) => format24);
|
||||
expect(time).toStrictEqual('13:00:00');
|
||||
});
|
||||
|
||||
it('parses same string in 12h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const time = formatTime(ms, {format12: "hh:mm:ss a", format24: "HH:mm:ss" }, (format12, _format24) => format12);
|
||||
const time = formatTime(ms, { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' }, (format12, _format24) => format12);
|
||||
expect(time).toStrictEqual('01:00:00 PM');
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('formatTime()', () => {
|
||||
|
||||
it('handles negative times', () => {
|
||||
const ms = 1 * 60 * 60 * 1000;
|
||||
const time = formatTime(-ms, {format12: "hh:mm a", format24: "HH:mm" }, (_format12, format24) => format24);
|
||||
const time = formatTime(-ms, { format12: 'hh:mm a', format24: 'HH:mm' }, (_format12, format24) => format24);
|
||||
expect(time).toStrictEqual('-01:00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
@@ -55,7 +56,9 @@ function checkMatchers(value: string) {
|
||||
const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0;
|
||||
|
||||
if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) {
|
||||
return hoursMatchValue * MILLIS_PER_HOUR + minutesMatchValue * MILLIS_PER_MINUTE + secondsMatchValue * MILLIS_PER_SECOND;
|
||||
return (
|
||||
hoursMatchValue * MILLIS_PER_HOUR + minutesMatchValue * MILLIS_PER_MINUTE + secondsMatchValue * MILLIS_PER_SECOND
|
||||
);
|
||||
}
|
||||
return { hoursMatchValue };
|
||||
}
|
||||
@@ -155,21 +158,22 @@ export const forgivingStringToMillis = (value: string): number => {
|
||||
return millis;
|
||||
};
|
||||
|
||||
export function millisToDelayString(millis: number | null, small = false): undefined | string | null {
|
||||
export function millisToDelayString(millis: MaybeNumber, format: 'compact' | 'expanded' = 'compact'): string {
|
||||
if (millis == null || millis === 0) {
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
|
||||
const isNegative = millis < 0;
|
||||
const absMillis = Math.abs(millis);
|
||||
const delayed = small ? '+' : 'delayed by ';
|
||||
const ahead = small ? '-' : 'ahead by ';
|
||||
const isCompact = format === 'compact';
|
||||
const delayed = isCompact ? '+' : 'delayed by ';
|
||||
const ahead = isCompact ? '-' : 'ahead by ';
|
||||
|
||||
if (absMillis < MILLIS_PER_MINUTE) {
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 's')} sec`;
|
||||
} else if (absMillis < MILLIS_PER_HOUR && absMillis % MILLIS_PER_MINUTE === 0) {
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'm')} min`;
|
||||
} else {
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
|
||||
}
|
||||
|
||||
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
|
||||
}
|
||||
|
||||
@@ -29,3 +29,7 @@ export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
||||
* @param classNames - css modules objects
|
||||
*/
|
||||
export const cx = (classNames: any[]) => classNames.filter(Boolean).join(' ');
|
||||
|
||||
export const enDash = '–';
|
||||
|
||||
export const timerPlaceholder = '––:––:––';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
|
||||
|
||||
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
|
||||
import { enDash } from '../../../common/utils/styleUtils';
|
||||
|
||||
import InputRow from './InputRow';
|
||||
|
||||
@@ -64,7 +65,7 @@ export default function MessageControl() {
|
||||
</div>
|
||||
<InputRow
|
||||
label='External Message'
|
||||
placeholder='-'
|
||||
placeholder={enDash}
|
||||
readonly
|
||||
text={message.external.text || ''}
|
||||
visible={message.external.visible || false}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import { cx, timerPlaceholder } from '../../../../common/utils/styleUtils';
|
||||
|
||||
import style from './TimerDisplay.module.scss';
|
||||
|
||||
@@ -11,16 +11,17 @@ interface TimerDisplayProps {
|
||||
|
||||
/**
|
||||
* Displays time in ms in formatted timetag
|
||||
* Typically used in production views
|
||||
*/
|
||||
export default function TimerDisplay(props: TimerDisplayProps) {
|
||||
const { time } = props;
|
||||
|
||||
if (time == null) {
|
||||
return <div className={style.timer}>-- : -- : --</div>;
|
||||
return <div className={style.timer}>{timerPlaceholder}</div>;
|
||||
}
|
||||
|
||||
const isNegative = time < 0;
|
||||
const display = millisToString(Math.abs(time), { fallback: '-- : -- : --' });
|
||||
const display = millisToString(Math.abs(time), { fallback: timerPlaceholder });
|
||||
const classes = cx([style.timer, isNegative ? style.finished : null]);
|
||||
|
||||
return <div className={classes}>{display}</div>;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
$table-font-size: calc(1rem - 2px);
|
||||
$table-header-font-size: calc(1rem - 3px);
|
||||
|
||||
@mixin ellipsis-overflow() {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cuesheetContainer {
|
||||
grid-area: table;
|
||||
display: flex;
|
||||
|
||||
@@ -10,7 +10,7 @@ interface DelayRowProps {
|
||||
|
||||
function DelayRow(props: DelayRowProps) {
|
||||
const { duration } = props;
|
||||
const delayTime = millisToDelayString(duration);
|
||||
const delayTime = millisToDelayString(duration, 'expanded');
|
||||
|
||||
return (
|
||||
<tr className={style.delayRow}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Playback, ProjectData } from 'ontime-types';
|
||||
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
||||
import useFullscreen from '../../../common/hooks/useFullscreen';
|
||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { cx, enDash } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
import { useCuesheetSettings } from '../store/CuesheetSettings';
|
||||
|
||||
@@ -42,15 +42,15 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
|
||||
|
||||
const selected = !featureData.numEvents
|
||||
? 'No events'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : enDash}/${
|
||||
featureData.numEvents ? featureData.numEvents : enDash
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={style.header}>
|
||||
<div className={style.event}>
|
||||
<div className={style.title}>{project?.title || '-'}</div>
|
||||
<div className={style.eventNow}>{featureData?.titleNow || '-'}</div>
|
||||
<div className={style.title}>{project?.title || enDash}</div>
|
||||
<div className={style.eventNow}>{featureData?.titleNow || enDash}</div>
|
||||
</div>
|
||||
<div className={style.playback}>
|
||||
<div className={style.playbackLabel}>{selected}</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UseFormRegister } from 'react-hook-form';
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||
import { IoEyeOutline } from '@react-icons/all-files/io5/IoEyeOutline';
|
||||
|
||||
import { enDash } from '../../../common/utils/styleUtils';
|
||||
|
||||
interface FormInput {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -21,7 +23,7 @@ export default function ModalPinInput({ register, formName, isDisabled }: ModalP
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
maxLength={4}
|
||||
{...register(formName)}
|
||||
placeholder='-'
|
||||
placeholder={enDash}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<InputRightElement>
|
||||
|
||||
@@ -2,54 +2,26 @@
|
||||
grid-area: overview;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
justify-content: space-between;
|
||||
font-size: $inner-section-text-size;
|
||||
gap: 2rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.titles {
|
||||
flex: 1;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
color: $label-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
.ahead {
|
||||
color: $green-500;
|
||||
}
|
||||
|
||||
@mixin indicator($bg-color) {
|
||||
&::before {
|
||||
content: '';
|
||||
background-color: $bg-color;
|
||||
display: inline-flex;
|
||||
height: 0.75em;
|
||||
width: 0.75em;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.start {
|
||||
@include indicator($green-500);
|
||||
}
|
||||
|
||||
.end {
|
||||
@include indicator($red-500);
|
||||
.behind {
|
||||
color: $ontime-delay-text;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import PlaybackIcon from '../../common/components/playback-icon/PlaybackIcon';
|
||||
import { useRuntimeOverview } from '../../common/hooks/useSocket';
|
||||
import { useRuntimeOverview, useRuntimePlaybackOverview } from '../../common/hooks/useSocket';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
import { enDash, timerPlaceholder } from '../../common/utils/styleUtils';
|
||||
|
||||
import styles from './Overview.module.scss';
|
||||
import { TimeColumn, TimeRow } from './composite/TimeLayout';
|
||||
|
||||
import style from './Overview.module.scss';
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for formatting time in overview
|
||||
* @param time
|
||||
* @returns
|
||||
*/
|
||||
function formattedTime(time: MaybeNumber) {
|
||||
return millisToString(time, { fallback: timerPlaceholder });
|
||||
}
|
||||
|
||||
export default function Overview() {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={style.overview}>
|
||||
<ErrorBoundary>
|
||||
<TitlesOverview />
|
||||
<div className={styles.clocks}>
|
||||
<Tooltip label='Planned start'>
|
||||
<div className={styles.start}>Planned start</div>
|
||||
</Tooltip>
|
||||
<Tooltip label='Actual start'>
|
||||
<div className={styles.start}>Actual start</div>
|
||||
</Tooltip>
|
||||
<div className={style.column}>
|
||||
<TimeRow label='Planned start' value={formattedTime(plannedStart)} className={style.start} />
|
||||
<TimeRow label='Actual start' value={formattedTime(actualStart)} className={style.start} />
|
||||
</div>
|
||||
<RuntimeOverview />
|
||||
<div className={styles.clocks}>
|
||||
<Tooltip label='Planned end'>
|
||||
<div className={styles.end}>Planned end</div>
|
||||
</Tooltip>
|
||||
<Tooltip label='Expected end'>
|
||||
<div className={styles.end}>Expected end</div>
|
||||
</Tooltip>
|
||||
<div className={style.column}>
|
||||
<TimeRow label='Planned end' value={formattedTime(plannedEnd)} className={style.end} />
|
||||
<TimeRow label='Expected end' value={formattedTime(expectedEnd)} className={style.end} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@@ -39,31 +44,31 @@ function TitlesOverview() {
|
||||
const { data } = useProjectData();
|
||||
|
||||
return (
|
||||
<div className={styles.titles}>
|
||||
<div className={styles.title}>{data.title}</div>
|
||||
<div className={styles.description}>{data.description}</div>
|
||||
<div className={style.titles}>
|
||||
<div className={style.title}>{data.title}</div>
|
||||
<div className={style.description}>{data.description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeOverview() {
|
||||
const { playback, clock, numEvents, selectedEventIndex } = useRuntimeOverview();
|
||||
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
||||
|
||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : '-';
|
||||
const ofTotal = numEvents || '-';
|
||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||
const ofTotal = numEvents || enDash;
|
||||
const progressText = numEvents ? `${current} of ${ofTotal}` : '';
|
||||
|
||||
const display = formatTime(clock);
|
||||
const isAhead = offset <= 0;
|
||||
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
||||
if (offsetText !== enDash) {
|
||||
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.clocks}>
|
||||
<div className={styles.inline}>
|
||||
<PlaybackIcon state={playback} skipTooltip />
|
||||
{display}
|
||||
</div>
|
||||
<div className={styles.inline}>
|
||||
<span>{`(${current} / ${ofTotal})`}</span>
|
||||
Over / Under
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<TimeColumn label='Progress' value={progressText} />
|
||||
<TimeColumn label='Offset' value={offsetText} className={isAhead ? style.ahead : style.behind} />
|
||||
<TimeColumn label='Time now' value={formattedTime(clock)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.label {
|
||||
color: $label-gray;
|
||||
font-size: calc(1rem - 2px);
|
||||
width: 10em; // a number large enough to force right alignment
|
||||
}
|
||||
|
||||
.clock {
|
||||
text-align: left;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 5em;
|
||||
|
||||
&::after {
|
||||
content: '\200b';
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.label {
|
||||
line-height: 0.9em;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import style from './TimeLayout.module.scss';
|
||||
|
||||
interface TimeLayoutProps {
|
||||
label: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimeColumn({ label, value, className }: TimeLayoutProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<span className={cx([style.clock, className])}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeRow({ label, value, className }: TimeLayoutProps) {
|
||||
return (
|
||||
<div className={style.row}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<span className={cx([style.clock, className])}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -64,9 +64,9 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
|
||||
const hasDelay = delay !== 0;
|
||||
const delayLabel = hasDelay
|
||||
? `Event is ${millisToDelayString(delay)}. New schedule ${millisToString(timeStart + delay)} → ${millisToString(
|
||||
timeEnd + delay,
|
||||
)}`
|
||||
? `Event is ${millisToDelayString(delay, 'expanded')}. New schedule ${millisToString(
|
||||
timeStart + delay,
|
||||
)} → ${millisToString(timeEnd + delay)}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ import ViewParamsEditor from '../../../common/components/view-params-editor/View
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { OverridableOptions } from '../../../common/models/View.types';
|
||||
import { timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { getTimerByType } from '../common/viewerUtils';
|
||||
@@ -153,7 +154,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
: viewSettings.normalColor;
|
||||
|
||||
const stageTimer = getTimerByType(time);
|
||||
let display = millisToString(stageTimer, { fallback: '-- : -- : --' });
|
||||
let display = millisToString(stageTimer, { fallback: timerPlaceholder });
|
||||
if (stageTimer !== null) {
|
||||
if (hideTimerSeconds) {
|
||||
display = removeSeconds(display);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTimerOptions } from '../../../common/components/view-params-editor/c
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
@@ -115,7 +116,7 @@ export default function Timer(props: TimerProps) {
|
||||
: viewSettings.normalColor;
|
||||
|
||||
const stageTimer = getTimerByType(time);
|
||||
let display = millisToString(stageTimer, { fallback: '-- : -- : --' });
|
||||
let display = millisToString(stageTimer, { fallback: timerPlaceholder });
|
||||
if (stageTimer !== null) {
|
||||
if (hideTimerSeconds) {
|
||||
display = removeSeconds(display);
|
||||
|
||||
@@ -69,3 +69,9 @@ $min-tablet: 500px;
|
||||
opacity: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin ellipsis-overflow() {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||
import { restoreService } from './services/RestoreService.js';
|
||||
import { messageService } from './services/message-service/MessageService.js';
|
||||
import { populateDemo } from './modules/loadDemo.js';
|
||||
import { getState, updateNumEvents } from './stores/runtimeState.js';
|
||||
import { getNumEvents, setRundown } from './services/rundown-service/RundownService.js';
|
||||
import { getState, updateRundownData } from './stores/runtimeState.js';
|
||||
import { setRundown, getPlayableEvents } from './services/rundown-service/RundownService.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -184,8 +184,7 @@ export const startServer = async () => {
|
||||
setRundown(persistedRundown);
|
||||
|
||||
// TODO: do this on the init of the runtime service
|
||||
const numEvents = getNumEvents();
|
||||
updateNumEvents(numEvents);
|
||||
updateRundownData(getPlayableEvents());
|
||||
|
||||
// load restore point if it exists
|
||||
const maybeRestorePoint = await restoreService.load();
|
||||
|
||||
@@ -37,19 +37,6 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getIndexOf(eventId: string) {
|
||||
return data.rundown.findIndex((e) => e.id === eventId);
|
||||
}
|
||||
|
||||
static getRundownLength() {
|
||||
return data.rundown.length;
|
||||
}
|
||||
|
||||
static async clearRundown() {
|
||||
data.rundown = [];
|
||||
await db.write();
|
||||
}
|
||||
|
||||
static getSettings() {
|
||||
return data.settings;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type RestorePoint = {
|
||||
startedAt: MaybeNumber;
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
firstStart: MaybeNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,10 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof restorePoint.firstStart !== 'number' && restorePoint.pausedAt !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
startedAt: state.timer.startedAt,
|
||||
addedTime: state.timer.addedTime,
|
||||
pausedAt: state._timer.pausedAt,
|
||||
firstStart: state.runtime.actualStart,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('isRestorePoint()', () => {
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
pausedAt: 3,
|
||||
firstStart: 1,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
|
||||
@@ -22,6 +23,7 @@ describe('isRestorePoint()', () => {
|
||||
startedAt: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
firstStart: 1,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
});
|
||||
@@ -68,6 +70,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 5678,
|
||||
pausedAt: 9087,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -84,6 +87,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -100,6 +104,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -118,6 +123,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
normaliseEndTime,
|
||||
skippedOutOfEvent,
|
||||
updateRoll,
|
||||
@@ -1370,3 +1371,68 @@ describe('updateRoll()', () => {
|
||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimeOffset()', () => {
|
||||
it('calculates the difference between schedule and actual start', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
},
|
||||
timer: {
|
||||
startedAt: 150,
|
||||
addedTime: 10,
|
||||
current: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(60);
|
||||
});
|
||||
|
||||
it('adds the overtime time of the current timer', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
timeEnd: 140,
|
||||
},
|
||||
timer: {
|
||||
startedAt: 100,
|
||||
current: -10,
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(10);
|
||||
});
|
||||
|
||||
it('accounts for paused time', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
timeEnd: 150,
|
||||
},
|
||||
clock: 150,
|
||||
timer: {
|
||||
startedAt: 100,
|
||||
current: 25,
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: 125,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { block as blockDef, delay as delayDef } from '../../models/eventsDefinit
|
||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { createEvent } from '../../utils/parser.js';
|
||||
import { updateNumEvents } from '../../stores/runtimeState.js';
|
||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
@@ -159,8 +159,7 @@ export async function swapEvents(from: string, to: string) {
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateChangeNumEvents() {
|
||||
const numEvents = getPlayableEvents().length;
|
||||
updateNumEvents(numEvents);
|
||||
updateRundownData(getPlayableEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +285,10 @@ export function findNext(currentEventId?: string): OntimeEvent | null {
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the rundown with the given
|
||||
* @param rundown
|
||||
*/
|
||||
export async function setRundown(rundown: OntimeRundown) {
|
||||
cache.init(rundown);
|
||||
notifyChanges({ timer: true });
|
||||
|
||||
@@ -289,3 +289,23 @@ export const updateRoll = (state: RuntimeState) => {
|
||||
|
||||
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates difference between the runtime and the schedule of an event
|
||||
* @param state
|
||||
* @returns
|
||||
*/
|
||||
export function getRuntimeOffset(state: RuntimeState): number {
|
||||
if (state.eventNow === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { timeStart } = state.eventNow;
|
||||
const { addedTime, current, startedAt } = state.timer;
|
||||
|
||||
const overtime = Math.min(current, 0);
|
||||
const startOffset = startedAt - timeStart;
|
||||
const pausedTime = state._timer.pausedAt === null ? 0 : state.clock - state._timer.pausedAt;
|
||||
|
||||
return startOffset + addedTime + pausedTime + Math.abs(overtime);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import { RuntimeState, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
import { RuntimeState, addTime, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
|
||||
const mockEvent = {
|
||||
type: 'event',
|
||||
id: 'mock',
|
||||
cue: 'mock',
|
||||
timeStart: 0,
|
||||
@@ -88,6 +89,7 @@ describe('mutation on runtimeState', () => {
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Play,
|
||||
});
|
||||
expect(newState.runtime.actualStart).toBe(newState.clock);
|
||||
|
||||
// 3. Pause event
|
||||
success = pause();
|
||||
@@ -122,7 +124,7 @@ describe('mutation on runtimeState', () => {
|
||||
);
|
||||
expect(newState._timer.pausedAt).toBeNull();
|
||||
|
||||
// 4. Stop event
|
||||
// 5. Stop event
|
||||
success = stop();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.eventNow).toBe(null);
|
||||
@@ -133,8 +135,55 @@ describe('mutation on runtimeState', () => {
|
||||
expectedFinish: null,
|
||||
startedAt: null,
|
||||
});
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
});
|
||||
|
||||
test('runtime offset', () => {
|
||||
const event1 = { ...mockEvent, id: 'event1', timeStart: 0, timeEnd: 1000, duration: 1000 };
|
||||
const event2 = { ...mockEvent, id: 'event2', timeStart: 1000, timeEnd: 1500, duration: 500 };
|
||||
|
||||
// 1. Load event
|
||||
load(event1, [event1, event2]);
|
||||
let newState = getState();
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
expect(newState.runtime.plannedStart).toBe(0);
|
||||
expect(newState.runtime.plannedEnd).toBe(1500);
|
||||
|
||||
// 2. Start event
|
||||
start();
|
||||
newState = getState();
|
||||
const firstStart = newState.clock;
|
||||
expect(newState.runtime.actualStart).toBe(newState.clock);
|
||||
expect(newState.runtime.offset).toBe(newState.clock - event1.timeStart);
|
||||
expect(newState.runtime.expectedEnd).toBe(newState.runtime.offset + event2.timeEnd);
|
||||
|
||||
// 3. Next event
|
||||
load(event2, [event1, event2]);
|
||||
start();
|
||||
newState = getState();
|
||||
expect(newState.runtime.actualStart).toBe(firstStart);
|
||||
// we are over-under, the difference between the schedule and the actual start
|
||||
const delayBefore = newState.clock - event2.timeStart;
|
||||
expect(newState.runtime.offset).toBe(delayBefore);
|
||||
// finish is the difference between the runtime and the schedule
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd + newState.runtime.offset);
|
||||
|
||||
// 4. Add time
|
||||
addTime(10);
|
||||
newState = getState();
|
||||
expect(newState.runtime.offset).toBe(delayBefore + 10);
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd + newState.runtime.offset);
|
||||
|
||||
// 5. Stop event
|
||||
stop();
|
||||
newState = getState();
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
expect(newState.runtime.offset).toBe(0);
|
||||
expect(newState.runtime.expectedEnd).toBeNull();
|
||||
});
|
||||
|
||||
test.todo('runtime offset on timers in overtime', () => {});
|
||||
|
||||
test.todo('roll mode', () => {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { Runtime, OntimeEvent, Playback, TimerState, TimerType, MaybeNumber } from 'ontime-types';
|
||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||
import { calculateDuration, dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { RestorePoint } from '../services/RestoreService.js';
|
||||
import { getPlayableEvents } from '../services/rundown-service/RundownService.js';
|
||||
import { getCurrent, getExpectedFinish, getRollTimers, skippedOutOfEvent, updateRoll } from '../services/timerUtils.js';
|
||||
import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
skippedOutOfEvent,
|
||||
updateRoll,
|
||||
} from '../services/timerUtils.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
offset: 0,
|
||||
plannedStart: 0,
|
||||
plannedEnd: 0,
|
||||
actualStart: null,
|
||||
expectedEnd: null,
|
||||
};
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
@@ -64,13 +76,13 @@ export function getState(): Readonly<RuntimeState> {
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
// TODO: check that entire state is reset here
|
||||
runtimeState.eventNow = null;
|
||||
runtimeState.publicEventNow = null;
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
runtimeState.runtime = { ...initialRuntime };
|
||||
runtimeState.runtime = { ...initialRuntime, actualStart: runtimeState.runtime.actualStart };
|
||||
// TODO: can we cleanup the initialisation of runtime state?
|
||||
runtimeState.runtime.numEvents = fetchNumEvents();
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
@@ -106,11 +118,17 @@ function fetchNumEvents(): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility, allows updating the number of events
|
||||
* Utility, allows updating data derived from the rundown
|
||||
* @param numEvents
|
||||
*/
|
||||
export function updateNumEvents(numEvents: number) {
|
||||
runtimeState.runtime.numEvents = numEvents;
|
||||
export function updateRundownData(playableRundown: OntimeEvent[]) {
|
||||
runtimeState.runtime.numEvents = playableRundown.length;
|
||||
|
||||
const { firstEvent } = getFirstEvent(playableRundown);
|
||||
const { lastEvent } = getLastEvent(playableRundown);
|
||||
|
||||
runtimeState.runtime.plannedStart = firstEvent?.timeStart ?? null;
|
||||
runtimeState.runtime.plannedEnd = lastEvent?.timeEnd ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,9 +137,11 @@ export function updateNumEvents(numEvents: number) {
|
||||
* @param rundown
|
||||
* @param initialData
|
||||
*/
|
||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState>) {
|
||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState & RestorePoint>) {
|
||||
clear();
|
||||
|
||||
updateRundownData(rundown);
|
||||
|
||||
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
|
||||
runtimeState.runtime.selectedEventIndex = eventIndex;
|
||||
@@ -137,6 +157,13 @@ export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: P
|
||||
|
||||
if (initialData) {
|
||||
patchTimer(initialData);
|
||||
|
||||
const firstStart = initialData?.firstStart;
|
||||
if (firstStart === null || typeof firstStart === 'number') {
|
||||
runtimeState.runtime.actualStart = firstStart;
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +283,15 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
state.timer.playback = Playback.Play;
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
state.timer.elapsed = 0;
|
||||
|
||||
// update runtime delays: over - under
|
||||
if (state.runtime.actualStart === null) {
|
||||
state.runtime.actualStart = state.clock;
|
||||
}
|
||||
|
||||
state.runtime.offset = getRuntimeOffset(state);
|
||||
state.runtime.expectedEnd = state.runtime.plannedEnd + state.runtime.offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -274,6 +310,7 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback === Playback.Stop) {
|
||||
return false;
|
||||
}
|
||||
runtimeState.runtime.actualStart = null;
|
||||
clear();
|
||||
return true;
|
||||
}
|
||||
@@ -298,6 +335,10 @@ export function addTime(amount: number) {
|
||||
runtimeState.timer.finishedAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// update runtime delays: over - under
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,6 +359,9 @@ export function update(force: boolean, updateInterval: number) {
|
||||
_force = true;
|
||||
}
|
||||
|
||||
// update offset
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
|
||||
// we call integrations if we update timers
|
||||
if (runtimeState.timer.playback === Playback.Roll) {
|
||||
const result = roll();
|
||||
|
||||
@@ -3,4 +3,9 @@ import { MaybeNumber } from '../../utils/utils.type.js';
|
||||
export type Runtime = {
|
||||
numEvents: number;
|
||||
selectedEventIndex: MaybeNumber;
|
||||
offset: number;
|
||||
plannedStart: MaybeNumber;
|
||||
actualStart: MaybeNumber;
|
||||
plannedEnd: MaybeNumber;
|
||||
expectedEnd: MaybeNumber;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
||||
import { getLastEvent, getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
||||
|
||||
describe('getNext()', () => {
|
||||
it('returns the next event of type event', () => {
|
||||
@@ -189,3 +189,23 @@ describe('swapEventData', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastEvent', () => {
|
||||
it('returns the last event of type event', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
{ id: '4', type: SupportedEvent.Block },
|
||||
];
|
||||
|
||||
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||
expect(lastEvent?.id).toBe('3');
|
||||
});
|
||||
it('handles rundowns with a single event', () => {
|
||||
const testRundown = [{ id: '1', type: SupportedEvent.Event }];
|
||||
|
||||
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||
expect(lastEvent?.id).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ export function getLastEvent(rundown: OntimeRundown): {
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
for (let i = rundown.length - 1; i > 0; i--) {
|
||||
for (let i = rundown.length - 1; i >= 0; i--) {
|
||||
const lastEvent = rundown.at(i);
|
||||
if (isOntimeEvent(lastEvent)) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
@@ -100,7 +100,7 @@ export function getLastEventNormal(
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
for (let i = order.length - 1; i > 0; i--) {
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const lastId = order[i];
|
||||
const lastEvent = rundown[lastId];
|
||||
if (isOntimeEvent(lastEvent)) {
|
||||
|
||||
Reference in New Issue
Block a user