Expected time for views (#1798)

* refactor: use correct metadata in op view

* refactor: use correct metadata in backstage view

* remove uneeded test

* fix: correct offset colour

* refactor: use correct metadata in timline view

* refactor: schedule item dont pass the whole event

* chore: lint
This commit is contained in:
Alex Christoffer Rasmussen
2025-10-05 14:46:42 +02:00
parent 700948bfbb
commit 22559c59d4
14 changed files with 263 additions and 263 deletions
+6 -2
View File
@@ -4,6 +4,7 @@ import { isOntimeEvent, isPlayableEvent, OntimeEntry, PlayableEvent } from 'onti
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { cx } from '../../common/utils/styleUtils';
import TimelineMarkers from './timeline-markers/TimelineMarkers';
@@ -15,7 +16,7 @@ import style from './Timeline.module.scss';
interface TimelineProps {
firstStart: number;
rundown: OntimeEntry[];
rundown: ExtendedEntry<OntimeEntry>[];
selectedEventId: string | null;
totalDuration: number;
}
@@ -45,7 +46,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
const { positions, totalWidth } = useMemo(() => {
const playableEvents = rundown
.filter((event): event is PlayableEvent => isOntimeEvent(event) && isPlayableEvent(event))
.filter((event): event is ExtendedEntry<PlayableEvent> => isOntimeEvent(event) && isPlayableEvent(event))
.map((event) => ({
start: event.timeStart + (event.dayOffset ?? 0) * dayInMs + (event.delay ?? 0),
duration: event.duration,
@@ -96,6 +97,9 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
left={position.left}
status={statusMap[event.id]}
start={event.timeStart + (event.dayOffset ?? 0) * dayInMs}
totalGap={event.totalGap}
isLinkedToLoaded={event.isLinkedToLoaded}
dayOffset={event.dayOffset}
title={event.title}
width={position.width}
/>
@@ -1,12 +1,12 @@
import { RefObject } from 'react';
import { useTimelineStatus, useTimer } from '../../common/hooks/useSocket';
import { useExpectedStartData, useTimer } from '../../common/hooks/useSocket';
import { getProgress } from '../../common/utils/getProgress';
import { alpha, cx } from '../../common/utils/styleUtils';
import { formatDuration, formatTime } from '../../common/utils/time';
import { formatDuration, formatTime, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
import { useTranslation } from '../../translation/TranslationProvider';
import { getStatusLabel, getTimeToStart } from './timeline.utils';
import { getStatusLabel } from './timeline.utils';
import style from './Timeline.module.scss';
@@ -20,6 +20,9 @@ interface TimelineEntryProps {
left: number;
status: ProgressStatus;
start: number;
dayOffset: number;
totalGap: number;
isLinkedToLoaded: boolean;
title: string;
width: number;
ref?: RefObject<HTMLDivElement | null>;
@@ -38,6 +41,9 @@ export function TimelineEntry({
left,
status,
start,
dayOffset,
totalGap,
isLinkedToLoaded,
title,
width,
ref,
@@ -73,11 +79,29 @@ export function TimelineEntry({
<div className={style.maybeInline}>
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</div>}
{smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
{smallArea && (
<TimelineEntryStatus
delay={delay}
start={start}
dayOffset={dayOffset}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
status={status}
/>
)}
</div>
{showTitle && (
<>
{!smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
{!smallArea && (
<TimelineEntryStatus
delay={delay}
start={start}
dayOffset={dayOffset}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
status={status}
/>
)}
<div>{title}</div>
</>
)}
@@ -92,16 +116,31 @@ export function TimelineEntry({
interface TimelineEntryStatusProps {
delay: number;
start: number;
dayOffset: number;
totalGap: number;
isLinkedToLoaded: boolean;
status: ProgressStatus;
}
// extract component to isolate re-renders provoked by the clock changes
function TimelineEntryStatus({ delay, start, status }: TimelineEntryStatusProps) {
const { clock, offset } = useTimelineStatus();
function TimelineEntryStatus({
delay,
start,
dayOffset,
totalGap,
isLinkedToLoaded,
status,
}: TimelineEntryStatusProps) {
const state = useExpectedStartData();
const { getLocalizedString } = useTranslation();
// start times need to be normalised in a rundown that crosses midnight
let statusText = getStatusLabel(getTimeToStart(clock, start, delay, offset), status);
const { timeToStart } = getExpectedTimesFromExtendedEvent(
{ timeStart: start, delay, dayOffset, totalGap, isLinkedToLoaded, countToEnd: false, duration: 0 },
state,
);
let statusText = getStatusLabel(timeToStart, status);
if (statusText === 'live') {
statusText = getLocalizedString('timeline.live');
} else if (statusText === 'pending') {
@@ -1,21 +1,21 @@
import { OntimeEvent } from 'ontime-types';
import { useTimelineSocket } from '../../common/hooks/useSocket';
import { formatDuration } from '../../common/utils/time';
import { useExpectedStartData } from '../../common/hooks/useSocket';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { formatDuration, getExpectedTimesFromExtendedEvent } 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;
now: ExtendedEntry<OntimeEvent> | null;
next: ExtendedEntry<OntimeEvent> | null;
followedBy: ExtendedEntry<OntimeEvent> | null;
}
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
const { getLocalizedString } = useTranslation();
const { clock, offset } = useTimelineSocket();
const state = useExpectedStartData();
// gather card data
const titleNow = now?.title ?? '-';
@@ -26,20 +26,20 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
let followedByStatus: string | undefined;
if (next !== null) {
const timeToStart = getTimeToStart(clock, next.timeStart, next?.delay ?? 0, offset);
if (timeToStart < 0) {
const { timeToStart } = getExpectedTimesFromExtendedEvent(next, state);
if (timeToStart <= 0) {
nextStatus = dueText;
} else {
nextStatus = `T - ${formatDuration(timeToStart)}`;
nextStatus = formatDuration(timeToStart);
}
}
if (followedBy !== null) {
const timeToStart = getTimeToStart(clock, followedBy.timeStart, followedBy?.delay ?? 0, offset);
if (timeToStart < 0) {
const { timeToStart } = getExpectedTimesFromExtendedEvent(followedBy, state);
if (timeToStart <= 0) {
followedByStatus = dueText;
} else {
followedByStatus = `T - ${formatDuration(timeToStart)}`;
followedByStatus = formatDuration(timeToStart);
}
}
@@ -10,6 +10,7 @@ import {
MILLIS_PER_HOUR,
} from 'ontime-utils';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { formatDuration } from '../../common/utils/time';
import { useTimelineOptions } from './timeline.options';
@@ -76,7 +77,7 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
return status;
}
if (timeToStart < 0) {
if (timeToStart <= 0) {
return 'pending';
}
@@ -84,12 +85,15 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
}
interface ScopedRundownData {
scopedRundown: PlayableEvent[];
scopedRundown: ExtendedEntry<PlayableEvent>[];
firstStart: number;
totalDuration: number;
}
export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData {
export function useScopedRundown(
rundown: ExtendedEntry<OntimeEntry>[],
selectedEventId: MaybeString,
): ScopedRundownData {
const { hidePast } = useTimelineOptions();
const data = useMemo(() => {
@@ -97,11 +101,11 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
}
const scopedRundown: PlayableEvent[] = [];
const scopedRundown: ExtendedEntry<PlayableEvent>[] = [];
let selectedIndex = selectedEventId ? Infinity : -1;
let firstStart = null;
let totalDuration = 0;
let lastEntry: PlayableEvent | null = null;
let lastEntry: ExtendedEntry<PlayableEvent> | null = null;
for (let i = 0; i < rundown.length; i++) {
const currentEntry = rundown[i];
@@ -150,26 +154,28 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
}
type UpcomingEvents = {
now: OntimeEvent | null;
next: OntimeEvent | null;
followedBy: OntimeEvent | null;
now: ExtendedEntry<OntimeEvent> | null;
next: ExtendedEntry<OntimeEvent> | null;
followedBy: ExtendedEntry<OntimeEvent> | null;
};
/**
* Returns upcoming events from current: now, next and followedBy
*/
export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeString): UpcomingEvents {
export function getUpcomingEvents(events: ExtendedEntry<PlayableEvent>[], selectedId: MaybeString): UpcomingEvents {
if (events.length === 0) {
return { now: null, next: null, followedBy: null };
}
let now = selectedId ? getEventWithId(events, selectedId) : null;
let now = selectedId ? (getEventWithId(events, selectedId) as ExtendedEntry<OntimeEvent>) : null;
if (!isOntimeEvent(now)) {
now = null;
}
const next = now ? getNextEvent(events, now.id)?.nextEvent : getFirstEvent(events).firstEvent;
const followedBy = next ? getNextEvent(events, next.id)?.nextEvent : null;
const next = now
? (getNextEvent(events, now.id)?.nextEvent as ExtendedEntry<OntimeEvent> | null)
: (getFirstEvent(events).firstEvent as ExtendedEntry<OntimeEvent> | null);
const followedBy = next ? (getNextEvent(events, next.id)?.nextEvent as ExtendedEntry<OntimeEvent> | null) : null;
// Return the titles, handling nulls appropriately
return {
@@ -1,19 +1,20 @@
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
import useProjectData from '../../common/hooks-query/useProjectData';
import { useFlatRundown } from '../../common/hooks-query/useRundown';
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
import useSettings from '../../common/hooks-query/useSettings';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
export interface TimelineData {
events: OntimeEntry[];
events: ExtendedEntry<OntimeEntry>[];
projectData: ProjectData;
settings: Settings;
}
export function useTimelineData(): ViewData<TimelineData> {
// HTTP API data
const { data: rundownData, status: rundownStatus } = useFlatRundown();
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
const { data: projectData, status: projectDataStatus } = useProjectData();
const { data: settings, status: settingsStatus } = useSettings();