refactor: extract countdown data to hook

This commit is contained in:
Carlos Valente
2025-07-24 06:17:19 +02:00
committed by Carlos Valente
parent 20ee391525
commit f23adfa45c
6 changed files with 119 additions and 82 deletions
+1
View File
@@ -51,6 +51,7 @@ export default function AppRouter() {
path='countdown' path='countdown'
element={ element={
<ViewLoader> <ViewLoader>
<ViewNavigationMenu isLockable />
<Countdown /> <Countdown />
</ViewLoader> </ViewLoader>
} }
@@ -242,3 +242,9 @@ export const useTimerSocket = createSelector((state: RuntimeStore) => ({
aux3: state.auxtimer3.current, aux3: state.auxtimer3.current,
}, },
})); }));
export const useCountdownSocket = createSelector((state: RuntimeStore) => ({
playback: state.timer.playback,
current: state.timer.current,
clock: state.clock,
}));
+42 -53
View File
@@ -1,64 +1,52 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { IoAdd } from 'react-icons/io5'; import { IoAdd } from 'react-icons/io5';
import { import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
CustomFields,
EntryId,
isOntimeEvent,
isPlayableEvent,
OntimeEvent,
OntimeView,
ProjectData,
Settings,
} from 'ontime-types';
import Button from '../../common/components/buttons/Button'; import Button from '../../common/components/buttons/Button';
import Empty from '../../common/components/state/Empty'; import Empty from '../../common/components/state/Empty';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useClock } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../common/utils/time'; import { formatTime, getDefaultFormat } from '../../common/utils/time';
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
import { useTranslation } from '../../translation/TranslationProvider'; import { useTranslation } from '../../translation/TranslationProvider';
import Loader from '../common/loader/Loader';
import { getCountdownOptions, useCountdownOptions } from './countdown.options'; import { getCountdownOptions, useCountdownOptions } from './countdown.options';
import { getOrderedSubscriptions } from './countdown.utils'; import { getOrderedSubscriptions } from './countdown.utils';
import CountdownSelect from './CountdownSelect'; import CountdownSelect from './CountdownSelect';
import CountdownSubscriptions from './CountdownSubscriptions'; import CountdownSubscriptions from './CountdownSubscriptions';
import { CountdownData, useCountdownData } from './useCountdownData';
import './Countdown.scss'; import './Countdown.scss';
interface CountdownProps { export default function CountdownLoader() {
customFields: CustomFields; const { data, status } = useCountdownData();
events: OntimeEvent[];
general: ProjectData;
time: ViewExtendedTimer;
isMirrored: boolean;
selectedId: EntryId | null;
settings: Settings | undefined;
}
export default function Countdown({
customFields,
events,
general,
time,
isMirrored,
selectedId,
settings,
}: CountdownProps) {
const { getLocalizedString } = useTranslation();
const { subscriptions } = useCountdownOptions();
const [editMode, setEditMode] = useState(false);
useWindowTitle('Countdown'); useWindowTitle('Countdown');
if (status === 'pending') {
return <Loader />;
}
if (status === 'error') {
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
}
return <Countdown {...data} />;
}
function Countdown({ customFields, events, projectData, isMirrored, settings }: CountdownData) {
const { getLocalizedString } = useTranslation();
const { subscriptions } = useCountdownOptions();
const [editMode, setEditMode] = useState(false);
// gather rundown data // gather rundown data
const playableEvents = events.filter((event) => isOntimeEvent(event) && isPlayableEvent(event)); const playableEvents = events.filter((event) => isOntimeEvent(event) && isPlayableEvent(event));
// gather timer data
const clock = formatTime(time.clock);
// gather presentation data // gather presentation data
const hasEvents = playableEvents.length > 0; const hasEvents = playableEvents.length > 0;
@@ -73,12 +61,9 @@ export default function Countdown({
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'> <div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} /> <ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
<div className='project-header'> <div className='project-header'>
{general?.logo && <ViewLogo name={general.logo} className='logo' />} {projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />}
<div className='title'>{general.title}</div> <div className='title'>{projectData.title}</div>
<div className='clock-container'> <CountdownClock />
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={clock} className='time' />
</div>
</div> </div>
{!hasEvents && <Empty text={getLocalizedString('common.no_data')} className='empty-container' />} {!hasEvents && <Empty text={getLocalizedString('common.no_data')} className='empty-container' />}
@@ -91,9 +76,7 @@ export default function Countdown({
<CountdownContents <CountdownContents
playableEvents={playableEvents} playableEvents={playableEvents}
subscriptions={subscriptions} subscriptions={subscriptions}
time={time}
goToEditMode={() => setEditMode(true)} goToEditMode={() => setEditMode(true)}
selectedId={selectedId}
/> />
)} )}
</div> </div>
@@ -102,13 +85,11 @@ export default function Countdown({
interface CountdownContentsProps { interface CountdownContentsProps {
playableEvents: OntimeEvent[]; playableEvents: OntimeEvent[];
selectedId: EntryId | null;
subscriptions: EntryId[]; subscriptions: EntryId[];
time: ViewExtendedTimer;
goToEditMode: () => void; goToEditMode: () => void;
} }
function CountdownContents({ playableEvents, selectedId, subscriptions, time, goToEditMode }: CountdownContentsProps) { function CountdownContents({ playableEvents, subscriptions, goToEditMode }: CountdownContentsProps) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
if (subscriptions.length === 0) { if (subscriptions.length === 0) {
@@ -135,12 +116,20 @@ function CountdownContents({ playableEvents, selectedId, subscriptions, time, go
); );
} }
return <CountdownSubscriptions subscribedEvents={subscribedEvents} goToEditMode={goToEditMode} />;
}
function CountdownClock() {
const { getLocalizedString } = useTranslation();
const { clock } = useClock();
// gather timer data
const formattedClock = formatTime(clock);
return ( return (
<CountdownSubscriptions <div className='clock-container'>
subscribedEvents={subscribedEvents} <div className='label'>{getLocalizedString('common.time_now')}</div>
selectedId={selectedId} <SuperscriptTime time={formattedClock} className='time' />
time={time} </div>
goToEditMode={goToEditMode}
/>
); );
} }
@@ -5,8 +5,13 @@ import { EntryId, OntimeEvent } from 'ontime-types';
import Button from '../../common/components/buttons/Button'; import Button from '../../common/components/buttons/Button';
import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity'; import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity';
import useFollowComponent from '../../common/hooks/useFollowComponent'; import useFollowComponent from '../../common/hooks/useFollowComponent';
import { useCurrentDay, useRuntimeOffset } from '../../common/hooks/useSocket'; import {
import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; useCountdownSocket,
useCurrentDay,
usePlayback,
useRuntimeOffset,
useSelectedEventId,
} from '../../common/hooks/useSocket';
import { getOffsetState } from '../../common/utils/offset'; import { getOffsetState } from '../../common/utils/offset';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
import { throttle } from '../../common/utils/throttle'; import { throttle } from '../../common/utils/throttle';
@@ -22,18 +27,13 @@ import './Countdown.scss';
interface CountdownSubscriptionsProps { interface CountdownSubscriptionsProps {
subscribedEvents: OntimeEvent[]; subscribedEvents: OntimeEvent[];
selectedId: EntryId | null;
time: ViewExtendedTimer;
goToEditMode: () => void; goToEditMode: () => void;
} }
export default function CountdownSubscriptions({ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) {
time,
subscribedEvents,
selectedId,
goToEditMode,
}: CountdownSubscriptionsProps) {
const { secondarySource, showExpected } = useCountdownOptions(); const { secondarySource, showExpected } = useCountdownOptions();
const { playback } = usePlayback();
const { selectedEventId } = useSelectedEventId();
const showFab = useFadeOutOnInactivity(true); const showFab = useFadeOutOnInactivity(true);
const timeoutId = useRef<NodeJS.Timeout | null>(null); const timeoutId = useRef<NodeJS.Timeout | null>(null);
@@ -49,16 +49,16 @@ export default function CountdownSubscriptions({
// reset scroll if nothing is selected // reset scroll if nothing is selected
useEffect(() => { useEffect(() => {
if (!selectedId) { if (!selectedEventId) {
if (!lockAutoScroll) { if (!lockAutoScroll) {
scrollRef.current?.scrollTo(0, 0); scrollRef.current?.scrollTo(0, 0);
} }
} }
}, [selectedId, lockAutoScroll, scrollRef]); }, [selectedEventId, lockAutoScroll, scrollRef]);
// scroll to component if user clicks the Follow button // scroll to component if user clicks the Follow button
const handleOffset = () => { const handleOffset = () => {
if (selectedId) { if (selectedEventId) {
scrollToComponent(); scrollToComponent();
} }
setLockAutoScroll(false); setLockAutoScroll(false);
@@ -91,7 +91,7 @@ export default function CountdownSubscriptions({
<div className='list-container' onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}> <div className='list-container' onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
{subscribedEvents.map((event) => { {subscribedEvents.map((event) => {
const secondaryData = getPropertyValue(event, secondarySource); const secondaryData = getPropertyValue(event, secondarySource);
const isLive = getIsLive(event.id, selectedId, time.playback); const isLive = getIsLive(event.id, selectedEventId, playback);
return ( return (
<div key={event.id} ref={isLive ? selectedRef : undefined} className={cx(['sub', isLive && 'sub--live'])}> <div key={event.id} ref={isLive ? selectedRef : undefined} className={cx(['sub', isLive && 'sub--live'])}>
@@ -107,7 +107,7 @@ export default function CountdownSubscriptions({
</> </>
)} )}
</div> </div>
<SubscriptionStatus key={event.id} event={event} selectedId={selectedId} time={time} /> <SubscriptionStatus key={event.id} event={event} selectedEventId={selectedEventId} />
<div className={cx(['sub__title', !event.title && 'subdued'])}>{sanitiseTitle(event.title)}</div> <div className={cx(['sub__title', !event.title && 'subdued'])}>{sanitiseTitle(event.title)}</div>
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>} {secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
</div> </div>
@@ -152,22 +152,24 @@ function ExpectedSchedule(props: ExpectedScheduleProps) {
} }
interface SubscriptionStatusProps { interface SubscriptionStatusProps {
time: ViewExtendedTimer;
event: OntimeEvent; event: OntimeEvent;
selectedId: EntryId | null; selectedEventId: EntryId | null;
} }
function SubscriptionStatus({ time, event, selectedId }: SubscriptionStatusProps) { function SubscriptionStatus({ event, selectedEventId }: SubscriptionStatusProps) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const { currentDay } = useCurrentDay(); const { currentDay } = useCurrentDay();
const { offset } = useRuntimeOffset(); const { offset } = useRuntimeOffset();
const { showExpected } = useCountdownOptions(); const { showExpected } = useCountdownOptions();
const { playback, current, clock } = useCountdownSocket();
// TODO: use reporter values as in the event block chip // TODO: use reporter values as in the event block chip
const { status, timer } = getSubscriptionDisplayData( const { status, timer } = getSubscriptionDisplayData(
time, current,
playback,
clock,
event, event,
selectedId, selectedEventId,
offset, offset,
currentDay, currentDay,
getLocalizedString('common.minutes'), getLocalizedString('common.minutes'),
@@ -1,7 +1,6 @@
import { EntryId, OntimeEvent, Playback, TimerType } from 'ontime-types'; import { EntryId, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils'; import { dayInMs } from 'ontime-utils';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
import { getFormattedTimer } from '../../features/viewers/common/viewUtils'; import { getFormattedTimer } from '../../features/viewers/common/viewUtils';
import type { TranslationKey } from '../../translation/TranslationProvider'; import type { TranslationKey } from '../../translation/TranslationProvider';
@@ -46,7 +45,9 @@ export const timerProgress: TimerMessage = {
* TODO: get timer data granularly * TODO: get timer data granularly
*/ */
export function getSubscriptionDisplayData( export function getSubscriptionDisplayData(
time: ViewExtendedTimer, current: MaybeNumber,
playback: Playback,
clock: number,
subscribedEvent: OntimeEvent, subscribedEvent: OntimeEvent,
selectedId: EntryId | null, selectedId: EntryId | null,
offset: number, offset: number,
@@ -58,7 +59,7 @@ export function getSubscriptionDisplayData(
if (selectedId === subscribedEvent.id) { if (selectedId === subscribedEvent.id) {
// 1. An event that is loaded but not running is {'due': <countdown | overtime>} // 1. An event that is loaded but not running is {'due': <countdown | overtime>}
if (time.playback === Playback.Armed) { if (playback === Playback.Armed) {
// if we are following the event, but it is not running, we show the scheduled start // if we are following the event, but it is not running, we show the scheduled start
return { return {
status: 'due', status: 'due',
@@ -74,7 +75,7 @@ export function getSubscriptionDisplayData(
// 2. An event with a time-to-start lower than 0 is {'due': <countdown | scheduledStart>}, show the running timer // 2. An event with a time-to-start lower than 0 is {'due': <countdown | scheduledStart>}, show the running timer
return { return {
status: 'live', status: 'live',
timer: getFormattedTimer(time.current, TimerType.CountDown, minutesString, subscriptionTimerDisplayOptions), timer: getFormattedTimer(current, TimerType.CountDown, minutesString, subscriptionTimerDisplayOptions),
}; };
} }
@@ -89,7 +90,7 @@ export function getSubscriptionDisplayData(
return { return {
status: 'future', status: 'future',
timer: getFormattedTimer( timer: getFormattedTimer(
subscribedEvent.timeStart + dayOffset - time.clock - offsetAndDelay, subscribedEvent.timeStart + dayOffset - clock - offsetAndDelay,
TimerType.CountDown, TimerType.CountDown,
minutesString, minutesString,
subscriptionTimerDisplayOptions, subscriptionTimerDisplayOptions,
@@ -113,11 +114,11 @@ export function getSubscriptionDisplayData(
// 5. if event is in future, we count to the scheduled start // 5. if event is in future, we count to the scheduled start
// TODO: get time until // TODO: get time until
if (time.clock < subscribedEvent.timeStart) { if (clock < subscribedEvent.timeStart) {
return { return {
status: 'future', status: 'future',
timer: getFormattedTimer( timer: getFormattedTimer(
subscribedEvent.timeStart - time.clock - offsetAndDelay, subscribedEvent.timeStart - clock - offsetAndDelay,
TimerType.CountDown, TimerType.CountDown,
minutesString, minutesString,
subscriptionTimerDisplayOptions, subscriptionTimerDisplayOptions,
@@ -127,7 +128,7 @@ export function getSubscriptionDisplayData(
// 6. if event has ended, we show the scheduled end // 6. if event has ended, we show the scheduled end
// TODO: get the time from the reporter // TODO: get the time from the reporter
if (time.clock > subscribedEvent.timeEnd) { if (clock > subscribedEvent.timeEnd) {
return { return {
status: 'done', status: 'done',
timer: getFormattedTimer( timer: getFormattedTimer(
@@ -0,0 +1,38 @@
import { CustomFields, OntimeEntry, ProjectData, Settings } from 'ontime-types';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import useProjectData from '../../common/hooks-query/useProjectData';
import { useFlatRundown } from '../../common/hooks-query/useRundown';
import useSettings from '../../common/hooks-query/useSettings';
import { useViewOptionsStore } from '../../common/stores/viewOptions';
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
export interface CountdownData {
customFields: CustomFields;
events: OntimeEntry[];
projectData: ProjectData;
isMirrored: boolean;
settings: Settings;
}
export function useCountdownData(): ViewData<CountdownData> {
// persisted app state
const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data
const { data: rundownData, status: rundownStatus } = useFlatRundown();
const { data: projectData, status: projectDataStatus } = useProjectData();
const { data: settings, status: settingsStatus } = useSettings();
const { data: customFields, status: customFieldsStatus } = useCustomFields();
return {
data: {
customFields,
events: rundownData,
projectData,
isMirrored,
settings,
},
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]),
};
}