External field (#580)

* feat: add external endpoint

* add external to messagecontrol

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2023-11-22 08:06:48 +01:00
committed by GitHub
parent c4886a617c
commit 797a0edf57
18 changed files with 229 additions and 49 deletions
@@ -27,6 +27,7 @@ export const useMessageControl = () => {
timerMessage: state.timerMessage, timerMessage: state.timerMessage,
publicMessage: state.publicMessage, publicMessage: state.publicMessage,
lowerMessage: state.lowerMessage, lowerMessage: state.lowerMessage,
externalMessage: state.externalMessage,
onAir: state.onAir, onAir: state.onAir,
}); });
@@ -40,6 +41,8 @@ export const setMessage = {
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload), publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload), lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload), lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
externalText: (payload: string) => socketSendJson('set-external-message-text', payload),
externalVisible: (payload: boolean) => socketSendJson('set-external-message-visible', payload),
onAir: (payload: boolean) => socketSendJson('set-onAir', payload), onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload), timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload), timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
+5 -1
View File
@@ -3,7 +3,7 @@ import { Playback, RuntimeStore } from 'ontime-types';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla'; import { createStore } from 'zustand/vanilla';
export const runtimeStorePlaceholder = { export const runtimeStorePlaceholder: RuntimeStore = {
timer: { timer: {
clock: 0, clock: 0,
current: null, current: null,
@@ -33,6 +33,10 @@ export const runtimeStorePlaceholder = {
text: '', text: '',
visible: false, visible: false,
}, },
externalMessage: {
text: '',
visible: false,
},
onAir: false, onAir: false,
loaded: { loaded: {
numEvents: 0, numEvents: 0,
@@ -1,4 +1,4 @@
import { Input } from '@chakra-ui/react'; import { IconButton, 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';
@@ -13,13 +13,14 @@ interface InputRowProps {
placeholder: string; placeholder: string;
text: string; text: string;
visible?: boolean; visible?: boolean;
readonly?: boolean;
actionHandler: (action: string, payload: object) => void; actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void; changeHandler: (newValue: string) => void;
className?: string; className?: string;
} }
export default function InputRow(props: InputRowProps) { export default function InputRow(props: InputRowProps) {
const { label, placeholder, text, visible, actionHandler, changeHandler, className } = props; const { label, placeholder, text, visible, actionHandler, changeHandler, className, readonly } = props;
const handleInputChange = (newValue: string) => { const handleInputChange = (newValue: string) => {
changeHandler(newValue); changeHandler(newValue);
@@ -33,19 +34,31 @@ export default function InputRow(props: InputRowProps) {
<Input <Input
size='sm' size='sm'
variant='ontime-filled' variant='ontime-filled'
readOnly={readonly}
disabled={readonly}
value={text} value={text}
onChange={(event) => handleInputChange(event.target.value)} onChange={(event) => handleInputChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
/> />
<TooltipActionBtn {readonly ? (
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })} <IconButton
tooltip={visible ? 'Make invisible' : 'Make visible'} size='sm'
aria-label={`Toggle ${label}`} isDisabled
openDelay={tooltipDelayMid} icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />} aria-label={`Toggle ${label}`}
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>
); );
@@ -27,7 +27,3 @@
color: $action-text-color; color: $action-text-color;
} }
} }
.padTop {
margin-top: $section-spacing;
}
@@ -34,34 +34,48 @@ export default function MessageControl() {
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)} actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
/> />
<InputRow <InputRow
label='Timer message' label='Timer'
placeholder='Shown in stage timer' placeholder='Message shown in stage timer'
text={data.timerMessage.text || ''} text={data.timerMessage.text || ''}
visible={data.timerMessage.visible || false} visible={data.timerMessage.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)} changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)} actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
/> />
<div className={style.buttonSection}> <div className={style.buttonSection}>
<label className={style.label}>Timer message blink</label>
<label className={style.label}>Blackout timer screens</label>
<Button <Button
size='sm'
className={`${data.timerMessage.timerBlink ? style.blink : ''}`} className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'} variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='24px' /> : <IoSunnyOutline size='24px' />} leftIcon={data.timerMessage.timerBlink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />}
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)} onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
data-testid='toggle timer blink' data-testid='toggle timer blink'
/> >
Blink message
</Button>
<Button <Button
size='sm'
className={style.blackoutButton} className={style.blackoutButton}
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'} variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='24px' /> : <IoEyeOffOutline size='24px' />} leftIcon={data.timerMessage.timerBlackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />}
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)} onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
data-testid='toggle timer blackout' data-testid='toggle timer blackout'
/> >
Blackout screen
</Button>
</div> </div>
<div className={`${style.onAirSection} ${style.padTop}`}> <InputRow
label='External Message'
placeholder='-'
readonly
text={data.externalMessage.text || ''}
visible={data.externalMessage.visible || false}
changeHandler={() => undefined}
actionHandler={() => undefined}
/>
<div className={style.onAirSection}>
<label className={style.label}>Toggle On Air state</label> <label className={style.label}>Toggle On Air state</label>
<Button <Button
size='sm'
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'} variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />} leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
onClick={() => setMessage.onAir(!data.onAir)} onClick={() => setMessage.onAir(!data.onAir)}
@@ -14,6 +14,7 @@ type WithDataProps = {
pres: TimerMessage; pres: TimerMessage;
publ: Message; publ: Message;
lower: Message; lower: Message;
external: Message;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
publicEventNow: OntimeEvent | null; publicEventNow: OntimeEvent | null;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
@@ -51,12 +52,12 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
}, [rundownData]); }, [rundownData]);
// websocket data // websocket data
const data = useStore(runtime);
const { const {
timer, timer,
publicMessage, publicMessage,
timerMessage, timerMessage,
lowerMessage, lowerMessage,
externalMessage,
playback, playback,
onAir, onAir,
eventNext, eventNext,
@@ -64,7 +65,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
publicEventNow, publicEventNow,
eventNow, eventNow,
loaded, loaded,
} = data; } = useStore(runtime);
const publicSelectedId = loaded.selectedPublicEventId; const publicSelectedId = loaded.selectedPublicEventId;
const selectedId = loaded.selectedEventId; const selectedId = loaded.selectedEventId;
const nextId = loaded.nextEventId; const nextId = loaded.nextEventId;
@@ -92,6 +93,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
pres={timerMessage} pres={timerMessage}
publ={publicMessage} publ={publicMessage}
lower={lowerMessage} lower={lowerMessage}
external={externalMessage}
eventNow={eventNow} eventNow={eventNow}
publicEventNow={publicEventNow} publicEventNow={publicEventNow}
eventNext={eventNext} eventNext={eventNext}
@@ -79,6 +79,9 @@
justify-self: center; justify-self: center;
align-self: center; align-self: center;
width: 100%;
overflow: hidden;
.end-message { .end-message {
text-align: center; text-align: center;
font-size: 11.5vw; font-size: 11.5vw;
@@ -90,7 +93,6 @@
.timer { .timer {
opacity: 1; opacity: 1;
font-size: 20vw;
font-family: var(--font-family-override, $viewer-font-family); font-family: var(--font-family-override, $viewer-font-family);
color: var(--timer-color-override, $timer-color); color: var(--timer-color-override, $timer-color);
line-height: 0.9em; line-height: 0.9em;
@@ -98,6 +100,9 @@
letter-spacing: 0.05em; letter-spacing: 0.05em;
font-weight: 600; font-weight: 600;
transition-property: font-size;
transition-duration: $viewer-transition-time;
&--paused { &--paused {
opacity: $viewer-opacity-disabled; opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time; transition: $viewer-transition-time;
@@ -109,6 +114,27 @@
} }
} }
.external {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
text-align: center;
color: var(--external-color-override, $external-color);
letter-spacing: 0.5px;
line-height: 0.9em;
padding-bottom: 0.2em;
transition-property: opacity, height;
transition-duration: $viewer-transition-time;
border-top: 1px solid rgba(white, 0.1);
&--hidden {
opacity: 0;
height: 0;
}
}
.progress-container { .progress-container {
grid-area: progress; grid-area: progress;
width: 100%; width: 100%;
@@ -122,7 +148,6 @@
} }
} }
/* =================== OVERLAY ===================*/ /* =================== OVERLAY ===================*/
.message-overlay { .message-overlay {
@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { OntimeEvent, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types'; import { Message, OntimeEvent, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar'; import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
@@ -41,6 +41,7 @@ const titleVariants = {
interface TimerProps { interface TimerProps {
isMirrored: boolean; isMirrored: boolean;
pres: TimerMessage; pres: TimerMessage;
external: Message;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
time: TimeManagerType; time: TimeManagerType;
@@ -48,10 +49,9 @@ interface TimerProps {
} }
export default function Timer(props: TimerProps) { export default function Timer(props: TimerProps) {
const { isMirrored, pres, eventNow, eventNext, time, viewSettings } = props; const { isMirrored, pres, eventNow, eventNext, time, viewSettings, external } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
useEffect(() => { useEffect(() => {
document.title = 'ontime - Timer'; document.title = 'ontime - Timer';
}, []); }, []);
@@ -78,6 +78,7 @@ export default function Timer(props: TimerProps) {
const showBlinking = pres.timerBlink; const showBlinking = pres.timerBlink;
const showBlackout = pres.timerBlackout; const showBlackout = pres.timerBlackout;
const showClock = time.timerType !== TimerType.Clock; const showClock = time.timerType !== TimerType.Clock;
const showExternal = external.visible && external.text;
const timerColor = const timerColor =
showProgress && showDanger showProgress && showDanger
@@ -94,7 +95,13 @@ export default function Timer(props: TimerProps) {
} }
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`; const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`;
const timerFontSize = 89 / (stageTimerCharacters - 1); let timerFontSize = 89 / (stageTimerCharacters - 1);
// we need to shrink the timer if the external is going to be there
if (showExternal) {
timerFontSize *= 0.8;
}
const externalFontSize = timerFontSize * 0.4
const timerContainerClasses = `timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`;
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
return ( return (
@@ -110,7 +117,7 @@ export default function Timer(props: TimerProps) {
<SuperscriptTime time={clock} className='clock' /> <SuperscriptTime time={clock} className='clock' />
</div> </div>
<div className={`timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`}> <div className={timerContainerClasses}>
{showEndMessage ? ( {showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div> <div className='end-message'>{viewSettings.endMessage}</div>
) : ( ) : (
@@ -124,6 +131,12 @@ export default function Timer(props: TimerProps) {
{display} {display}
</div> </div>
)} )}
<div
className={`external${showExternal ? '' : ' external--hidden'}`}
style={{ fontSize: `${externalFontSize}vw` }}
>
{external.text}
</div>
</div> </div>
<MultiPartProgressBar <MultiPartProgressBar
+1 -1
View File
@@ -5,7 +5,6 @@
// General // General
$viewer-transition-time: 0.5s; $viewer-transition-time: 0.5s;
// Text // Text
$viewer-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override $viewer-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
$viewer-opacity-disabled: 0.6; $viewer-opacity-disabled: 0.6;
@@ -28,3 +27,4 @@ $timer-color: rgba(white, 80%); // --timer-color-override
$timer-finished-color: $playback-negative; $timer-finished-color: $playback-negative;
$timer-bold-font-family: "Arial Black", sans-serif; // --card-background-color-override $timer-bold-font-family: "Arial Black", sans-serif; // --card-background-color-override
$external-color: rgba(white, 70%); // --external-color-override
+15
View File
@@ -12,6 +12,11 @@ const commonStyles = {
border: '1px solid #578AF4', // $blue-500 border: '1px solid #578AF4', // $blue-500
}, },
_placeholder: { color: '#9d9d9d' }, // $gray-500 _placeholder: { color: '#9d9d9d' }, // $gray-500
_disabled: {
_hover: {
backgroundColor: '#262626', // $gray-1200
},
},
}; };
export const ontimeInputFilled = { export const ontimeInputFilled = {
@@ -30,6 +35,11 @@ export const ontimeInputFilledOnLight = {
_focus: { _focus: {
border: '2px solid #578AF4', // $blue-500 border: '2px solid #578AF4', // $blue-500
}, },
_disabled: {
_hover: {
backgroundColor: 'white',
},
},
}, },
}; };
@@ -58,4 +68,9 @@ export const ontimeTextAreaFilledOnLight = {
border: '2px solid #578AF4', // $blue-500 border: '2px solid #578AF4', // $blue-500
}, },
_placeholder: { color: '#9d9d9d' }, // $gray-500 _placeholder: { color: '#9d9d9d' }, // $gray-500
_disabled: {
_hover: {
backgroundColor: 'white',
},
},
}; };
+4
View File
@@ -33,6 +33,7 @@ import { populateStyles } from './modules/loadStyles.js';
import { eventStore, getInitialPayload } from './stores/EventStore.js'; import { eventStore, getInitialPayload } from './stores/EventStore.js';
import { PlaybackService } from './services/PlaybackService.js'; import { PlaybackService } from './services/PlaybackService.js';
import { RestorePoint, restoreService } from './services/RestoreService.js'; import { RestorePoint, restoreService } from './services/RestoreService.js';
import { messageService } from './services/message-service/MessageService.js';
console.log(`Starting Ontime version ${ONTIME_VERSION}`); console.log(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -155,6 +156,9 @@ export const startServer = async () => {
const initialPayload = getInitialPayload(); const initialPayload = getInitialPayload();
eventStore.init(initialPayload); eventStore.init(initialPayload);
// eventStore set is a dependency of the services that publish to it
messageService.init(eventStore.set.bind(eventStore));
expressServer.listen(serverPort, '0.0.0.0'); expressServer.listen(serverPort, '0.0.0.0');
return { message: returnMessage, serverPort }; return { message: returnMessage, serverPort };
@@ -109,6 +109,20 @@ export function dispatchFromAdapter(
break; break;
} }
case 'set-external-message-text': {
if (typeof payload !== 'string') {
throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setExternalText(payload);
return;
}
case 'set-external-message-visible': {
if (typeof payload === 'undefined') {
throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setExternalVisibility(Boolean(payload));
break;
}
case 'start': { case 'start': {
PlaybackService.start(); PlaybackService.start();
break; break;
+2
View File
@@ -12,6 +12,8 @@
--timer-progress-bg-override: #fff; --timer-progress-bg-override: #fff;
--timer-progress-override: #202020; --timer-progress-override: #202020;
--external-color-override: #161616;
--cuesheet-running-bg-override: #D20300; --cuesheet-running-bg-override: #D20300;
--operator-running-bg-override: #D20300; --operator-running-bg-override: #D20300;
@@ -1,7 +1,9 @@
import { Message } from 'ontime-types'; import { Message } from 'ontime-types';
import { eventStore } from '../../stores/EventStore.js';
import { TimerMessage } from 'ontime-types/src/definitions/runtime/MessageControl.type.js'; import { TimerMessage } from 'ontime-types/src/definitions/runtime/MessageControl.type.js';
import { throttle } from '../../utils/throttle.js';
import type { PublishFn } from '../../stores/EventStore.js';
let instance; let instance;
@@ -9,8 +11,12 @@ class MessageService {
timerMessage: TimerMessage; timerMessage: TimerMessage;
publicMessage: Message; publicMessage: Message;
lowerMessage: Message; lowerMessage: Message;
externalMessage: Message;
onAir: boolean; onAir: boolean;
private throttledSet: PublishFn;
private publish: PublishFn | null;
constructor() { constructor() {
if (instance) { if (instance) {
throw new Error('There can be only one'); throw new Error('There can be only one');
@@ -36,7 +42,40 @@ class MessageService {
visible: false, visible: false,
}; };
this.externalMessage = {
text: '',
visible: false,
};
this.onAir = false; this.onAir = false;
this.throttledSet = () => {
throw new Error('Published called before initialisation');
};
}
init(publish: PublishFn) {
this.publish = publish;
this.throttledSet = throttle((key, value) => this.publish(key, value), 100);
}
/**
* @description sets message on stage timer screen
*/
setExternalText(payload: string) {
if (this.externalMessage.text !== payload) {
this.externalMessage.text = payload;
this.throttledSet('externalMessage', this.externalMessage);
}
return this.getAll();
}
/**
* @description sets message visibility on stage timer screen
*/
setExternalVisibility(status: boolean) {
this.externalMessage.visible = status;
this.throttledSet('externalMessage', this.externalMessage);
return this.getAll();
} }
/** /**
@@ -44,7 +83,7 @@ class MessageService {
*/ */
setTimerText(payload: string) { setTimerText(payload: string) {
this.timerMessage.text = payload; this.timerMessage.text = payload;
eventStore.set('timerMessage', this.timerMessage); this.throttledSet('timerMessage', this.timerMessage);
return this.getAll(); return this.getAll();
} }
@@ -53,7 +92,7 @@ class MessageService {
*/ */
setTimerVisibility(status: boolean) { setTimerVisibility(status: boolean) {
this.timerMessage.visible = status; this.timerMessage.visible = status;
eventStore.set('timerMessage', this.timerMessage); this.throttledSet('timerMessage', this.timerMessage);
return this.getAll(); return this.getAll();
} }
@@ -62,7 +101,7 @@ class MessageService {
*/ */
setPublicText(payload: string) { setPublicText(payload: string) {
this.publicMessage.text = payload; this.publicMessage.text = payload;
eventStore.set('publicMessage', this.publicMessage); this.throttledSet('publicMessage', this.publicMessage);
return this.getAll(); return this.getAll();
} }
@@ -71,7 +110,7 @@ class MessageService {
*/ */
setPublicVisibility(status: boolean) { setPublicVisibility(status: boolean) {
this.publicMessage.visible = status; this.publicMessage.visible = status;
eventStore.set('publicMessage', this.publicMessage); this.throttledSet('publicMessage', this.publicMessage);
return this.getAll(); return this.getAll();
} }
@@ -80,7 +119,7 @@ class MessageService {
*/ */
setLowerText(payload: string) { setLowerText(payload: string) {
this.lowerMessage.text = payload; this.lowerMessage.text = payload;
eventStore.set('lowerMessage', this.lowerMessage); this.throttledSet('lowerMessage', this.lowerMessage);
return this.getAll(); return this.getAll();
} }
@@ -89,7 +128,7 @@ class MessageService {
*/ */
setLowerVisibility(status: boolean) { setLowerVisibility(status: boolean) {
this.lowerMessage.visible = status; this.lowerMessage.visible = status;
eventStore.set('lowerMessage', this.lowerMessage); this.throttledSet('lowerMessage', this.lowerMessage);
return this.getAll(); return this.getAll();
} }
@@ -102,7 +141,7 @@ class MessageService {
} else { } else {
this.onAir = status; this.onAir = status;
} }
eventStore.set('onAir', this.onAir); this.throttledSet('onAir', this.onAir);
return this.getAll(); return this.getAll();
} }
@@ -116,7 +155,7 @@ class MessageService {
} else { } else {
this.timerMessage.timerBlink = status; this.timerMessage.timerBlink = status;
} }
eventStore.set('timerMessage', this.timerMessage); this.throttledSet('timerMessage', this.timerMessage);
return this.getAll(); return this.getAll();
} }
@@ -130,7 +169,7 @@ class MessageService {
} else { } else {
this.timerMessage.timerBlackout = status; this.timerMessage.timerBlackout = status;
} }
eventStore.set('timerMessage', this.timerMessage); this.throttledSet('timerMessage', this.timerMessage);
return this.getAll(); return this.getAll();
} }
+3
View File
@@ -4,6 +4,8 @@ import { eventTimer } from '../services/TimerService.js';
import { messageService } from '../services/message-service/MessageService.js'; import { messageService } from '../services/message-service/MessageService.js';
import { eventLoader } from '../classes/event-loader/EventLoader.js'; import { eventLoader } from '../classes/event-loader/EventLoader.js';
export type PublishFn = <T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) => void;
let store: Partial<RuntimeStore> = {}; let store: Partial<RuntimeStore> = {};
/** /**
@@ -68,6 +70,7 @@ export const getInitialPayload = () => ({
timerMessage: messageService.timerMessage, timerMessage: messageService.timerMessage,
publicMessage: messageService.publicMessage, publicMessage: messageService.publicMessage,
lowerMessage: messageService.lowerMessage, lowerMessage: messageService.lowerMessage,
externalMessage: messageService.externalMessage,
onAir: messageService.onAir, onAir: messageService.onAir,
loaded: eventLoader.loaded, loaded: eventLoader.loaded,
eventNow: eventLoader.eventNow, eventNow: eventLoader.eventNow,
+32
View File
@@ -0,0 +1,32 @@
/**
* Creates a throttled version of the passed function
* This function uses a leading algorithm
* which means that the function will be executed immediately on first call
* @param {Function} cb - function to throttle
* @param {number} delay - time (in ms) to throttle
* @returns {Function}
*/
export function throttle<T extends any[], U>(cb: (...args: T) => U, delay: number) {
let shouldWait = false;
let waitingArgs;
const timeoutFunc = () => {
if (waitingArgs == null) {
shouldWait = false;
} else {
cb(...waitingArgs);
waitingArgs = null;
setTimeout(timeoutFunc, delay);
}
};
return (...args: T) => {
if (shouldWait) {
waitingArgs = args;
return;
}
cb(...args);
shouldWait = true;
setTimeout(timeoutFunc, delay);
};
}
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
test('test', async ({ context }) => { test('message control sends messages to screens', async ({ context }) => {
const editorPage = await context.newPage(); const editorPage = await context.newPage();
const featurePage = await context.newPage(); const featurePage = await context.newPage();
@@ -25,9 +25,9 @@ test('test', async ({ context }) => {
await featurePage.getByText('testing lower').click({ timeout: 5000 }); await featurePage.getByText('testing lower').click({ timeout: 5000 });
// stage timer message // stage timer message
await editorPage.getByPlaceholder('Shown in stage timer').click(); await editorPage.getByPlaceholder('Timer').click();
await editorPage.getByPlaceholder('Shown in stage timer').fill('testing stage'); await editorPage.getByPlaceholder('Timer').fill('testing stage');
await editorPage.getByRole('button', { name: /toggle timer message/i }).click({ timeout: 5000 }); await editorPage.getByRole('button', { name: /toggle timer/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 });
@@ -13,6 +13,7 @@ export type RuntimeStore = {
timerMessage: TimerMessage; timerMessage: TimerMessage;
publicMessage: Message; publicMessage: Message;
lowerMessage: Message; lowerMessage: Message;
externalMessage: Message;
onAir: boolean; onAir: boolean;
// event loader // event loader