diff --git a/apps/client/src/views/teleprompter/Teleprompter.tsx b/apps/client/src/views/teleprompter/Teleprompter.tsx index 4df93e69d..747700704 100644 --- a/apps/client/src/views/teleprompter/Teleprompter.tsx +++ b/apps/client/src/views/teleprompter/Teleprompter.tsx @@ -1,5 +1,5 @@ import { OntimeView } from 'ontime-types'; -import { type CSSProperties, useEffect, useState } from 'react'; +import { type CSSProperties, useState } from 'react'; import EmptyPage from '../../common/components/state/EmptyPage'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; @@ -46,19 +46,25 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa const isMirrored = useViewOptionsStore((state) => state.mirror); const [fontScale, setFontScale] = useState(1); - const [flipH, setFlipH] = useState(options.flipH); - const [flipV, setFlipV] = useState(options.flipV); const [showHelp, setShowHelp] = useState(false); /** - * The flip params seed the local state, which the F key then owns. - * Without this the params would only ever apply on mount, so changing them in - * the view options, or from another device by redirecting this client to the - * same view with a different query, would silently do nothing. - * The speed option is kept live the same way, inside useTeleprompterScroll. + * The flip params seed the local state, which the F key then owns until the + * params change again. Without that the params would only apply on mount, so + * editing them in the view options, or redirecting this client to the same + * view with a different query, would silently do nothing. + * + * The reset happens during render rather than in an effect so React can throw + * the stale render away before it reaches the screen. An effect would let a + * frame of the previous orientation paint first, which on a prompter reads as + * a flash. The speed option is kept live the same way, in useTeleprompterScroll. */ - useEffect(() => setFlipH(options.flipH), [options.flipH]); - useEffect(() => setFlipV(options.flipV), [options.flipV]); + const [flip, setFlip] = useState({ h: options.flipH, v: options.flipV }); + const [flipFromParams, setFlipFromParams] = useState({ h: options.flipH, v: options.flipV }); + if (flipFromParams.h !== options.flipH || flipFromParams.v !== options.flipV) { + setFlipFromParams({ h: options.flipH, v: options.flipV }); + setFlip({ h: options.flipH, v: options.flipV }); + } const viewOptions = getTeleprompterOptions(customFields); @@ -88,13 +94,7 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa blocks, }); - const handleFlip = (axis: 'h' | 'v') => { - if (axis === 'h') { - setFlipH((current) => !current); - } else { - setFlipV((current) => !current); - } - }; + const handleFlip = (axis: 'h' | 'v') => setFlip((current) => ({ ...current, [axis]: !current[axis] })); const handleFontSize = (delta: number) => setFontScale((current) => clampFontScale(current + delta)); const handleResetFontSize = () => setFontScale(1); @@ -111,7 +111,7 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa const hasScriptSource = options.scriptSource !== 'none'; // Flip Screen is a flip on both axes, so it folds into the per view flips - const flip = composeFlip(flipH, flipV, isMirrored); + const effectiveFlip = composeFlip(flip.h, flip.v, isMirrored); const viewStyles = { '--tp-font-size': `${options.fontSize * fontScale}px`, @@ -129,7 +129,11 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa return (