import { OffsetMode, OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types'; import { dayInMs, isPlaybackActive, millisToString } from 'ontime-utils'; import { useMemo } from 'react'; import { TbCalendarClock, TbCalendarPin, TbCalendarStar, TbFlagPin, TbFlagStar, TbFolderPin, TbFolderStar, } from 'react-icons/tb'; import Tooltip from '../../../common/components/tooltip/Tooltip'; import { useEntry } from '../../../common/hooks-query/useRundown'; import { useClock, useCurrentGroupId, useFlagTimerOverView, useGroupTimerOverView, useNextFlag, useOffsetOverview, useProgressOverview, useRundownExpectedEnd, useStartTimesOverview, useTimer, } from '../../../common/hooks/useSocket'; import { getOffsetState, getOffsetText } from '../../../common/utils/offset'; import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../../common/utils/time'; import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod'; import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils'; import { OverUnder, TimeColumn, WrappedInTimeColumn } from './TimeLayout'; import style from './TimeElements.module.scss'; interface OverviewTimeElementsProps { shouldFormat?: boolean; } const timeFormatOptions = { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' }; function formatTimeValue(time: number | null, shouldFormat: boolean | undefined): string { if (time === null) return timerPlaceholder; if (shouldFormat) return formatTime(time, timeFormatOptions); return millisToString(time, { fallback: timerPlaceholder }); } export function StartTimesRuntime({ shouldFormat }: OverviewTimeElementsProps) { const { plannedEnd, plannedStart, actualStart } = useStartTimesOverview(); const plannedStartText = formatTimeValue(plannedStart, shouldFormat); const actualStartText = formatTimeValue(actualStart, shouldFormat); const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]); const plannedEndText = formatTimeValue(maybePlannedEnd, shouldFormat); const multipleDays = maybePlannedDaySpan > 0; const plannedEndTooltip = multipleDays ? `Planned end time (rundown spans over ${maybePlannedDaySpan + 1} days)` : 'Planned end time'; return (
Start
} />
} />
End {multipleDays && ( )}
} /> ); } export function StartTimesPlanning({ shouldFormat }: OverviewTimeElementsProps) { const { plannedEnd, plannedStart } = useStartTimesOverview(); const plannedStartText = formatTimeValue(plannedStart, shouldFormat); const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]); const plannedEndText = formatTimeValue(maybePlannedEnd, shouldFormat); const multipleDays = maybePlannedDaySpan > 0; const plannedEndTooltip = multipleDays ? `Planned end time (rundown spans over ${maybePlannedDaySpan + 1} days)` : 'Planned end time'; return (
Start
} />
End {multipleDays && ( )}
} /> ); } /** * Shows the expected end for the rundown * Extracted to improve performance as this is a ticking value */ function RundownExpectedEnd({ shouldFormat }: OverviewTimeElementsProps) { const expectedEnd = useRundownExpectedEnd(); const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]); const maybeExpectedEndText = formatTimeValue(maybeExpectedEnd, shouldFormat); const multipleDays = maybeExpectedEnd !== null && maybeExpectedDaySpan > 0; const tooltip = multipleDays ? `Expected end time (rundown spans over ${maybeExpectedDaySpan + 1} days)` : 'Expected end time'; return ( {multipleDays && } } /> ); } export function MetadataTimes() { return (
); } function GroupTimes() { const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView(); const currentGroupId = useCurrentGroupId(); const group = useEntry(currentGroupId) as OntimeGroup | null; const active = isPlaybackActive(playback); // the group end time dose not encode any day offsets so it is calculated with group start time and duration const plannedGroupEnd = (() => { if (!active) return null; if (!group || group.timeStart === null) return null; const normalizedClock = clock + currentDay * dayInMs; return mode === OffsetMode.Absolute ? group.timeStart + group.duration - normalizedClock : actualGroupStart + group.duration - normalizedClock; })(); const plannedTimeUntilGroupEnd = formatDueTime(plannedGroupEnd, 3, TimerType.CountDown); const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null; const expectedTimeUntilGroupEnd = formatDueTime(expectedGroupEnd, 3, TimerType.CountDown); return (
{`${group?.title || 'Group'} `}
} /> {plannedTimeUntilGroupEnd}
} /> {expectedTimeUntilGroupEnd}
); } function FlagTimes() { const { clock, mode, actualStart, plannedStart, playback, currentDay } = useFlagTimerOverView(); const { id, expectedStart } = useNextFlag(); const entry = useEntry(id) as OntimeEvent | null; const active = isPlaybackActive(playback); const plannedFlagStart = (() => { if (!active) return null; if (!entry) return null; const normalizedTimeStart = entry.timeStart + entry.dayOffset * dayInMs; const normalizedClock = clock + currentDay * dayInMs; return mode === OffsetMode.Absolute ? normalizedTimeStart - normalizedClock : normalizedTimeStart + actualStart - plannedStart - normalizedClock; })(); const plannedTimeUntilDisplay = formatDueTime(plannedFlagStart, 3, TimerType.CountDown); const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null; const expectedTimeUntilDisplay = formatDueTime(expectedTimeUntil, 3, TimerType.CountDown); const title = entry?.title ?? null; return (
{`${title || 'Flag'} `}
} /> {plannedTimeUntilDisplay}
} /> {expectedTimeUntilDisplay}
); } export function ProgressOverview() { const { numEvents, selectedEventIndex } = useProgressOverview(); const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash; const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash; return ; } export function OffsetOverview() { const { offset, playback } = useOffsetOverview(); const isPlaying = isPlaybackActive(playback); const offsetState = getOffsetState(isPlaying ? offset : null); const offsetText = getOffsetText(isPlaying ? offset : null); return ; } export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) { const clock = useClock(); const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock); return ( } /> ); } export function TimerOverview({ className }: { className?: string }) { const timer = useTimer(); const isWaiting = timer.phase === TimerPhase.Pending; const title = isWaiting ? 'Count to start' : 'Running timer'; const display = millisToString(isWaiting ? timer.secondaryTimer : timer.current, { fallback: timerPlaceholder }); function getTimerState(): 'waiting' | 'muted' | 'active' { if (isWaiting) return 'waiting'; if (timer.current === null) return 'muted'; return 'active'; } return ; } export function PlanningStats() { const { numEvents } = useProgressOverview(); const { plannedEnd, plannedStart } = useStartTimesOverview(); const hasTimes = plannedStart !== null && plannedEnd !== null; const formattedDuration = hasTimes ? formatDuration(plannedEnd - plannedStart) : timerPlaceholder; return ( <> ); }