fix: format clock from preset (#2066)

* fix: format clock from preset

* fix: add to all views

* chore: format

* refactor: return flat value from common.options.ts
This commit is contained in:
Alex Christoffer Rasmussen
2026-05-03 18:43:58 +02:00
committed by GitHub
parent 1a08b39b8b
commit a5e733246d
14 changed files with 84 additions and 37 deletions
@@ -26,3 +26,14 @@ export const showLeadingZeros: ParamField = {
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}; };
export type TimeOptions = {
timeformat: string | null;
};
/**
* Helper to get value of 'timeformat' from either source, prioritizing defaultValues
*/
export function getTimeOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
return defaultValues?.get('timeformat') ?? searchParams.get('timeformat');
}
+5 -4
View File
@@ -1,4 +1,4 @@
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types'; import { MaybeNumber, MaybeString, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
import { import {
MILLIS_PER_HOUR, MILLIS_PER_HOUR,
MILLIS_PER_MINUTE, MILLIS_PER_MINUTE,
@@ -75,8 +75,9 @@ function resolveTimeFormat(fallback12: string, fallback24: string): string {
} }
type FormatOptions = { type FormatOptions = {
format12: string; format12?: string;
format24: string; format24?: string;
override?: MaybeString;
}; };
/** /**
@@ -97,7 +98,7 @@ export const formatTime = (
return '...'; return '...';
} }
const timeFormat = resolver(options?.format12 ?? FORMAT_12, options?.format24 ?? FORMAT_24); const timeFormat = options?.override ?? resolver(options?.format12 ?? FORMAT_12, options?.format24 ?? FORMAT_24);
const display = formatFromMillis(Math.abs(milliseconds), timeFormat); const display = formatFromMillis(Math.abs(milliseconds), timeFormat);
const isNegative = milliseconds < 0; const isNegative = milliseconds < 0;
+13 -8
View File
@@ -43,7 +43,7 @@ export default function BackstageLoader() {
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) { function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const { mainSource, secondarySource, extraInfo } = useBackstageOptions(); const { mainSource, secondarySource, extraInfo, timeformat } = useBackstageOptions();
const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket(); const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket();
const [blinkClass, setBlinkClass] = useState(false); const [blinkClass, setBlinkClass] = useState(false);
const { height: screenHeight } = useViewportSize(); const { height: screenHeight } = useViewportSize();
@@ -71,18 +71,20 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
// gather timer data // gather timer data
const isPendingStart = getIsPendingStart(time.playback, time.phase); const isPendingStart = getIsPendingStart(time.playback, time.phase);
const startedAt = isPendingStart ? formatTime(time.secondaryTimer) : formatTime(time.startedAt); const startedAt = isPendingStart
? formatTime(time.secondaryTimer, { override: timeformat })
: formatTime(time.startedAt, { override: timeformat });
const scheduledStart = (() => { const scheduledStart = (() => {
if (showNow) return undefined; if (showNow) return undefined;
if (!hasEvents) return undefined; if (!hasEvents) return undefined;
return formatTime(rundown.plannedStart, { format12: 'h:mm a', format24: 'HH:mm' }); return formatTime(rundown.plannedStart, { format12: 'h:mm a', format24: 'HH:mm', override: timeformat });
})(); })();
const scheduledEnd = (() => { const scheduledEnd = (() => {
if (showNow) return undefined; if (showNow) return undefined;
if (!hasEvents) return undefined; if (!hasEvents) return undefined;
return formatTime(rundown.plannedEnd, { format12: 'h:mm a', format24: 'HH:mm' }); return formatTime(rundown.plannedEnd, { format12: 'h:mm a', format24: 'HH:mm', override: timeformat });
})(); })();
let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin });
@@ -107,7 +109,7 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
<div className='project-header'> <div className='project-header'>
{projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />} {projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />}
<div className='title'>{projectData.title}</div> <div className='title'>{projectData.title}</div>
<BackstageClock /> <BackstageClock timeformat={timeformat} />
</div> </div>
{showProgress && <ProgressBar className='progress-container' current={time.current} duration={time.duration} />} {showProgress && <ProgressBar className='progress-container' current={time.current} duration={time.duration} />}
@@ -131,7 +133,10 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
{isOvertime(time.current) ? ( {isOvertime(time.current) ? (
<div className='time-entry__value'>{getLocalizedString('countdown.overtime')}</div> <div className='time-entry__value'>{getLocalizedString('countdown.overtime')}</div>
) : ( ) : (
<SuperscriptTime time={formatTime(time.expectedFinish)} className='time-entry__value' /> <SuperscriptTime
time={formatTime(time.expectedFinish, { override: timeformat })}
className='time-entry__value'
/>
)} )}
</div> </div>
<div className='timer-gap' /> <div className='timer-gap' />
@@ -213,12 +218,12 @@ function ExtraInfo({ projectData, size, source }: ExtraInfoProps) {
); );
} }
function BackstageClock() { function BackstageClock({ timeformat }: { timeformat: string | null }) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const clock = useAutoTickingClock(); const clock = useAutoTickingClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock, { override: timeformat });
return ( return (
<div className='clock-container'> <div className='clock-container'>
@@ -2,7 +2,11 @@ import { CustomFields, OntimeEvent, ProjectData } from 'ontime-types';
import { use, useMemo } from 'react'; import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import {
getTimeOption,
getTimeOptionsFromParams,
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { import {
@@ -71,7 +75,7 @@ type BackstageOptions = {
mainSource: keyof OntimeEvent | null; mainSource: keyof OntimeEvent | null;
secondarySource: keyof OntimeEvent | null; secondarySource: keyof OntimeEvent | null;
extraInfo: string | null; extraInfo: string | null;
}; } & TimeOptions;
/** /**
* Utility extract the view options from URL Params * Utility extract the view options from URL Params
@@ -85,6 +89,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
mainSource: getValue('main') as keyof OntimeEvent | null, mainSource: getValue('main') as keyof OntimeEvent | null,
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null, secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
extraInfo: getValue('extra-info'), extraInfo: getValue('extra-info'),
timeformat: getTimeOptionsFromParams(searchParams, defaultValues),
}; };
} }
+2 -1
View File
@@ -101,6 +101,7 @@ export function getPropertyValue(
type FormattingOptions = { type FormattingOptions = {
removeSeconds: boolean; removeSeconds: boolean;
removeLeadingZero: boolean; removeLeadingZero: boolean;
clockFormat?: MaybeString;
}; };
export function getFormattedTimer( export function getFormattedTimer(
@@ -114,7 +115,7 @@ export function getFormattedTimer(
} }
if (timerType === TimerType.Clock) { if (timerType === TimerType.Clock) {
return formatTime(timer); return formatTime(timer, { override: options?.clockFormat });
} }
let timeToParse = timer; let timeToParse = timer;
@@ -139,11 +139,12 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun
} }
function CountdownClock() { function CountdownClock() {
const { timeformat } = useCountdownOptions();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const clock = useAutoTickingClock(); const clock = useAutoTickingClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock, { override: timeformat });
return ( return (
<div className='clock-container'> <div className='clock-container'>
@@ -2,7 +2,11 @@ import { CustomFields, EntryId, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react'; import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import {
getTimeOption,
getTimeOptionsFromParams,
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
@@ -91,7 +95,7 @@ type CountdownOptions = {
secondarySource: keyof OntimeEvent | null; secondarySource: keyof OntimeEvent | null;
showExpected: boolean; showExpected: boolean;
hidePast: boolean; hidePast: boolean;
}; } & TimeOptions;
/** /**
* Utility extract the view options from URL Params * Utility extract the view options from URL Params
@@ -115,6 +119,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null, secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
showExpected: isStringBoolean(getValue('showExpected')), showExpected: isStringBoolean(getValue('showExpected')),
hidePast: isStringBoolean(getValue('hidePast')), hidePast: isStringBoolean(getValue('hidePast')),
timeformat: getTimeOptionsFromParams(searchParams, defaultValues),
}; };
} }
+7 -4
View File
@@ -6,6 +6,7 @@ import { useStudioClockSocket } from '../../common/hooks/useSocket';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
import { formatTime } from '../../common/utils/time'; import { formatTime } from '../../common/utils/time';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import { useStudioOptions } from './studio.options';
import { getLargeClockData } from './studioClock.utils'; import { getLargeClockData } from './studioClock.utils';
import './StudioClock.scss'; import './StudioClock.scss';
@@ -18,6 +19,7 @@ interface StudioClockProps {
} }
export default function StudioClock({ hideCards }: StudioClockProps) { export default function StudioClock({ hideCards }: StudioClockProps) {
const { timeformat } = useStudioOptions();
const isSmallScreen = useIsSmallScreen(); const isSmallScreen = useIsSmallScreen();
const clock = useAutoTickingClock(); const clock = useAutoTickingClock();
const { playback } = useStudioClockSocket(); const { playback } = useStudioClockSocket();
@@ -25,10 +27,10 @@ export default function StudioClock({ hideCards }: StudioClockProps) {
// if we are on mobile and have to show the cards // if we are on mobile and have to show the cards
if (isSmallScreen && !hideCards) { if (isSmallScreen && !hideCards) {
return <StudioClockMobile clock={clock} onAir={onAir} />; return <StudioClockMobile clock={clock} onAir={onAir} timeformat={timeformat} />;
} }
const { seconds, display, meridian } = getLargeClockData(clock); const { seconds, display, meridian } = getLargeClockData(clock, timeformat);
return ( return (
<div className='studio__clock'> <div className='studio__clock'>
@@ -62,10 +64,11 @@ export default function StudioClock({ hideCards }: StudioClockProps) {
interface StudioClockMobileProps { interface StudioClockMobileProps {
clock: number; clock: number;
onAir: boolean; onAir: boolean;
timeformat: string | null;
} }
function StudioClockMobile({ clock, onAir }: StudioClockMobileProps) { function StudioClockMobile({ clock, onAir, timeformat }: StudioClockMobileProps) {
const displayClock = formatTime(clock); const displayClock = formatTime(clock, { override: timeformat });
return ( return (
<div className='studio__clock studio__clock--small'> <div className='studio__clock studio__clock--small'>
@@ -2,7 +2,11 @@ import { CustomFields, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react'; import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import {
getTimeOption,
getTimeOptionsFromParams,
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
@@ -47,7 +51,7 @@ export const getStudioOptions = (timeFormat: string, customFields: CustomFields)
type StudioOptions = { type StudioOptions = {
mainSource: keyof OntimeEvent | null; mainSource: keyof OntimeEvent | null;
hideCards: boolean; hideCards: boolean;
}; } & TimeOptions;
/** /**
* Utility extract the view options from URL Params * Utility extract the view options from URL Params
@@ -60,6 +64,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
return { return {
mainSource: getValue('main') as keyof OntimeEvent | null, mainSource: getValue('main') as keyof OntimeEvent | null,
hideCards: isStringBoolean(getValue('hideCards')), hideCards: isStringBoolean(getValue('hideCards')),
timeformat: getTimeOptionsFromParams(searchParams, defaultValues),
}; };
} }
@@ -5,9 +5,9 @@ import { formatTime } from '../../common/utils/time';
/** /**
* Gathers display elements for the large studio clock * Gathers display elements for the large studio clock
*/ */
export function getLargeClockData(clock: number) { export function getLargeClockData(clock: number, timeformat: string | null) {
const [display, meridian] = (() => { const [display, meridian] = (() => {
const formatted = formatTime(clock); const formatted = formatTime(clock, { override: timeformat });
if (formatted.endsWith('AM')) { if (formatted.endsWith('AM')) {
return [formatted.slice(0, -2), 'AM']; return [formatted.slice(0, -2), 'AM'];
} }
@@ -37,7 +37,7 @@ export default function TimelinePageLoader() {
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) { function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
const selectedEventId = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const { mainSource } = useTimelineOptions(); const { mainSource, timeformat } = useTimelineOptions();
// holds copy of the rundown with only relevant events // holds copy of the rundown with only relevant events
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId); const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
@@ -56,7 +56,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
<div className='project-header'> <div className='project-header'>
{projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />} {projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />}
<div className='title'>{projectData.title}</div> <div className='title'>{projectData.title}</div>
<TimelineClock /> <TimelineClock timeformat={timeformat} />
</div> </div>
<TimelineSections now={now} next={next} followedBy={followedBy} mainSource={mainSource} /> <TimelineSections now={now} next={next} followedBy={followedBy} mainSource={mainSource} />
@@ -71,12 +71,12 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
); );
} }
function TimelineClock() { function TimelineClock({ timeformat }: { timeformat: string | null }) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const clock = useAutoTickingClock(); const clock = useAutoTickingClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock, { override: timeformat });
return ( return (
<div className='clock-container'> <div className='clock-container'>
@@ -2,7 +2,11 @@ import { CustomFields, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react'; import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import {
getTimeOption,
getTimeOptionsFromParams,
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
@@ -55,7 +59,7 @@ type TimelineOptions = {
mainSource: keyof OntimeEvent | null; mainSource: keyof OntimeEvent | null;
hidePast: boolean; hidePast: boolean;
fixedSize: boolean; fixedSize: boolean;
}; } & TimeOptions;
/** /**
* Utility extract the view options from URL Params * Utility extract the view options from URL Params
@@ -69,6 +73,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
mainSource: getValue('main') as keyof OntimeEvent | null, mainSource: getValue('main') as keyof OntimeEvent | null,
hidePast: isStringBoolean(getValue('hidePast')), hidePast: isStringBoolean(getValue('hidePast')),
fixedSize: isStringBoolean(getValue('fixedSize')), fixedSize: isStringBoolean(getValue('fixedSize')),
timeformat: getTimeOptionsFromParams(searchParams, defaultValues),
}; };
} }
+6 -4
View File
@@ -1,4 +1,4 @@
import { OntimeView, TimerType } from 'ontime-types'; import { MaybeString, OntimeView, TimerType } from 'ontime-types';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FitText } from '../../common/components/fit-text/FitText'; import { FitText } from '../../common/components/fit-text/FitText';
@@ -69,6 +69,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
font, font,
keyColour, keyColour,
timerColour, timerColour,
timeformat,
} = useTimerOptions(); } = useTimerOptions();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
@@ -106,6 +107,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, { const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
removeSeconds: hideTimerSeconds, removeSeconds: hideTimerSeconds,
removeLeadingZero: removeLeadingZeros, removeLeadingZero: removeLeadingZeros,
clockFormat: timeformat,
}); });
const currentAux = (() => { const currentAux = (() => {
@@ -164,7 +166,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
</div> </div>
)} )}
{showClock && <TimerAutoTickingClock />} {showClock && <TimerAutoTickingClock clockFormat={timeformat} />}
<div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}> <div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}>
{showEndMessage ? ( {showEndMessage ? (
@@ -212,9 +214,9 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
); );
} }
function TimerAutoTickingClock() { function TimerAutoTickingClock({ clockFormat }: { clockFormat: MaybeString }) {
const autoTickingClock = useAutoTickingClock(); const autoTickingClock = useAutoTickingClock();
const formattedClock = formatTime(autoTickingClock); const formattedClock = formatTime(autoTickingClock, { override: clockFormat });
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
return ( return (
+4 -1
View File
@@ -6,8 +6,10 @@ import { useSearchParams } from 'react-router';
import type { SelectOption } from '../../common/components/select/Select'; import type { SelectOption } from '../../common/components/select/Select';
import { import {
getTimeOption, getTimeOption,
getTimeOptionsFromParams,
hideTimerSeconds, hideTimerSeconds,
showLeadingZeros, showLeadingZeros,
TimeOptions,
} from '../../common/components/view-params-editor/common.options'; } from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
@@ -194,7 +196,7 @@ type TimerOptions = {
font?: string; font?: string;
keyColour?: string; keyColour?: string;
timerColour?: string; timerColour?: string;
}; } & TimeOptions;
/** /**
* Utility extract the view options from URL Params * Utility extract the view options from URL Params
@@ -229,6 +231,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
font: getValue('font') ?? undefined, font: getValue('font') ?? undefined,
keyColour: makeColourString(getValue('keyColour')), keyColour: makeColourString(getValue('keyColour')),
timerColour: makeColourString(getValue('timerColour')), timerColour: makeColourString(getValue('timerColour')),
timeformat: getTimeOptionsFromParams(searchParams, defaultValues),
}; };
} }