From f23adfa45c9d6aea5a1983a33a3bdfff29ca37ca Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Thu, 24 Jul 2025 06:17:19 +0200 Subject: [PATCH] refactor: extract countdown data to hook --- apps/client/src/AppRouter.tsx | 1 + apps/client/src/common/hooks/useSocket.ts | 6 ++ apps/client/src/views/countdown/Countdown.tsx | 95 ++++++++----------- .../countdown/CountdownSubscriptions.tsx | 42 ++++---- .../src/views/countdown/countdown.utils.ts | 19 ++-- .../src/views/countdown/useCountdownData.ts | 38 ++++++++ 6 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 apps/client/src/views/countdown/useCountdownData.ts diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 7d4eeab62..de9aa56c2 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -51,6 +51,7 @@ export default function AppRouter() { path='countdown' element={ + } diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 8a23ee623..ccbc5bc56 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -242,3 +242,9 @@ export const useTimerSocket = createSelector((state: RuntimeStore) => ({ aux3: state.auxtimer3.current, }, })); + +export const useCountdownSocket = createSelector((state: RuntimeStore) => ({ + playback: state.timer.playback, + current: state.timer.current, + clock: state.clock, +})); diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index a1b83410b..873216a78 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -1,64 +1,52 @@ import { useMemo, useState } from 'react'; import { IoAdd } from 'react-icons/io5'; -import { - CustomFields, - EntryId, - isOntimeEvent, - isPlayableEvent, - OntimeEvent, - OntimeView, - ProjectData, - Settings, -} from 'ontime-types'; +import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types'; import Button from '../../common/components/buttons/Button'; import Empty from '../../common/components/state/Empty'; +import EmptyPage from '../../common/components/state/EmptyPage'; import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; +import { useClock } from '../../common/hooks/useSocket'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; -import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; import { formatTime, getDefaultFormat } from '../../common/utils/time'; import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; import { useTranslation } from '../../translation/TranslationProvider'; +import Loader from '../common/loader/Loader'; import { getCountdownOptions, useCountdownOptions } from './countdown.options'; import { getOrderedSubscriptions } from './countdown.utils'; import CountdownSelect from './CountdownSelect'; import CountdownSubscriptions from './CountdownSubscriptions'; +import { CountdownData, useCountdownData } from './useCountdownData'; import './Countdown.scss'; -interface CountdownProps { - customFields: CustomFields; - 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); +export default function CountdownLoader() { + const { data, status } = useCountdownData(); useWindowTitle('Countdown'); + if (status === 'pending') { + return ; + } + + if (status === 'error') { + return ; + } + + return ; +} + +function Countdown({ customFields, events, projectData, isMirrored, settings }: CountdownData) { + const { getLocalizedString } = useTranslation(); + const { subscriptions } = useCountdownOptions(); + + const [editMode, setEditMode] = useState(false); + // gather rundown data const playableEvents = events.filter((event) => isOntimeEvent(event) && isPlayableEvent(event)); - // gather timer data - const clock = formatTime(time.clock); - // gather presentation data const hasEvents = playableEvents.length > 0; @@ -73,12 +61,9 @@ export default function Countdown({
- {general?.logo && } -
{general.title}
-
-
{getLocalizedString('common.time_now')}
- -
+ {projectData?.logo && } +
{projectData.title}
+
{!hasEvents && } @@ -91,9 +76,7 @@ export default function Countdown({ setEditMode(true)} - selectedId={selectedId} /> )}
@@ -102,13 +85,11 @@ export default function Countdown({ interface CountdownContentsProps { playableEvents: OntimeEvent[]; - selectedId: EntryId | null; subscriptions: EntryId[]; - time: ViewExtendedTimer; goToEditMode: () => void; } -function CountdownContents({ playableEvents, selectedId, subscriptions, time, goToEditMode }: CountdownContentsProps) { +function CountdownContents({ playableEvents, subscriptions, goToEditMode }: CountdownContentsProps) { const { getLocalizedString } = useTranslation(); if (subscriptions.length === 0) { @@ -135,12 +116,20 @@ function CountdownContents({ playableEvents, selectedId, subscriptions, time, go ); } + return ; +} + +function CountdownClock() { + const { getLocalizedString } = useTranslation(); + const { clock } = useClock(); + + // gather timer data + const formattedClock = formatTime(clock); + return ( - +
+
{getLocalizedString('common.time_now')}
+ +
); } diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index 49cc22239..1c956af7d 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -5,8 +5,13 @@ import { EntryId, OntimeEvent } from 'ontime-types'; import Button from '../../common/components/buttons/Button'; import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity'; import useFollowComponent from '../../common/hooks/useFollowComponent'; -import { useCurrentDay, useRuntimeOffset } from '../../common/hooks/useSocket'; -import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; +import { + useCountdownSocket, + useCurrentDay, + usePlayback, + useRuntimeOffset, + useSelectedEventId, +} from '../../common/hooks/useSocket'; import { getOffsetState } from '../../common/utils/offset'; import { cx } from '../../common/utils/styleUtils'; import { throttle } from '../../common/utils/throttle'; @@ -22,18 +27,13 @@ import './Countdown.scss'; interface CountdownSubscriptionsProps { subscribedEvents: OntimeEvent[]; - selectedId: EntryId | null; - time: ViewExtendedTimer; goToEditMode: () => void; } -export default function CountdownSubscriptions({ - time, - subscribedEvents, - selectedId, - goToEditMode, -}: CountdownSubscriptionsProps) { +export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) { const { secondarySource, showExpected } = useCountdownOptions(); + const { playback } = usePlayback(); + const { selectedEventId } = useSelectedEventId(); const showFab = useFadeOutOnInactivity(true); const timeoutId = useRef(null); @@ -49,16 +49,16 @@ export default function CountdownSubscriptions({ // reset scroll if nothing is selected useEffect(() => { - if (!selectedId) { + if (!selectedEventId) { if (!lockAutoScroll) { scrollRef.current?.scrollTo(0, 0); } } - }, [selectedId, lockAutoScroll, scrollRef]); + }, [selectedEventId, lockAutoScroll, scrollRef]); // scroll to component if user clicks the Follow button const handleOffset = () => { - if (selectedId) { + if (selectedEventId) { scrollToComponent(); } setLockAutoScroll(false); @@ -91,7 +91,7 @@ export default function CountdownSubscriptions({
{subscribedEvents.map((event) => { const secondaryData = getPropertyValue(event, secondarySource); - const isLive = getIsLive(event.id, selectedId, time.playback); + const isLive = getIsLive(event.id, selectedEventId, playback); return (
@@ -107,7 +107,7 @@ export default function CountdownSubscriptions({ )}
- +
{sanitiseTitle(event.title)}
{secondaryData &&
{secondaryData}
}
@@ -152,22 +152,24 @@ function ExpectedSchedule(props: ExpectedScheduleProps) { } interface SubscriptionStatusProps { - time: ViewExtendedTimer; event: OntimeEvent; - selectedId: EntryId | null; + selectedEventId: EntryId | null; } -function SubscriptionStatus({ time, event, selectedId }: SubscriptionStatusProps) { +function SubscriptionStatus({ event, selectedEventId }: SubscriptionStatusProps) { const { getLocalizedString } = useTranslation(); const { currentDay } = useCurrentDay(); const { offset } = useRuntimeOffset(); const { showExpected } = useCountdownOptions(); + const { playback, current, clock } = useCountdownSocket(); // TODO: use reporter values as in the event block chip const { status, timer } = getSubscriptionDisplayData( - time, + current, + playback, + clock, event, - selectedId, + selectedEventId, offset, currentDay, getLocalizedString('common.minutes'), diff --git a/apps/client/src/views/countdown/countdown.utils.ts b/apps/client/src/views/countdown/countdown.utils.ts index 618bc3355..b755cae8d 100644 --- a/apps/client/src/views/countdown/countdown.utils.ts +++ b/apps/client/src/views/countdown/countdown.utils.ts @@ -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 { ViewExtendedTimer } from '../../common/models/TimeManager.type'; import { getFormattedTimer } from '../../features/viewers/common/viewUtils'; import type { TranslationKey } from '../../translation/TranslationProvider'; @@ -46,7 +45,9 @@ export const timerProgress: TimerMessage = { * TODO: get timer data granularly */ export function getSubscriptionDisplayData( - time: ViewExtendedTimer, + current: MaybeNumber, + playback: Playback, + clock: number, subscribedEvent: OntimeEvent, selectedId: EntryId | null, offset: number, @@ -58,7 +59,7 @@ export function getSubscriptionDisplayData( if (selectedId === subscribedEvent.id) { // 1. An event that is loaded but not running is {'due': } - 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 return { status: 'due', @@ -74,7 +75,7 @@ export function getSubscriptionDisplayData( // 2. An event with a time-to-start lower than 0 is {'due': }, show the running timer return { 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 { status: 'future', timer: getFormattedTimer( - subscribedEvent.timeStart + dayOffset - time.clock - offsetAndDelay, + subscribedEvent.timeStart + dayOffset - clock - offsetAndDelay, TimerType.CountDown, minutesString, subscriptionTimerDisplayOptions, @@ -113,11 +114,11 @@ export function getSubscriptionDisplayData( // 5. if event is in future, we count to the scheduled start // TODO: get time until - if (time.clock < subscribedEvent.timeStart) { + if (clock < subscribedEvent.timeStart) { return { status: 'future', timer: getFormattedTimer( - subscribedEvent.timeStart - time.clock - offsetAndDelay, + subscribedEvent.timeStart - clock - offsetAndDelay, TimerType.CountDown, minutesString, subscriptionTimerDisplayOptions, @@ -127,7 +128,7 @@ export function getSubscriptionDisplayData( // 6. if event has ended, we show the scheduled end // TODO: get the time from the reporter - if (time.clock > subscribedEvent.timeEnd) { + if (clock > subscribedEvent.timeEnd) { return { status: 'done', timer: getFormattedTimer( diff --git a/apps/client/src/views/countdown/useCountdownData.ts b/apps/client/src/views/countdown/useCountdownData.ts new file mode 100644 index 000000000..84c56b251 --- /dev/null +++ b/apps/client/src/views/countdown/useCountdownData.ts @@ -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 { + // 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]), + }; +}