diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts
index 46d384c9cf..eab5f0f529 100644
--- a/apps/client/src/common/hooks/useSocket.ts
+++ b/apps/client/src/common/hooks/useSocket.ts
@@ -187,15 +187,6 @@ export const useRuntimePlaybackOverview = () => {
return useRuntimeStore(featureSelector);
};
-export const useTimelineOverview = () => {
- const featureSelector = (state: RuntimeStore) => ({
- plannedStart: state.runtime.plannedStart,
- plannedEnd: state.runtime.plannedEnd,
- });
-
- return useRuntimeStore(featureSelector);
-};
-
export const useTimelineStatus = () => {
const featureSelector = (state: RuntimeStore) => ({
clock: state.clock,
diff --git a/apps/client/src/features/viewers/timeline/Timeline.tsx b/apps/client/src/features/viewers/timeline/Timeline.tsx
index cd7a9b00ff..a8463f36d1 100644
--- a/apps/client/src/features/viewers/timeline/Timeline.tsx
+++ b/apps/client/src/features/viewers/timeline/Timeline.tsx
@@ -3,33 +3,33 @@ import { useViewportSize } from '@mantine/hooks';
import { isOntimeEvent, isPlayableEvent, MaybeNumber, OntimeRundown } from 'ontime-types';
import { checkIsNextDay, dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
-import { useTimelineOverview } from '../../../common/hooks/useSocket';
-
import TimelineMarkers from './timeline-markers/TimelineMarkers';
-import ProgressBar from './timeline-progress-bar/TimelineProgressBar';
+import TimelineProgressBar from './timeline-progress-bar/TimelineProgressBar';
import { getElementPosition, getEndHour, getStartHour } from './timeline.utils';
import { ProgressStatus, TimelineEntry } from './TimelineEntry';
import style from './Timeline.module.scss';
interface TimelineProps {
- selectedEventId: string | null;
+ firstStart: number;
rundown: OntimeRundown;
+ selectedEventId: string | null;
+ totalDuration: number;
}
export default memo(Timeline);
function Timeline(props: TimelineProps) {
- const { selectedEventId, rundown } = props;
+ const { firstStart, rundown, selectedEventId, totalDuration } = props;
const { width: screenWidth } = useViewportSize();
- const { plannedStart, plannedEnd } = useTimelineOverview();
- if (plannedStart === null || plannedEnd === null) {
+ if (totalDuration === 0) {
return null;
}
+
const { lastEvent } = getLastEvent(rundown);
- const startHour = getStartHour(plannedStart);
- const endHour = getEndHour(plannedEnd + (lastEvent?.delay ?? 0));
+ const startHour = getStartHour(firstStart);
+ const endHour = getEndHour(firstStart + totalDuration + (lastEvent?.delay ?? 0));
let previousEventStartTime: MaybeNumber = null;
// we use selectedEventId as a signifier on whether the timeline is live
@@ -39,7 +39,7 @@ function Timeline(props: TimelineProps) {
return (
-
+
{rundown.map((event) => {
// for now we dont render delays and blocks
diff --git a/apps/client/src/features/viewers/timeline/TimelinePage.tsx b/apps/client/src/features/viewers/timeline/TimelinePage.tsx
index 06b4d9dfd4..28cf9d9ec1 100644
--- a/apps/client/src/features/viewers/timeline/TimelinePage.tsx
+++ b/apps/client/src/features/viewers/timeline/TimelinePage.tsx
@@ -35,7 +35,7 @@ export default function TimelinePage(props: TimelinePageProps) {
const { backstageEvents, general, selectedId, settings, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
// holds copy of the rundown with only relevant events
- const scopedRundown = useScopedRundown(backstageEvents, selectedId);
+ const { scopedRundown, firstStart, totalDuration } = useScopedRundown(backstageEvents, selectedId);
const { getLocalizedString } = useTranslation();
const clock = formatTime(time.clock);
@@ -82,7 +82,12 @@ export default function TimelinePage(props: TimelinePageProps) {
category='next'
/>
-
+
);
}
diff --git a/apps/client/src/features/viewers/timeline/timeline.utils.ts b/apps/client/src/features/viewers/timeline/timeline.utils.ts
index 665545c8f1..1903abaf84 100644
--- a/apps/client/src/features/viewers/timeline/timeline.utils.ts
+++ b/apps/client/src/features/viewers/timeline/timeline.utils.ts
@@ -1,11 +1,13 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
-import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown } from 'ontime-types';
+import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEvent, OntimeRundown, PlayableEvent } from 'ontime-types';
import {
dayInMs,
getEventWithId,
getFirstEvent,
getNextEvent,
+ getTimeFromPrevious,
+ isNewLatest,
MILLIS_PER_HOUR,
millisToString,
removeSeconds,
@@ -89,31 +91,80 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
return formatDuration(timeToStart);
}
-export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeString): OntimeRundown {
+interface ScopedRundownData {
+ scopedRundown: PlayableEvent[];
+ firstStart: number;
+ totalDuration: number;
+}
+
+export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeString): ScopedRundownData {
const [searchParams] = useSearchParams();
const data = useMemo(() => {
if (rundown.length === 0) {
- return [];
+ return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
}
const hideBackstage = isStringBoolean(searchParams.get('hideBackstage'));
const hidePast = isStringBoolean(searchParams.get('hidePast'));
- let scopedRundown = [...rundown];
-
- if (hidePast && selectedEventId) {
- const currentIndex = rundown.findIndex((event) => event.id === selectedEventId);
- if (currentIndex >= 0) {
- scopedRundown = scopedRundown.slice(currentIndex);
+ const scopedRundown: PlayableEvent[] = [];
+ let selectedIndex = selectedEventId ? Infinity : -1;
+ let firstStart = null;
+ let totalDuration = 0;
+ let lastEntry: PlayableEvent | null = null;
+
+ for (let i = 0; i < rundown.length; i++) {
+ const currentEntry = rundown[i];
+ // we only deal with playableEvents
+ if (isOntimeEvent(currentEntry) && isPlayableEvent(currentEntry)) {
+ if (currentEntry.id === selectedEventId) {
+ selectedIndex = i;
+ }
+
+ // maybe filter past
+ if (hidePast && i < selectedIndex) {
+ continue;
+ }
+
+ // maybe filter backstage
+ if (!currentEntry.isPublic && hideBackstage) {
+ continue;
+ }
+
+ // add to scopedRundown
+ scopedRundown.push(currentEntry);
+
+ /**
+ * Derive timers
+ * This logic is partially from rundownCache.generate
+ * With the addition of deriving the current day offset
+ */
+ if (firstStart === null) {
+ firstStart = currentEntry.timeStart;
+ }
+
+ const timeFromPrevious: number = getTimeFromPrevious(
+ currentEntry.timeStart,
+ lastEntry?.timeStart,
+ lastEntry?.timeEnd,
+ lastEntry?.duration,
+ );
+
+ if (timeFromPrevious === 0) {
+ totalDuration += currentEntry.duration;
+ } else if (timeFromPrevious > 0) {
+ totalDuration += timeFromPrevious + currentEntry.duration;
+ } else if (timeFromPrevious < 0) {
+ totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
+ }
+ if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
+ lastEntry = currentEntry;
+ }
}
}
- if (hideBackstage) {
- scopedRundown = scopedRundown.filter((event) => !isOntimeEvent(event) || event.isPublic);
- }
-
- return scopedRundown;
+ return { scopedRundown, firstStart: firstStart ?? 0, totalDuration };
}, [rundown, searchParams, selectedEventId]);
return data;