Compare commits

...

2 Commits

Author SHA1 Message Date
Carlos Valente 9813643c96 feat: parse timeToEnd from excel 2024-12-16 13:38:49 +01:00
Carlos Valente b88c2a6bd8 refactor: extract time-to-end 2024-12-16 13:38:46 +01:00
36 changed files with 451 additions and 254 deletions
@@ -65,6 +65,7 @@ export const useMessagePreview = () => {
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external), showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text), showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
timerType: state.eventNow?.timerType ?? null, timerType: state.eventNow?.timerType ?? null,
isTimeToEnd: state.eventNow?.isTimeToEnd ?? false,
}); });
return useRuntimeStore(featureSelector); return useRuntimeStore(featureSelector);
@@ -15,4 +15,5 @@ export type ViewExtendedTimer = {
clock: number; clock: number;
timerType: TimerType; timerType: TimerType;
isTimeToEnd: boolean;
}; };
@@ -17,6 +17,7 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
timeEnd: event.timeEnd, timeEnd: event.timeEnd,
timerType: event.timerType, timerType: event.timerType,
timeStrategy: event.timeStrategy, timeStrategy: event.timeStrategy,
isTimeToEnd: event.isTimeToEnd,
linkStart: event.linkStart, linkStart: event.linkStart,
endAction: event.endAction, endAction: event.endAction,
isPublic: event.isPublic, isPublic: event.isPublic,
@@ -88,7 +88,6 @@ export default function EditorSettingsForm() {
> >
<option value={TimerType.CountDown}>Count down</option> <option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option> <option value={TimerType.CountUp}>Count up</option>
<option value={TimerType.TimeToEnd}>Time to end</option>
<option value={TimerType.Clock}>Clock</option> <option value={TimerType.Clock}>Clock</option>
<option value={TimerType.None}>None</option> <option value={TimerType.None}>None</option>
</Select> </Select>
@@ -11,6 +11,7 @@ export const namedImportMap = {
Duration: 'duration', Duration: 'duration',
Cue: 'cue', Cue: 'cue',
Title: 'title', Title: 'title',
'Time to end': 'time to end',
'Is Public': 'public', 'Is Public': 'public',
Skip: 'skip', Skip: 'skip',
Note: 'notes', Note: 'notes',
@@ -47,6 +48,7 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
duration: namedImportMap.Duration, duration: namedImportMap.Duration,
cue: namedImportMap.Cue, cue: namedImportMap.Cue,
title: namedImportMap.Title, title: namedImportMap.Title,
isTimeToEnd: namedImportMap['Time to end'],
isPublic: namedImportMap['Is Public'], isPublic: namedImportMap['Is Public'],
skip: namedImportMap.Skip, skip: namedImportMap.Skip,
note: namedImportMap.Note, note: namedImportMap.Note,
@@ -40,6 +40,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<th>Duration</th> <th>Duration</th>
<th>Warning Time</th> <th>Warning Time</th>
<th>Danger Time</th> <th>Danger Time</th>
<th>Is Time to End</th>
<th>Is Public</th> <th>Is Public</th>
<th>Skip</th> <th>Skip</th>
<th>Colour</th> <th>Colour</th>
@@ -71,6 +72,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
} }
eventIndex += 1; eventIndex += 1;
const colour = event.colour ? getAccessibleColour(event.colour) : {}; const colour = event.colour ? getAccessibleColour(event.colour) : {};
const isTimeToEnd = booleanToText(event.isTimeToEnd);
const isPublic = booleanToText(event.isPublic); const isPublic = booleanToText(event.isPublic);
const skip = booleanToText(event.skip); const skip = booleanToText(event.skip);
@@ -93,6 +95,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<td>{millisToString(event.duration)}</td> <td>{millisToString(event.duration)}</td>
<td>{millisToString(event.timeWarning)}</td> <td>{millisToString(event.timeWarning)}</td>
<td>{millisToString(event.timeDanger)}</td> <td>{millisToString(event.timeDanger)}</td>
<td className={style.center}>{isTimeToEnd && <Tag>{isTimeToEnd}</Tag>}</td>
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td> <td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
<td>{skip && <Tag>{skip}</Tag>}</td> <td>{skip && <Tag>{skip}</Tag>}</td>
<td style={{ ...colour }}>{event.colour}</td> <td style={{ ...colour }}>{event.colour}</td>
@@ -16,7 +16,7 @@ import { Corner } from '../../editors/editor-utils/EditorUtils';
import style from './MessageControl.module.scss'; import style from './MessageControl.module.scss';
export default function TimerPreview() { export default function TimerPreview() {
const { blink, blackout, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } = const { blink, blackout, isTimeToEnd, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } =
useMessagePreview(); useMessagePreview();
const { data } = useViewSettings(); const { data } = useViewSettings();
@@ -24,11 +24,11 @@ export default function TimerPreview() {
const main = (() => { const main = (() => {
if (showTimerMessage) return 'Message'; if (showTimerMessage) return 'Message';
if (timerType === TimerType.None) return timerPlaceholder;
if (phase === TimerPhase.Pending) return 'Standby to start'; if (phase === TimerPhase.Pending) return 'Standby to start';
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message'; if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
if (timerType === TimerType.TimeToEnd) return 'Time to end';
if (timerType === TimerType.Clock) return 'Clock'; if (timerType === TimerType.Clock) return 'Clock';
if (timerType === TimerType.None) return timerPlaceholder; if (isTimeToEnd) return 'Target event scheduled end';
return 'Timer'; return 'Timer';
})(); })();
@@ -74,12 +74,16 @@ export default function TimerPreview() {
<Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren> <Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoTime className={style.statusIcon} data-active={timerType === TimerType.Clock} /> <IoTime className={style.statusIcon} data-active={timerType === TimerType.Clock} />
</Tooltip> </Tooltip>
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoFlag className={style.statusIcon} data-active={timerType === TimerType.TimeToEnd} />
</Tooltip>
<Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren> <Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoBan className={style.statusIcon} data-active={timerType === TimerType.None} /> <IoBan className={style.statusIcon} data-active={timerType === TimerType.None} />
</Tooltip> </Tooltip>
<Tooltip
label={isTimeToEnd ? 'Count to schedule' : 'Count from start'}
openDelay={tooltipDelayMid}
shouldWrapChildren
>
<IoFlag className={style.statusIcon} data-active={isTimeToEnd} />
</Tooltip>
</div> </div>
</div> </div>
); );
@@ -27,7 +27,7 @@
.title { .title {
font-size: 1rem; font-size: 1rem;
color: $label-gray; color: $ui-white;
} }
.label { .label {
@@ -150,6 +150,7 @@ export default function RundownEntry(props: RundownEntryProps) {
if (data.type === SupportedEvent.Event) { if (data.type === SupportedEvent.Event) {
return ( return (
<EventBlock <EventBlock
eventId={data.id}
eventIndex={eventIndex} eventIndex={eventIndex}
cue={data.cue} cue={data.cue}
timeStart={data.timeStart} timeStart={data.timeStart}
@@ -157,7 +158,7 @@ export default function RundownEntry(props: RundownEntryProps) {
duration={data.duration} duration={data.duration}
timeStrategy={data.timeStrategy} timeStrategy={data.timeStrategy}
linkStart={data.linkStart} linkStart={data.linkStart}
eventId={data.id} isTimeToEnd={data.isTimeToEnd}
isPublic={data.isPublic} isPublic={data.isPublic}
endAction={data.endAction} endAction={data.endAction}
timerType={data.timerType} timerType={data.timerType}
@@ -24,13 +24,14 @@ import RundownIndicators from './RundownIndicators';
import style from './EventBlock.module.scss'; import style from './EventBlock.module.scss';
interface EventBlockProps { interface EventBlockProps {
eventId: string;
cue: string; cue: string;
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
duration: number; duration: number;
timeStrategy: TimeStrategy; timeStrategy: TimeStrategy;
linkStart: MaybeString; linkStart: MaybeString;
eventId: string; isTimeToEnd: boolean;
eventIndex: number; eventIndex: number;
isPublic: boolean; isPublic: boolean;
endAction: EndAction; endAction: EndAction;
@@ -68,6 +69,7 @@ export default function EventBlock(props: EventBlockProps) {
duration, duration,
timeStrategy, timeStrategy,
linkStart, linkStart,
isTimeToEnd,
isPublic = true, isPublic = true,
eventIndex, eventIndex,
endAction, endAction,
@@ -286,6 +288,7 @@ export default function EventBlock(props: EventBlockProps) {
timeEnd={timeEnd} timeEnd={timeEnd}
duration={duration} duration={duration}
linkStart={linkStart} linkStart={linkStart}
isTimeToEnd={isTimeToEnd}
timeStrategy={timeStrategy} timeStrategy={timeStrategy}
eventId={eventId} eventId={eventId}
eventIndex={eventIndex} eventIndex={eventIndex}
@@ -23,12 +23,13 @@ import EventBlockProgressBar from './composite/EventBlockProgressBar';
import style from './EventBlock.module.scss'; import style from './EventBlock.module.scss';
interface EventBlockInnerProps { interface EventBlockInnerProps {
eventId: string;
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
duration: number; duration: number;
timeStrategy: TimeStrategy; timeStrategy: TimeStrategy;
linkStart: MaybeString; linkStart: MaybeString;
eventId: string; isTimeToEnd: boolean;
eventIndex: number; eventIndex: number;
isPublic: boolean; isPublic: boolean;
endAction: EndAction; endAction: EndAction;
@@ -43,14 +44,15 @@ interface EventBlockInnerProps {
isRolling: boolean; isRolling: boolean;
} }
const EventBlockInner = (props: EventBlockInnerProps) => { function EventBlockInner(props: EventBlockInnerProps) {
const { const {
eventId,
timeStart, timeStart,
timeEnd, timeEnd,
duration, duration,
timeStrategy, timeStrategy,
linkStart, linkStart,
eventId, isTimeToEnd,
isPublic = true, isPublic = true,
endAction, endAction,
timerType, timerType,
@@ -91,7 +93,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
delay={delay} delay={delay}
timeStrategy={timeStrategy} timeStrategy={timeStrategy}
linkStart={linkStart} linkStart={linkStart}
timerType={timerType} isTimeToEnd={isTimeToEnd}
/> />
</div> </div>
<div className={style.titleSection}> <div className={style.titleSection}>
@@ -122,6 +124,11 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
<EndActionIcon action={endAction} className={style.statusIcon} /> <EndActionIcon action={endAction} className={style.statusIcon} />
</span> </span>
</Tooltip> </Tooltip>
<Tooltip label={`${isTimeToEnd ? 'Time to End' : 'Count from start'}`} openDelay={tooltipDelayMid}>
<span>
<IoFlag className={`${style.statusIcon} ${isTimeToEnd ? style.active : style.disabled}`} />
</span>
</Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}> <Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}>
<span> <span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} /> <IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
@@ -131,7 +138,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
</div> </div>
</> </>
); );
}; }
export default memo(EventBlockInner); export default memo(EventBlockInner);
@@ -162,9 +169,5 @@ function TimerIcon(props: { type: TimerType; className: string }) {
if (type === TimerType.None) { if (type === TimerType.None) {
return <IoBan className={className} />; return <IoBan className={className} />;
} }
if (type === TimerType.TimeToEnd) {
const classes = cx([style.active, className]);
return <IoFlag className={classes} />;
}
return <IoArrowDown className={className} />; return <IoArrowDown className={className} />;
} }
@@ -16,7 +16,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 1rem;
overflow-y: auto; overflow-y: auto;
} }
@@ -38,6 +38,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
margin-top: 0.5rem;
} }
.decorated { .decorated {
@@ -63,6 +64,7 @@
gap: 0.5rem; gap: 0.5rem;
max-width: max-content; max-width: max-content;
cursor: pointer; cursor: pointer;
height: 30px; // manually match the height of a text input
} }
.inline { .inline {
@@ -83,6 +83,7 @@ export default function EventEditor() {
duration={event.duration} duration={event.duration}
timeStrategy={event.timeStrategy} timeStrategy={event.timeStrategy}
linkStart={event.linkStart} linkStart={event.linkStart}
isTimeToEnd={event.isTimeToEnd}
delay={event.delay ?? 0} delay={event.delay ?? 0}
isPublic={event.isPublic} isPublic={event.isPublic}
endAction={event.endAction} endAction={event.endAction}
@@ -18,6 +18,7 @@ interface EventEditorTimesProps {
duration: number; duration: number;
timeStrategy: TimeStrategy; timeStrategy: TimeStrategy;
linkStart: MaybeString; linkStart: MaybeString;
isTimeToEnd: boolean;
delay: number; delay: number;
isPublic: boolean; isPublic: boolean;
endAction: EndAction; endAction: EndAction;
@@ -26,9 +27,9 @@ interface EventEditorTimesProps {
timeDanger: number; timeDanger: number;
} }
type HandledActions = 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger'; type HandledActions = 'isTimeToEnd' | 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger';
const EventEditorTimes = (props: EventEditorTimesProps) => { function EventEditorTimes(props: EventEditorTimesProps) {
const { const {
eventId, eventId,
timeStart, timeStart,
@@ -36,6 +37,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
duration, duration,
timeStrategy, timeStrategy,
linkStart, linkStart,
isTimeToEnd,
delay, delay,
isPublic, isPublic,
endAction, endAction,
@@ -51,6 +53,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
return; return;
} }
if (field === 'isTimeToEnd') {
updateEvent({ id: eventId, isTimeToEnd: !(value as boolean) });
return;
}
if (field === 'timeWarning' || field === 'timeDanger') { if (field === 'timeWarning' || field === 'timeDanger') {
const newTime = parseUserTime(value as string); const newTime = parseUserTime(value as string);
updateEvent({ id: eventId, [field]: newTime }); updateEvent({ id: eventId, [field]: newTime });
@@ -71,95 +78,115 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
: ''; : '';
return ( return (
<div className={style.column}> <>
<div> <div className={style.column}>
<Editor.Label>Event schedule</Editor.Label> <div>
<div className={style.inline}> <Editor.Label>Event schedule</Editor.Label>
<TimeInputFlow <div className={style.inline}>
eventId={eventId} <TimeInputFlow
timeStart={timeStart} eventId={eventId}
timeEnd={timeEnd} timeStart={timeStart}
duration={duration} timeEnd={timeEnd}
timeStrategy={timeStrategy} duration={duration}
linkStart={linkStart} timeStrategy={timeStrategy}
delay={delay} linkStart={linkStart}
timerType={timerType} delay={delay}
/> isTimeToEnd={isTimeToEnd}
/>
</div>
<div className={style.delayLabel}>{delayLabel}</div>
</div> </div>
<div className={style.delayLabel}>{delayLabel}</div>
</div>
<div className={style.splitTwo}> <Editor.Title>Event behaviour</Editor.Title>
<div> <div className={style.splitTwo}>
<Editor.Label htmlFor='timeWarning'>Warning Time</Editor.Label> <div>
<TimeInput <Editor.Label htmlFor='endAction'>End Action</Editor.Label>
id='timeWarning' <Select
name='timeWarning' id='endAction'
submitHandler={handleSubmit} size='sm'
time={timeWarning} name='endAction'
placeholder='Duration' value={endAction}
/> onChange={(event) => handleSubmit('endAction', event.target.value)}
</div> variant='ontime'
<div> >
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label> <option value={EndAction.None}>None</option>
<Select <option value={EndAction.Stop}>Stop rundown</option>
size='sm' <option value={EndAction.LoadNext}>Load next event</option>
id='timerType' <option value={EndAction.PlayNext}>Play next event</option>
name='timerType' </Select>
value={timerType} </div>
onChange={(event) => handleSubmit('timerType', event.target.value)} <div>
variant='ontime' <Editor.Label htmlFor='timeToEnd'>Target Event Scheduled End</Editor.Label>
> <Editor.Label className={style.switchLabel}>
<option value={TimerType.CountDown}>Count down</option> <Switch
<option value={TimerType.CountUp}>Count up</option> id='timeToEnd'
<option value={TimerType.TimeToEnd}>Time to end</option> size='md'
<option value={TimerType.Clock}>Clock</option> isChecked={isTimeToEnd}
<option value={TimerType.None}>None</option> onChange={() => handleSubmit('isTimeToEnd', isTimeToEnd)}
</Select> variant='ontime'
</div> />
<div> {isTimeToEnd ? 'On' : 'Off'}
<Editor.Label htmlFor='timeDanger'>Danger Time</Editor.Label> </Editor.Label>
<TimeInput </div>
id='timeDanger'
name='timeDanger'
submitHandler={handleSubmit}
time={timeDanger}
placeholder='Duration'
/>
</div>
<div>
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
<Select
id='endAction'
size='sm'
name='endAction'
value={endAction}
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.None}>None</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
</Select>
</div> </div>
</div> </div>
<div className={style.column}>
<Editor.Title>Display options</Editor.Title>
<div className={style.splitTwo}>
<div>
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
<Select
size='sm'
id='timerType'
name='timerType'
value={timerType}
onChange={(event) => handleSubmit('timerType', event.target.value)}
variant='ontime'
>
<option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option>
<option value={TimerType.Clock}>Clock</option>
<option value={TimerType.None}>None</option>
</Select>
</div>
<div>
<Editor.Label htmlFor='timeWarning'>Warning Time</Editor.Label>
<TimeInput
id='timeWarning'
name='timeWarning'
submitHandler={handleSubmit}
time={timeWarning}
placeholder='Duration'
/>
</div>
<div> <div>
<Editor.Label htmlFor='isPublic'>Event Visibility</Editor.Label> <Editor.Label htmlFor='isPublic'>Event Visibility</Editor.Label>
<Editor.Label className={style.switchLabel}> <Editor.Label className={style.switchLabel}>
<Switch <Switch
id='isPublic' id='isPublic'
size='md' size='md'
isChecked={isPublic} isChecked={isPublic}
onChange={() => handleSubmit('isPublic', isPublic)} onChange={() => handleSubmit('isPublic', isPublic)}
variant='ontime' variant='ontime'
/> />
{isPublic ? 'Public' : 'Private'} {isPublic ? 'Public' : 'Private'}
</Editor.Label> </Editor.Label>
</div>
<div>
<Editor.Label htmlFor='timeDanger'>Danger Time</Editor.Label>
<TimeInput
id='timeDanger'
name='timeDanger'
submitHandler={handleSubmit}
time={timeDanger}
placeholder='Duration'
/>
</div>
</div>
</div> </div>
</div> </>
); );
}; }
export default memo(EventEditorTimes); export default memo(EventEditorTimes);
@@ -29,6 +29,7 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
return ( return (
<div className={style.column}> <div className={style.column}>
<Editor.Title>Event data</Editor.Title>
<div className={style.splitTwo}> <div className={style.splitTwo}>
<div> <div>
<Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label> <Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label>
@@ -5,7 +5,7 @@ import { IoLink } from '@react-icons/all-files/io5/IoLink';
import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed'; import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed';
import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline'; import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline';
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink'; import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
import { MaybeString, TimerType, TimeStrategy } from 'ontime-types'; import { MaybeString, TimeStrategy } from 'ontime-types';
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton'; import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
@@ -16,19 +16,19 @@ import style from './TimeInputFlow.module.scss';
interface EventBlockTimerProps { interface EventBlockTimerProps {
eventId: string; eventId: string;
isTimeToEnd: boolean;
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
duration: number; duration: number;
timeStrategy: TimeStrategy; timeStrategy: TimeStrategy;
linkStart: MaybeString; linkStart: MaybeString;
delay: number; delay: number;
timerType: TimerType;
} }
type TimeActions = 'timeStart' | 'timeEnd' | 'duration'; type TimeActions = 'timeStart' | 'timeEnd' | 'duration';
const TimeInputFlow = (props: EventBlockTimerProps) => { function TimeInputFlow(props: EventBlockTimerProps) {
const { eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, timerType } = props; const { eventId, isTimeToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay } = props;
const { updateEvent, updateTimer } = useEventAction(); const { updateEvent, updateTimer } = useEventAction();
// In sync with EventEditorTimes // In sync with EventEditorTimes
@@ -49,8 +49,8 @@ const TimeInputFlow = (props: EventBlockTimerProps) => {
warnings.push('Over midnight'); warnings.push('Over midnight');
} }
if (timerType === TimerType.TimeToEnd) { if (isTimeToEnd) {
warnings.push('Time to end'); warnings.push('Target event scheduled end');
} }
const hasDelay = delay !== 0; const hasDelay = delay !== 0;
@@ -128,6 +128,6 @@ const TimeInputFlow = (props: EventBlockTimerProps) => {
)} )}
</> </>
); );
}; }
export default memo(TimeInputFlow); export default memo(TimeInputFlow);
@@ -9,6 +9,7 @@ import {
Settings, Settings,
SimpleTimerState, SimpleTimerState,
SupportedEvent, SupportedEvent,
TimerType,
ViewSettings, ViewSettings,
} from 'ontime-types'; } from 'ontime-types';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
@@ -74,16 +75,14 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
const selectedId = eventNow?.id ?? null; const selectedId = eventNow?.id ?? null;
const nextId = eventNext?.id ?? null; const nextId = eventNext?.id ?? null;
/******************************************/ /**
/*** + TimeManagerType ***/ * Contains an extended timer object with properties from the current event
/*** WRAP INFORMATION RELATED TO TIME ***/ */
/*** -------------------------------- ***/ const timeManagerType: ViewExtendedTimer = {
/******************************************/
const TimeManagerType = {
...timer, ...timer,
clock, clock,
timerType: eventNow?.timerType ?? null, timerType: eventNow?.timerType ?? TimerType.CountDown,
isTimeToEnd: eventNow?.isTimeToEnd ?? false,
}; };
return ( return (
@@ -108,7 +107,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
runtime={runtime} runtime={runtime}
selectedId={selectedId} selectedId={selectedId}
settings={settings} settings={settings}
time={TimeManagerType} time={timeManagerType}
viewSettings={viewSettings} viewSettings={viewSettings}
/> />
</> </>
@@ -5,16 +5,22 @@ import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type'
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils'; import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
type TimerTypeParams = Pick<ViewExtendedTimer, 'timerType' | 'current' | 'elapsed' | 'clock'>; type TimerTypeParams = Pick<ViewExtendedTimer, 'isTimeToEnd' | 'timerType' | 'current' | 'elapsed' | 'clock'>;
export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null { export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null {
if (!timerObject) { if (!timerObject) {
return null; return null;
} }
if (timerObject.isTimeToEnd) {
if (timerObject.current === null) {
return null;
}
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
}
switch (timerObject.timerType) { switch (timerObject.timerType) {
case TimerType.CountDown: case TimerType.CountDown:
case TimerType.TimeToEnd:
if (timerObject.current === null) { if (timerObject.current === null) {
return null; return null;
} }
@@ -124,7 +124,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const isPlaying = time.playback !== Playback.Pause; const isPlaying = time.playback !== Playback.Pause;
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.timerType === TimerType.TimeToEnd; const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd;
const finished = time.phase === TimerPhase.Overtime; const finished = time.phase === TimerPhase.Overtime;
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage; const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage;
const showFinished = const showFinished =
@@ -118,7 +118,7 @@ export default function Timer(props: TimerProps) {
const finished = time.phase === TimerPhase.Overtime; const finished = time.phase === TimerPhase.Overtime;
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0); const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.timerType === TimerType.TimeToEnd; const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd;
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage; const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage;
const showProgress = const showProgress =
eventNow !== null && eventNow !== null &&
+14 -1
View File
@@ -10,6 +10,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.01', note: 'SF1.01',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 36000000, timeStart: 36000000,
@@ -34,6 +35,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.02', note: 'SF1.02',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 37500000, timeStart: 37500000,
@@ -58,6 +60,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.03', note: 'SF1.03',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 39000000, timeStart: 39000000,
@@ -82,6 +85,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.04', note: 'SF1.04',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 40500000, timeStart: 40500000,
@@ -106,6 +110,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.05', note: 'SF1.05',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 42000000, timeStart: 42000000,
@@ -135,6 +140,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.06', note: 'SF1.06',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 47100000, timeStart: 47100000,
@@ -159,6 +165,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.07', note: 'SF1.07',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 48600000, timeStart: 48600000,
@@ -183,6 +190,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.08', note: 'SF1.08',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 50100000, timeStart: 50100000,
@@ -207,6 +215,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.09', note: 'SF1.09',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 51600000, timeStart: 51600000,
@@ -231,6 +240,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.10', note: 'SF1.10',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 53100000, timeStart: 53100000,
@@ -260,6 +270,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.11', note: 'SF1.11',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 56100000, timeStart: 56100000,
@@ -284,9 +295,9 @@ export const demoDb: DatabaseModel = {
note: 'SF1.12', note: 'SF1.12',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 57600000, timeStart: 57600000,
timeEnd: 58800000, timeEnd: 58800000,
duration: 1200000, duration: 1200000,
@@ -309,6 +320,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.13', note: 'SF1.13',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 59100000, timeStart: 59100000,
@@ -333,6 +345,7 @@ export const demoDb: DatabaseModel = {
note: 'SF1.14', note: 'SF1.14',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
linkStart: null, linkStart: null,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
timeStart: 60600000, timeStart: 60600000,
@@ -15,6 +15,7 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
timeStrategy: TimeStrategy.LockDuration, timeStrategy: TimeStrategy.LockDuration,
linkStart: null, linkStart: null,
isTimeToEnd: false,
timeStart: 0, timeStart: 0,
timeEnd: 0, timeEnd: 0,
duration: 0, duration: 0,
@@ -175,7 +175,7 @@ describe('getExpectedFinish()', () => {
const state = { const state = {
eventNow: { eventNow: {
timeEnd: 30, timeEnd: 30,
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
timer: { timer: {
addedTime: 10, addedTime: 10,
@@ -195,7 +195,7 @@ describe('getExpectedFinish()', () => {
const state = { const state = {
eventNow: { eventNow: {
timeEnd: 600000, // 00:10:00 timeEnd: 600000, // 00:10:00
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
timer: { timer: {
addedTime: 0, addedTime: 0,
@@ -351,7 +351,7 @@ describe('getCurrent()', () => {
const state = { const state = {
eventNow: { eventNow: {
timeEnd: 100, timeEnd: 100,
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
clock: 30, clock: 30,
timer: { timer: {
@@ -376,7 +376,7 @@ describe('getCurrent()', () => {
const state = { const state = {
eventNow: { eventNow: {
timeEnd: 100, timeEnd: 100,
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
clock: 30, clock: 30,
timer: { timer: {
@@ -401,7 +401,7 @@ describe('getCurrent()', () => {
const state = { const state = {
eventNow: { eventNow: {
timeEnd: 100, timeEnd: 100,
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
clock: 30, clock: 30,
timer: { timer: {
@@ -427,7 +427,7 @@ describe('getCurrent()', () => {
eventNow: { eventNow: {
timeStart: 79200000, // 22:00:00 timeStart: 79200000, // 22:00:00
timeEnd: 600000, // 00:10:00 timeEnd: 600000, // 00:10:00
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
clock: 79500000, // 22:05:00 clock: 79500000, // 22:05:00
timer: { timer: {
@@ -456,7 +456,7 @@ describe('getCurrent()', () => {
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
timerType: TimerType.TimeToEnd, isTimeToEnd: true,
}, },
timer: { timer: {
addedTime: 0, addedTime: 0,
@@ -909,7 +909,8 @@ describe('getRuntimeOffset()', () => {
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.TimeToEnd, timerType: TimerType.CountDown,
isTimeToEnd: true,
isPublic: true, isPublic: true,
skip: false, skip: false,
note: '', note: '',
@@ -961,7 +962,8 @@ describe('getRuntimeOffset()', () => {
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.TimeToEnd, timerType: TimerType.CountDown,
isTimeToEnd: true,
isPublic: true, isPublic: true,
skip: false, skip: false,
note: '', note: '',
@@ -1011,7 +1013,8 @@ describe('getRuntimeOffset()', () => {
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.TimeToEnd, // <--- but this is time to end timerType: TimerType.CountDown,
isTimeToEnd: true,
}, },
runtime: { runtime: {
selectedEventIndex: 0, selectedEventIndex: 0,
@@ -565,6 +565,7 @@ describe('calculateRuntimeDelays', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -591,6 +592,7 @@ describe('calculateRuntimeDelays', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -617,6 +619,7 @@ describe('calculateRuntimeDelays', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -643,6 +646,7 @@ describe('calculateRuntimeDelays', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -678,6 +682,7 @@ describe('getDelayAt()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -705,6 +710,7 @@ describe('getDelayAt()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -732,6 +738,7 @@ describe('getDelayAt()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -759,6 +766,7 @@ describe('getDelayAt()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -812,6 +820,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -839,6 +848,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -866,6 +876,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 600000, timeStart: 600000,
@@ -893,6 +904,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
note: '', note: '',
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
timeStart: 1200000, timeStart: 1200000,
@@ -31,6 +31,7 @@ describe('cellRequestFromEvent()', () => {
linkStart: null, linkStart: null,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
duration: 10800000, duration: 10800000,
isPublic: false, isPublic: false,
skip: false, skip: false,
@@ -73,6 +74,7 @@ describe('cellRequestFromEvent()', () => {
timeEnd: 57600000, timeEnd: 57600000,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
duration: 10800000, duration: 10800000,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
@@ -119,6 +121,7 @@ describe('cellRequestFromEvent()', () => {
timeEnd: 57600000, timeEnd: 57600000,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
duration: 10800000, duration: 10800000,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
@@ -164,6 +167,7 @@ describe('cellRequestFromEvent()', () => {
timeEnd: 57600000, timeEnd: 57600000,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
duration: 10800000, duration: 10800000,
@@ -195,6 +199,7 @@ describe('cellRequestFromEvent()', () => {
timeEnd: 57600000, timeEnd: 57600000,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
duration: 10800000, duration: 10800000,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
@@ -227,6 +232,7 @@ describe('cellRequestFromEvent()', () => {
timeEnd: 57600000, timeEnd: 57600000,
endAction: EndAction.None, endAction: EndAction.None,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
isTimeToEnd: false,
duration: 10800000, duration: 10800000,
timeStrategy: TimeStrategy.LockEnd, timeStrategy: TimeStrategy.LockEnd,
linkStart: null, linkStart: null,
+7 -7
View File
@@ -1,4 +1,4 @@
import { MaybeNumber, Playback, TimerPhase, TimerType } from 'ontime-types'; import { MaybeNumber, Playback, TimerPhase } from 'ontime-types';
import { dayInMs } from 'ontime-utils'; import { dayInMs } from 'ontime-utils';
import { RuntimeState } from '../stores/runtimeState.js'; import { RuntimeState } from '../stores/runtimeState.js';
@@ -19,7 +19,7 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
return null; return null;
} }
const { timerType, timeEnd } = state.eventNow; const { isTimeToEnd, timeEnd } = state.eventNow;
const { pausedAt } = state._timer; const { pausedAt } = state._timer;
const { clock } = state; const { clock } = state;
@@ -33,7 +33,7 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber {
const pausedTime = pausedAt != null ? clock - pausedAt : 0; const pausedTime = pausedAt != null ? clock - pausedAt : 0;
if (timerType === TimerType.TimeToEnd) { if (isTimeToEnd) {
return timeEnd + addedTime + pausedTime; return timeEnd + addedTime + pausedTime;
} }
@@ -62,11 +62,11 @@ export function getCurrent(state: RuntimeState): number {
} }
} }
const { startedAt, duration, addedTime } = state.timer; const { startedAt, duration, addedTime } = state.timer;
const { timerType, timeStart, timeEnd } = state.eventNow; const { isTimeToEnd, timeStart, timeEnd } = state.eventNow;
const { pausedAt } = state._timer; const { pausedAt } = state._timer;
const { clock } = state; const { clock } = state;
if (timerType === TimerType.TimeToEnd) { if (isTimeToEnd) {
const isEventOverMidnight = timeStart > timeEnd; const isEventOverMidnight = timeStart > timeEnd;
const correctDay = isEventOverMidnight ? dayInMs : 0; const correctDay = isEventOverMidnight ? dayInMs : 0;
return correctDay - clock + timeEnd + addedTime; return correctDay - clock + timeEnd + addedTime;
@@ -131,7 +131,7 @@ export function getRuntimeOffset(state: RuntimeState): number {
} }
const { clock } = state; const { clock } = state;
const { timeStart, timerType } = state.eventNow; const { isTimeToEnd, timeStart } = state.eventNow;
const { addedTime, current, startedAt } = state.timer; const { addedTime, current, startedAt } = state.timer;
// if we havent started, but the timer is armed // if we havent started, but the timer is armed
@@ -142,7 +142,7 @@ export function getRuntimeOffset(state: RuntimeState): number {
const overtime = Math.min(current, 0); const overtime = Math.min(current, 0);
// in time-to-end, offset is overtime // in time-to-end, offset is overtime
if (timerType === TimerType.TimeToEnd) { if (isTimeToEnd) {
return overtime; return overtime;
} }
+102 -53
View File
@@ -463,7 +463,7 @@ describe('test event validator', () => {
const event = { const event = {
title: 'test', title: 'test',
}; };
const validated = createEvent(event, 'test'); const validated = createEvent(event, 1);
expect(validated).toEqual( expect(validated).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -471,12 +471,13 @@ describe('test event validator', () => {
note: expect.any(String), note: expect.any(String),
timeStart: expect.any(Number), timeStart: expect.any(Number),
timeEnd: expect.any(Number), timeEnd: expect.any(Number),
isTimeToEnd: expect.any(Boolean),
isPublic: expect.any(Boolean), isPublic: expect.any(Boolean),
skip: expect.any(Boolean), skip: expect.any(Boolean),
revision: expect.any(Number), revision: expect.any(Number),
type: expect.any(String), type: expect.any(String),
id: expect.any(String), id: expect.any(String),
cue: 'test', cue: '2',
colour: expect.any(String), colour: expect.any(String),
custom: expect.any(Object), custom: expect.any(Object),
}), }),
@@ -485,7 +486,7 @@ describe('test event validator', () => {
it('fails an empty object', () => { it('fails an empty object', () => {
const event = {}; const event = {};
const validated = createEvent(event, 'none'); const validated = createEvent(event, 1);
expect(validated).toEqual(null); expect(validated).toEqual(null);
}); });
@@ -495,7 +496,7 @@ describe('test event validator', () => {
note: '1899-12-30T08:00:10.000Z', note: '1899-12-30T08:00:10.000Z',
}; };
// @ts-expect-error -- we know this is wrong, testing imports outside domain // @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = createEvent(event, 'not-used'); const validated = createEvent(event, 1);
if (validated === null) { if (validated === null) {
throw new Error('unexpected value'); throw new Error('unexpected value');
} }
@@ -774,6 +775,7 @@ describe('getCustomFieldData()', () => {
duration: 'duration', duration: 'duration',
cue: 'cue', cue: 'cue',
title: 'title', title: 'title',
isTimeToEnd: 'time to end',
isPublic: 'public', isPublic: 'public',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'notes',
@@ -824,6 +826,7 @@ describe('getCustomFieldData()', () => {
duration: 'duration', duration: 'duration',
cue: 'cue', cue: 'cue',
title: 'title', title: 'title',
isTimeToEnd: 'time to end',
isPublic: 'public', isPublic: 'public',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'notes',
@@ -883,6 +886,7 @@ describe('parseExcel()', () => {
'Title', 'Title',
'End Action', 'End Action',
'Timer type', 'Timer type',
'Time to end',
'Public', 'Public',
'Skip', 'Skip',
'Notes', 'Notes',
@@ -905,7 +909,8 @@ describe('parseExcel()', () => {
'Guest Welcome', 'Guest Welcome',
'', '',
'', '',
'x', 'x', // <-- time to end
'x', // <-- public
'', '',
'Ballyhoo', 'Ballyhoo',
'a0', 'a0',
@@ -927,7 +932,8 @@ describe('parseExcel()', () => {
'A song from the hearth', 'A song from the hearth',
'load-next', 'load-next',
'clock', 'clock',
'', 'x', // <-- time to end
'', // <-- public
'x', 'x',
'Rainbow chase', 'Rainbow chase',
'b0', 'b0',
@@ -971,6 +977,7 @@ describe('parseExcel()', () => {
timerType: 'count-down', timerType: 'count-down',
endAction: 'none', endAction: 'none',
isPublic: true, isPublic: true,
isTimeToEnd: true,
skip: false, skip: false,
note: 'Ballyhoo', note: 'Ballyhoo',
custom: { custom: {
@@ -994,6 +1001,7 @@ describe('parseExcel()', () => {
timeEnd: 30600000, timeEnd: 30600000,
title: 'A song from the hearth', title: 'A song from the hearth',
timerType: 'clock', timerType: 'clock',
isTimeToEnd: true,
endAction: 'load-next', endAction: 'load-next',
isPublic: false, isPublic: false,
skip: true, skip: true,
@@ -1454,12 +1462,17 @@ describe('parseExcel()', () => {
timeDanger: 'danger time', timeDanger: 'danger time',
custom: {}, custom: {},
}; };
const result = parseExcel(testdata, {}, importMap); const result = parseExcel(testdata, {}, importMap);
expect(result.rundown.length).toBe(2); expect(result.rundown.length).toBe(2);
expect((result.rundown.at(0) as OntimeEvent).type).toBe(SupportedEvent.Event); expect(result.rundown[0]).toMatchObject({
expect((result.rundown.at(0) as OntimeEvent).timerType).toBe(TimerType.CountDown); type: SupportedEvent.Event,
expect((result.rundown.at(1) as OntimeEvent).type).toBe(SupportedEvent.Event); timerType: TimerType.CountDown,
expect((result.rundown.at(1) as OntimeEvent).timerType).toBe(TimerType.CountDown); });
expect(result.rundown[1]).toMatchObject({
type: SupportedEvent.Event,
timerType: TimerType.CountDown,
});
}); });
it('imports as events if timer type is empty or has whitespace', () => { it('imports as events if timer type is empty or has whitespace', () => {
@@ -1659,7 +1672,7 @@ describe('parseExcel()', () => {
expect((events.at(0) as OntimeEvent).colour).toEqual('#F00'); //<--trailing white space in Excel data expect((events.at(0) as OntimeEvent).colour).toEqual('#F00'); //<--trailing white space in Excel data
}); });
it('link start', () => { it('parses link start and checks that is applicable', () => {
const testData = [ const testData = [
[ [
'Time Start', 'Time Start',
@@ -1675,11 +1688,11 @@ describe('parseExcel()', () => {
'Timer type', 'Timer type',
], ],
['4:30:00', '9:45:00', 'A', 'load-next', '', '', 'Rainbow chase', '#F00', 102, '', 'count-down'], ['4:30:00', '9:45:00', 'A', 'load-next', '', '', 'Rainbow chase', '#F00', 102, '', 'count-down'],
['9:45:00', '10:56:00', 'C', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'], ['9:45:00', '10:56:00', 'B', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'],
['10:00:00', '16:36:00', 'D', 'load-next', 'x', '', 'Rainbow chase', '#F00', 102, 'x', 'count-down'], //<-- incorrect start times are overridden ['10:00:00', '16:36:00', 'C', 'load-next', 'x', '', 'Rainbow chase', '#F00', 102, 'x', 'count-down'], // <-- incorrect start times are overridden
['21:45:00', '22:56:00', 'E', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, '', 'count-down'], ['21:45:00', '22:56:00', 'D', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, '', 'count-down'],
['', '', 'BLOCK', '', '', '', '', '', '', '', 'block'], ['', '', 'BLOCK', '', '', '', '', '', '', '', 'block'],
['00:0:00', '23:56:00', 'G', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'], //<-- link past blocks ['00:0:00', '23:56:00', 'E', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'], // <-- link past blocks
[], [],
]; ];
@@ -1709,30 +1722,46 @@ describe('parseExcel()', () => {
const { rundown, order } = cache.get(); const { rundown, order } = cache.get();
const firstId = order.at(0); // A const firstId = order.at(0); // A
const secondId = order.at(1); // C const secondId = order.at(1); // B
const thirdId = order.at(2); // D const thirdId = order.at(2); // C
const fourthId = order.at(3); // E const fourthId = order.at(3); // D
const fifhtId = order.at(4); // Block const fifthId = order.at(4); // Block
const sixthId = order.at(5); // G const sixthId = order.at(5); // E
if (!firstId || !secondId || !thirdId || !fourthId || !fifhtId || !sixthId) { if (!firstId || !secondId || !thirdId || !fourthId || !fifthId || !sixthId) {
throw new Error('Unexpected value'); throw new Error('Unexpected value');
} }
expect((rundown[firstId] as OntimeEvent).timeStart).toEqual(16200000); expect(rundown).toMatchObject({
[firstId]: {
expect((rundown[secondId] as OntimeEvent).timeStart).toEqual((rundown[firstId] as OntimeEvent).timeEnd); title: 'A',
expect((rundown[secondId] as OntimeEvent).linkStart).toEqual((rundown[firstId] as OntimeEvent).id); timeStart: 16200000,
},
expect((rundown[thirdId] as OntimeEvent).timeStart).toEqual((rundown[secondId] as OntimeEvent).timeEnd); [secondId]: {
expect((rundown[thirdId] as OntimeEvent).linkStart).toEqual((rundown[secondId] as OntimeEvent).id); title: 'B',
timeStart: (rundown[firstId] as OntimeEvent).timeEnd,
expect((rundown[fourthId] as OntimeEvent).timeStart).toEqual(78300000); linkStart: (rundown[firstId] as OntimeEvent).id,
},
expect((rundown[fifhtId] as OntimeEvent).type).toEqual(SupportedEvent.Block); [thirdId]: {
title: 'C',
expect((rundown[sixthId] as OntimeEvent).timeStart).toEqual((rundown[fourthId] as OntimeEvent).timeEnd); timeStart: (rundown[secondId] as OntimeEvent).timeEnd,
expect((rundown[sixthId] as OntimeEvent).linkStart).toEqual((rundown[fourthId] as OntimeEvent).id); linkStart: (rundown[secondId] as OntimeEvent).id,
},
[fourthId]: {
title: 'D',
timeStart: 78300000,
linkStart: null,
},
[fifthId]: {
title: 'BLOCK',
type: SupportedEvent.Block,
},
[sixthId]: {
title: 'E',
timeStart: (rundown[fourthId] as OntimeEvent).timeEnd,
linkStart: (rundown[fourthId] as OntimeEvent).id,
},
});
}); });
it('#971 BUG: parses time fields and booleans', () => { it('#971 BUG: parses time fields and booleans', () => {
@@ -1762,7 +1791,7 @@ describe('parseExcel()', () => {
'false', 'false',
'Setup', 'Setup',
'', '',
'time-to-end', 'count-down',
'none', 'none',
'15', '15',
'00:05:00', '00:05:00',
@@ -1778,7 +1807,7 @@ describe('parseExcel()', () => {
'false', 'false',
'Meeting 1', 'Meeting 1',
'', '',
'time-to-end', 'count-down',
'none', 'none',
15, 15,
'00:05:00', '00:05:00',
@@ -1794,7 +1823,7 @@ describe('parseExcel()', () => {
'false', 'false',
'Meeting 2', 'Meeting 2',
'', '',
'time-to-end', 'count-down',
'none', 'none',
'13', '13',
'5', '5',
@@ -1810,7 +1839,7 @@ describe('parseExcel()', () => {
'true', 'true',
'Lunch', 'Lunch',
'', '',
'time-to-end', 'count-down',
'none', 'none',
13, 13,
5, 5,
@@ -1839,26 +1868,46 @@ describe('parseExcel()', () => {
const parsedData = parseExcel(testData, {}); const parsedData = parseExcel(testData, {});
const { rundown } = parsedData; const { rundown } = parsedData;
// '15' as a string is parsed by smart time entry as minutes
expect(rundown[0]).toMatchObject({
cue: 'SETUP',
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// elements in bug report // elements in bug report
// 15 is a number, in which case we parse it as a minutes value // 15 is a number, in which case we parse it as a minutes value
expect((rundown.at(1) as OntimeEvent).timeWarning).toBe(15 * MILLIS_PER_MINUTE); expect(rundown[1]).toMatchObject({
cue: 'MEET1',
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// in the case where a string is passed, we need to check whether it is an ISO 8601 date // in the case where a string is passed, we need to check whether it is an ISO 8601 date
expect((rundown.at(2) as OntimeEvent).duration).toBe(60 * MILLIS_PER_MINUTE); expect(rundown[2]).toMatchObject({
expect((rundown.at(2) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE); cue: 'MEET2',
duration: 60 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(3) as OntimeEvent).timeWarning).toBe(13 * MILLIS_PER_MINUTE); expect(rundown[3]).toMatchObject({
expect((rundown.at(3) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE); cue: 'lunch',
timeWarning: 13 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(4) as OntimeEvent).duration).toBe(90 * MILLIS_PER_MINUTE); expect(rundown[4]).toMatchObject({
expect((rundown.at(4) as OntimeEvent).linkStart).toBe(false); cue: 'MEET3',
expect((rundown.at(4) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE); duration: 90 * MILLIS_PER_MINUTE,
expect((rundown.at(4) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE); linkStart: false,
timeWarning: 11 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(5) as OntimeEvent).duration).toBe(30 * MILLIS_PER_MINUTE); expect(rundown[5]).toMatchObject({
cue: 'MEET4',
// if we get a boolean, we should just use that duration: 30 * MILLIS_PER_MINUTE,
expect((rundown.at(5) as OntimeEvent).linkStart).toBe(true); timeWarning: 11 * MILLIS_PER_MINUTE,
expect((rundown.at(5) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE); // if we get a boolean, we should just use that
linkStart: true,
});
}); });
}); });
@@ -405,28 +405,6 @@ describe('sanitiseCustomFields()', () => {
}); });
describe('parseRundown() linking', () => { describe('parseRundown() linking', () => {
const blankEvent: OntimeEvent = {
id: '',
type: SupportedEvent.Event,
cue: '',
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
linkStart: null,
timeStrategy: TimeStrategy.LockDuration,
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
};
it('returns linked events', () => { it('returns linked events', () => {
const data: Partial<DatabaseModel> = { const data: Partial<DatabaseModel> = {
rundown: [ rundown: [
@@ -445,12 +423,11 @@ describe('parseRundown() linking', () => {
customFields: {}, customFields: {},
}; };
const expected: OntimeRundown = [
{ ...blankEvent, id: '1', cue: '0' },
{ ...blankEvent, id: '2', cue: '1', linkStart: '1' },
];
const result = parseRundown(data); const result = parseRundown(data);
expect(result.rundown).toEqual(expected); expect(result.rundown[1]).toMatchObject({
id: '2',
linkStart: '1',
});
}); });
it('returns unlinked if no previous', () => { it('returns unlinked if no previous', () => {
@@ -466,9 +443,11 @@ describe('parseRundown() linking', () => {
customFields: {}, customFields: {},
}; };
const expected: OntimeRundown = [{ ...blankEvent, id: '2', cue: '0' }];
const result = parseRundown(data); const result = parseRundown(data);
expect(result.rundown).toEqual(expected); expect(result.rundown[0]).toMatchObject({
id: '2',
linkStart: null,
});
}); });
it('returns linked events past blocks and delays', () => { it('returns linked events past blocks and delays', () => {
@@ -505,14 +484,65 @@ describe('parseRundown() linking', () => {
customFields: {}, customFields: {},
}; };
const expected: OntimeRundown = [
{ ...blankEvent, id: '1', cue: '0' },
{ id: 'delay1', type: SupportedEvent.Delay, duration: 0 },
{ ...blankEvent, id: '2', cue: '1', linkStart: '1' },
{ id: 'block1', type: SupportedEvent.Block, title: '' },
{ ...blankEvent, id: '3', cue: '2', linkStart: '2' },
];
const result = parseRundown(data); const result = parseRundown(data);
expect(result.rundown).toEqual(expected); expect(result.rundown[0]).toMatchObject({
id: '1',
cue: '1',
});
// skip delay
expect(result.rundown[2]).toMatchObject({
id: '2',
cue: '2',
linkStart: '1',
});
// skip block
expect(result.rundown[4]).toMatchObject({
id: '3',
cue: '3',
linkStart: '2',
});
});
});
describe('parseRundown() migrations', () => {
const legacyEvent = {
id: '1',
type: SupportedEvent.Event,
cue: '',
title: '',
note: '',
endAction: EndAction.None,
timerType: 'time-to-end',
linkStart: null,
timeStrategy: TimeStrategy.LockDuration,
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
};
it('migrates an event with time-to-end', () => {
const result = parseRundown({ rundown: [legacyEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
isTimeToEnd: true,
});
});
it('migrates an event without time-to-end', () => {
const countdownEvent = { ...legacyEvent, timerType: TimerType.CountDown };
const result = parseRundown({ rundown: [countdownEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
isTimeToEnd: false,
});
}); });
}); });
+17 -8
View File
@@ -13,8 +13,8 @@ import {
CustomFields, CustomFields,
DatabaseModel, DatabaseModel,
EventCustomFields, EventCustomFields,
isOntimeBlock,
LogOrigin, LogOrigin,
OntimeBlock,
OntimeEvent, OntimeEvent,
OntimeRundown, OntimeRundown,
SupportedEvent, SupportedEvent,
@@ -110,6 +110,7 @@ export const parseExcel = (
// options: booleans // options: booleans
let isPublicIndex: number | null = null; let isPublicIndex: number | null = null;
let skipIndex: number | null = null; let skipIndex: number | null = null;
let isTimeToEndIndex: number | null = null;
let linkStartIndex: number | null = null; let linkStartIndex: number | null = null;
@@ -159,6 +160,10 @@ export const parseExcel = (
titleIndex = col; titleIndex = col;
rundownMetadata['title'] = { row, col }; rundownMetadata['title'] = { row, col };
}, },
[importMap.isTimeToEnd]: (row: number, col: number) => {
isTimeToEndIndex = col;
rundownMetadata['isTimeToEnd'] = { row, col };
},
[importMap.isPublic]: (row: number, col: number) => { [importMap.isPublic]: (row: number, col: number) => {
isPublicIndex = col; isPublicIndex = col;
rundownMetadata['isPublic'] = { row, col }; rundownMetadata['isPublic'] = { row, col };
@@ -226,6 +231,8 @@ export const parseExcel = (
event.duration = parseExcelDate(column); event.duration = parseExcelDate(column);
} else if (j === cueIndex) { } else if (j === cueIndex) {
event.cue = makeString(column, ''); event.cue = makeString(column, '');
} else if (j === isTimeToEndIndex) {
event.isTimeToEnd = parseBooleanString(column);
} else if (j === isPublicIndex) { } else if (j === isPublicIndex) {
event.isPublic = parseBooleanString(column); event.isPublic = parseBooleanString(column);
} else if (j === skipIndex) { } else if (j === skipIndex) {
@@ -273,8 +280,8 @@ export const parseExcel = (
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length; const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
if (keysFound > 0) { if (keysFound > 0) {
// if it is a Block type drop all other filed // if it is a Block type drop all other filed
if (event.type === SupportedEvent.Block) { if (isOntimeBlock(event)) {
rundown.push({ type: event.type, id: event.id, title: event.title } as OntimeBlock); rundown.push({ type: event.type, id: event.id, title: event.title });
} else { } else {
if (timerTypeIndex === null) { if (timerTypeIndex === null) {
event.timerType = TimerType.CountDown; event.timerType = TimerType.CountDown;
@@ -360,7 +367,6 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
patchEvent?.duration ?? originalEvent.duration, patchEvent?.duration ?? originalEvent.duration,
patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy), patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy),
); );
const maybeLinkStart = patchEvent.linkStart !== undefined ? patchEvent.linkStart : originalEvent.linkStart;
return { return {
id: originalEvent.id, id: originalEvent.id,
@@ -370,9 +376,10 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
timeEnd, timeEnd,
duration, duration,
timeStrategy, timeStrategy,
linkStart: validateLinkStart(maybeLinkStart), linkStart: validateLinkStart(patchEvent.linkStart),
endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction), endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction),
timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType), timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType),
isTimeToEnd: typeof patchEvent.isTimeToEnd === 'boolean' ? patchEvent.isTimeToEnd : originalEvent.isTimeToEnd,
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic, isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip, skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
note: makeString(patchEvent.note, originalEvent.note), note: makeString(patchEvent.note, originalEvent.note),
@@ -389,17 +396,19 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
/** /**
* @description Enforces formatting for events * @description Enforces formatting for events
* @param {object} eventArgs - attributes of event * @param {object} eventArgs - attributes of event
* @param cueFallback * @param {number} eventIndex - can be a string when we pass the a suggested cue name
* @returns {object|null} - formatted object or null in case is invalid * @returns {object|null} - formatted object or null in case is invalid
*/ */
export const createEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string): OntimeEvent | null => { export const createEvent = (eventArgs: Partial<OntimeEvent>, eventIndex: number | string): OntimeEvent | null => {
if (Object.keys(eventArgs).length === 0) { if (Object.keys(eventArgs).length === 0) {
return null; return null;
} }
const cue = typeof eventIndex === 'number' ? String(eventIndex + 1) : eventIndex;
const baseEvent = { const baseEvent = {
id: eventArgs?.id ?? generateId(), id: eventArgs?.id ?? generateId(),
cue: cueFallback, cue,
...eventDef, ...eventDef,
}; };
const event = createPatch(baseEvent, eventArgs); const event = createPatch(baseEvent, eventArgs);
+27 -10
View File
@@ -12,6 +12,7 @@ import {
OscSubscription, OscSubscription,
ProjectData, ProjectData,
Settings, Settings,
TimerType,
URLPreset, URLPreset,
ViewSettings, ViewSettings,
isOntimeBlock, isOntimeBlock,
@@ -19,13 +20,7 @@ import {
isOntimeDelay, isOntimeDelay,
isOntimeEvent, isOntimeEvent,
} from 'ontime-types'; } from 'ontime-types';
import { import { customFieldLabelToKey, generateId, getErrorMessage, isAlphanumericWithSpace } from 'ontime-utils';
customFieldLabelToKey,
generateId,
getErrorMessage,
getLastEvent,
isAlphanumericWithSpace,
} from 'ontime-utils';
import { dbModel } from '../models/dataModel.js'; import { dbModel } from '../models/dataModel.js';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js'; import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
@@ -52,6 +47,7 @@ export function parseRundown(
const rundown: OntimeRundown = []; const rundown: OntimeRundown = [];
let eventIndex = 0; let eventIndex = 0;
let previousId: string | null = null;
const ids: string[] = []; const ids: string[] = [];
for (const event of data.rundown) { for (const event of data.rundown) {
@@ -64,12 +60,13 @@ export function parseRundown(
let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null; let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
if (isOntimeEvent(event)) { if (isOntimeEvent(event)) {
const maybeEvent = runEventMigrations({ ...event, id });
if (event.linkStart) { if (event.linkStart) {
const prevId = getLastEvent(rundown).lastEvent?.id ?? null; maybeEvent.linkStart = previousId;
event.linkStart = prevId;
} }
newEvent = createEvent(event, eventIndex.toString()); newEvent = createEvent(maybeEvent, eventIndex);
// skip if event is invalid // skip if event is invalid
if (newEvent == null) { if (newEvent == null) {
emitError?.('Skipping event without payload'); emitError?.('Skipping event without payload');
@@ -84,6 +81,7 @@ export function parseRundown(
} }
} }
previousId = id;
eventIndex += 1; eventIndex += 1;
} else if (isOntimeDelay(event)) { } else if (isOntimeDelay(event)) {
newEvent = { ...delayDef, duration: event.duration, id }; newEvent = { ...delayDef, duration: event.duration, id };
@@ -349,3 +347,22 @@ export function sanitiseCustomFields(data: object): CustomFields {
return newCustomFields; return newCustomFields;
} }
/**
* Time to end was moved from a TimerType to a standalone boolean
* Released as part of v3.10.0
*/
function migrateTimeToEnd(event: any): OntimeEvent {
if (event.timerType === 'time-to-end') {
event.timerType = TimerType.CountDown;
event.isTimeToEnd = true;
}
return event;
}
/**
* Mutating function migrates event data entries
*/
function runEventMigrations(event: any): OntimeEvent {
return migrateTimeToEnd(event);
}
Binary file not shown.
@@ -1,7 +1,6 @@
export enum TimerType { export enum TimerType {
CountDown = 'count-down', CountDown = 'count-down',
CountUp = 'count-up', CountUp = 'count-up',
TimeToEnd = 'time-to-end',
Clock = 'clock', Clock = 'clock',
None = 'none', None = 'none',
} }
@@ -29,6 +29,7 @@ export type OntimeEvent = OntimeBaseEvent & {
note: string; note: string;
endAction: EndAction; endAction: EndAction;
timerType: TimerType; timerType: TimerType;
isTimeToEnd: boolean;
linkStart: MaybeString; // ID of event to link to linkStart: MaybeString; // ID of event to link to
timeStrategy: TimeStrategy; timeStrategy: TimeStrategy;
timeStart: number; timeStart: number;
@@ -11,6 +11,7 @@ describe('isImportMap()', () => {
duration: 'duration', duration: 'duration',
cue: 'cue', cue: 'cue',
title: 'title', title: 'title',
isTimeToEnd: 'time to end',
isPublic: 'public', isPublic: 'public',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'notes',
@@ -34,6 +35,7 @@ describe('isImportMap()', () => {
duration: 'duration', duration: 'duration',
cue: 'cue', cue: 'cue',
title: 'title', title: 'title',
isTimeToEnd: 'time to end',
isPublic: 'public', isPublic: 'public',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'notes',
@@ -11,6 +11,7 @@ export const defaultImportMap = {
duration: 'duration', duration: 'duration',
cue: 'cue', cue: 'cue',
title: 'title', title: 'title',
isTimeToEnd: 'time to end',
isPublic: 'public', isPublic: 'public',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'notes',
@@ -18,8 +18,8 @@ describe('validateEndAction()', () => {
describe('validateTimerType()', () => { describe('validateTimerType()', () => {
it('recognises a string representation of an action', () => { it('recognises a string representation of an action', () => {
const timerType = validateTimerType('time-to-end'); const timerType = validateTimerType('count-up');
expect(timerType).toBe(TimerType.TimeToEnd); expect(timerType).toBe(TimerType.CountUp);
}); });
it('returns fallback otherwise', () => { it('returns fallback otherwise', () => {
const emptyType = validateTimerType('', TimerType.Clock); const emptyType = validateTimerType('', TimerType.Clock);