diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 60d53f9e3..16f28f510 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -78,6 +78,7 @@ export default function AppRouter() { path='timeline' element={ + } diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 0eb2b843b..941a49396 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -269,3 +269,8 @@ export const useStudioTimersSocket = createSelector((state: RuntimeStore) => ({ time: state.timer, runtime: state.runtime, })); + +export const useTimelineSocket = createSelector((state: RuntimeStore) => ({ + clock: state.clock, + offsetAbs: state.runtime.offsetAbs, +})); diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index e1f5c9f24..f0d4621d9 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -1,103 +1,86 @@ import { useMemo } from 'react'; -import { MaybeString, OntimeEvent, OntimeView, ProjectData, Runtime, Settings } from 'ontime-types'; +import { OntimeView } from 'ontime-types'; +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, useSelectedEventId } from '../../common/hooks/useSocket'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; -import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; -import { formatDuration, formatTime, getDefaultFormat } from '../../common/utils/time'; +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 Section from './timeline-section/TimelineSection'; import Timeline from './Timeline'; import { getTimelineOptions } from './timeline.options'; -import { getTimeToStart, getUpcomingEvents, useScopedRundown } from './timeline.utils'; +import { getUpcomingEvents, useScopedRundown } from './timeline.utils'; +import TimelineSections from './TimelineSections'; +import { TimelineData, useTimelineData } from './useTimelineData'; import './TimelinePage.scss'; -interface TimelinePageProps { - events: OntimeEvent[]; - general: ProjectData; - runtime: Runtime; - selectedId: MaybeString; - settings: Settings | undefined; - time: ViewExtendedTimer; -} - -/** - * since we inherit from viewPage - * which refreshes at least once a second - * There is little point splitting or memoising top level elements - */ -export default function TimelinePage({ events, general, runtime, selectedId, settings, time }: TimelinePageProps) { - // holds copy of the rundown with only relevant events - const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedId); - const { getLocalizedString } = useTranslation(); - const clock = formatTime(time.clock); - - const { now, next, followedBy } = useMemo(() => { - return getUpcomingEvents(scopedRundown, selectedId); - }, [scopedRundown, selectedId]); +export default function TimelinePageLoader() { + const { data, status } = useTimelineData(); useWindowTitle('Timeline'); + if (status === 'pending') { + return ; + } + + if (status === 'error') { + return ; + } + + return ; +} + +function TimelinePage({ events, projectData, settings }: TimelineData) { + const { selectedEventId } = useSelectedEventId(); + // holds copy of the rundown with only relevant events + const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId); + + // gather card options + const { now, next, followedBy } = useMemo(() => { + return getUpcomingEvents(scopedRundown, selectedEventId); + }, [scopedRundown, selectedEventId]); + // populate options const defaultFormat = getDefaultFormat(settings?.timeFormat); const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]); - const titleNow = now?.title ?? '-'; - const dueText = getLocalizedString('timeline.due').toUpperCase(); - const nextText = next !== null ? next.title : '-'; - const followedByText = followedBy !== null ? followedBy.title : '-'; - let nextStatus: string | undefined; - let followedByStatus: string | undefined; - - if (next !== null) { - const timeToStart = getTimeToStart(time.clock, next.timeStart, next?.delay ?? 0, runtime.offsetAbs); - if (timeToStart < 0) { - nextStatus = dueText; - } else { - nextStatus = `T - ${formatDuration(timeToStart)}`; - } - } - - if (followedBy !== null) { - const timeToStart = getTimeToStart(time.clock, followedBy.timeStart, followedBy?.delay ?? 0, runtime.offsetAbs); - if (timeToStart < 0) { - followedByStatus = dueText; - } else { - followedByStatus = `T - ${formatDuration(timeToStart)}`; - } - } return (
- {general?.logo && } -
{general.title}
-
-
{getLocalizedString('common.time_now')}
- -
+ {projectData?.logo && } +
{projectData.title}
+
-
-
-
-
-
+ +
); } + +function TimelineClock() { + 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/timeline/TimelineSections.tsx b/apps/client/src/views/timeline/TimelineSections.tsx new file mode 100644 index 000000000..9bfee8067 --- /dev/null +++ b/apps/client/src/views/timeline/TimelineSections.tsx @@ -0,0 +1,63 @@ +import { OntimeEvent } from 'ontime-types'; + +import { useTimelineSocket } from '../../common/hooks/useSocket'; +import { formatDuration } from '../../common/utils/time'; +import { useTranslation } from '../../translation/TranslationProvider'; + +import TimelineSection from './timeline-section/TimelineSection'; +import { getTimeToStart } from './timeline.utils'; + +interface TimelineSectionsProps { + now: OntimeEvent | null; + next: OntimeEvent | null; + followedBy: OntimeEvent | null; +} + +export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) { + const { getLocalizedString } = useTranslation(); + const { clock, offsetAbs } = useTimelineSocket(); + + // gather card data + const titleNow = now?.title ?? '-'; + const dueText = getLocalizedString('timeline.due').toUpperCase(); + const nextText = next !== null ? next.title : '-'; + const followedByText = followedBy !== null ? followedBy.title : '-'; + let nextStatus: string | undefined; + let followedByStatus: string | undefined; + + if (next !== null) { + const timeToStart = getTimeToStart(clock, next.timeStart, next?.delay ?? 0, offsetAbs); + if (timeToStart < 0) { + nextStatus = dueText; + } else { + nextStatus = `T - ${formatDuration(timeToStart)}`; + } + } + + if (followedBy !== null) { + const timeToStart = getTimeToStart(clock, followedBy.timeStart, followedBy?.delay ?? 0, offsetAbs); + if (timeToStart < 0) { + followedByStatus = dueText; + } else { + followedByStatus = `T - ${formatDuration(timeToStart)}`; + } + } + + return ( +
+ + + +
+ ); +} diff --git a/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx b/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx index 5caa87910..1d5983b3b 100644 --- a/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx +++ b/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx @@ -10,9 +10,9 @@ interface SectionProps { status?: string; } -export default memo(Section); +export default memo(TimelineSection); -function Section({ category, content, title, status }: SectionProps) { +function TimelineSection({ category, content, title, status }: SectionProps) { const sectionClasses = cx(['section', category === 'now' && 'section--now']); const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']); return ( diff --git a/apps/client/src/views/timeline/useTimelineData.ts b/apps/client/src/views/timeline/useTimelineData.ts new file mode 100644 index 000000000..5b4de6b4a --- /dev/null +++ b/apps/client/src/views/timeline/useTimelineData.ts @@ -0,0 +1,28 @@ +import { OntimeEntry, ProjectData, Settings } from 'ontime-types'; + +import useProjectData from '../../common/hooks-query/useProjectData'; +import { useFlatRundown } from '../../common/hooks-query/useRundown'; +import useSettings from '../../common/hooks-query/useSettings'; +import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils'; + +export interface TimelineData { + events: OntimeEntry[]; + projectData: ProjectData; + settings: Settings; +} + +export function useTimelineData(): ViewData { + // HTTP API data + const { data: rundownData, status: rundownStatus } = useFlatRundown(); + const { data: projectData, status: projectDataStatus } = useProjectData(); + const { data: settings, status: settingsStatus } = useSettings(); + + return { + data: { + events: rundownData, + projectData, + settings, + }, + status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus]), + }; +}