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]),
+ };
+}