mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
External field (#580)
* feat: add external endpoint * add external to messagecontrol --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
c4886a617c
commit
797a0edf57
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user