diff --git a/apps/client/src/common/components/view-params-editor/constants.ts b/apps/client/src/common/components/view-params-editor/constants.ts index 5f35063ba..a5023ccaa 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -508,4 +508,14 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin ]; }; -export const getCountdownOptions = (timeFormat: string): ParamField[] => [getTimeOption(timeFormat), hideTimerSeconds]; +export const getCountdownOptions = (timeFormat: string): ParamField[] => [ + getTimeOption(timeFormat), + hideTimerSeconds, + { + id: 'showProjected', + title: 'Show projected time', + description: 'Whether to show the projected delay of an event (taken from runtime offset).', + type: 'boolean', + defaultValue: false, + }, +]; diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index d195129f6..63b502329 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -5,6 +5,7 @@ import { Message, OntimeEvent, ProjectData, + Runtime, Settings, SupportedEvent, TimerMessage, @@ -36,6 +37,7 @@ type WithDataProps = { publicEventNext: OntimeEvent | null; publicEventNow: OntimeEvent | null; publicSelectedId: string | null; + runtime: Runtime; selectedId: string | null; settings: Settings | undefined; time: ViewExtendedTimer; @@ -66,7 +68,7 @@ const withData =

(Component: ComponentType

) => { }, [rundownData]); // websocket data - const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow } = + const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime } = useStore(runtimeStore); const publicSelectedId = publicEventNow?.id ?? null; const selectedId = eventNow?.id ?? null; @@ -108,6 +110,7 @@ const withData =

(Component: ComponentType

) => { publicEventNext={publicEventNext} publicEventNow={publicEventNow} publicSelectedId={publicSelectedId} + runtime={runtime} selectedId={selectedId} settings={settings} time={TimeManagerType} diff --git a/apps/client/src/features/viewers/countdown/Countdown.scss b/apps/client/src/features/viewers/countdown/Countdown.scss index c2c33a57b..22489b1b2 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.scss +++ b/apps/client/src/features/viewers/countdown/Countdown.scss @@ -21,11 +21,11 @@ flex-direction: column; &__title { - font-size: clamp(24px, 2vw, 32px); + font-size: clamp(1.5rem, 2vw, 2rem); } &__events { - font-size: clamp(16px, 1.5vw, 24px); + font-size: clamp(1rem, 1.5vw, 1.5rem); margin-top: 1em; overflow-y: auto; height: 70vh; @@ -38,18 +38,18 @@ .countdown-container { height: 100%; width: 100%; - gap: min(2vh, 16px); - padding: min(2vh, 16px) clamp(16px, 10vw, 64px); + gap: min(2vh, 1rem); + padding: min(2vh, 1rem) clamp(1rem, 10vw, 4rem); display: grid; grid-template-rows: auto auto auto auto 1fr; grid-template-columns: 100%; grid-template-areas: - 'header' - 'status' - 'clock' - 'title' - 'timers'; + 'header' + 'status' + 'clock' + 'title' + 'timers'; /* =================== HEADER + EXTRAS ===================*/ @@ -59,13 +59,13 @@ font-weight: 600; .label { - font-size: clamp(16px, 1.5vw, 24px); + font-size: $timer-label-size; color: var(--label-color-override, $viewer-label-color); text-transform: uppercase; } .time { - font-size: clamp(32px, 3.5vw, 50px); + font-size: $timer-value-size; color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; line-height: 0.95em; @@ -75,7 +75,7 @@ .status { grid-area: status; color: var(--label-color-override, $viewer-label-color); - font-size: clamp(32px, 3.5vw, 50px); + font-size: clamp(2rem, 3.5vw, 3.5rem); font-weight: 600; } @@ -105,7 +105,7 @@ .title { grid-area: title; background-color: var(--card-background-color-override, $viewer-card-bg-color); - padding: 16px 24px; + padding: 1rem 1.5rem; border-radius: 8px; font-weight: 600; @@ -119,28 +119,49 @@ .timer-group { grid-area: timers; - display: flex; - justify-content: space-evenly; - align-items: flex-end; + display: grid; + grid-template-areas: + 'projected-start projected-end' + 'scheduled-start scheduled-end'; + grid-template-columns: 1fr 1fr; + justify-items: center; + text-align: center; + row-gap: clamp(1rem, 5vh, 4rem); + + align-self: flex-end; + height: fit-content; - .aux-timers { - text-align: center; - font-size: clamp(24px, 1.75vw, 32px); + &__projected-start { + grid-area: projected-start; + } - &__label { - color: var(--label-color-override, $viewer-label-color); - text-transform: uppercase; - } + &__projected-end { + grid-area: projected-end; + } - &__value { - font-size: clamp(32px, 3.5vw, 50px); - color: var(--secondary-color-override, $viewer-secondary-color); - letter-spacing: 0.05em; - line-height: 0.95em; + &__scheduled-start { + grid-area: scheduled-start; + } - &--delayed { - color: $delay-color; - } + &__scheduled-end { + grid-area: scheduled-end; + } + + &__label { + font-size: $timer-label-size; + color: var(--label-color-override, $viewer-label-color); + text-transform: uppercase; + } + + &__value { + margin-top: 0.5rem; + font-size: $timer-value-size; + color: var(--secondary-color-override, $viewer-secondary-color); + letter-spacing: 0.05em; + line-height: 0.95em; + + &--delayed { + color: $delay-color; } } } diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index b67acdfce..cf68188fa 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -1,6 +1,14 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types'; +import { + OntimeEvent, + OntimeRundownEntry, + Playback, + Runtime, + Settings, + SupportedEvent, + ViewSettings, +} from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/constants'; import { getCountdownOptions } from '../../../common/components/view-params-editor/constants'; @@ -13,7 +21,7 @@ import { useTranslation } from '../../../translation/TranslationProvider'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getFormattedTimer, isStringBoolean } from '../common/viewUtils'; -import { fetchTimerData, TimerMessage } from './countdown.helpers'; +import { fetchTimerData, getTimerItems, TimerMessage } from './countdown.helpers'; import CountdownSelect from './CountdownSelect'; import './Countdown.scss'; @@ -21,14 +29,15 @@ import './Countdown.scss'; interface CountdownProps { isMirrored: boolean; backstageEvents: OntimeEvent[]; - time: ViewExtendedTimer; + runtime: Runtime; selectedId: string | null; - viewSettings: ViewSettings; settings: Settings | undefined; + time: ViewExtendedTimer; + viewSettings: ViewSettings; } export default function Countdown(props: CountdownProps) { - const { isMirrored, backstageEvents, time, selectedId, viewSettings, settings } = props; + const { isMirrored, backstageEvents, runtime, selectedId, settings, time, viewSettings } = props; const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const [searchParams] = useSearchParams(); const { getLocalizedString } = useTranslation(); @@ -71,10 +80,10 @@ export default function Countdown(props: CountdownProps) { return; } - const { message, timer } = fetchTimerData(time, follow, selectedId); + const { message, timer } = fetchTimerData(time, follow, selectedId, runtime.offset); setRunningMessage(message); setRunningTimer(timer); - }, [follow, selectedId, time]); + }, [follow, selectedId, time, runtime.offset]); // defer rendering until we load stylesheets if (!shouldRender) { @@ -87,8 +96,12 @@ export default function Countdown(props: CountdownProps) { const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : ''; const clock = formatTime(time.clock); - const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay); - const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay); + const { scheduledStart, scheduledEnd, projectedStart, projectedEnd } = getTimerItems( + follow?.timeStart, + follow?.timeEnd, + delay, + runtime.offset, + ); const hideSeconds = searchParams.get('hideTimerSeconds'); const formattedTimer = getFormattedTimer(runningTimer, time.timerType, getLocalizedString('common.minutes'), { @@ -122,13 +135,25 @@ export default function Countdown(props: CountdownProps) { {follow?.title &&

{follow.title}
}
-
-
{getLocalizedString('common.start_time')}
- + {projectedStart && projectedEnd && ( +
+
{getLocalizedString('common.projected_start')}
+ +
+ )} + {projectedStart && projectedEnd && ( +
+
{getLocalizedString('common.projected_end')}
+ +
+ )} +
+
{getLocalizedString('common.scheduled_start')}
+
-
-
{getLocalizedString('common.end_time')}
- +
+
{getLocalizedString('common.scheduled_end')}
+
diff --git a/apps/client/src/features/viewers/countdown/countdown.helpers.ts b/apps/client/src/features/viewers/countdown/countdown.helpers.ts index 19e20c9f9..92d166399 100644 --- a/apps/client/src/features/viewers/countdown/countdown.helpers.ts +++ b/apps/client/src/features/viewers/countdown/countdown.helpers.ts @@ -1,6 +1,8 @@ import { OntimeEvent, Playback } from 'ontime-types'; import { ViewExtendedTimer } from '../../../common/models/TimeManager.type'; +import { formatTime } from '../../../common/utils/time'; +import { isStringBoolean } from '../common/viewUtils'; export enum TimerMessage { toStart = 'to_start', @@ -22,46 +24,79 @@ export const fetchTimerData = ( time: ViewExtendedTimer, follow: OntimeEvent, selectedId: string | null, + offset: number, ): { message: TimerMessage; timer: number } => { - let message; - let timer; - if (selectedId === follow.id) { - // check that is not running - message = time.playback === Playback.Pause ? TimerMessage.waiting : TimerMessage.running; - timer = time.current ?? 0; - } else if (time.clock < follow.timeStart) { - // if it hasnt started, we count to start - message = TimerMessage.toStart; - timer = follow.timeStart - time.clock; - } else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) { - // if it has started, we show running timer - message = TimerMessage.waiting; - timer = time.current ?? 0; - } else { - // running timer timer is not the one we are following - - if (follow.timeStart > follow.timeEnd) { - // ends day after - - if (follow.timeStart > time.clock) { - // if it hasnt started, we count to start - message = TimerMessage.toStart; - timer = follow.timeStart - time.clock; - } else if (follow.timeStart <= time.clock) { - // if it has started, we show running timer - message = TimerMessage.waiting; - timer = time.current ?? 0; - } else { - // if it has ended, we show how long ago - message = TimerMessage.ended; - timer = follow.timeEnd; - } - } else { - // if it has ended, we show how long ago - message = TimerMessage.ended; - timer = follow.timeEnd; - } + // if it is selected, it may not be running + return { + message: time.playback === Playback.Pause ? TimerMessage.waiting : TimerMessage.running, + timer: time.current ?? 0, + }; } - return { message, timer }; + + const showProjected = getShouldShowProjected(); + const addedTime = showProjected ? offset : 0; + if (time.clock < follow.timeStart) { + // if it hasnt started, we count to start + return { message: TimerMessage.toStart, timer: follow.timeStart - time.clock - addedTime }; + } + + if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) { + // if it has started, we show running timer + return { message: TimerMessage.waiting, timer: time.current ?? 0 }; + } + + // running timer timer is not the one we are following + + // ends day after + if (follow.timeStart > follow.timeEnd) { + if (follow.timeStart > time.clock) { + // if it hasnt started, we count to start + return { message: TimerMessage.toStart, timer: follow.timeStart - time.clock - addedTime }; + } + if (follow.timeStart <= time.clock) { + // if it has started, we show running timer + return { message: TimerMessage.waiting, timer: time.current ?? 0 }; + } + // if it has ended, we show how long ago + return { message: TimerMessage.ended, timer: follow.timeEnd }; + } + + // if it has ended, we show how long ago + return { message: TimerMessage.ended, timer: follow.timeEnd }; }; + +/** + * Gets values for the timer items + */ +export function getTimerItems(start: number | undefined, end: number | undefined, delay: number, offset: number) { + if (start == null || end == null) { + return { + scheduledStart: '', + scheduledEnd: '', + projectedStart: '', + projectedEnd: '', + }; + } + + const showProjected = getShouldShowProjected(); + const scheduledStart = formatTime(start + delay); + const scheduledEnd = formatTime(end + delay); + const projectedStart = showProjected ? formatTime(start + delay - offset) : ''; + const projectedEnd = showProjected ? formatTime(end + delay - offset) : ''; + + return { + scheduledStart, + scheduledEnd, + projectedStart, + projectedEnd, + }; +} + +/** + * Gets from the URL whether the showProjected option is active + */ +function getShouldShowProjected() { + const params = new URL(document.location.href).searchParams; + return isStringBoolean(params.get('showProjected')); +} diff --git a/apps/client/src/features/viewers/public/Public.scss b/apps/client/src/features/viewers/public/Public.scss index bee2cef90..369692383 100644 --- a/apps/client/src/features/viewers/public/Public.scss +++ b/apps/client/src/features/viewers/public/Public.scss @@ -43,7 +43,7 @@ .time { font-size: clamp(32px, 3.5vw, 50px); - font-weight: 600; + color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; line-height: 0.95em; } diff --git a/apps/client/src/theme/_viewerDefs.scss b/apps/client/src/theme/_viewerDefs.scss index e68d53034..67bc2614d 100644 --- a/apps/client/src/theme/_viewerDefs.scss +++ b/apps/client/src/theme/_viewerDefs.scss @@ -28,3 +28,7 @@ $timer-finished-color: $playback-negative; $timer-bold-font-family: 'Arial Black', sans-serif; // --card-background-color-override $external-color: rgba(white, 70%); // --external-color-override + +// properties of other timers (clock and countdown) +$timer-label-size: clamp(16px, 1.5vw, 24px); +$timer-value-size: clamp(2rem, 3.5vw, 3.5rem); \ No newline at end of file diff --git a/apps/client/src/translation/languages/de.ts b/apps/client/src/translation/languages/de.ts index be22bd8c0..25470bf48 100644 --- a/apps/client/src/translation/languages/de.ts +++ b/apps/client/src/translation/languages/de.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langDe: TranslationObject = { - 'common.end_time': 'Endzeit', - 'common.expected_finish': 'Voraussichtliches Ende', + 'common.expected_finish': 'Erwartetes Ende', 'common.minutes': 'min', 'common.now': 'Jetzt', 'common.next': 'Nächste', 'common.public_message': 'Öffentliche Nachricht', - 'common.start_time': 'Startzeit', + 'common.scheduled_start': 'Geplanter beginn', + 'common.scheduled_end': 'Geplantes ende', + 'common.projected_start': 'Erwartetes beginn', + 'common.projected_end': 'Erwartetes ende', 'common.stage_timer': 'Bühnen-Timer', 'common.started_at': 'Gestartet am', 'common.time_now': 'Aktuelle Zeit', diff --git a/apps/client/src/translation/languages/en.ts b/apps/client/src/translation/languages/en.ts index a699a3a2b..429563357 100644 --- a/apps/client/src/translation/languages/en.ts +++ b/apps/client/src/translation/languages/en.ts @@ -1,11 +1,13 @@ export const langEn = { - 'common.end_time': 'End Time', 'common.expected_finish': 'Expected Finish', 'common.minutes': 'min', 'common.now': 'Now', 'common.next': 'Next', 'common.public_message': 'Public message', - 'common.start_time': 'Start Time', + 'common.scheduled_start': 'Scheduled start', + 'common.scheduled_end': 'Scheduled end', + 'common.projected_start': 'Projected start', + 'common.projected_end': 'Projected end', 'common.stage_timer': 'Stage Timer', 'common.started_at': 'Started At', 'common.time_now': 'Time now', diff --git a/apps/client/src/translation/languages/es.ts b/apps/client/src/translation/languages/es.ts index 2a01fe3d4..99c0d9d4c 100644 --- a/apps/client/src/translation/languages/es.ts +++ b/apps/client/src/translation/languages/es.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langEs: TranslationObject = { - 'common.end_time': 'Hora de finalización', 'common.expected_finish': 'Finalización esperada', 'common.minutes': 'min', 'common.now': 'Ahora', 'common.next': 'Siguiente', 'common.public_message': 'Mensaje público', - 'common.start_time': 'Hora de inicio', + 'common.scheduled_start': 'Inicio programado', + 'common.scheduled_end': 'Fin programado', + 'common.projected_start': 'Inicio previsto', + 'common.projected_end': 'Fin previsto', 'common.stage_timer': 'Temporizador de presentador', 'common.started_at': 'Iniciado en', 'common.time_now': 'Ahora', diff --git a/apps/client/src/translation/languages/fr.ts b/apps/client/src/translation/languages/fr.ts index 8ef85c6a2..09ed6d1c2 100644 --- a/apps/client/src/translation/languages/fr.ts +++ b/apps/client/src/translation/languages/fr.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langFr: TranslationObject = { - 'common.end_time': 'Termine à', 'common.expected_finish': 'Fin estimée à', 'common.minutes': 'min', 'common.now': 'Maintenant', 'common.next': 'A suivre', 'common.public_message': 'Message public', - 'common.start_time': 'Heure de début', + 'common.scheduled_start': 'Début prévu', + 'common.scheduled_end': 'Fin prévue', + 'common.projected_start': 'Début projeté', + 'common.projected_end': 'Fin projetée', 'common.stage_timer': 'Minuteur de scène', 'common.started_at': 'Commencé à', 'common.time_now': 'Heure', diff --git a/apps/client/src/translation/languages/it.ts b/apps/client/src/translation/languages/it.ts index c402f43aa..52b3d85bc 100644 --- a/apps/client/src/translation/languages/it.ts +++ b/apps/client/src/translation/languages/it.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langIt: TranslationObject = { - 'common.end_time': 'Ora di Fine', 'common.expected_finish': 'Fine Prevista', 'common.minutes': 'min', 'common.now': 'Adesso', 'common.next': 'Prossimo', 'common.public_message': 'Messaggio pubblico', - 'common.start_time': 'Ora di Inizio', + 'common.scheduled_start': 'Inizio programmato', + 'common.scheduled_end': 'Fine programmata', + 'common.projected_start': 'Inizio previsto', + 'common.projected_end': 'Fine prevista', 'common.stage_timer': 'Orologio Palco', 'common.started_at': 'Iniziato Alle', 'common.time_now': 'Ora attuale', diff --git a/apps/client/src/translation/languages/no.ts b/apps/client/src/translation/languages/no.ts index 0131b355b..118b99a9c 100644 --- a/apps/client/src/translation/languages/no.ts +++ b/apps/client/src/translation/languages/no.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langNo: TranslationObject = { - 'common.end_time': 'Sluttid', 'common.expected_finish': 'Forventet slutt', 'common.minutes': 'min', 'common.now': 'Nå', 'common.next': 'Neste', 'common.public_message': 'Offentlig beskjed', - 'common.start_time': 'Starttid', + 'common.scheduled_start': 'Planlagt start', + 'common.scheduled_end': 'Planlagt slutt', + 'common.projected_start': 'Forventet start', + 'common.projected_end': 'Forventet slutt', 'common.stage_timer': 'Scenetimer', 'common.started_at': 'Startet', 'common.time_now': 'Klokken nå', diff --git a/apps/client/src/translation/languages/pl.ts b/apps/client/src/translation/languages/pl.ts index 47d4877e2..a90fc9e3d 100644 --- a/apps/client/src/translation/languages/pl.ts +++ b/apps/client/src/translation/languages/pl.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langPl: TranslationObject = { - 'common.end_time': 'Czas zakończenia', 'common.expected_finish': 'Zakładany czas zakończenia', 'common.minutes': 'min', 'common.now': 'Teraz', 'common.next': 'Następnie', 'common.public_message': 'Wiadomość publiczna', - 'common.start_time': 'Czas rozpoczęcia', + 'common.scheduled_start': 'Planowany początek', + 'common.scheduled_end': 'Planowany koniec', + 'common.projected_start': 'Przewidywany początek', + 'common.projected_end': 'Przewidywany koniec', 'common.stage_timer': 'Timer Scena', 'common.started_at': 'Rozpoczęte o', 'common.time_now': 'Aktualny czas', @@ -18,4 +20,3 @@ export const langPl: TranslationObject = { 'countdown.waiting': 'Oczekiwanie na start', 'countdown.overtime': 'ponad czasem', }; - diff --git a/apps/client/src/translation/languages/pt.ts b/apps/client/src/translation/languages/pt.ts index 6817b6826..c4a5b8eb8 100644 --- a/apps/client/src/translation/languages/pt.ts +++ b/apps/client/src/translation/languages/pt.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langPt: TranslationObject = { - 'common.end_time': 'Hora de término', 'common.expected_finish': 'Término esperado', 'common.minutes': 'min', 'common.now': 'Agora', 'common.next': 'Próximo', 'common.public_message': 'Mensagem pública', - 'common.start_time': 'Hora de início', + 'common.scheduled_start': 'Início programado', + 'common.scheduled_end': 'Fim programado', + 'common.projected_start': 'Início previsto', + 'common.projected_end': 'Fim previsto', 'common.stage_timer': 'Temporizador do presentador', 'common.started_at': 'Iniciado em', 'common.time_now': 'Hora atual', diff --git a/apps/client/src/translation/languages/sv.ts b/apps/client/src/translation/languages/sv.ts index 1b4d7c6a9..c75119187 100644 --- a/apps/client/src/translation/languages/sv.ts +++ b/apps/client/src/translation/languages/sv.ts @@ -1,13 +1,15 @@ import { TranslationObject } from './en'; export const langSv: TranslationObject = { - 'common.end_time': 'Sluttid', 'common.expected_finish': 'Förväntat slut', 'common.minutes': 'min', 'common.now': 'Nu', 'common.next': 'Nästa', 'common.public_message': 'Offentligt meddelande', - 'common.start_time': 'Starttid', + 'common.scheduled_start': 'Planerad start', + 'common.scheduled_end': 'Planerad slut', + 'common.projected_start': 'Beräknad start', + 'common.projected_end': 'Beräknad slut', 'common.stage_timer': 'Timer för scenen', 'common.started_at': 'Började vid', 'common.time_now': 'Klockan nu',