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,
publicMessage: state.publicMessage,
lowerMessage: state.lowerMessage,
externalMessage: state.externalMessage,
onAir: state.onAir,
});
@@ -40,6 +41,8 @@ export const setMessage = {
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
lowerText: (payload: string) => socketSendJson('set-lower-message-text', 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),
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', 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 { createStore } from 'zustand/vanilla';
export const runtimeStorePlaceholder = {
export const runtimeStorePlaceholder: RuntimeStore = {
timer: {
clock: 0,
current: null,
@@ -33,6 +33,10 @@ export const runtimeStorePlaceholder = {
text: '',
visible: false,
},
externalMessage: {
text: '',
visible: false,
},
onAir: false,
loaded: {
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 { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
@@ -13,13 +13,14 @@ interface InputRowProps {
placeholder: string;
text: string;
visible?: boolean;
readonly?: boolean;
actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void;
className?: string;
}
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) => {
changeHandler(newValue);
@@ -33,19 +34,31 @@ export default function InputRow(props: InputRowProps) {
<Input
size='sm'
variant='ontime-filled'
readOnly={readonly}
disabled={readonly}
value={text}
onChange={(event) => handleInputChange(event.target.value)}
placeholder={placeholder}
/>
<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'
/>
{readonly ? (
<IconButton
size='sm'
isDisabled
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
aria-label={`Toggle ${label}`}
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
/>
) : (
<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>
);
@@ -27,7 +27,3 @@
color: $action-text-color;
}
}
.padTop {
margin-top: $section-spacing;
}
@@ -34,34 +34,48 @@ export default function MessageControl() {
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
/>
<InputRow
label='Timer message'
placeholder='Shown in stage timer'
label='Timer'
placeholder='Message shown in stage timer'
text={data.timerMessage.text || ''}
visible={data.timerMessage.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
/>
<div className={style.buttonSection}>
<label className={style.label}>Timer message blink</label>
<label className={style.label}>Blackout timer screens</label>
<Button
size='sm'
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
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)}
data-testid='toggle timer blink'
/>
>
Blink message
</Button>
<Button
size='sm'
className={style.blackoutButton}
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)}
data-testid='toggle timer blackout'
/>
>
Blackout screen
</Button>
</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>
<Button
size='sm'
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
onClick={() => setMessage.onAir(!data.onAir)}
@@ -14,6 +14,7 @@ type WithDataProps = {
pres: TimerMessage;
publ: Message;
lower: Message;
external: Message;
eventNow: OntimeEvent | null;
publicEventNow: OntimeEvent | null;
eventNext: OntimeEvent | null;
@@ -51,12 +52,12 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
}, [rundownData]);
// websocket data
const data = useStore(runtime);
const {
timer,
publicMessage,
timerMessage,
lowerMessage,
externalMessage,
playback,
onAir,
eventNext,
@@ -64,7 +65,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
publicEventNow,
eventNow,
loaded,
} = data;
} = useStore(runtime);
const publicSelectedId = loaded.selectedPublicEventId;
const selectedId = loaded.selectedEventId;
const nextId = loaded.nextEventId;
@@ -92,6 +93,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
pres={timerMessage}
publ={publicMessage}
lower={lowerMessage}
external={externalMessage}
eventNow={eventNow}
publicEventNow={publicEventNow}
eventNext={eventNext}
@@ -79,6 +79,9 @@
justify-self: center;
align-self: center;
width: 100%;
overflow: hidden;
.end-message {
text-align: center;
font-size: 11.5vw;
@@ -90,7 +93,6 @@
.timer {
opacity: 1;
font-size: 20vw;
font-family: var(--font-family-override, $viewer-font-family);
color: var(--timer-color-override, $timer-color);
line-height: 0.9em;
@@ -98,6 +100,9 @@
letter-spacing: 0.05em;
font-weight: 600;
transition-property: font-size;
transition-duration: $viewer-transition-time;
&--paused {
opacity: $viewer-opacity-disabled;
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 {
grid-area: progress;
width: 100%;
@@ -122,7 +148,6 @@
}
}
/* =================== OVERLAY ===================*/
.message-overlay {
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
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 MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
@@ -41,6 +41,7 @@ const titleVariants = {
interface TimerProps {
isMirrored: boolean;
pres: TimerMessage;
external: Message;
eventNow: OntimeEvent | null;
eventNext: OntimeEvent | null;
time: TimeManagerType;
@@ -48,10 +49,9 @@ interface 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 { getLocalizedString } = useTranslation();
useEffect(() => {
document.title = 'ontime - Timer';
}, []);
@@ -78,6 +78,7 @@ export default function Timer(props: TimerProps) {
const showBlinking = pres.timerBlink;
const showBlackout = pres.timerBlackout;
const showClock = time.timerType !== TimerType.Clock;
const showExternal = external.visible && external.text;
const timerColor =
showProgress && showDanger
@@ -94,7 +95,13 @@ export default function Timer(props: TimerProps) {
}
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' : ''}`;
return (
@@ -110,7 +117,7 @@ export default function Timer(props: TimerProps) {
<SuperscriptTime time={clock} className='clock' />
</div>
<div className={`timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`}>
<div className={timerContainerClasses}>
{showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
@@ -124,6 +131,12 @@ export default function Timer(props: TimerProps) {
{display}
</div>
)}
<div
className={`external${showExternal ? '' : ' external--hidden'}`}
style={{ fontSize: `${externalFontSize}vw` }}
>
{external.text}
</div>
</div>
<MultiPartProgressBar
+1 -1
View File
@@ -5,7 +5,6 @@
// General
$viewer-transition-time: 0.5s;
// Text
$viewer-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
$viewer-opacity-disabled: 0.6;
@@ -28,3 +27,4 @@ $timer-color: rgba(white, 80%); // --timer-color-override
$timer-finished-color: $playback-negative;
$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
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
_disabled: {
_hover: {
backgroundColor: '#262626', // $gray-1200
},
},
};
export const ontimeInputFilled = {
@@ -30,6 +35,11 @@ export const ontimeInputFilledOnLight = {
_focus: {
border: '2px solid #578AF4', // $blue-500
},
_disabled: {
_hover: {
backgroundColor: 'white',
},
},
},
};
@@ -58,4 +68,9 @@ export const ontimeTextAreaFilledOnLight = {
border: '2px solid #578AF4', // $blue-500
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
_disabled: {
_hover: {
backgroundColor: 'white',
},
},
};