diff --git a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx index feb8bb644..4a256b72a 100644 --- a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx +++ b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx @@ -9,6 +9,7 @@ import './MultiPartProgressBar.scss'; interface MultiPartProgressBar { now: MaybeNumber; complete: MaybeNumber; + eventId?: string | null; normalColor: string; warning?: MaybeNumber; warningColor: string; @@ -24,6 +25,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) { const { now, complete, + eventId, normalColor, warning, warningColor, @@ -35,7 +37,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) { className = '', } = props; - const percentRemaining = 100 - useAnimatedProgress(now, complete); + const percentRemaining = 100 - useAnimatedProgress(now, complete, eventId); const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0; const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0; const isOvertime = now !== null && now < 0; diff --git a/apps/client/src/common/components/progress-bar/ProgressBar.tsx b/apps/client/src/common/components/progress-bar/ProgressBar.tsx index f8def035e..0848dcdef 100644 --- a/apps/client/src/common/components/progress-bar/ProgressBar.tsx +++ b/apps/client/src/common/components/progress-bar/ProgressBar.tsx @@ -7,12 +7,12 @@ import './ProgressBar.scss'; interface ProgressBarProps { current: MaybeNumber; duration: MaybeNumber; + eventId?: string | null; className?: string; } -export default function ProgressBar(props: ProgressBarProps) { - const { current, duration, className } = props; - const progress = useAnimatedProgress(current, duration); +export default function ProgressBar({ current, duration, eventId, className }: ProgressBarProps) { + const progress = useAnimatedProgress(current, duration, eventId); return (
diff --git a/apps/client/src/common/hooks/useAnimatedProgress.ts b/apps/client/src/common/hooks/useAnimatedProgress.ts index 71f628dbd..816054c68 100644 --- a/apps/client/src/common/hooks/useAnimatedProgress.ts +++ b/apps/client/src/common/hooks/useAnimatedProgress.ts @@ -1,26 +1,34 @@ -import { MaybeNumber, Playback } from 'ontime-types'; +import { EntryId, MaybeNumber, Playback } from 'ontime-types'; import { useEffect, useRef, useState } from 'react'; import { getProgress } from '../utils/getProgress'; -import { usePlayback } from './useSocket'; +import { useIsOnline, usePlayback } from './useSocket'; /** * Returns the live completion percentage (0–100) of a countdown, interpolated locally. */ -export function useAnimatedProgress(current: MaybeNumber, duration: MaybeNumber): number { +export function useAnimatedProgress(current: MaybeNumber, duration: MaybeNumber, eventId?: EntryId | null): number { const playback = usePlayback(); + const isOnline = useIsOnline(); const isRunning = playback === Playback.Play || playback === Playback.Roll; - const baseline = useRef({ current, at: performance.now() }); + const baseline = useRef({ current, duration, eventId, playback, at: performance.now() }); const [, setTick] = useState(0); + const now = performance.now(); - // there is only something to animate while a running timer is counting down towards 0 - const shouldAnimate = isRunning && current !== null && current > 0 && duration !== null; + const hasAuthoritativeUpdate = + baseline.current.current !== current || // handle timer updates + baseline.current.duration !== duration || // handle duration changes + baseline.current.eventId !== eventId || // handle event changing + baseline.current.playback !== playback; // handle playback changes - // re-anchor to the authoritative value whenever the server pushes a new timer update - useEffect(() => { - baseline.current = { current, at: performance.now() }; - }, [current, duration, playback]); + if (hasAuthoritativeUpdate) { + // Reset during render so an event change is reflected in this very paint. + baseline.current = { current, duration, eventId, playback, at: now }; + } + + // There is only something to animate while a connected timer is counting down towards 0. + const shouldAnimate = isOnline && isRunning && current !== null && current > 0 && duration !== null; // while counting down, re-render every animation frame so the derived progress stays smooth useEffect(() => { @@ -34,8 +42,8 @@ export function useAnimatedProgress(current: MaybeNumber, duration: MaybeNumber) return () => cancelAnimationFrame(frame); }, [shouldAnimate]); - // derive from the anchor plus elapsed time at render; frozen to the anchor when not running + // Derive from the anchor plus elapsed time at render; freeze while disconnected or not running. const anchored = baseline.current.current; - const value = isRunning && anchored !== null ? anchored - (performance.now() - baseline.current.at) : anchored; + const value = isOnline && isRunning && anchored !== null ? anchored - (now - baseline.current.at) : anchored; return getProgress(value, duration); } diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 97f9b9448..b64cce05f 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -163,6 +163,7 @@ export const useNextFlag = createSelector((state: RuntimeStore) => ({ export const useProgressData = createSelector((state: RuntimeStore) => ({ current: state.timer.current, duration: state.timer.duration, + eventId: state.eventNow?.id ?? null, timeWarning: state.eventNow?.timeWarning ?? null, timeDanger: state.eventNow?.timeDanger ?? null, })); diff --git a/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx b/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx index 2cc44b5c0..5943da6c3 100644 --- a/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx +++ b/apps/client/src/features/operator/status-bar/StatusBarProgress.tsx @@ -10,12 +10,13 @@ interface StatusBarProgressProps { } export default function StatusBarProgress({ viewSettings }: StatusBarProgressProps) { - const { current, duration, timeWarning, timeDanger } = useProgressData(); + const { current, duration, eventId, timeWarning, timeDanger } = useProgressData(); return ( ; } diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 229ba5213..59492c06f 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -112,7 +112,14 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
- {showProgress && } + {showProgress && ( + + )} {!hasEvents && } diff --git a/apps/client/src/views/cuesheet/cuesheet-progress/CuesheetProgress.tsx b/apps/client/src/views/cuesheet/cuesheet-progress/CuesheetProgress.tsx index d7d084e4a..c05da7d4d 100644 --- a/apps/client/src/views/cuesheet/cuesheet-progress/CuesheetProgress.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-progress/CuesheetProgress.tsx @@ -6,12 +6,13 @@ import styles from './CuesheetProgress.module.scss'; export default function CuesheetProgress() { const { data } = useViewSettings(); - const { current, duration, timeWarning, timeDanger } = useProgressData(); + const { current, duration, eventId, timeWarning, timeDanger } = useProgressData(); return ( ); diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index 3ae139d26..d0e212083 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -195,6 +195,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings, className={cx(['progress-container', !isPlaying && 'progress-container--paused'])} now={time.current} complete={totalTime} + eventId={eventNow?.id} normalColor={viewSettings.normalColor} warning={eventNow?.timeWarning} warningColor={viewSettings.warningColor}