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;