refactor(progress bar): reset when event changes

This commit is contained in:
Carlos Valente
2026-08-01 12:50:37 +02:00
parent 5cf36f049a
commit d3d9fe0157
11 changed files with 47 additions and 23 deletions
@@ -9,6 +9,7 @@ import './MultiPartProgressBar.scss';
interface MultiPartProgressBar { interface MultiPartProgressBar {
now: MaybeNumber; now: MaybeNumber;
complete: MaybeNumber; complete: MaybeNumber;
eventId?: string | null;
normalColor: string; normalColor: string;
warning?: MaybeNumber; warning?: MaybeNumber;
warningColor: string; warningColor: string;
@@ -24,6 +25,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
const { const {
now, now,
complete, complete,
eventId,
normalColor, normalColor,
warning, warning,
warningColor, warningColor,
@@ -35,7 +37,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
className = '', className = '',
} = props; } = props;
const percentRemaining = 100 - useAnimatedProgress(now, complete); const percentRemaining = 100 - useAnimatedProgress(now, complete, eventId);
const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0; const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0;
const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0; const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0;
const isOvertime = now !== null && now < 0; const isOvertime = now !== null && now < 0;
@@ -7,12 +7,12 @@ import './ProgressBar.scss';
interface ProgressBarProps { interface ProgressBarProps {
current: MaybeNumber; current: MaybeNumber;
duration: MaybeNumber; duration: MaybeNumber;
eventId?: string | null;
className?: string; className?: string;
} }
export default function ProgressBar(props: ProgressBarProps) { export default function ProgressBar({ current, duration, eventId, className }: ProgressBarProps) {
const { current, duration, className } = props; const progress = useAnimatedProgress(current, duration, eventId);
const progress = useAnimatedProgress(current, duration);
return ( return (
<div className={`progress-bar__bg ${className}`}> <div className={`progress-bar__bg ${className}`}>
@@ -1,26 +1,34 @@
import { MaybeNumber, Playback } from 'ontime-types'; import { EntryId, MaybeNumber, Playback } from 'ontime-types';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { getProgress } from '../utils/getProgress'; import { getProgress } from '../utils/getProgress';
import { usePlayback } from './useSocket'; import { useIsOnline, usePlayback } from './useSocket';
/** /**
* Returns the live completion percentage (0100) of a countdown, interpolated locally. * Returns the live completion percentage (0100) 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 playback = usePlayback();
const isOnline = useIsOnline();
const isRunning = playback === Playback.Play || playback === Playback.Roll; 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 [, setTick] = useState(0);
const now = performance.now();
// there is only something to animate while a running timer is counting down towards 0 const hasAuthoritativeUpdate =
const shouldAnimate = isRunning && current !== null && current > 0 && duration !== null; 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 if (hasAuthoritativeUpdate) {
useEffect(() => { // Reset during render so an event change is reflected in this very paint.
baseline.current = { current, at: performance.now() }; baseline.current = { current, duration, eventId, playback, at: now };
}, [current, duration, playback]); }
// 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 // while counting down, re-render every animation frame so the derived progress stays smooth
useEffect(() => { useEffect(() => {
@@ -34,8 +42,8 @@ export function useAnimatedProgress(current: MaybeNumber, duration: MaybeNumber)
return () => cancelAnimationFrame(frame); return () => cancelAnimationFrame(frame);
}, [shouldAnimate]); }, [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 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); return getProgress(value, duration);
} }
@@ -163,6 +163,7 @@ export const useNextFlag = createSelector((state: RuntimeStore) => ({
export const useProgressData = createSelector((state: RuntimeStore) => ({ export const useProgressData = createSelector((state: RuntimeStore) => ({
current: state.timer.current, current: state.timer.current,
duration: state.timer.duration, duration: state.timer.duration,
eventId: state.eventNow?.id ?? null,
timeWarning: state.eventNow?.timeWarning ?? null, timeWarning: state.eventNow?.timeWarning ?? null,
timeDanger: state.eventNow?.timeDanger ?? null, timeDanger: state.eventNow?.timeDanger ?? null,
})); }));
@@ -10,12 +10,13 @@ interface StatusBarProgressProps {
} }
export default function StatusBarProgress({ viewSettings }: StatusBarProgressProps) { export default function StatusBarProgress({ viewSettings }: StatusBarProgressProps) {
const { current, duration, timeWarning, timeDanger } = useProgressData(); const { current, duration, eventId, timeWarning, timeDanger } = useProgressData();
return ( return (
<MultiPartProgressBar <MultiPartProgressBar
now={current} now={current}
complete={duration} complete={duration}
eventId={eventId}
normalColor={viewSettings.normalColor} normalColor={viewSettings.normalColor}
warning={timeWarning} warning={timeWarning}
warningColor={viewSettings.warningColor} warningColor={viewSettings.warningColor}
@@ -1,12 +1,13 @@
import { useAnimatedProgress } from '../../../../common/hooks/useAnimatedProgress'; import { useAnimatedProgress } from '../../../../common/hooks/useAnimatedProgress';
import { useTimer } from '../../../../common/hooks/useSocket'; import { useSelectedEventId, useTimer } from '../../../../common/hooks/useSocket';
import style from './RundownEventProgressBar.module.scss'; import style from './RundownEventProgressBar.module.scss';
export default function RundownEventProgressBar() { export default function RundownEventProgressBar() {
const timer = useTimer(); const timer = useTimer();
const eventId = useSelectedEventId();
const progress = useAnimatedProgress(timer.current, timer.duration); const progress = useAnimatedProgress(timer.current, timer.duration, eventId);
return <div className={style.progressBar} style={{ width: `${progress}%` }} />; return <div className={style.progressBar} style={{ width: `${progress}%` }} />;
} }
@@ -112,7 +112,14 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
<BackstageClock timeformat={timeformat} /> <BackstageClock timeformat={timeformat} />
</div> </div>
{showProgress && <ProgressBar className='progress-container' current={time.current} duration={time.duration} />} {showProgress && (
<ProgressBar
className='progress-container'
current={time.current}
duration={time.duration}
eventId={selectedEventId}
/>
)}
{!hasEvents && <Empty text={getLocalizedString('common.no_data')} className='empty-container' />} {!hasEvents && <Empty text={getLocalizedString('common.no_data')} className='empty-container' />}
@@ -6,12 +6,13 @@ import styles from './CuesheetProgress.module.scss';
export default function CuesheetProgress() { export default function CuesheetProgress() {
const { data } = useViewSettings(); const { data } = useViewSettings();
const { current, duration, timeWarning, timeDanger } = useProgressData(); const { current, duration, eventId, timeWarning, timeDanger } = useProgressData();
return ( return (
<MultiPartProgressBar <MultiPartProgressBar
now={current} now={current}
complete={duration} complete={duration}
eventId={eventId}
normalColor={data.normalColor} normalColor={data.normalColor}
warning={timeWarning} warning={timeWarning}
warningColor={data.warningColor} warningColor={data.warningColor}
@@ -96,6 +96,7 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
className={cx(['progress-container', !isPlaying && 'progress-container--paused'])} className={cx(['progress-container', !isPlaying && 'progress-container--paused'])}
now={time.current} now={time.current}
complete={totalTime} complete={totalTime}
eventId={eventNow?.id}
normalColor={viewSettings.normalColor} normalColor={viewSettings.normalColor}
warning={eventNow?.timeWarning} warning={eventNow?.timeWarning}
warningColor={viewSettings.warningColor} warningColor={viewSettings.warningColor}
@@ -2,7 +2,7 @@ import { Day } from 'ontime-types';
import { CSSProperties, RefObject } from 'react'; import { CSSProperties, RefObject } from 'react';
import { useAnimatedProgress } from '../../common/hooks/useAnimatedProgress'; import { useAnimatedProgress } from '../../common/hooks/useAnimatedProgress';
import { useExpectedStartData, useTimer } from '../../common/hooks/useSocket'; import { useExpectedStartData, useSelectedEventId, useTimer } from '../../common/hooks/useSocket';
import { alpha, cx } from '../../common/utils/styleUtils'; import { alpha, cx } from '../../common/utils/styleUtils';
import { formatDuration, formatTime, getExpectedTimesFromExtendedEvent } from '../../common/utils/time'; import { formatDuration, formatTime, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
import { useTranslation } from '../../translation/TranslationProvider'; import { useTranslation } from '../../translation/TranslationProvider';
@@ -169,7 +169,8 @@ function TimelineEntryStatus({
/** Generates a block level progress bar */ /** Generates a block level progress bar */
function ActiveBlock() { function ActiveBlock() {
const { current, duration } = useTimer(); const { current, duration } = useTimer();
const progress = useAnimatedProgress(current, duration); const eventId = useSelectedEventId();
const progress = useAnimatedProgress(current, duration, eventId);
return ( return (
<div data-status='live' className={style.timelineBlock} style={{ '--progress': `${progress}%` } as CSSProperties} /> <div data-status='live' className={style.timelineBlock} style={{ '--progress': `${progress}%` } as CSSProperties} />
); );
+1
View File
@@ -195,6 +195,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
className={cx(['progress-container', !isPlaying && 'progress-container--paused'])} className={cx(['progress-container', !isPlaying && 'progress-container--paused'])}
now={time.current} now={time.current}
complete={totalTime} complete={totalTime}
eventId={eventNow?.id}
normalColor={viewSettings.normalColor} normalColor={viewSettings.normalColor}
warning={eventNow?.timeWarning} warning={eventNow?.timeWarning}
warningColor={viewSettings.warningColor} warningColor={viewSettings.warningColor}