diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 449148944..cc10945d4 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -202,6 +202,9 @@ export const useStartTimesOverview = createSelector((state: RuntimeStore) => ({ plannedStart: state.rundown.plannedStart, actualStart: state.rundown.actualStart, plannedEnd: state.rundown.plannedEnd, +})); + +export const useRundownExpectedEnd = createSelector((state: RuntimeStore) => ({ expectedEnd: state.offset.expectedRundownEnd, })); diff --git a/apps/client/src/common/utils/__tests__/time.test.ts b/apps/client/src/common/utils/__tests__/time.test.ts index 429300f7d..be11e0ded 100644 --- a/apps/client/src/common/utils/__tests__/time.test.ts +++ b/apps/client/src/common/utils/__tests__/time.test.ts @@ -43,8 +43,8 @@ describe('formatTime()', () => { describe('formatDuration()', () => { it('formats durations correctly', () => { - expect(formatDuration(0)).toBe('0h 0m'); - expect(formatDuration(-5000)).toBe('0h 0m'); + expect(formatDuration(0)).toBe('0m'); + expect(formatDuration(-5000)).toBe('0m'); expect(formatDuration(MILLIS_PER_MINUTE)).toBe('1m'); expect(formatDuration(6 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND)).toBe('6m'); expect(formatDuration(MILLIS_PER_MINUTE * 10)).toBe('10m'); diff --git a/apps/client/src/common/utils/time.ts b/apps/client/src/common/utils/time.ts index 109436962..fc5bb6c9f 100644 --- a/apps/client/src/common/utils/time.ts +++ b/apps/client/src/common/utils/time.ts @@ -112,7 +112,7 @@ export const formatTime = ( export function formatDuration(duration: number, hideSeconds = true): string { // durations should never be negative, we handle it here to flag if there is an issue in future if (duration <= 0) { - return '0h 0m'; + return '0m'; } const hours = Math.floor(duration / MILLIS_PER_HOUR); diff --git a/apps/client/src/features/control/playback/playback-timer/PlaybackTimer.module.scss b/apps/client/src/features/control/playback/playback-timer/PlaybackTimer.module.scss index 1fd3fdbdb..972b37618 100644 --- a/apps/client/src/features/control/playback/playback-timer/PlaybackTimer.module.scss +++ b/apps/client/src/features/control/playback/playback-timer/PlaybackTimer.module.scss @@ -58,7 +58,6 @@ height: 1.5rem; display: flex; gap: $section-spacing; - margin-left: 1.5rem; } .tag { @@ -70,6 +69,7 @@ .time { color: $section-white; font-size: $text-body-size; + display: inline-block; } .rolltag { diff --git a/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss b/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss index 0de360990..994bbf740 100644 --- a/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss +++ b/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss @@ -81,6 +81,7 @@ .plannedStart { font-size: 1.5rem; margin-right: 0.5rem; + display: inline; } .timeUntil { diff --git a/apps/client/src/features/operator/operator-event/OperatorEvent.tsx b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx index c2e77add3..cd415407e 100644 --- a/apps/client/src/features/operator/operator-event/OperatorEvent.tsx +++ b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx @@ -1,11 +1,12 @@ import { memo, RefObject, SyntheticEvent } from 'react'; import { useLongPress } from '@mantine/hooks'; -import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString } from 'ontime-utils'; +import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; -import { formatDuration, useTimeUntilExpectedStart } from '../../../common/utils/time'; +import { formatDuration, formatTime, useTimeUntilExpectedStart } from '../../../common/utils/time'; import RunningTime from '../../viewers/common/running-time/RunningTime'; +import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod'; import type { EditEvent, Subscribed } from '../operator.types'; import style from './OperatorEvent.module.scss'; @@ -64,22 +65,24 @@ function OperatorEvent({ const mouseHandlers = useLongPress(handleLongPress); const cueColours = colour && getAccessibleColour(colour); - const operatorClasses = cx([ - style.event, - isSelected && style.running, - isPast && style.past, - ]); + const operatorClasses = cx([style.event, isSelected && style.running, isPast && style.past]); return ( -
+
{cue}
- {showStart && {millisToString(timeStart)}} + {showStart && } {main} - + {secondary} 2 * MILLIS_PER_MINUTE)}`; - return {timeUntilString}; + return ( + + {timeUntilString} + + ); } diff --git a/apps/client/src/features/operator/status-bar/StatusBarTimers.tsx b/apps/client/src/features/operator/status-bar/StatusBarTimers.tsx index c666aea28..c949ed939 100644 --- a/apps/client/src/features/operator/status-bar/StatusBarTimers.tsx +++ b/apps/client/src/features/operator/status-bar/StatusBarTimers.tsx @@ -6,7 +6,7 @@ export default function StatusBarTimers() { return (
- +
); } diff --git a/apps/client/src/features/overview/CuesheetOverview.tsx b/apps/client/src/features/overview/CuesheetOverview.tsx index dca456e39..85c09f2c5 100644 --- a/apps/client/src/features/overview/CuesheetOverview.tsx +++ b/apps/client/src/features/overview/CuesheetOverview.tsx @@ -27,11 +27,11 @@ function CuesheetDesktop({ children }: PropsWithChildren) { return ( - + - + ); } diff --git a/apps/client/src/features/overview/Overview.module.scss b/apps/client/src/features/overview/Overview.module.scss index 92dfb13df..85b42fb61 100644 --- a/apps/client/src/features/overview/Overview.module.scss +++ b/apps/client/src/features/overview/Overview.module.scss @@ -34,4 +34,5 @@ display: flex; align-items: center; justify-content: space-between; + overflow-x: auto; } diff --git a/apps/client/src/features/overview/composite/TimeElements.tsx b/apps/client/src/features/overview/composite/TimeElements.tsx index ca59accaf..19d48e8b7 100644 --- a/apps/client/src/features/overview/composite/TimeElements.tsx +++ b/apps/client/src/features/overview/composite/TimeElements.tsx @@ -20,6 +20,7 @@ import { useNextFlag, useOffsetOverview, useProgressOverview, + useRundownExpectedEnd, useStartTimesOverview, useTimer, } from '../../../common/hooks/useSocket'; @@ -27,69 +28,136 @@ import { useEntry } from '../../../common/hooks-query/useRundown'; import { getOffsetState, getOffsetText } from '../../../common/utils/offset'; import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils'; import { formatTime } from '../../../common/utils/time'; -import { calculateEndAndDaySpan, formatDueTime, formattedTime } from '../overview.utils'; +import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod'; +import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils'; -import { OverUnder, TimeColumn } from './TimeLayout'; +import { OverUnder, TimeColumn, WrappedInTimeColumn } from './TimeLayout'; import style from './TimeElements.module.scss'; -export function StartTimes() { - const { plannedEnd, plannedStart, actualStart, expectedEnd } = useStartTimesOverview(); +interface OverviewTimeElementsProps { + shouldFormat?: boolean; +} - const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart); +export function StartTimes({ shouldFormat }: OverviewTimeElementsProps) { + const { plannedEnd, plannedStart, actualStart } = useStartTimesOverview(); + + const formatOptions = { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' }; + + const plannedStartText = (() => { + if (plannedStart === null) return timerPlaceholder; + if (shouldFormat) return formatTime(plannedStart, formatOptions); + return millisToString(plannedStart, { fallback: timerPlaceholder }); + })(); + + const actualStartText = (() => { + if (actualStart === null) return timerPlaceholder; + if (shouldFormat) return formatTime(actualStart, formatOptions); + return millisToString(actualStart, { fallback: timerPlaceholder }); + })(); const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]); - const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]); - const plannedEndText = maybePlannedEnd === null ? timerPlaceholder : formatTime(maybePlannedEnd); + + const plannedEndText = (() => { + if (maybePlannedEnd === null) return timerPlaceholder; + if (shouldFormat) return formatTime(maybePlannedEnd, formatOptions); + return millisToString(maybePlannedEnd, { fallback: timerPlaceholder }); + })(); + + const multipleDays = maybePlannedDaySpan > 0; + const plannedEndTooltip = multipleDays + ? `Planned end time (rundown spans over ${maybePlannedDaySpan + 1} days)` + : 'Planned end time'; return (
Start -
- } /> - {plannedStartText} -
-
- } /> - {formattedTime(actualStart)} -
+ + + +
+ } + /> + + + +
+ } + />
+
End -
- } /> - {maybePlannedDaySpan > 0 ? ( - } - > - {plannedEndText} - - ) : ( - {plannedEndText} - )} -
-
- } /> - {maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? ( - } - > - {formattedTime(maybeExpectedEnd)} - - ) : ( - - {formattedTime(maybeExpectedEnd)} - - )} -
+ + + + {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 = (() => { + if (maybeExpectedEnd === null) return timerPlaceholder; + if (shouldFormat) return formatTime(maybeExpectedEnd, { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' }); + return millisToString(maybeExpectedEnd, { fallback: timerPlaceholder }); + })(); + + 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 (
@@ -229,11 +297,17 @@ export function OffsetOverview() { return ; } -export function ClockOverview({ className }: { className?: string }) { +export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) { const { clock } = useClock(); - const formattedClock = formatTime(clock); + const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock); - return ; + return ( + } + /> + ); } export function TimerOverview({ className }: { className?: string }) { diff --git a/apps/client/src/features/overview/composite/TimeLayout.tsx b/apps/client/src/features/overview/composite/TimeLayout.tsx index 501c26ebf..1ff986ccc 100644 --- a/apps/client/src/features/overview/composite/TimeLayout.tsx +++ b/apps/client/src/features/overview/composite/TimeLayout.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react'; + import { cx } from '../../../common/utils/styleUtils'; import style from './TimeLayout.module.scss'; @@ -22,6 +24,21 @@ export function TimeColumn({ label, value, state = 'active', className, testId } ); } +interface WrappedInTimeColumnProps { + label: string; + state?: 'muted' | 'waiting' | 'active'; + className?: string; + render: (className: string) => ReactNode; +} + +export function WrappedInTimeColumn({ label, state = 'active', className, render }: WrappedInTimeColumnProps) { + return ( +
+ {label} + {render(style.clock)} +
+ ); +} interface OverUnderProps { state: 'over' | 'under' | 'muted' | null; value: string; diff --git a/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx b/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx index 0a055c86d..cad96468b 100644 --- a/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/GroupEditor.tsx @@ -59,9 +59,6 @@ export default function GroupEditor({ group }: GroupEditorProps) { Group schedule
- { - // TODO: format with user time settings - } First event start {millisToString(group.timeStart, { fallback: timerPlaceholder })} diff --git a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx index 199e6eb44..fab8562c9 100644 --- a/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx +++ b/apps/client/src/features/rundown/rundown-group/RundownGroup.tsx @@ -10,14 +10,14 @@ import { import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { EntryId, OntimeGroup } from 'ontime-types'; -import { MILLIS_PER_MINUTE } from 'ontime-utils'; +import { MILLIS_PER_MINUTE, millisToString } from 'ontime-utils'; import IconButton from '../../../common/components/buttons/IconButton'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { getOffsetState } from '../../../common/utils/offset'; -import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; -import { formatDuration, formatTime } from '../../../common/utils/time'; +import { cx, getAccessibleColour, timerPlaceholder } from '../../../common/utils/styleUtils'; +import { formatDuration } from '../../../common/utils/time'; import TitleEditor from '../common/TitleEditor'; import { canDrop } from '../rundown.utils'; import { useEventSelection } from '../useEventSelection'; @@ -149,11 +149,11 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
Start
-
{formatTime(data.timeStart)}
+
{millisToString(data.timeStart, { fallback: timerPlaceholder })}
End
-
{formatTime(data.timeEnd)}
+
{millisToString(data.timeEnd, { fallback: timerPlaceholder })}
Duration
diff --git a/apps/client/src/features/viewers/common/superscript-time/SuperscriptPeriod.tsx b/apps/client/src/features/viewers/common/superscript-time/SuperscriptPeriod.tsx new file mode 100644 index 000000000..2b3e66f1d --- /dev/null +++ b/apps/client/src/features/viewers/common/superscript-time/SuperscriptPeriod.tsx @@ -0,0 +1,23 @@ +import './SuperscriptTime.scss'; + +interface SuperscriptPeriodProps { + time: string; + className?: string; +} + +/** + * Receives a time string and formats periods (am/pm) as superscript + * @example 12:00 AM -> AM becomes a superscript + * @example 12:00:10 -> no formatting changes applied + */ +export default function SuperscriptPeriod({ time, className }: SuperscriptPeriodProps) { + // we assume anything after space is a period tag + const [timeString, period] = time.split(' '); + + return ( +
+ {timeString} + {period && {period}} +
+ ); +} diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 0ef444ecb..8ad0daf43 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -76,13 +76,13 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }: const scheduledStart = (() => { if (showNow) return undefined; if (!hasEvents) return undefined; - return formatTime(rundown.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' }); + return formatTime(rundown.plannedStart, { format12: 'h:mm a', format24: 'HH:mm' }); })(); const scheduledEnd = (() => { if (showNow) return undefined; if (!hasEvents) return undefined; - return formatTime(rundown.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' }); + return formatTime(rundown.plannedEnd, { format12: 'h:mm a', format24: 'HH:mm' }); })(); let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); diff --git a/apps/client/src/views/common/schedule/ScheduleItem.tsx b/apps/client/src/views/common/schedule/ScheduleItem.tsx index f04cbdb78..16e190e63 100644 --- a/apps/client/src/views/common/schedule/ScheduleItem.tsx +++ b/apps/client/src/views/common/schedule/ScheduleItem.tsx @@ -5,14 +5,14 @@ import { getOffsetState } from '../../../common/utils/offset'; import { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import { cx } from '../../../common/utils/styleUtils'; import { formatTime, getExpectedTimesFromExtendedEvent } from '../../../common/utils/time'; -import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; +import SuperscriptPeriod from '../../../features/viewers/common/superscript-time/SuperscriptPeriod'; import { useScheduleOptions } from './schedule.options'; import './Schedule.scss'; const formatOptions = { - format12: 'hh:mm a', + format12: 'h:mm a', format24: 'HH:mm', }; @@ -84,9 +84,9 @@ function PlannedScheduleItem({ return ( <> - + → - + ); } @@ -106,14 +106,14 @@ function DelayedScheduleItem({ <> - + → - + - + → - + ); @@ -161,5 +161,5 @@ interface ExpectedTimeProps { function ExpectedTime({ expectedTime, plannedTime }: ExpectedTimeProps) { const timeDisplay = formatTime(expectedTime); const expectedState = getOffsetState(expectedTime - plannedTime); - return ; + return ; } diff --git a/apps/client/src/views/countdown/CountdownSelect.tsx b/apps/client/src/views/countdown/CountdownSelect.tsx index f060e73bb..68bed2d65 100644 --- a/apps/client/src/views/countdown/CountdownSelect.tsx +++ b/apps/client/src/views/countdown/CountdownSelect.tsx @@ -73,9 +73,9 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }: >
- + → - +
{isSelected ? 'Click to remove' : 'Click to add'}
{title}
diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx index db9d3e4e0..75598c561 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx @@ -23,7 +23,7 @@ function MakeStart({ getValue, row, table, column }: CellContext
Over / under
-
- {schedule.offset} -
+
{schedule.offset}
{getLocalizedString('common.expected_end')}
diff --git a/apps/client/src/views/timeline/TimelineEntry.tsx b/apps/client/src/views/timeline/TimelineEntry.tsx index c7f5b3578..52b0d9931 100644 --- a/apps/client/src/views/timeline/TimelineEntry.tsx +++ b/apps/client/src/views/timeline/TimelineEntry.tsx @@ -30,7 +30,7 @@ interface TimelineEntryProps { } const formatOptions = { - format12: 'hh:mm a', + format12: 'h:mm a', format24: 'HH:mm', };