refactor: simplify l3

This commit is contained in:
Carlos Valente
2024-04-19 16:38:01 +02:00
committed by Carlos Valente
parent b14ac07568
commit dd81b92584
15 changed files with 81 additions and 120 deletions
@@ -249,12 +249,11 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => { export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => {
const topSourceOptions = makeOptionsFromCustomFields(customFields, { const topSourceOptions = makeOptionsFromCustomFields(customFields, {
title: 'Title', title: 'Title',
lowerMsg: 'Lower Third Message',
}); });
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, { const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
title: 'Title', title: 'Title',
lowerMsg: 'Lower Third Message', none: 'None',
}); });
return [ return [
@@ -267,7 +266,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] =
event: 'Event Load', event: 'Event Load',
manual: 'Manual', manual: 'Manual',
}, },
defaultValue: 'event', defaultValue: 'manual',
}, },
{ {
id: 'top-src', id: 'top-src',
@@ -283,7 +282,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] =
description: 'Select the data source for the bottom element', description: 'Select the data source for the bottom element',
type: 'option', type: 'option',
values: bottomSourceOptions, values: bottomSourceOptions,
defaultValue: 'lowerMsg', defaultValue: 'none',
}, },
{ {
id: 'top-colour', id: 'top-colour',
@@ -25,7 +25,6 @@ export const useOperator = () => {
export const useMessageControl = () => { export const useMessageControl = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
timer: state.message.timer, timer: state.message.timer,
lower: state.message.lower,
external: state.message.external, external: state.message.external,
onAir: state.onAir, onAir: state.onAir,
}); });
@@ -36,8 +35,6 @@ 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 } }),
lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: 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 } }),
}; };
-4
View File
@@ -23,10 +23,6 @@ export const runtimeStorePlaceholder: RuntimeStore = {
blink: false, blink: false,
blackout: false, blackout: false,
}, },
lower: {
text: '',
visible: false,
},
external: { external: {
text: '', text: '',
visible: false, visible: false,
@@ -1,3 +1,5 @@
@import "./MessageControl.module.scss";
.inputItems { .inputItems {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@@ -6,10 +8,5 @@
} }
.label { .label {
font-size: $inner-section-text-size; @extend .label;
color: $label-gray;
&.active {
color: $action-text-color;
}
} }
@@ -10,3 +10,19 @@
gap: $element-spacing; gap: $element-spacing;
margin-top: -0.5rem; margin-top: -0.5rem;
} }
.singleAction {
display: flex;
flex-direction: column;
gap: $element-spacing;
margin-top: $element-inner-spacing;
}
.label {
font-size: $inner-section-text-size;
color: $label-gray;
&.active {
color: $action-text-color;
}
}
@@ -17,21 +17,14 @@ export default function MessageControl() {
const message = useMessageControl(); const message = useMessageControl();
const blink = message.timer.blink; const blink = message.timer.blink;
const blackout = message.timer.blackout; const blackout = message.timer.blackout;
return ( return (
<div className={style.messageContainer}> <div className={style.messageContainer}>
<InputRow
label='Lower third message'
placeholder='Shown in lower third'
text={message.lower.text || ''}
visible={message.lower.visible || false}
changeHandler={(newValue) => setMessage.lowerText(newValue)}
actionHandler={() => setMessage.lowerVisible(!message.lower.visible)}
/>
<InputRow <InputRow
label='Timer' label='Timer'
placeholder='Message shown in stage timer' placeholder='Message shown in stage timer'
text={message.timer.text || ''} text={message.timer.text}
visible={message.timer.visible || false} visible={message.timer.visible}
changeHandler={(newValue) => setMessage.timerText(newValue)} changeHandler={(newValue) => setMessage.timerText(newValue)}
actionHandler={() => setMessage.timerVisible(!message.timer.visible)} actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
/> />
@@ -61,8 +54,8 @@ export default function MessageControl() {
label='External Message (read only)' label='External Message (read only)'
placeholder={enDash} placeholder={enDash}
readonly readonly
text={message.external.text || ''} text={message.external.text}
visible={message.external.visible || false} visible={message.external.visible}
changeHandler={noop} changeHandler={noop}
actionHandler={noop} actionHandler={noop}
/> />
@@ -30,7 +30,6 @@ type WithDataProps = {
external: Message; external: Message;
general: ProjectData; general: ProjectData;
isMirrored: boolean; isMirrored: boolean;
lower: Message;
nextId: string | null; nextId: string | null;
onAir: boolean; onAir: boolean;
pres: TimerMessage; pres: TimerMessage;
@@ -105,7 +104,6 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
external={message.external} external={message.external}
general={project} general={project}
isMirrored={isMirrored} isMirrored={isMirrored}
lower={message.lower}
nextId={nextId} nextId={nextId}
onAir={onAir} onAir={onAir}
pres={message.timer} pres={message.timer}
@@ -41,10 +41,7 @@ export function isStringBoolean(text: string | null) {
* Considers custom fields * Considers custom fields
*/ */
export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined { export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined {
if (!event) { if (!event || typeof property !== 'string' || property === 'none') {
return undefined;
}
if (typeof property !== 'string') {
return undefined; return undefined;
} }
@@ -72,6 +72,9 @@
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif; font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
text-align: var(--lowerThird-text-align-override, left); text-align: var(--lowerThird-text-align-override, left);
white-space: nowrap; white-space: nowrap;
&::after {
content: '\200b';
}
} }
.data-top { .data-top {
@@ -86,7 +89,6 @@
} }
} }
@keyframes in { @keyframes in {
0% { 0% {
transform: translateX(-100%); transform: translateX(-100%);
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types'; import { CustomFields, OntimeEvent, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants'; import { overrideStylesURL } from '../../../common/api/constants';
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants'; import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
@@ -11,14 +11,8 @@ import { getPropertyValue } from '../common/viewUtils';
import './LowerThird.scss'; import './LowerThird.scss';
enum TriggerType {
Event = 'event',
Manual = 'manual',
}
type LowerOptions = { type LowerOptions = {
width: number; width: number;
trigger: TriggerType;
topSrc: string; topSrc: string;
bottomSrc: string; bottomSrc: string;
topColour: string; topColour: string;
@@ -38,12 +32,10 @@ interface LowerProps {
customFields: CustomFields; customFields: CustomFields;
eventNow: OntimeEvent | null; eventNow: OntimeEvent | null;
viewSettings: ViewSettings; viewSettings: ViewSettings;
lower: Message;
} }
const defaultOptions: Readonly<LowerOptions> = { const defaultOptions: Readonly<LowerOptions> = {
width: 45, width: 45,
trigger: TriggerType.Event,
topSrc: 'title', topSrc: 'title',
bottomSrc: 'lowerMsg', bottomSrc: 'lowerMsg',
topColour: '000000ff', topColour: '000000ff',
@@ -60,8 +52,14 @@ const defaultOptions: Readonly<LowerOptions> = {
}; };
export default function LowerThird(props: LowerProps) { export default function LowerThird(props: LowerProps) {
const { customFields, eventNow, lower, viewSettings } = props; const { customFields, eventNow, viewSettings } = props;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const previousId = useRef<string>();
const animationTimeout = useRef<NodeJS.Timeout>();
const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre');
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
useWindowTitle('Lower Third');
const options = useMemo(() => { const options = useMemo(() => {
const newOptions = { ...defaultOptions }; const newOptions = { ...defaultOptions };
@@ -71,11 +69,6 @@ export default function LowerThird(props: LowerProps) {
newOptions.width = Number(width); newOptions.width = Number(width);
} }
const trigger = Object.values(TriggerType).find((s) => s === searchParams.get('trigger'));
if (trigger) {
newOptions.trigger = trigger;
}
const topSrc = searchParams.get('top-src'); const topSrc = searchParams.get('top-src');
if (topSrc) { if (topSrc) {
newOptions.topSrc = topSrc; newOptions.topSrc = topSrc;
@@ -139,50 +132,51 @@ export default function LowerThird(props: LowerProps) {
return newOptions; return newOptions;
}, [searchParams]); }, [searchParams]);
const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre'); // on unmount, cancel any ongoing animations
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
useWindowTitle('Lower Third');
const trigger = useMemo(() => {
if (options.trigger === TriggerType.Event) {
return eventNow?.id;
} else if (options.trigger === TriggerType.Manual) {
return lower.visible;
}
return false;
}, [eventNow?.id, lower.visible, options.trigger]);
// coordinate load-unload of lower third
useEffect(() => { useEffect(() => {
if (options.trigger === TriggerType.Event && trigger) { return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
clearTimeout(animationTimeout.current);
};
}, []);
// check if data has changed and schedule animations
useEffect(() => {
const hasChanged = eventNow?.id !== previousId.current;
if (!hasChanged) {
return;
}
previousId.current = eventNow?.id;
const animateOutInMs = options.delay * 1000 + options.transition * 1000;
const reschedule = (newState: 'pre' | 'in' | 'out') => {
clearTimeout(animationTimeout.current);
animationTimeout.current = setTimeout(() => setPlayState(newState), animateOutInMs);
};
if (eventNow?.id == null) {
setPlayState('out');
reschedule('pre');
return;
}
if (eventNow.id && !previousId.current) {
setPlayState('in'); setPlayState('in');
const animateOutInMs = options.delay * 1000 + options.transition * 1000; reschedule('out');
const timeout = setTimeout(() => { return;
setPlayState('out');
}, animateOutInMs);
return () => clearTimeout(timeout);
} else if (options.trigger === TriggerType.Manual) {
setPlayState(trigger ? 'in' : 'out');
} else {
setPlayState('pre');
} }
return () => null;
}, [options.delay, options.transition, options.trigger, trigger]);
const topText = useMemo(() => { if (playState === 'in') {
if (options.topSrc === 'lowerMsg') { // event has changed, we just reschedule the timeout
return lower.text; reschedule('out');
return;
} }
return getPropertyValue(eventNow, options.topSrc) ?? ''; setPlayState('in');
}, [eventNow, lower.text, options]); reschedule('out');
}, [eventNow?.id, options.delay, options.transition, playState, previousId]);
const bottomText = useMemo(() => { const topText = getPropertyValue(eventNow, options.topSrc) ?? '';
if (options.bottomSrc === 'lowerMsg') { const bottomText = getPropertyValue(eventNow, options.bottomSrc) ?? '';
return lower.text;
}
return getPropertyValue(eventNow, options.bottomSrc) ?? '';
}, [eventNow, lower.text, options]);
const transition = `${options.transition}s`; const transition = `${options.transition}s`;
@@ -66,7 +66,6 @@ const actionHandlers: Record<string, ActionHandler> = {
const patch: DeepPartial<MessageState> = { const patch: DeepPartial<MessageState> = {
timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined, timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
lower: 'lower' in payload ? validateMessage(payload.lower) : undefined,
external: 'external' in payload ? validateMessage(payload.external) : undefined, external: 'external' in payload ? validateMessage(payload.external) : undefined,
}; };
@@ -8,7 +8,6 @@ let instance: MessageService | null = null;
class MessageService { class MessageService {
timer: TimerMessage; timer: TimerMessage;
lower: Message;
external: Message; external: Message;
private throttledSet: PublishFn; private throttledSet: PublishFn;
@@ -37,11 +36,6 @@ class MessageService {
blackout: false, blackout: false,
}; };
this.lower = {
text: '',
visible: false,
};
this.external = { this.external = {
text: '', text: '',
visible: false, visible: false,
@@ -56,14 +50,12 @@ class MessageService {
getState(): MessageState { getState(): MessageState {
return { return {
timer: this.timer, timer: this.timer,
lower: this.lower,
external: this.external, external: this.external,
}; };
} }
patch(message: DeepPartial<MessageState>) { patch(message: DeepPartial<MessageState>) {
if (message.timer) this.timer = { ...this.timer, ...message.timer }; if (message.timer) this.timer = { ...this.timer, ...message.timer };
if (message.lower) this.lower = { ...this.lower, ...message.lower };
if (message.external) this.external = { ...this.external, ...message.external }; if (message.external) this.external = { ...this.external, ...message.external };
const newState = this.getState(); const newState = this.getState();
@@ -14,7 +14,6 @@ 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 },
lower: { text: 'lower text' },
external: { visible: true }, external: { visible: true },
}; };
@@ -22,10 +21,6 @@ describe('MessageService', () => {
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 },
lower: {
text: 'lower text',
visible: false,
},
external: { external: {
text: '', text: '',
visible: true, visible: true,
@@ -42,10 +37,6 @@ describe('MessageService', () => {
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 },
lower: {
text: '',
visible: false,
},
external: { external: {
text: '', text: '',
visible: false, visible: false,
+2 -11
View File
@@ -1,4 +1,4 @@
import { test } from '@playwright/test'; import { expect, test } from '@playwright/test';
test('message control sends messages to screens', async ({ context }) => { test('message control sends messages to screens', async ({ context }) => {
const editorPage = await context.newPage(); const editorPage = await context.newPage();
@@ -6,15 +6,6 @@ test('message control sends messages to screens', async ({ context }) => {
await editorPage.goto('http://localhost:4001/messagecontrol'); await editorPage.goto('http://localhost:4001/messagecontrol');
// lower third message
await editorPage.getByPlaceholder('Shown in lower third').click();
await editorPage.getByPlaceholder('Shown in lower third').fill('testing lower');
await editorPage.getByRole('button', { name: /toggle lower third message/i }).click();
await featurePage.goto('http://localhost:4001/lower?trigger=manual&bottom-src=lowerMsg');
await featurePage.waitForLoadState('load', { timeout: 5000 });
await featurePage.getByText('testing lower').click({ timeout: 5000 });
// stage timer message // stage timer message
await editorPage.getByPlaceholder('Timer').click(); await editorPage.getByPlaceholder('Timer').click();
await editorPage.getByPlaceholder('Timer').fill('testing stage'); await editorPage.getByPlaceholder('Timer').fill('testing stage');
@@ -22,5 +13,5 @@ test('message control sends messages to screens', async ({ context }) => {
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 });
await featurePage.getByText('testing stage').click(); await expect(featurePage.getByText('testing stage')).toBeVisible();
}); });
@@ -10,6 +10,5 @@ export type TimerMessage = Message & {
export type MessageState = { export type MessageState = {
timer: TimerMessage; timer: TimerMessage;
lower: Message;
external: Message; external: Message;
}; };