diff --git a/apps/client/src/common/components/view-params-editor/constants.ts b/apps/client/src/common/components/view-params-editor/constants.ts index 74f94ec76..ab86c440f 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -249,12 +249,11 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => { const topSourceOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', - lowerMsg: 'Lower Third Message', }); const bottomSourceOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', - lowerMsg: 'Lower Third Message', + none: 'None', }); return [ @@ -267,7 +266,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] = event: 'Event Load', manual: 'Manual', }, - defaultValue: 'event', + defaultValue: 'manual', }, { id: 'top-src', @@ -283,7 +282,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] = description: 'Select the data source for the bottom element', type: 'option', values: bottomSourceOptions, - defaultValue: 'lowerMsg', + defaultValue: 'none', }, { id: 'top-colour', diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 2f41b3804..287e07520 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -25,7 +25,6 @@ export const useOperator = () => { export const useMessageControl = () => { const featureSelector = (state: RuntimeStore) => ({ timer: state.message.timer, - lower: state.message.lower, external: state.message.external, onAir: state.onAir, }); @@ -36,8 +35,6 @@ export const useMessageControl = () => { export const setMessage = { timerText: (payload: string) => socketSendJson('message', { timer: { text: 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 } }), timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }), }; diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index c2d8def6f..e72e39b32 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -23,10 +23,6 @@ export const runtimeStorePlaceholder: RuntimeStore = { blink: false, blackout: false, }, - lower: { - text: '', - visible: false, - }, external: { text: '', visible: false, diff --git a/apps/client/src/features/control/message/InputRow.module.scss b/apps/client/src/features/control/message/InputRow.module.scss index 877549f99..769b4eaf2 100644 --- a/apps/client/src/features/control/message/InputRow.module.scss +++ b/apps/client/src/features/control/message/InputRow.module.scss @@ -1,3 +1,5 @@ +@import "./MessageControl.module.scss"; + .inputItems { display: grid; grid-template-columns: 1fr auto; @@ -6,10 +8,5 @@ } .label { - font-size: $inner-section-text-size; - color: $label-gray; - - &.active { - color: $action-text-color; - } + @extend .label; } diff --git a/apps/client/src/features/control/message/MessageControl.module.scss b/apps/client/src/features/control/message/MessageControl.module.scss index b2040d597..3419afec2 100644 --- a/apps/client/src/features/control/message/MessageControl.module.scss +++ b/apps/client/src/features/control/message/MessageControl.module.scss @@ -10,3 +10,19 @@ gap: $element-spacing; 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; + } +} diff --git a/apps/client/src/features/control/message/MessageControl.tsx b/apps/client/src/features/control/message/MessageControl.tsx index 21dc84c86..ae08096b7 100644 --- a/apps/client/src/features/control/message/MessageControl.tsx +++ b/apps/client/src/features/control/message/MessageControl.tsx @@ -17,21 +17,14 @@ export default function MessageControl() { const message = useMessageControl(); const blink = message.timer.blink; const blackout = message.timer.blackout; + return (
- setMessage.lowerText(newValue)} - actionHandler={() => setMessage.lowerVisible(!message.lower.visible)} - /> setMessage.timerText(newValue)} actionHandler={() => setMessage.timerVisible(!message.timer.visible)} /> @@ -61,8 +54,8 @@ export default function MessageControl() { label='External Message (read only)' placeholder={enDash} readonly - text={message.external.text || ''} - visible={message.external.visible || false} + text={message.external.text} + visible={message.external.visible} changeHandler={noop} actionHandler={noop} /> diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index 644fd5e1a..1fe94b3ac 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -30,7 +30,6 @@ type WithDataProps = { external: Message; general: ProjectData; isMirrored: boolean; - lower: Message; nextId: string | null; onAir: boolean; pres: TimerMessage; @@ -105,7 +104,6 @@ const withData =

(Component: ComponentType

) => { external={message.external} general={project} isMirrored={isMirrored} - lower={message.lower} nextId={nextId} onAir={onAir} pres={message.timer} diff --git a/apps/client/src/features/viewers/common/viewUtils.ts b/apps/client/src/features/viewers/common/viewUtils.ts index a3b44888f..4b2159ac5 100644 --- a/apps/client/src/features/viewers/common/viewUtils.ts +++ b/apps/client/src/features/viewers/common/viewUtils.ts @@ -41,10 +41,7 @@ export function isStringBoolean(text: string | null) { * Considers custom fields */ export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined { - if (!event) { - return undefined; - } - if (typeof property !== 'string') { + if (!event || typeof property !== 'string' || property === 'none') { return undefined; } diff --git a/apps/client/src/features/viewers/lower-thirds/LowerThird.scss b/apps/client/src/features/viewers/lower-thirds/LowerThird.scss index b17ffe078..8a5797803 100644 --- a/apps/client/src/features/viewers/lower-thirds/LowerThird.scss +++ b/apps/client/src/features/viewers/lower-thirds/LowerThird.scss @@ -72,6 +72,9 @@ font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif; text-align: var(--lowerThird-text-align-override, left); white-space: nowrap; + &::after { + content: '\200b'; + } } .data-top { @@ -86,7 +89,6 @@ } } - @keyframes in { 0% { transform: translateX(-100%); diff --git a/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx b/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx index 5c6b3c348..34dcf582d 100644 --- a/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx +++ b/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; 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 { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants'; @@ -11,14 +11,8 @@ import { getPropertyValue } from '../common/viewUtils'; import './LowerThird.scss'; -enum TriggerType { - Event = 'event', - Manual = 'manual', -} - type LowerOptions = { width: number; - trigger: TriggerType; topSrc: string; bottomSrc: string; topColour: string; @@ -38,12 +32,10 @@ interface LowerProps { customFields: CustomFields; eventNow: OntimeEvent | null; viewSettings: ViewSettings; - lower: Message; } const defaultOptions: Readonly = { width: 45, - trigger: TriggerType.Event, topSrc: 'title', bottomSrc: 'lowerMsg', topColour: '000000ff', @@ -60,8 +52,14 @@ const defaultOptions: Readonly = { }; export default function LowerThird(props: LowerProps) { - const { customFields, eventNow, lower, viewSettings } = props; + const { customFields, eventNow, viewSettings } = props; const [searchParams] = useSearchParams(); + const previousId = useRef(); + const animationTimeout = useRef(); + const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre'); + useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); + + useWindowTitle('Lower Third'); const options = useMemo(() => { const newOptions = { ...defaultOptions }; @@ -71,11 +69,6 @@ export default function LowerThird(props: LowerProps) { 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'); if (topSrc) { newOptions.topSrc = topSrc; @@ -139,50 +132,51 @@ export default function LowerThird(props: LowerProps) { return newOptions; }, [searchParams]); - const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre'); - 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 + // on unmount, cancel any ongoing animations 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'); - const animateOutInMs = options.delay * 1000 + options.transition * 1000; - const timeout = setTimeout(() => { - setPlayState('out'); - }, animateOutInMs); - return () => clearTimeout(timeout); - } else if (options.trigger === TriggerType.Manual) { - setPlayState(trigger ? 'in' : 'out'); - } else { - setPlayState('pre'); + reschedule('out'); + return; } - return () => null; - }, [options.delay, options.transition, options.trigger, trigger]); - const topText = useMemo(() => { - if (options.topSrc === 'lowerMsg') { - return lower.text; + if (playState === 'in') { + // event has changed, we just reschedule the timeout + reschedule('out'); + return; } - return getPropertyValue(eventNow, options.topSrc) ?? ''; - }, [eventNow, lower.text, options]); + setPlayState('in'); + reschedule('out'); + }, [eventNow?.id, options.delay, options.transition, playState, previousId]); - const bottomText = useMemo(() => { - if (options.bottomSrc === 'lowerMsg') { - return lower.text; - } - return getPropertyValue(eventNow, options.bottomSrc) ?? ''; - }, [eventNow, lower.text, options]); + const topText = getPropertyValue(eventNow, options.topSrc) ?? ''; + const bottomText = getPropertyValue(eventNow, options.bottomSrc) ?? ''; const transition = `${options.transition}s`; diff --git a/apps/server/src/api-integration/integration.controller.ts b/apps/server/src/api-integration/integration.controller.ts index 2ec06f05c..2190e1d48 100644 --- a/apps/server/src/api-integration/integration.controller.ts +++ b/apps/server/src/api-integration/integration.controller.ts @@ -66,7 +66,6 @@ const actionHandlers: Record = { const patch: DeepPartial = { timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined, - lower: 'lower' in payload ? validateMessage(payload.lower) : undefined, external: 'external' in payload ? validateMessage(payload.external) : undefined, }; diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts index a7f4a6373..0721a4fcc 100644 --- a/apps/server/src/services/message-service/MessageService.ts +++ b/apps/server/src/services/message-service/MessageService.ts @@ -8,7 +8,6 @@ let instance: MessageService | null = null; class MessageService { timer: TimerMessage; - lower: Message; external: Message; private throttledSet: PublishFn; @@ -37,11 +36,6 @@ class MessageService { blackout: false, }; - this.lower = { - text: '', - visible: false, - }; - this.external = { text: '', visible: false, @@ -56,14 +50,12 @@ class MessageService { getState(): MessageState { return { timer: this.timer, - lower: this.lower, external: this.external, }; } patch(message: DeepPartial) { 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 }; const newState = this.getState(); diff --git a/apps/server/src/services/message-service/__tests__/MessageService.test.ts b/apps/server/src/services/message-service/__tests__/MessageService.test.ts index 2223191f5..23773cab0 100644 --- a/apps/server/src/services/message-service/__tests__/MessageService.test.ts +++ b/apps/server/src/services/message-service/__tests__/MessageService.test.ts @@ -14,7 +14,6 @@ describe('MessageService', () => { it('should patch the message state', () => { const message = { timer: { text: 'new text', visible: true }, - lower: { text: 'lower text' }, external: { visible: true }, }; @@ -22,10 +21,6 @@ describe('MessageService', () => { expect(newState).toEqual({ timer: { text: 'new text', visible: true, blackout: false, blink: false }, - lower: { - text: 'lower text', - visible: false, - }, external: { text: '', visible: true, @@ -42,10 +37,6 @@ describe('MessageService', () => { expect(newState).toEqual({ timer: { text: 'initial text', visible: true, blackout: false, blink: false }, - lower: { - text: '', - visible: false, - }, external: { text: '', visible: false, diff --git a/e2e/tests/features/201-message-control.spec.ts b/e2e/tests/features/201-message-control.spec.ts index 3a9a4c3c5..b18e63462 100644 --- a/e2e/tests/features/201-message-control.spec.ts +++ b/e2e/tests/features/201-message-control.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test('message control sends messages to screens', async ({ context }) => { 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'); - // 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 await editorPage.getByPlaceholder('Timer').click(); 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.waitForLoadState('load', { timeout: 5000 }); - await featurePage.getByText('testing stage').click(); + await expect(featurePage.getByText('testing stage')).toBeVisible(); }); diff --git a/packages/types/src/definitions/runtime/MessageControl.type.ts b/packages/types/src/definitions/runtime/MessageControl.type.ts index da5c0da1f..4eeaa5992 100644 --- a/packages/types/src/definitions/runtime/MessageControl.type.ts +++ b/packages/types/src/definitions/runtime/MessageControl.type.ts @@ -10,6 +10,5 @@ export type TimerMessage = Message & { export type MessageState = { timer: TimerMessage; - lower: Message; external: Message; };