import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEntry, OntimeEvent, PlayableEvent } from 'ontime-types'; import { dayInMs, getEventWithId, getFirstEvent, getNextEvent, getTimeFrom, isNewLatest, MILLIS_PER_HOUR, } from 'ontime-utils'; import { formatDuration } from '../../common/utils/time'; import { isStringBoolean } from '../../features/viewers/common/viewUtils'; import type { ProgressStatus } from './TimelineEntry'; type CSSPosition = { left: number; width: number; }; /** * Calculates an absolute position of an element based on a schedule */ export function getElementPosition( scheduleStart: number, scheduleEnd: number, eventStart: number, eventDuration: number, containerWidth: number, ): CSSPosition { const normalEnd = scheduleEnd < scheduleStart ? scheduleEnd + dayInMs : scheduleEnd; const totalDuration = normalEnd - scheduleStart; const width = (eventDuration * containerWidth) / totalDuration; const left = ((eventStart - scheduleStart) * containerWidth) / totalDuration; return { left, width }; } /** * Gets rounded down hour for a given time */ export function getStartHour(startTime: number): number { const hours = Math.floor(startTime / MILLIS_PER_HOUR); return hours; } /** * Gets rounded up hour for a given time */ export function getEndHour(endTime: number): number { const hours = Math.ceil(endTime / MILLIS_PER_HOUR); return hours; } /** * converts a time span into an array of hours */ export function makeTimelineSections(firstHour: number, lastHour: number) { const timelineSections = []; for (let i = firstHour; i < lastHour; i++) { timelineSections.push(i); } return timelineSections; } /** * Returns a formatted label for a progress status */ export function getStatusLabel(timeToStart: number, status: ProgressStatus): string { if (status === 'done' || status === 'live') { return status; } if (timeToStart < 0) { return 'pending'; } return formatDuration(timeToStart); } interface ScopedRundownData { scopedRundown: PlayableEvent[]; firstStart: number; totalDuration: number; } export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData { const [searchParams] = useSearchParams(); const data = useMemo(() => { if (rundown.length === 0) { return { scopedRundown: [], firstStart: 0, totalDuration: 0 }; } const hidePast = isStringBoolean(searchParams.get('hidePast')); 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; } // 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 = getTimeFrom(currentEntry, lastEntry); 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, lastEntry)) { lastEntry = currentEntry; } } } return { scopedRundown, firstStart: firstStart ?? 0, totalDuration }; }, [rundown, searchParams, selectedEventId]); return data; } type UpcomingEvents = { now: OntimeEvent | null; next: OntimeEvent | null; followedBy: OntimeEvent | null; }; /** * Returns upcoming events from current: now, next and followedBy */ export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeString): UpcomingEvents { if (events.length === 0) { return { now: null, next: null, followedBy: null }; } let now = selectedId ? getEventWithId(events, selectedId) : 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; // Return the titles, handling nulls appropriately return { now, next, followedBy, }; } /** * Utility function calculates time to start */ export function getTimeToStart(now: number, start: number, delay: number, offset: number): number { return start + delay - now - offset; }