mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 07:29:08 +00:00
refactor(progress bar): reset when event changes
This commit is contained in:
@@ -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 (0–100) of a countdown, interpolated locally.
|
* 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 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}
|
||||||
|
|||||||
+3
-2
@@ -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} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user