feat: timer preview

This commit is contained in:
Carlos Valente
2024-09-03 21:30:35 +02:00
committed by Carlos Valente
parent fa709dc9be
commit 4d8bea2940
27 changed files with 581 additions and 288 deletions
+40 -5
View File
@@ -1,4 +1,4 @@
import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types'; import { RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
import { useRuntimeStore } from '../stores/runtime'; import { useRuntimeStore } from '../stores/runtime';
import { socketSendJson } from '../utils/socket'; import { socketSendJson } from '../utils/socket';
@@ -28,11 +28,43 @@ export const useOperator = () => {
return useRuntimeStore(featureSelector); return useRuntimeStore(featureSelector);
}; };
export const useMessageControl = () => { export const useTimerViewControl = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
timer: state.message.timer, blackout: state.message.timer.blackout,
external: state.message.external, blink: state.message.timer.blink,
onAir: state.onAir, secondarySource: state.message.timer.secondarySource,
});
return useRuntimeStore(featureSelector);
};
export const useTimerMessageInput = () => {
const featureSelector = (state: RuntimeStore) => ({
text: state.message.timer.text,
visible: state.message.timer.visible,
});
return useRuntimeStore(featureSelector);
};
export const useExternalMessageInput = () => {
const featureSelector = (state: RuntimeStore) => ({
text: state.message.external,
visible: state.message.timer.secondarySource === 'external',
});
return useRuntimeStore(featureSelector);
};
export const useMessagePreview = () => {
const featureSelector = (state: RuntimeStore) => ({
blink: state.message.timer.blink,
blackout: state.message.timer.blackout,
phase: state.timer.phase,
showAuxTimer: state.message.timer.secondarySource === 'aux',
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
timerType: state.eventNow?.timerType ?? null,
}); });
return useRuntimeStore(featureSelector); return useRuntimeStore(featureSelector);
@@ -41,8 +73,11 @@ export const useMessageControl = () => {
export const setMessage = { export const setMessage = {
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }), timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }), timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
externalText: (payload: string) => socketSendJson('message', { external: payload }),
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }), timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }), timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
timerSecondary: (payload: TimerMessage['secondarySource']) =>
socketSendJson('message', { timer: { secondarySource: payload } }),
}; };
export const usePlaybackControl = () => { export const usePlaybackControl = () => {
+2 -4
View File
@@ -23,11 +23,9 @@ export const runtimeStorePlaceholder: RuntimeStore = {
visible: false, visible: false,
blink: false, blink: false,
blackout: false, blackout: false,
secondarySource: null,
}, },
external: { external: '',
text: '',
visible: false,
},
}, },
runtime: { runtime: {
selectedEventIndex: null, selectedEventIndex: null,
@@ -147,7 +147,7 @@ export default function ViewSettingsForm() {
variant='ontime-filled' variant='ontime-filled'
maxLength={150} maxLength={150}
width='275px' width='275px'
placeholder='Message shown when timer reaches end' placeholder='Shown when timer reaches end'
{...register('endMessage')} {...register('endMessage')}
/> />
</Panel.ListItem> </Panel.ListItem>
@@ -1,10 +1,9 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { IconButton, Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { IoEye } from '@react-icons/all-files/io5/IoEye'; import { IoEye } from '@react-icons/all-files/io5/IoEye';
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline'; import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn'; import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './InputRow.module.scss'; import style from './InputRow.module.scss';
@@ -13,15 +12,13 @@ interface InputRowProps {
label: string; label: string;
placeholder: string; placeholder: string;
text: string; text: string;
visible?: boolean; visible: boolean;
readonly?: boolean; actionHandler: () => void;
actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void; changeHandler: (newValue: string) => void;
className?: string;
} }
export default function InputRow(props: InputRowProps) { export default function InputRow(props: InputRowProps) {
const { label, placeholder, text, visible, actionHandler, changeHandler, className, readonly } = props; const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const cursorPositionRef = useRef(0); const cursorPositionRef = useRef(0);
@@ -39,41 +36,27 @@ export default function InputRow(props: InputRowProps) {
changeHandler(event.target.value); changeHandler(event.target.value);
}; };
const classes = cx([style.inputRow, className]);
return ( return (
<div className={classes}> <div className={style.inputRow}>
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label> <label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
<div className={style.inputItems}> <div className={style.inputItems}>
<Input <Input
ref={inputRef} ref={inputRef}
size='sm' size='sm'
variant='ontime-filled' variant='ontime-filled'
readOnly={readonly}
disabled={readonly}
value={text} value={text}
onChange={handleInputChange} onChange={handleInputChange}
placeholder={placeholder} placeholder={placeholder}
/> />
{readonly ? ( <TooltipActionBtn
<IconButton clickHandler={actionHandler}
size='sm' tooltip={visible ? 'Make invisible' : 'Make visible'}
isDisabled aria-label={`Toggle ${label}`}
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />} openDelay={tooltipDelayMid}
aria-label={`Toggle ${label}`} icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
variant={visible ? 'ontime-filled' : 'ontime-subtle'} variant={visible ? 'ontime-filled' : 'ontime-subtle'}
/> size='sm'
) : ( />
<TooltipActionBtn
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
tooltip={visible ? 'Make invisible' : 'Make visible'}
aria-label={`Toggle ${label}`}
openDelay={tooltipDelayMid}
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
size='sm'
/>
)}
</div> </div>
</div> </div>
); );
@@ -1,23 +1,3 @@
.messageContainer {
display: flex;
flex-direction: column;
gap: $section-spacing;
}
.buttonSection {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $element-spacing;
margin-top: -0.5rem;
}
.singleAction {
display: flex;
flex-direction: column;
gap: $element-spacing;
margin-top: $element-inner-spacing;
}
.label { .label {
font-size: $inner-section-text-size; font-size: $inner-section-text-size;
color: $label-gray; color: $label-gray;
@@ -26,3 +6,73 @@
color: $action-text-color; color: $action-text-color;
} }
} }
.previewContainer {
display: grid;
gap: $element-spacing;
grid-template-columns: 2fr 1fr;
}
.preview {
background-color: $ui-black;
display: grid;
place-content: center;
text-align: center;
position: relative;
}
.options {
display: flex;
flex-direction: column;
gap: $element-spacing;
}
.eventStatus {
position: absolute;
right: 0;
margin: 0.5rem 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.mainContent {
font-size: 1rem;
font-weight: 600;
color: var(--override-colour, $ui-white);
&[data-phase='pending'] {
color: $ontime-roll;
}
&[data-phase='overtime'] {
color: $playback-negative;
}
&[data-phase='none'] {
opacity: $opacity-disabled;
}
}
.secondaryContent {
border-top: 1px solid $white-7;
}
.blackout {
display: none;
}
.timerIndicators {
display: flex;
flex-direction: column;
}
.statusIcon {
color: $gray-1000;
&[data-active='true'] {
color: $active-indicator;
}
}
.divider {
border-top: 1px solid $gray-1000;
}
@@ -1,64 +1,52 @@
import { Button } from '@chakra-ui/react'; import { setMessage, useExternalMessageInput, useTimerMessageInput } from '../../../common/hooks/useSocket';
import { IoEye } from '@react-icons/all-files/io5/IoEye';
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
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'; import InputRow from './InputRow';
import TimerControlsPreview from './TimerViewControl';
import style from './MessageControl.module.scss';
const noop = () => undefined;
export default function MessageControl() { export default function MessageControl() {
const message = useMessageControl();
const blink = message.timer.blink;
const blackout = message.timer.blackout;
return ( return (
<div className={style.messageContainer}> <>
<InputRow <TimerControlsPreview />
label='Timer' <TimerMessageInput />
placeholder='Message shown in stage timer' <ExternalInput />
text={message.timer.text} </>
visible={message.timer.visible} );
changeHandler={(newValue) => setMessage.timerText(newValue)} }
actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
/> function TimerMessageInput() {
<div className={style.buttonSection}> const { text, visible } = useTimerMessageInput();
<Button
size='sm' return (
className={`${blink ? style.blink : ''}`} <InputRow
variant={blink ? 'ontime-filled' : 'ontime-subtle'} label='Timer Message'
leftIcon={blink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />} placeholder='Message shown fullscreen in stage timer'
onClick={() => setMessage.timerBlink(!blink)} text={text}
data-testid='toggle timer blink' visible={visible}
> changeHandler={(newValue) => setMessage.timerText(newValue)}
Blink actionHandler={() => setMessage.timerVisible(!visible)}
</Button> />
<Button );
size='sm' }
className={style.blackoutButton}
variant={blackout ? 'ontime-filled' : 'ontime-subtle'} function ExternalInput() {
leftIcon={blackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />} const { text, visible } = useExternalMessageInput();
onClick={() => setMessage.timerBlackout(!blackout)}
data-testid='toggle timer blackout' const toggleExternal = () => {
> if (visible) {
Blackout screen setMessage.timerSecondary(null);
</Button> } else {
</div> setMessage.timerSecondary('external');
<InputRow }
label='External Message (read only)' };
placeholder={enDash}
readonly return (
text={message.external.text} <InputRow
visible={message.external.visible} label='External Message'
changeHandler={noop} placeholder='Message shown as secondary text in stage timer'
actionHandler={noop} text={text}
/> visible={visible}
</div> changeHandler={(newValue) => setMessage.externalText(newValue)}
actionHandler={toggleExternal}
/>
); );
} }
@@ -3,16 +3,19 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary'; import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
import { handleLinks } from '../../../common/utils/linkUtils'; import { handleLinks } from '../../../common/utils/linkUtils';
import { cx } from '../../../common/utils/styleUtils';
import MessageControl from './MessageControl'; import MessageControl from './MessageControl';
import style from '../../editors/Editor.module.scss'; import style from '../../editors/Editor.module.scss';
const MessageControlExport = () => { const MessageControlExport = () => {
const classes = cx([style.content, style.contentColumnLayout]);
return ( return (
<div className={style.messages} data-testid='panel-messages-control'> <div className={style.messages} data-testid='panel-messages-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} /> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<div className={style.content}> <div className={classes}>
<ErrorBoundary> <ErrorBoundary>
<MessageControl /> <MessageControl />
</ErrorBoundary> </ErrorBoundary>
@@ -0,0 +1,76 @@
import { Tooltip } from '@chakra-ui/react';
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
import { IoTime } from '@react-icons/all-files/io5/IoTime';
import { TimerPhase, TimerType } from 'ontime-types';
import { useMessagePreview } from '../../../common/hooks/useSocket';
import useViewSettings from '../../../common/hooks-query/useViewSettings';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './MessageControl.module.scss';
export default function TimerPreview() {
const { blink, blackout, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } =
useMessagePreview();
const { data } = useViewSettings();
const contentClasses = cx([style.previewContent, blink && style.blink, blackout && style.blackout]);
const main = (() => {
if (showTimerMessage) return 'Message';
if (phase === TimerPhase.Pending) return 'Standby to start';
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
return 'Timer';
})();
const secondary = (() => {
// message is a fullscreen overlay
if (showTimerMessage) return null;
// we need to check aux first since it takes priority
if (showAuxTimer) return 'Aux Timer';
if (showExternalMessage) return 'External message';
return null;
})();
const overrideColour = (() => {
// override fallback colours from starter project
if (phase === TimerPhase.Warning) return data.warningColor ?? '#FFAB33';
if (phase === TimerPhase.Danger) return data.dangerColor ?? '#ED3333';
return data.normalColor ?? '#FFFC';
})();
const showColourOverride = main == 'Timer';
return (
<div className={style.preview}>
<div className={contentClasses}>
<div
className={style.mainContent}
data-phase={phase}
style={showColourOverride ? { '--override-colour': overrideColour } : {}}
>
{main}
</div>
{secondary !== null && <div className={style.secondaryContent}>{secondary}</div>}
</div>
<div className={style.eventStatus}>
<Tooltip label='Time type: Count down' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoArrowDown className={style.statusIcon} data-active={timerType === TimerType.CountDown} />
</Tooltip>
<Tooltip label='Time type: Count up' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoArrowUp className={style.statusIcon} data-active={timerType === TimerType.CountUp} />
</Tooltip>
<Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoTime className={style.statusIcon} data-active={timerType === TimerType.Clock} />
</Tooltip>
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoFlag className={style.statusIcon} data-active={timerType === TimerType.TimeToEnd} />
</Tooltip>
</div>
</div>
);
}
@@ -0,0 +1,62 @@
import { Button } from '@chakra-ui/react';
import { setMessage, useTimerViewControl } from '../../../common/hooks/useSocket';
import TimerPreview from './TimerPreview';
import style from './MessageControl.module.scss';
export default function TimerControlsPreview() {
const { blackout, blink, secondarySource } = useTimerViewControl();
const toggleSecondary = (newValue: 'aux' | 'external' | null) => {
if (secondarySource === newValue) {
setMessage.timerSecondary(null);
} else {
setMessage.timerSecondary(newValue);
}
};
return (
<div className={style.previewContainer}>
<TimerPreview />
<div className={style.options}>
<Button
size='sm'
variant={secondarySource === 'aux' ? 'ontime-filled' : 'ontime-subtle'}
onClick={() => toggleSecondary('aux')}
>
Show Aux timer
</Button>
<Button
size='sm'
variant={secondarySource === 'external' ? 'ontime-filled' : 'ontime-subtle'}
onClick={() => toggleSecondary('external')}
>
Show external
</Button>
<hr className={style.divider} />
<Button
size='sm'
variant={blink ? 'ontime-filled' : 'ontime-subtle'}
onClick={() => setMessage.timerBlink(!blink)}
data-testid='toggle timer blink'
>
Blink
</Button>
<Button
size='sm'
className={style.blackoutButton}
variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
onClick={() => setMessage.timerBlackout(!blackout)}
data-testid='toggle timer blackout'
>
Blackout screen
</Button>
</div>
</div>
);
}
@@ -68,7 +68,7 @@ function AuxTimerInput() {
const handleTimeUpdate = (_field: string, value: string) => { const handleTimeUpdate = (_field: string, value: string) => {
const newTime = parseUserTime(value); const newTime = parseUserTime(value);
setDuration(newTime / 1000); //frontend api is seconds based; setDuration(newTime / 1000); // frontend api is seconds based
}; };
return ( return (
@@ -40,7 +40,7 @@ $panel-gap: 0.5rem;
.playback, .playback,
.messages { .messages {
position: relative; position: relative;
border-radius: 8px; border-radius: var(--editor--panel__br);
background-color: $bg-container-l2; background-color: $bg-container-l2;
padding: 1rem; padding: 1rem;
} }
@@ -62,3 +62,10 @@ $panel-gap: 0.5rem;
.content { .content {
padding-top: 1.5rem; padding-top: 1.5rem;
} }
.contentColumnLayout {
display: flex;
flex-direction: column;
gap: $section-spacing;
color: $ui-white;
}
@@ -1,10 +1,18 @@
@use '../../theme/ontimeColours' as *; @use '../../theme/ontimeColours' as *;
@use '../../theme/ontimeStyles' as *; @use '../../theme/ontimeStyles' as *;
@mixin absolute-top-right($distance) { // declare editor specific styling constants
:root {
--editor--panel__br: 8px;
}
@mixin corner() {
display: none;
transform: rotate(45deg);
position: absolute; position: absolute;
top: $distance; top: 0.5rem;
right: $distance; right: 0.5rem;
cursor: pointer; cursor: pointer;
color: $ui-white; color: $ui-white;
transition-property: color; transition-property: color;
@@ -15,17 +23,11 @@
} }
} }
@mixin corner() {
display: none;
@include absolute-top-right(0.5rem);
transform: rotate(45deg);
}
@mixin panel() { @mixin panel() {
display: flex; display: flex;
position: relative; position: relative;
border-radius: 8px; border-radius: var(--editor--panel__br);
height: 100%; height: 100%;
background-color: $bg-container-l2; background-color: $bg-container-l2;
padding: 1rem; padding: 1rem;
@@ -21,10 +21,6 @@ import EventBlockProgressBar from './composite/EventBlockProgressBar';
import style from './EventBlock.module.scss'; import style from './EventBlock.module.scss';
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockInnerProps { interface EventBlockInnerProps {
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
@@ -98,11 +94,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
</div> </div>
<div className={style.titleSection}> <div className={style.titleSection}>
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} /> <EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
{isNext && ( {isNext && <span className={style.nextTag}>UP NEXT</span>}
<Tooltip label='Next event' {...tooltipProps}>
<span className={style.nextTag}>UP NEXT</span>
</Tooltip>
)}
</div> </div>
<EventBlockPlayback <EventBlockPlayback
eventId={eventId} eventId={eventId}
@@ -118,17 +110,17 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
{loaded && <EventBlockProgressBar />} {loaded && <EventBlockProgressBar />}
</div> </div>
<div className={style.eventStatus} tabIndex={-1}> <div className={style.eventStatus} tabIndex={-1}>
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}> <Tooltip label={`Time type: ${timerType}`} openDelay={tooltipDelayMid}>
<span> <span>
<TimerIcon type={timerType} className={style.statusIcon} /> <TimerIcon type={timerType} className={style.statusIcon} />
</span> </span>
</Tooltip> </Tooltip>
<Tooltip label={`End action: ${endAction}`} {...tooltipProps}> <Tooltip label={`End action: ${endAction}`} openDelay={tooltipDelayMid}>
<span> <span>
<EndActionIcon action={endAction} className={style.statusIcon} /> <EndActionIcon action={endAction} className={style.statusIcon} />
</span> </span>
</Tooltip> </Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}> <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}`} />
</span> </span>
@@ -2,13 +2,13 @@ import { ComponentType, useMemo } from 'react';
import { ViewExtendedTimer } from 'common/models/TimeManager.type'; import { ViewExtendedTimer } from 'common/models/TimeManager.type';
import { import {
CustomFields, CustomFields,
Message, MessageState,
OntimeEvent, OntimeEvent,
ProjectData, ProjectData,
Runtime, Runtime,
Settings, Settings,
SimpleTimerState,
SupportedEvent, SupportedEvent,
TimerMessage,
ViewSettings, ViewSettings,
} from 'ontime-types'; } from 'ontime-types';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
@@ -23,17 +23,17 @@ import { runtimeStore } from '../../common/stores/runtime';
import { useViewOptionsStore } from '../../common/stores/viewOptions'; import { useViewOptionsStore } from '../../common/stores/viewOptions';
type WithDataProps = { type WithDataProps = {
auxTimer: SimpleTimerState;
backstageEvents: OntimeEvent[]; backstageEvents: OntimeEvent[];
customFields: CustomFields; customFields: CustomFields;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
events: OntimeEvent[]; events: OntimeEvent[];
external: Message;
general: ProjectData; general: ProjectData;
isMirrored: boolean; isMirrored: boolean;
message: MessageState;
nextId: string | null; nextId: string | null;
onAir: boolean; onAir: boolean;
pres: TimerMessage;
publicEventNext: OntimeEvent | null; publicEventNext: OntimeEvent | null;
publicEventNow: OntimeEvent | null; publicEventNow: OntimeEvent | null;
publicSelectedId: string | null; publicSelectedId: string | null;
@@ -68,7 +68,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
}, [rundownData]); }, [rundownData]);
// websocket data // websocket data
const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime } = const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime, auxtimer1 } =
useStore(runtimeStore); useStore(runtimeStore);
const publicSelectedId = publicEventNow?.id ?? null; const publicSelectedId = publicEventNow?.id ?? null;
const selectedId = eventNow?.id ?? null; const selectedId = eventNow?.id ?? null;
@@ -96,17 +96,17 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
<ViewNavigationMenu /> <ViewNavigationMenu />
<Component <Component
{...props} {...props}
auxTimer={auxtimer1}
backstageEvents={rundownData} backstageEvents={rundownData}
customFields={customFields} customFields={customFields}
eventNext={eventNext} eventNext={eventNext}
eventNow={eventNow} eventNow={eventNow}
events={publicEvents} events={publicEvents}
external={message.external}
general={project} general={project}
isMirrored={isMirrored} isMirrored={isMirrored}
message={message}
nextId={nextId} nextId={nextId}
onAir={onAir} onAir={onAir}
pres={message.timer}
publicEventNext={publicEventNext} publicEventNext={publicEventNext}
publicEventNow={publicEventNow} publicEventNow={publicEventNow}
publicSelectedId={publicSelectedId} publicSelectedId={publicSelectedId}
@@ -2,11 +2,11 @@ import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { import {
CustomFields, CustomFields,
Message, MessageState,
OntimeEvent, OntimeEvent,
Playback, Playback,
Settings, Settings,
TimerMessage, SimpleTimerState,
TimerPhase, TimerPhase,
TimerType, TimerType,
ViewSettings, ViewSettings,
@@ -48,19 +48,19 @@ const titleVariants = {
export const MotionTitleCard = motion(TitleCard); export const MotionTitleCard = motion(TitleCard);
interface TimerProps { interface TimerProps {
auxTimer: SimpleTimerState;
customFields: CustomFields; customFields: CustomFields;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
external: Message;
isMirrored: boolean; isMirrored: boolean;
pres: TimerMessage; message: MessageState;
settings: Settings | undefined; settings: Settings | undefined;
time: ViewExtendedTimer; time: ViewExtendedTimer;
viewSettings: ViewSettings; viewSettings: ViewSettings;
} }
export default function Timer(props: TimerProps) { export default function Timer(props: TimerProps) {
const { customFields, isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props; const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
@@ -114,7 +114,7 @@ export default function Timer(props: TimerProps) {
const mainFieldNow = (main ? getPropertyValue(eventNow, main) : eventNow?.title) ?? ''; const mainFieldNow = (main ? getPropertyValue(eventNow, main) : eventNow?.title) ?? '';
const mainFieldNext = (main ? getPropertyValue(eventNext, main) : eventNext?.title) ?? ''; const mainFieldNext = (main ? getPropertyValue(eventNext, main) : eventNext?.title) ?? '';
const showOverlay = pres.text !== '' && pres.visible; const showOverlay = message.timer.text !== '' && message.timer.visible;
const isPlaying = time.playback !== Playback.Pause; const isPlaying = time.playback !== Playback.Pause;
const timerIsTimeOfDay = time.timerType === TimerType.Clock; const timerIsTimeOfDay = time.timerType === TimerType.Clock;
@@ -128,10 +128,20 @@ export default function Timer(props: TimerProps) {
const showFinished = finished && (shouldShowModifiers || showEndMessage); const showFinished = finished && (shouldShowModifiers || showEndMessage);
const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning; const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning;
const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger; const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger;
const showBlinking = pres.blink;
const showBlackout = pres.blackout;
const showClock = time.timerType !== TimerType.Clock; const showClock = time.timerType !== TimerType.Clock;
const showExternal = external.visible && external.text;
const secondaryContent = ((): string | undefined => {
if (message.timer.secondarySource === 'aux') {
return getFormattedTimer(auxTimer.current, TimerType.CountDown, getLocalizedString('common.minutes'), {
removeSeconds: userOptions.hideTimerSeconds,
removeLeadingZero: userOptions.removeLeadingZeros,
});
}
if (message.timer.secondarySource === 'external' && message.external) {
return message.external;
}
return;
})();
let timerColor = viewSettings.normalColor; let timerColor = viewSettings.normalColor;
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor; if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
@@ -146,13 +156,14 @@ export default function Timer(props: TimerProps) {
const stageTimerCharacters = display.replace('/:/g', '').length; const stageTimerCharacters = display.replace('/:/g', '').length;
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`; const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
let timerFontSize = 89 / (stageTimerCharacters - 1); let timerFontSize = 89 / (stageTimerCharacters - 1);
// we need to shrink the timer if the external is going to be there // we need to shrink the timer if the external is going to be there
if (showExternal) { if (secondaryContent) {
timerFontSize *= 0.8; timerFontSize *= 0.8;
} }
const externalFontSize = timerFontSize * 0.4; const externalFontSize = timerFontSize * 0.4;
const timerContainerClasses = `timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`; const timerContainerClasses = `timer-container ${message.timer.blink ? (showOverlay ? '' : 'blink') : ''}`;
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
const defaultFormat = getDefaultFormat(settings?.timeFormat); const defaultFormat = getDefaultFormat(settings?.timeFormat);
@@ -161,11 +172,11 @@ export default function Timer(props: TimerProps) {
return ( return (
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'> <div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
<ViewParamsEditor viewOptions={timerOptions} /> <ViewParamsEditor viewOptions={timerOptions} />
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} /> <div className={message.timer.blackout ? 'blackout blackout--active' : 'blackout'} />
{!userOptions.hideMessage && ( {!userOptions.hideMessage && (
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}> <div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
<FitText mode='multi' min={32} max={256} className={`message ${showBlinking ? 'blink' : ''}`}> <FitText mode='multi' min={32} max={256} className={`message ${message.timer.blink ? 'blink' : ''}`}>
{pres.text} {message.timer.text}
</FitText> </FitText>
</div> </div>
)} )}
@@ -192,10 +203,10 @@ export default function Timer(props: TimerProps) {
</div> </div>
)} )}
<div <div
className={`external${showExternal ? '' : ' external--hidden'}`} className={`external${secondaryContent ? '' : ' external--hidden'}`}
style={{ fontSize: `${externalFontSize}vw` }} style={{ fontSize: `${externalFontSize}vw` }}
> >
{external.text} {secondaryContent}
</div> </div>
</div> </div>
@@ -0,0 +1,36 @@
import { handleLegacyMessageConversion } from '../integration.legacy.js';
describe('handleLegacyConversion', () => {
it('should return the payload as is if it is not a legacy message', () => {
expect(handleLegacyMessageConversion({})).toEqual({});
const newPayload = {
timer: {
text: 'text',
visible: true,
blink: true,
blackout: true,
},
external: 'text',
};
expect(handleLegacyMessageConversion(newPayload)).toEqual(newPayload);
});
it('should convert a legacy payload with external message', () => {
expect(handleLegacyMessageConversion({ external: { text: 'text', visible: true } })).toEqual({
external: 'text',
timer: {
secondarySource: 'external',
},
});
expect(handleLegacyMessageConversion({ external: { visible: true } })).toEqual({
timer: {
secondarySource: 'external',
},
});
expect(handleLegacyMessageConversion({ external: { text: 'text' } })).toEqual({
external: 'text',
});
});
});
@@ -1,9 +1,11 @@
import { DeepPartial, MessageState, OntimeEvent, SimpleDirection, SimplePlayback } from 'ontime-types'; import { MessageState, OntimeEvent, SimpleDirection, SimplePlayback } from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils'; import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils';
import { DeepPartial } from 'ts-essentials';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js'; import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
import { auxTimerService } from '../services/aux-timer-service/AuxTimerService.js'; import { auxTimerService } from '../services/aux-timer-service/AuxTimerService.js';
import { messageService } from '../services/message-service/MessageService.js'; import * as messageService from '../services/message-service/MessageService.js';
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js'; import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
import { runtimeService } from '../services/runtime-service/RuntimeService.js'; import { runtimeService } from '../services/runtime-service/RuntimeService.js';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
@@ -14,6 +16,8 @@ import { socket } from '../adapters/WebsocketAdapter.js';
import { throttle } from '../utils/throttle.js'; import { throttle } from '../utils/throttle.js';
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js'; import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
import { handleLegacyMessageConversion } from './integration.legacy.js';
const throttledUpdateEvent = throttle(updateEvent, 20); const throttledUpdateEvent = throttle(updateEvent, 20);
export function dispatchFromAdapter(type: string, payload: unknown, _source?: 'osc' | 'ws' | 'http') { export function dispatchFromAdapter(type: string, payload: unknown, _source?: 'osc' | 'ws' | 'http') {
@@ -78,9 +82,12 @@ const actionHandlers: Record<string, ActionHandler> = {
message: (payload) => { message: (payload) => {
assert.isObject(payload); assert.isObject(payload);
// TODO: remove this once we feel its been enough time, ontime 3.6.0, 20/09/2024
const migratedPayload = handleLegacyMessageConversion(payload);
const patch: DeepPartial<MessageState> = { const patch: DeepPartial<MessageState> = {
timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined, timer: 'timer' in migratedPayload ? validateTimerMessage(migratedPayload.timer) : undefined,
external: 'external' in payload ? validateMessage(payload.external) : undefined, external: 'external' in migratedPayload ? validateMessage(migratedPayload.external) : undefined,
}; };
const newMessage = messageService.patch(patch); const newMessage = messageService.patch(patch);
@@ -0,0 +1,67 @@
import { MessageState } from 'ontime-types';
import { DeepPartial } from 'ts-essentials';
export type LegacyMessageState = DeepPartial<{
timer: {
text: string;
visible: boolean;
blink: boolean;
blackout: boolean;
};
external: {
text: string;
visible: boolean;
};
}>;
function isLegacyMessageState(value: object): value is LegacyMessageState {
// @ts-expect-error -- good enough here
return value?.external?.text !== undefined || value?.external?.visible !== undefined;
}
/**
* This function is used to maintain support for legacy data in the /message endpoint
* The previous message endpoint expected a patch of the message state
* @example {
* timer: { blink: boolean, blackout: boolean, text: string, visible: boolean },
* external: { visible: boolean, text: string }
* }
*
* This change is introduced in version 3.6.0
*/
export function handleLegacyMessageConversion(payload: object): object | Partial<MessageState> {
// if it is not a legacy message, we pass it as is
if (!isLegacyMessageState(payload)) {
return payload;
}
/**
* The current migration only needs to handle the cases
* for the deprecated external message controls
*/
// Migrate external message
// 2.1 the user gives us the text and a visible flag
if (payload?.external?.text !== undefined && payload.external.visible !== undefined) {
return {
timer: { secondarySource: payload.external.visible ? 'external' : null },
external: payload.external.text,
} as Partial<MessageState>;
}
// 2.2 the user gives us the text
else if (payload?.external?.text !== undefined) {
return {
external: payload.external.text,
} as Partial<MessageState>;
}
// 2.3 the user gives us the visible flag
else if (payload?.external?.visible !== undefined) {
return {
timer: { secondarySource: payload.external.visible ? 'external' : null },
} as Partial<MessageState>;
}
// there should be no case for us to reach this since
// the type guard would have ensured one of the above states
return payload;
}
+3 -3
View File
@@ -38,7 +38,7 @@ import { populateStyles } from './setup/loadStyles.js';
import { eventStore } from './stores/EventStore.js'; import { eventStore } from './stores/EventStore.js';
import { runtimeService } from './services/runtime-service/RuntimeService.js'; import { runtimeService } from './services/runtime-service/RuntimeService.js';
import { restoreService } from './services/RestoreService.js'; import { restoreService } from './services/RestoreService.js';
import { messageService } from './services/message-service/MessageService.js'; import * as messageService from './services/message-service/MessageService.js';
import { populateDemo } from './setup/loadDemo.js'; import { populateDemo } from './setup/loadDemo.js';
import { getState } from './stores/runtimeState.js'; import { getState } from './stores/runtimeState.js';
import { initRundown } from './services/rundown-service/RundownService.js'; import { initRundown } from './services/rundown-service/RundownService.js';
@@ -198,7 +198,7 @@ export const startServer = async (
// initialise logging service, escalateErrorFn is only exists in electron // initialise logging service, escalateErrorFn is only exists in electron
logger.init(escalateErrorFn); logger.init(escalateErrorFn);
// initialise rundown service // initialise rundown service
const persistedRundown = getDataProvider().getRundown(); const persistedRundown = getDataProvider().getRundown();
const persistedCustomFields = getDataProvider().getCustomFields(); const persistedCustomFields = getDataProvider().getCustomFields();
initRundown(persistedRundown, persistedCustomFields); initRundown(persistedRundown, persistedCustomFields);
@@ -210,7 +210,7 @@ export const startServer = async (
runtimeService.init(maybeRestorePoint); runtimeService.init(maybeRestorePoint);
// eventStore set is a dependency of the services that publish to it // eventStore set is a dependency of the services that publish to it
messageService.init(eventStore.set.bind(eventStore)); messageService.init(eventStore.set);
expressServer.listen(serverPort, '0.0.0.0', () => { expressServer.listen(serverPort, '0.0.0.0', () => {
const nif = getNetworkInterfaces(); const nif = getNetworkInterfaces();
@@ -1,68 +1,63 @@
import { DeepPartial, Message, TimerMessage, MessageState } from 'ontime-types'; import { TimerMessage, MessageState } from 'ontime-types';
import { DeepPartial } from 'ts-essentials';
import { throttle } from '../../utils/throttle.js'; import { throttle } from '../../utils/throttle.js';
import type { PublishFn } from '../../stores/EventStore.js'; import type { PublishFn } from '../../stores/EventStore.js';
let instance: MessageService | null = null; const defaultTimer: TimerMessage = {
text: '',
visible: false,
blink: false,
blackout: false,
secondarySource: null,
};
class MessageService { let timer = { ...defaultTimer };
timer: TimerMessage; let external = '';
external: Message;
private throttledSet: PublishFn; let throttledSet: PublishFn | null = null;
private publish: PublishFn | null;
constructor() { /**
if (instance) { * Initialises the message service with a publish function
throw new Error('There can be only one'); * @param publishFn
} */
export function init(publishFn: PublishFn) {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton throttledSet = throttle(publishFn, 100);
instance = this;
this.throttledSet = () => {
throw new Error('Published called before initialisation');
};
this.clear();
}
clear() {
this.timer = {
text: '',
visible: false,
blink: false,
blackout: false,
};
this.external = {
text: '',
visible: false,
};
}
init(publish: PublishFn) {
this.publish = publish;
this.throttledSet = throttle((key, value) => this.publish?.(key, value), 100);
}
getState(): MessageState {
return {
timer: this.timer,
external: this.external,
};
}
patch(message: DeepPartial<MessageState>) {
if (message.timer) this.timer = { ...this.timer, ...message.timer };
if (message.external) this.external = { ...this.external, ...message.external };
const newState = this.getState();
this.throttledSet('message', newState);
return newState;
}
} }
export const messageService = new MessageService(); /**
* Exposes function to reset the internal state
*/
export function clear() {
timer = { ...defaultTimer };
external = '';
}
/**
* Exposes the internal state of the message service
*/
export function getState(): MessageState {
return {
external,
timer,
};
}
/**
* Utility function allows patching internal object
*/
export function patch(patch: DeepPartial<MessageState>): MessageState {
// we cannot call patch before init
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
if (throttledSet === null) {
throw new Error('MessageService.patch() called before init()');
}
}
if ('timer' in patch) timer = { ...timer, ...patch.timer };
if ('external' in patch && patch.external !== undefined) external = patch.external;
const newState = getState();
throttledSet?.('message', newState);
return newState;
}
@@ -1,4 +1,4 @@
import { messageService } from '../MessageService.js'; import * as messageService from '../MessageService.js';
describe('MessageService', () => { describe('MessageService', () => {
const publishFunction = () => {}; const publishFunction = () => {};
@@ -14,17 +14,14 @@ describe('MessageService', () => {
it('should patch the message state', () => { it('should patch the message state', () => {
const message = { const message = {
timer: { text: 'new text', visible: true }, timer: { text: 'new text', visible: true },
external: { visible: true }, external: 'external',
}; };
const newState = messageService.patch(message); const newState = messageService.patch(message);
expect(newState).toEqual({ expect(newState).toEqual({
timer: { text: 'new text', visible: true, blackout: false, blink: false }, timer: { text: 'new text', visible: true, blackout: false, blink: false, secondarySource: null },
external: { external: 'external',
text: '',
visible: true,
},
}); });
}); });
@@ -36,11 +33,8 @@ describe('MessageService', () => {
const newState = messageService.patch(initialMessage); const newState = messageService.patch(initialMessage);
expect(newState).toEqual({ expect(newState).toEqual({
timer: { text: 'initial text', visible: true, blackout: false, blink: false }, timer: { text: 'initial text', visible: true, blackout: false, blink: false, secondarySource: null },
external: { external: '',
text: '',
visible: false,
},
}); });
}); });
}); });
@@ -2,26 +2,7 @@ import { validateMessage, validateTimerMessage } from '../messageUtils.js';
describe('validateMessage()', () => { describe('validateMessage()', () => {
it('returns a valid Message object', () => { it('returns a valid Message object', () => {
const payload = { expect(validateMessage('test')).toEqual('test');
text: '12312',
visible: 'true',
};
const expected = {
text: '12312',
visible: true,
};
expect(validateMessage(payload)).toEqual(expected);
});
it('skips keys not given', () => {
const payload = {
visible: 'true',
};
const expected = {
visible: true,
};
expect(validateMessage(payload)).toStrictEqual(expected);
}); });
}); });
@@ -1,4 +1,4 @@
import { Message, TimerMessage } from 'ontime-types'; import { TimerMessage } from 'ontime-types';
import * as assert from '../../utils/assert.js'; import * as assert from '../../utils/assert.js';
import { coerceBoolean, coerceString } from '../../utils/coerceType.js'; import { coerceBoolean, coerceString } from '../../utils/coerceType.js';
@@ -7,14 +7,8 @@ import { coerceBoolean, coerceString } from '../../utils/coerceType.js';
* Creates a valid Message object from a payload * Creates a valid Message object from a payload
* @throws if the payload is not an object * @throws if the payload is not an object
*/ */
export function validateMessage(message: unknown): Partial<Message> { export function validateMessage(message: unknown): string {
assert.isObject(message); return decodeURI(coerceString(message));
const result: Partial<Message> = {};
if ('text' in message) result.text = coerceString(message.text);
if ('visible' in message) result.visible = coerceBoolean(message.visible);
return result;
} }
/** /**
@@ -26,10 +20,28 @@ export function validateTimerMessage(message: unknown): Partial<TimerMessage> {
const result: Partial<TimerMessage> = {}; const result: Partial<TimerMessage> = {};
if ('text' in message) result.text = coerceString(message.text); if ('text' in message) result.text = decodeURI(coerceString(message.text));
if ('visible' in message) result.visible = coerceBoolean(message.visible); if ('visible' in message) result.visible = coerceBoolean(message.visible);
if ('blink' in message) result.blink = coerceBoolean(message.blink); if ('blink' in message) result.blink = coerceBoolean(message.blink);
if ('blackout' in message) result.blackout = coerceBoolean(message.blackout); if ('blackout' in message) result.blackout = coerceBoolean(message.blackout);
if ('secondarySource' in message) result.secondarySource = coerceSecondary(message.secondarySource);
return result; return result;
} }
/**
* Asserts that the secondary value is one of the permitted values
*/
function assertSecondary(source: unknown): source is TimerMessage['secondarySource'] {
return source === 'aux' || source === 'external' || source === null;
}
/**
* Ensures that the secondary value is one of the permitted values
*/
function coerceSecondary(source: unknown): TimerMessage['secondarySource'] {
if (!assertSecondary(source)) {
return null;
}
return source;
}
@@ -7,9 +7,9 @@ test('message control sends messages to screens', async ({ context }) => {
await editorPage.goto('http://localhost:4001/messagecontrol'); await editorPage.goto('http://localhost:4001/messagecontrol');
// stage timer message // stage timer message
await editorPage.getByPlaceholder('Timer').click(); await editorPage.getByPlaceholder('Message shown fullscreen in stage timer').click();
await editorPage.getByPlaceholder('Timer').fill('testing stage'); await editorPage.getByPlaceholder('Message shown fullscreen in stage timer').fill('testing stage');
await editorPage.getByRole('button', { name: /toggle timer/i }).click({ timeout: 5000 }); await editorPage.getByRole('button', { name: /toggle timer message/i }).click({ timeout: 5000 });
await featurePage.goto('http://localhost:4001/timer'); await featurePage.goto('http://localhost:4001/timer');
await featurePage.waitForLoadState('load', { timeout: 5000 }); await featurePage.waitForLoadState('load', { timeout: 5000 });
@@ -1,14 +1,12 @@
export type Message = { export type TimerMessage = {
text: string; text: string;
visible: boolean; visible: boolean;
};
export type TimerMessage = Message & {
blink: boolean; blink: boolean;
blackout: boolean; blackout: boolean;
secondarySource: 'aux' | 'external' | null;
}; };
export type MessageState = { export type MessageState = {
timer: TimerMessage; timer: TimerMessage;
external: Message; external: string;
}; };
+2 -2
View File
@@ -58,7 +58,7 @@ export type { RundownCached, NormalisedRundown } from './api/rundown-controller/
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js'; export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
export { Playback } from './definitions/runtime/Playback.type.js'; export { Playback } from './definitions/runtime/Playback.type.js';
export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js'; export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
export type { Message, TimerMessage, MessageState } from './definitions/runtime/MessageControl.type.js'; export type { TimerMessage, MessageState } from './definitions/runtime/MessageControl.type.js';
export type { Runtime } from './definitions/runtime/Runtime.type.js'; export type { Runtime } from './definitions/runtime/Runtime.type.js';
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
@@ -80,4 +80,4 @@ export {
isOntimeCycle, isOntimeCycle,
isKeyOfType, isKeyOfType,
} from './utils/guards.js'; } from './utils/guards.js';
export type { DeepPartial, MaybeNumber, MaybeString } from './utils/utils.type.js'; export type { MaybeNumber, MaybeString } from './utils/utils.type.js';
-4
View File
@@ -1,6 +1,2 @@
export type MaybeNumber = number | null; export type MaybeNumber = number | null;
export type MaybeString = string | null; export type MaybeString = string | null;
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};