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}