From 8a3c7258b139d6379cf665b8d5956b2596fcc926 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 7 Jul 2024 20:26:41 +0200 Subject: [PATCH 1/5] refactor: offset cannot be null --- apps/server/src/services/__tests__/timerUtils.test.ts | 2 +- apps/server/src/services/timerUtils.ts | 4 ++-- apps/server/src/stores/__tests__/runtimeState.test.ts | 2 +- apps/server/src/stores/runtimeState.ts | 4 ++-- packages/types/src/definitions/runtime/Runtime.type.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 01cb2d53b8..1097a324cc 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1558,7 +1558,7 @@ describe('getRuntimeOffset()', () => { } as RuntimeState; const offset = getRuntimeOffset(state); - expect(offset).toBe(null); + expect(offset).toBe(0); }); it('handles loaded event', () => { diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index af3dc83d2e..f746778e7a 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -299,10 +299,10 @@ export const updateRoll = (state: RuntimeState) => { * Positive offset is time ahead * Negative offset is time delayed */ -export function getRuntimeOffset(state: RuntimeState): MaybeNumber { +export function getRuntimeOffset(state: RuntimeState): number { // nothing to calculate if there are no loaded events or if we havent started if (state.eventNow === null || state.runtime.actualStart === null) { - return null; + return 0; } const { clock } = state; diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 2e4e06052c..a1053dd65c 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -201,7 +201,7 @@ describe('mutation on runtimeState', () => { stop(); newState = getState(); expect(newState.runtime.actualStart).toBeNull(); - expect(newState.runtime.offset).toBeNull(); + expect(newState.runtime.offset).toBe(0); expect(newState.runtime.expectedEnd).toBeNull(); }); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index f0ef1acb03..70516dcef4 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -19,7 +19,7 @@ import { timerConfig } from '../config/config.js'; const initialRuntime: Runtime = { selectedEventIndex: null, numEvents: 0, - offset: null, + offset: 0, plannedStart: 0, plannedEnd: 0, actualStart: null, @@ -82,7 +82,7 @@ export function clear() { runtimeState.eventNext = null; runtimeState.publicEventNext = null; - runtimeState.runtime.offset = null; + runtimeState.runtime.offset = 0; runtimeState.runtime.actualStart = null; runtimeState.runtime.expectedEnd = null; runtimeState.runtime.selectedEventIndex = null; diff --git a/packages/types/src/definitions/runtime/Runtime.type.ts b/packages/types/src/definitions/runtime/Runtime.type.ts index 6dd09b723e..1fc669d705 100644 --- a/packages/types/src/definitions/runtime/Runtime.type.ts +++ b/packages/types/src/definitions/runtime/Runtime.type.ts @@ -3,7 +3,7 @@ import type { MaybeNumber } from '../../utils/utils.type.js'; export type Runtime = { numEvents: number; selectedEventIndex: MaybeNumber; - offset: MaybeNumber; + offset: number; plannedStart: MaybeNumber; actualStart: MaybeNumber; plannedEnd: MaybeNumber; From 64b64333f2d1d66bf82d612793020f2c0d8821e3 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 7 Jul 2024 20:48:16 +0200 Subject: [PATCH 2/5] feat: projected times in countdown --- .../view-params-editor/constants.ts | 12 +- .../src/features/viewers/ViewWrapper.tsx | 5 +- .../features/viewers/countdown/Countdown.scss | 83 ++++++++------ .../features/viewers/countdown/Countdown.tsx | 55 +++++++--- .../viewers/countdown/countdown.helpers.ts | 103 ++++++++++++------ .../src/features/viewers/public/Public.scss | 2 +- apps/client/src/theme/_viewerDefs.scss | 4 + apps/client/src/translation/languages/de.ts | 8 +- apps/client/src/translation/languages/en.ts | 6 +- apps/client/src/translation/languages/es.ts | 6 +- apps/client/src/translation/languages/fr.ts | 6 +- apps/client/src/translation/languages/it.ts | 6 +- apps/client/src/translation/languages/no.ts | 6 +- apps/client/src/translation/languages/pl.ts | 7 +- apps/client/src/translation/languages/pt.ts | 6 +- apps/client/src/translation/languages/sv.ts | 6 +- 16 files changed, 218 insertions(+), 103 deletions(-) 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 5f35063ba2..a5023ccaa6 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 d195129f60..63b5023291 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 c2c33a57ba..22489b1b26 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; + } + + &__scheduled-start { + grid-area: scheduled-start; + } + + &__scheduled-end { + grid-area: scheduled-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; + &__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; - } + &--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 b67acdfce2..cf68188fa3 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 19e20c9f93..92d166399f 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 is selected, it may not be running + return { + message: time.playback === Playback.Pause ? TimerMessage.waiting : TimerMessage.running, + timer: time.current ?? 0, + }; + } + + const showProjected = getShouldShowProjected(); + const addedTime = showProjected ? offset : 0; + 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) { + 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 - message = TimerMessage.waiting; - timer = time.current ?? 0; - } else { - // running timer timer is not the one we are following + return { message: TimerMessage.waiting, timer: time.current ?? 0 }; + } - if (follow.timeStart > follow.timeEnd) { - // ends day after + // running timer timer is not the one we are following - 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; + // 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 }; } - return { message, timer }; + + // 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 bee2cef901..3696923831 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 e68d530344..67bc2614d6 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 be22bd8c09..25470bf488 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 a699a3a2bd..4295633577 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 2a01fe3d4a..99c0d9d4c6 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 8ef85c6a24..09ed6d1c26 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 c402f43aa4..52b3d85bc0 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 0131b355ba..118b99a9cc 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 47d4877e2b..a90fc9e3da 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 6817b6826b..c4a5b8eb86 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 1b4d7c6a95..c75119187b 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', From 8a1186ece2b859879b5dc08fc571133f3549133e Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 7 Jul 2024 21:21:31 +0200 Subject: [PATCH 3/5] refactor: allow reset selection by refreshing --- .../features/viewers/countdown/Countdown.tsx | 25 ++++++++----------- .../viewers/countdown/countdown.helpers.ts | 6 ++++- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index cf68188fa3..53040217c6 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -7,6 +7,7 @@ import { Runtime, Settings, SupportedEvent, + TimerPhase, ViewSettings, } from 'ontime-types'; @@ -43,14 +44,12 @@ export default function Countdown(props: CountdownProps) { const { getLocalizedString } = useTranslation(); const [follow, setFollow] = useState(null); - const [runningTimer, setRunningTimer] = useState(0); - const [runningMessage, setRunningMessage] = useState(TimerMessage.unhandled); const [delay, setDelay] = useState(0); useWindowTitle('Countdown'); // eg. http://localhost:4001/countdown?eventId=ei0us - // Check for user options + // update data to the event we are following useEffect(() => { if (!backstageEvents) { return; @@ -59,6 +58,12 @@ export default function Countdown(props: CountdownProps) { const eventId = searchParams.get('eventid'); const eventIndex = searchParams.get('event'); + // if there is no event selected, we reset the data + if (!eventId && !eventIndex) { + setFollow(null); + return; + } + let followThis: OntimeEvent | null = null; const events: OntimeEvent[] = [...backstageEvents].filter((event) => event.type === SupportedEvent.Event); @@ -75,23 +80,15 @@ export default function Countdown(props: CountdownProps) { } }, [backstageEvents, searchParams]); - useEffect(() => { - if (!follow) { - return; - } - - const { message, timer } = fetchTimerData(time, follow, selectedId, runtime.offset); - setRunningMessage(message); - setRunningTimer(timer); - }, [follow, selectedId, time, runtime.offset]); - // defer rendering until we load stylesheets if (!shouldRender) { return null; } + const { message: runningMessage, timer: runningTimer } = fetchTimerData(time, follow, selectedId, runtime.offset); + const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id; - const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt; + const finished = time.phase === TimerPhase.Overtime; const isRunningFinished = finished && runningMessage === TimerMessage.running; const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : ''; diff --git a/apps/client/src/features/viewers/countdown/countdown.helpers.ts b/apps/client/src/features/viewers/countdown/countdown.helpers.ts index 92d166399f..e19b8caf1c 100644 --- a/apps/client/src/features/viewers/countdown/countdown.helpers.ts +++ b/apps/client/src/features/viewers/countdown/countdown.helpers.ts @@ -22,10 +22,14 @@ export const sanitiseTitle = (title: string | null) => (title ? title : '{no tit */ export const fetchTimerData = ( time: ViewExtendedTimer, - follow: OntimeEvent, + follow: OntimeEvent | null, selectedId: string | null, offset: number, ): { message: TimerMessage; timer: number } => { + if (follow === null) { + return { message: TimerMessage.unhandled, timer: 0 }; + } + if (selectedId === follow.id) { // if it is selected, it may not be running return { From b54f3c4fc4ca48cdf2666e206571253f32d25ade Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Jul 2024 09:23:21 +0200 Subject: [PATCH 4/5] refactor: extract options per view --- .../view-params-editor/ParamInput.tsx | 4 + .../ViewParamsEditor.module.scss | 14 +- .../view-params-editor/ViewParamsEditor.tsx | 67 ++- .../view-params-editor/constants.ts | 494 +----------------- .../components/view-params-editor/types.ts | 16 +- .../client/src/features/operator/Operator.tsx | 4 +- .../features/operator/operator.options.tsx | 57 ++ .../features/viewers/backstage/Backstage.tsx | 5 +- .../viewers/backstage/backstage.options.ts | 44 ++ .../src/features/viewers/clock/Clock.tsx | 5 +- .../features/viewers/clock/clock.options.ts | 76 +++ .../features/viewers/countdown/Countdown.tsx | 18 +- .../viewers/countdown/countdown.options.ts | 29 + .../viewers/lower-thirds/LowerThird.tsx | 5 +- .../lower-thirds/lowerThird.options.ts | 132 +++++ .../viewers/minimal-timer/MinimalTimer.tsx | 5 +- .../minimal-timer/minimalTimer.options.ts | 91 ++++ .../src/features/viewers/public/Public.tsx | 5 +- .../features/viewers/public/public.options.ts | 44 ++ .../features/viewers/studio/StudioClock.tsx | 4 +- .../viewers/studio/studioClock.options.ts | 9 + .../src/features/viewers/timer/Timer.tsx | 5 +- .../features/viewers/timer/timer.options.ts | 74 +++ 23 files changed, 675 insertions(+), 532 deletions(-) create mode 100644 apps/client/src/features/operator/operator.options.tsx create mode 100644 apps/client/src/features/viewers/backstage/backstage.options.ts create mode 100644 apps/client/src/features/viewers/clock/clock.options.ts create mode 100644 apps/client/src/features/viewers/countdown/countdown.options.ts create mode 100644 apps/client/src/features/viewers/lower-thirds/lowerThird.options.ts create mode 100644 apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts create mode 100644 apps/client/src/features/viewers/public/public.options.ts create mode 100644 apps/client/src/features/viewers/studio/studioClock.options.ts create mode 100644 apps/client/src/features/viewers/timer/timer.options.ts diff --git a/apps/client/src/common/components/view-params-editor/ParamInput.tsx b/apps/client/src/common/components/view-params-editor/ParamInput.tsx index 56380e85bf..cec09b0f9f 100644 --- a/apps/client/src/common/components/view-params-editor/ParamInput.tsx +++ b/apps/client/src/common/components/view-params-editor/ParamInput.tsx @@ -27,6 +27,10 @@ export default function ParamInput(props: EditFormInputProps) { const { paramField } = props; const { id, type, defaultValue } = paramField; + if (type === 'persist') { + return null; + } + if (type === 'option') { const optionFromParams = searchParams.get(id); const defaultOptionValue = optionFromParams || defaultValue; diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss index 53de3badc5..efe885c525 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss @@ -13,14 +13,24 @@ color: $label-gray; display: flex; flex-direction: column; - gap: 0.25rem + gap: 0.25rem; } -.columnSection { +.section { + color: $ui-white; + font-size: 1rem; + + &:not(:first-child) { + margin-top: 2rem; + } +} + +.fieldSet { display: flex; padding: $section-spacing 0; flex-direction: column; gap: $element-spacing; + margin-left: 0.5rem; } .title { diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx index c2e6291970..d3ebd13103 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx @@ -13,7 +13,7 @@ import { } from '@chakra-ui/react'; import ParamInput from './ParamInput'; -import { ParamField } from './types'; +import { isSection, ViewOption } from './types'; import style from './ViewParamsEditor.module.scss'; @@ -22,25 +22,36 @@ type ViewParamsObj = { [key: string]: string | FormDataEntryValue }; /** * Makes a new URLSearchParams object from the given params object */ -const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ParamField[]) => { - const defaultValues = paramFields.reduce>((acc, { id, defaultValue }) => { - acc[id] = String(defaultValue); - return acc; - }, {}); +const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ViewOption[]) => { + const newSearchParams = new URLSearchParams(); + + // Convert paramFields to an object that contains default values + const defaultValues: Record = {}; + paramFields.forEach((option) => { + if (!isSection(option)) { + defaultValues[option.id] = String(option.defaultValue); + } + + // extract persisted values + if ('type' in option && option.type === 'persist') { + newSearchParams.set(option.id, option.value); + } + }); - return Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => { + // compare which values are different from the default values + Object.entries(paramsObj).forEach(([id, value]) => { if (typeof value === 'string' && value.length && defaultValues[id] !== value) { newSearchParams.set(id, value); } - return newSearchParams; - }, new URLSearchParams()); + }); + return newSearchParams; }; interface EditFormDrawerProps { - paramFields: ParamField[]; + viewOptions: ViewOption[]; } -export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) { +export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) { const [searchParams, setSearchParams] = useSearchParams(); const { isOpen, onClose, onOpen } = useDisclosure(); @@ -68,7 +79,7 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) { formEvent.preventDefault(); const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget)); - const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields); + const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions); setSearchParams(newSearchParams); onClose(); @@ -85,15 +96,29 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
- {paramFields.map((field) => ( -
- -
- ))} + {viewOptions.map((option) => { + if (isSection(option)) { + return ( +
+ {option.section} +
+ ); + } + + if (option.type === 'persist') { + return null; + } + + return ( +
+ +
+ ); + })}
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 a5023ccaa6..be4a2cc70b 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -1,15 +1,15 @@ import { CustomFields } from 'ontime-types'; -import { type ParamField } from './types'; +import type { ParamField } from './types'; -const makeOptionsFromCustomFields = (customFields: CustomFields, additionalOptions?: Record) => { +export const makeOptionsFromCustomFields = (customFields: CustomFields, additionalOptions?: Record) => { const customFieldOptions = Object.entries(customFields).reduce((acc, [key, value]) => { return { ...acc, [`custom-${key}`]: `Custom: ${value.label}` }; }, additionalOptions ?? {}); return customFieldOptions; }; -const getTimeOption = (timeFormat: string): ParamField => { +export const getTimeOption = (timeFormat: string): ParamField => { const placeholder = `${timeFormat} (default)`; return { id: 'timeformat', @@ -20,7 +20,7 @@ const getTimeOption = (timeFormat: string): ParamField => { }; }; -const hideTimerSeconds: ParamField = { +export const hideTimerSeconds: ParamField = { id: 'hideTimerSeconds', title: 'Hide seconds in timer', description: 'Whether to hide seconds in the running timer', @@ -28,494 +28,10 @@ const hideTimerSeconds: ParamField = { defaultValue: false, }; -const showLeadingZeros: ParamField = { +export const showLeadingZeros: ParamField = { id: 'showLeadingZeros', title: 'Show leading zeros in timer', description: 'Whether to show leading zeros in the running timer', type: 'boolean', defaultValue: false, }; - -export const getClockOptions = (timeFormat: string): ParamField[] => [ - getTimeOption(timeFormat), - { - id: 'key', - title: 'Key Colour', - description: 'Background colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'text', - title: 'Text Colour', - description: 'Text colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: 'fffff (default)', - }, - { - id: 'textbg', - title: 'Text Background', - description: 'Colour of text background in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'font', - title: 'Font', - description: 'Font family, will use the fonts available in the system', - type: 'string', - placeholder: 'Arial Black (default)', - }, - { - id: 'size', - title: 'Text Size', - description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)', - type: 'number', - placeholder: '1 (default)', - }, - { - id: 'alignx', - title: 'Align Horizontal', - description: 'Moves the horizontally in page to start = left | center | end = right', - type: 'option', - values: { start: 'Start', center: 'Center', end: 'End' }, - defaultValue: 'center', - }, - { - id: 'offsetx', - title: 'Offset Horizontal', - description: 'Offsets the timer horizontal position by a given amount in pixels', - type: 'number', - placeholder: '0 (default)', - }, - { - id: 'aligny', - title: 'Align Vertical', - description: 'Moves the vertically in page to start = left | center | end = right', - type: 'option', - values: { start: 'Start', center: 'Center', end: 'End' }, - defaultValue: 'center', - }, - { - id: 'offsety', - title: 'Offset Vertical', - description: 'Offsets the timer vertical position by a given amount in pixels', - type: 'number', - placeholder: '0 (default)', - }, -]; - -export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => { - const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title' }); - const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); - return [ - getTimeOption(timeFormat), - hideTimerSeconds, - showLeadingZeros, - { - id: 'hideClock', - title: 'Hide Time Now', - description: 'Hides the Time Now field', - type: 'boolean', - defaultValue: false, - }, - { - id: 'main', - title: 'Main text', - description: 'Select the data source for the main text', - type: 'option', - values: mainOptions, - defaultValue: 'Title', - }, - { - id: 'secondary-src', - title: 'Secondary text', - description: 'Select the data source for the secondary text', - type: 'option', - values: secondaryOptions, - defaultValue: '', - }, - { - id: 'hideCards', - title: 'Hide Cards', - description: 'Hides the Now and Next cards', - type: 'boolean', - defaultValue: false, - }, - { - id: 'hideProgress', - title: 'Hide progress bar', - description: 'Hides the progress bar', - type: 'boolean', - defaultValue: false, - }, - { - id: 'hideMessage', - title: 'Hide Presenter Message', - description: 'Prevents the screen from displaying messages from the presenter', - type: 'boolean', - defaultValue: false, - }, - { - id: 'hideExternal', - title: 'Hide External', - description: 'Prevents the screen from displaying the external field', - type: 'boolean', - defaultValue: false, - }, - ]; -}; - -export const MINIMAL_TIMER_OPTIONS: ParamField[] = [ - hideTimerSeconds, - { - id: 'key', - title: 'Key Colour', - description: 'Background colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'text', - title: 'Text Colour', - description: 'Text colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: 'fffff (default)', - }, - { - id: 'textbg', - title: 'Text Background', - description: 'Colour of text background in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'font', - title: 'Font', - description: 'Font family, will use the fonts available in the system', - type: 'string', - placeholder: 'Arial Black (default)', - }, - { - id: 'size', - title: 'Text Size', - description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)', - type: 'number', - placeholder: '1 (default)', - }, - { - id: 'alignx', - title: 'Align Horizontal', - description: 'Moves the horizontally in page to start = left | center | end = right', - type: 'option', - values: { start: 'Start', center: 'Center', end: 'End' }, - defaultValue: 'center', - }, - { - id: 'offsetx', - title: 'Offset Horizontal', - description: 'Offsets the timer horizontal position by a given amount in pixels', - type: 'number', - placeholder: '0 (default)', - }, - { - id: 'aligny', - title: 'Align Vertical', - description: 'Moves the vertically in page to start = left | center | end = right', - type: 'option', - values: { start: 'Start', center: 'Center', end: 'End' }, - defaultValue: 'center', - }, - { - id: 'offsety', - title: 'Offset Vertical', - description: 'Offsets the timer vertical position by a given amount in pixels', - type: 'number', - placeholder: '0 (default)', - }, - { - id: 'hideovertime', - title: 'Hide Overtime', - description: 'Whether to suppress overtime styles (red borders and red text)', - type: 'boolean', - defaultValue: false, - }, - { - id: 'hideendmessage', - title: 'Hide End Message', - description: 'Whether to hide end message and continue showing the clock if timer is in overtime', - type: 'boolean', - defaultValue: false, - }, -]; - -export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => { - const topSourceOptions = makeOptionsFromCustomFields(customFields, { - title: 'Title', - }); - - const bottomSourceOptions = makeOptionsFromCustomFields(customFields, { - title: 'Title', - none: 'None', - }); - - return [ - { - id: 'trigger', - title: 'Animation Trigger', - description: '', - type: 'option', - values: { - event: 'Event Load', - manual: 'Manual', - }, - defaultValue: 'manual', - }, - { - id: 'top-src', - title: 'Top Text', - description: '', - type: 'option', - values: topSourceOptions, - defaultValue: 'title', - }, - { - id: 'bottom-src', - title: 'Bottom Text', - description: 'Select the data source for the bottom element', - type: 'option', - values: bottomSourceOptions, - defaultValue: 'none', - }, - { - id: 'top-colour', - title: 'Top Text Colour', - description: 'Top text colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '0000ff (default)', - }, - { - id: 'bottom-colour', - title: 'Bottom Text Colour', - description: 'Bottom text colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '0000ff (default)', - }, - { - id: 'top-bg', - title: 'Top Background Colour', - description: 'Top text background colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'bottom-bg', - title: 'Bottom Background Colour', - description: 'Bottom text background colour in hexadecimal', - prefix: '#', - type: 'string', - placeholder: '00000000 (default)', - }, - { - id: 'top-size', - title: 'Top Text Size', - description: 'Font size of the top text', - type: 'string', - placeholder: '65px', - }, - { - id: 'bottom-size', - title: 'Bottom Text Size', - description: 'Font size of the bottom text', - type: 'string', - placeholder: '64px', - }, - { - id: 'width', - title: 'Minimum Width', - description: 'Minimum Width of the element', - type: 'number', - prefix: '%', - placeholder: '45 (default)', - }, - { - id: 'transition', - title: 'Transition', - description: 'Transition in time in seconds (default 3)', - type: 'number', - placeholder: '3 (default)', - }, - { - id: 'delay', - title: 'Delay', - description: 'Delay between transition in and out in seconds (default 3)', - type: 'number', - placeholder: '3 (default)', - }, - { - id: 'key', - title: 'Key Colour', - description: 'Colour of the background', - prefix: '#', - type: 'string', - placeholder: 'ffffffff (default)', - }, - { - id: 'line-colour', - title: 'Line Colour', - description: 'Colour of the line', - prefix: '#', - type: 'string', - placeholder: 'ff0000ff (default)', - }, - ]; -}; - -export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => { - const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); - - return [ - getTimeOption(timeFormat), - { - id: 'hidePast', - title: 'Hide past events', - description: 'Scheduler will only show upcoming events', - type: 'boolean', - defaultValue: false, - }, - { - id: 'stopCycle', - title: 'Stop cycling through event pages', - description: 'Schedule will not auto-cycle through events', - type: 'boolean', - defaultValue: false, - }, - { - id: 'eventsPerPage', - title: 'Events per page', - description: 'Sets the number of events on the page, can cause overflow', - type: 'number', - placeholder: '8 (default)', - }, - { - id: 'secondary-src', - title: 'Event secondary text', - description: 'Select the data source for auxiliary text shown in now and next cards', - type: 'option', - values: secondaryOptions, - defaultValue: '', - }, - ]; -}; - -export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => { - const secondaryOptions = makeOptionsFromCustomFields(customFields); - - return [ - getTimeOption(timeFormat), - { - id: 'hidePast', - title: 'Hide past events', - description: 'Scheduler will only show upcoming events', - type: 'boolean', - defaultValue: false, - }, - { - id: 'stopCycle', - title: 'Stop cycling through event pages', - description: 'Schedule will not auto-cycle through events', - type: 'boolean', - defaultValue: false, - }, - { - id: 'eventsPerPage', - title: 'Events per page', - description: 'Sets the number of events on the page, can cause overflow', - type: 'number', - placeholder: '8 (default)', - }, - { - id: 'secondary-src', - title: 'Event secondary text', - description: 'Select the data source for auxiliary text shown in now and next cards', - type: 'option', - values: secondaryOptions, - defaultValue: '', - }, - ]; -}; - -export const getStudioClockOptions = (timeFormat: string): ParamField[] => [ - getTimeOption(timeFormat), - hideTimerSeconds, -]; - -export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ParamField[] => { - const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); - - const customFieldSelect = Object.entries(customFields).reduce((acc, [key, field]) => { - return { ...acc, [key]: { value: key, label: field.label, colour: field.colour } }; - }, {}); - - return [ - getTimeOption(timeFormat), - { - id: 'hidepast', - title: 'Hide Past Events', - description: 'Whether to events that have passed', - type: 'boolean', - defaultValue: false, - }, - { - id: 'main', - title: 'Main data field', - description: 'Field to be shown in the first line of text', - type: 'option', - values: fieldOptions, - defaultValue: 'title', - }, - { - id: 'secondary', - title: 'Secondary data field', - description: 'Field to be shown in the second line of text', - type: 'option', - values: fieldOptions, - defaultValue: '', - }, - { - id: 'subscribe', - title: 'Highlight Field', - description: 'Choose a custom field to highlight', - type: 'multi-option', - values: customFieldSelect, - }, - { - id: 'shouldEdit', - title: 'Edit custom field', - description: 'Allows editing an events selected custom field by long pressing.', - type: 'boolean', - defaultValue: false, - }, - ]; -}; - -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/common/components/view-params-editor/types.ts b/apps/client/src/common/components/view-params-editor/types.ts index ab80f3e1fc..530e233f92 100644 --- a/apps/client/src/common/components/view-params-editor/types.ts +++ b/apps/client/src/common/components/view-params-editor/types.ts @@ -1,3 +1,7 @@ +type ParamSection = { + section: string; +}; + type BaseField = { id: string; title: string; @@ -19,5 +23,15 @@ type MultiOptionsField = { type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string }; type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string }; type BooleanField = { type: 'boolean'; defaultValue: boolean }; +type PersistedField = { type: 'persist'; defaultValue?: string; value: string }; + +export type ParamField = BaseField & + (StringField | BooleanField | NumberField | OptionsField | MultiOptionsField | PersistedField); +export type ViewOption = ParamSection | ParamField; -export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField | MultiOptionsField); +/** + * Type assertion utility checks whether an entry is a section separator + */ +export function isSection(entry: ViewOption): entry is ParamSection { + return 'section' in entry; +} diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index b95cc488a8..a63ce674c7 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -4,7 +4,6 @@ import { isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types'; import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils'; import Empty from '../../common/components/state/Empty'; -import { getOperatorOptions } from '../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useOperator } from '../../common/hooks/useSocket'; @@ -22,6 +21,7 @@ import FollowButton from './follow-button/FollowButton'; import OperatorBlock from './operator-block/OperatorBlock'; import OperatorEvent from './operator-event/OperatorEvent'; import StatusBar from './status-bar/StatusBar'; +import { getOperatorOptions } from './operator.options'; import style from './Operator.module.scss'; @@ -134,7 +134,7 @@ export default function Operator() { return (
- + {editEvent && setEditEvent(null)} />} { + const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); + + const customFieldSelect = Object.entries(customFields).reduce((acc, [key, field]) => { + return { ...acc, [key]: { value: key, label: field.label, colour: field.colour } }; + }, {}); + + return [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Data sources' }, + + { + id: 'main', + title: 'Main data field', + description: 'Field to be shown in the first line of text', + type: 'option', + values: fieldOptions, + defaultValue: 'title', + }, + { + id: 'secondary', + title: 'Secondary data field', + description: 'Field to be shown in the second line of text', + type: 'option', + values: fieldOptions, + defaultValue: '', + }, + { + id: 'subscribe', + title: 'Highlight Field', + description: 'Choose a custom field to highlight', + type: 'multi-option', + values: customFieldSelect, + }, + { section: 'Element visibility' }, + { + id: 'hidepast', + title: 'Hide Past Events', + description: 'Whether to events that have passed', + type: 'boolean', + defaultValue: false, + }, + { + id: 'shouldEdit', + title: 'Edit custom field', + description: 'Allows editing an events selected custom field by long pressing.', + type: 'boolean', + defaultValue: false, + }, + ]; +}; diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx index afad7e9c5f..dca3a69334 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ b/apps/client/src/features/viewers/backstage/Backstage.tsx @@ -11,7 +11,6 @@ import Schedule from '../../../common/components/schedule/Schedule'; import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext'; import ScheduleNav from '../../../common/components/schedule/ScheduleNav'; import TitleCard from '../../../common/components/title-card/TitleCard'; -import { getBackstageOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -23,6 +22,8 @@ import { titleVariants } from '../common/animation'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getPropertyValue } from '../common/viewUtils'; +import { getBackstageOptions } from './backstage.options'; + import './Backstage.scss'; export const MotionTitleCard = motion(TitleCard); @@ -99,7 +100,7 @@ export default function Backstage(props: BackstageProps) { return (
- +
{general.title}
diff --git a/apps/client/src/features/viewers/backstage/backstage.options.ts b/apps/client/src/features/viewers/backstage/backstage.options.ts new file mode 100644 index 0000000000..77859bdfdd --- /dev/null +++ b/apps/client/src/features/viewers/backstage/backstage.options.ts @@ -0,0 +1,44 @@ +import { CustomFields } from 'ontime-types'; + +import { getTimeOption, makeOptionsFromCustomFields } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/types'; + +export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { + const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); + + return [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Data sources' }, + { + id: 'secondary-src', + title: 'Event secondary text', + description: 'Select the data source for auxiliary text shown in now and next cards', + type: 'option', + values: secondaryOptions, + defaultValue: '', + }, + { section: 'Schedule options' }, + { + id: 'eventsPerPage', + title: 'Events per page', + description: 'Sets the number of events on the page, can cause overflow', + type: 'number', + placeholder: '8 (default)', + }, + { + id: 'hidePast', + title: 'Hide past events', + description: 'Scheduler will only show upcoming events', + type: 'boolean', + defaultValue: false, + }, + { + id: 'stopCycle', + title: 'Stop cycling through event pages', + description: 'Schedule will not auto-cycle through events', + type: 'boolean', + defaultValue: false, + }, + ]; +}; diff --git a/apps/client/src/features/viewers/clock/Clock.tsx b/apps/client/src/features/viewers/clock/Clock.tsx index ec6c7ccbe4..b95f46d803 100644 --- a/apps/client/src/features/viewers/clock/Clock.tsx +++ b/apps/client/src/features/viewers/clock/Clock.tsx @@ -2,7 +2,6 @@ import { useSearchParams } from 'react-router-dom'; import { Settings, ViewSettings } from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/constants'; -import { getClockOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -11,6 +10,8 @@ import { OverridableOptions } from '../../../common/models/View.types'; import { formatTime, getDefaultFormat } from '../../../common/utils/time'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; +import { getClockOptions } from './clock.options'; + import './Clock.scss'; interface ClockProps { @@ -130,7 +131,7 @@ export default function Clock(props: ClockProps) { }} data-testid='clock-view' > - + [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'View style override' }, + { + id: 'key', + title: 'Key Colour', + description: 'Background colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'text', + title: 'Text Colour', + description: 'Text colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: 'fffff (default)', + }, + { + id: 'textbg', + title: 'Text Background', + description: 'Colour of text background in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'font', + title: 'Font', + description: 'Font family, will use the fonts available in the system', + type: 'string', + placeholder: 'Arial Black (default)', + }, + { + id: 'size', + title: 'Text Size', + description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)', + type: 'number', + placeholder: '1 (default)', + }, + { + id: 'alignx', + title: 'Align Horizontal', + description: 'Moves the horizontally in page to start = left | center | end = right', + type: 'option', + values: { start: 'Start', center: 'Center', end: 'End' }, + defaultValue: 'center', + }, + { + id: 'offsetx', + title: 'Offset Horizontal', + description: 'Offsets the timer horizontal position by a given amount in pixels', + type: 'number', + placeholder: '0 (default)', + }, + { + id: 'aligny', + title: 'Align Vertical', + description: 'Moves the vertically in page to start = left | center | end = right', + type: 'option', + values: { start: 'Start', center: 'Center', end: 'End' }, + defaultValue: 'center', + }, + { + id: 'offsety', + title: 'Offset Vertical', + description: 'Offsets the timer vertical position by a given amount in pixels', + type: 'number', + placeholder: '0 (default)', + }, +]; diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index 53040217c6..40ae2f3d39 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -12,7 +12,6 @@ import { } from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/constants'; -import { getCountdownOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -23,6 +22,7 @@ import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getFormattedTimer, isStringBoolean } from '../common/viewUtils'; import { fetchTimerData, getTimerItems, TimerMessage } from './countdown.helpers'; +import { getCountdownOptions } from './countdown.options'; import CountdownSelect from './CountdownSelect'; import './Countdown.scss'; @@ -106,12 +106,24 @@ export default function Countdown(props: CountdownProps) { removeLeadingZero: false, }); + const persistParam = () => { + const eventId = searchParams.get('eventid'); + if (eventId !== null) { + return { id: 'eventid', value: eventId }; + } + const eventIndex = searchParams.get('event'); + if (eventIndex !== null) { + return { id: 'eventindex', value: eventIndex }; + } + return undefined; + }; + const defaultFormat = getDefaultFormat(settings?.timeFormat); - const timeOption = getCountdownOptions(defaultFormat); + const viewOptions = getCountdownOptions(defaultFormat, persistParam()); return (
- + {follow === null ? ( ) : ( diff --git a/apps/client/src/features/viewers/countdown/countdown.options.ts b/apps/client/src/features/viewers/countdown/countdown.options.ts new file mode 100644 index 0000000000..3afe965895 --- /dev/null +++ b/apps/client/src/features/viewers/countdown/countdown.options.ts @@ -0,0 +1,29 @@ +import { getTimeOption, hideTimerSeconds } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/types'; + +const makePersistedField = (id: string, value: string): ViewOption => { + return { + id, + title: 'Used to keep the selection on submit', + description: 'Used to keep the selection on submit', + type: 'persist', + value, + }; +}; + +type Persisted = { id: string; value: string }; +export const getCountdownOptions = (timeFormat: string, persisted?: Persisted): ViewOption[] => [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Timer Options' }, + hideTimerSeconds, + { section: 'View behaviour' }, + { + id: 'showProjected', + title: 'Show projected time', + description: 'Show projected times for the event, as well as apply the runtime offset to the timer.', + type: 'boolean', + defaultValue: false, + }, + ...(persisted ? [makePersistedField(persisted.id, persisted.value)] : []), +]; diff --git a/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx b/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx index 34dcf582d1..040e10e168 100644 --- a/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx +++ b/apps/client/src/features/viewers/lower-thirds/LowerThird.tsx @@ -3,12 +3,13 @@ import { useSearchParams } from 'react-router-dom'; import { CustomFields, OntimeEvent, ViewSettings } from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/constants'; -import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; import { getPropertyValue } from '../common/viewUtils'; +import { getLowerThirdOptions } from './lowerThird.options'; + import './LowerThird.scss'; type LowerOptions = { @@ -182,7 +183,7 @@ export default function LowerThird(props: LowerProps) { return (
- +
{ + const topSourceOptions = makeOptionsFromCustomFields(customFields, { + title: 'Title', + }); + + const bottomSourceOptions = makeOptionsFromCustomFields(customFields, { + title: 'Title', + none: 'None', + }); + + return [ + { section: 'View behaviour' }, + { + id: 'trigger', + title: 'Animation Trigger', + description: '', + type: 'option', + values: { + event: 'Event Load', + manual: 'Manual', + }, + defaultValue: 'manual', + }, + { section: 'Data sources' }, + { + id: 'top-src', + title: 'Top Text', + description: '', + type: 'option', + values: topSourceOptions, + defaultValue: 'title', + }, + { + id: 'bottom-src', + title: 'Bottom Text', + description: 'Select the data source for the bottom element', + type: 'option', + values: bottomSourceOptions, + defaultValue: 'none', + }, + { section: 'View style override' }, + { + id: 'top-colour', + title: 'Top Text Colour', + description: 'Top text colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '0000ff (default)', + }, + { + id: 'bottom-colour', + title: 'Bottom Text Colour', + description: 'Bottom text colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '0000ff (default)', + }, + { + id: 'top-bg', + title: 'Top Background Colour', + description: 'Top text background colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'bottom-bg', + title: 'Bottom Background Colour', + description: 'Bottom text background colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'top-size', + title: 'Top Text Size', + description: 'Font size of the top text', + type: 'string', + placeholder: '65px', + }, + { + id: 'bottom-size', + title: 'Bottom Text Size', + description: 'Font size of the bottom text', + type: 'string', + placeholder: '64px', + }, + { + id: 'width', + title: 'Minimum Width', + description: 'Minimum Width of the element', + type: 'number', + prefix: '%', + placeholder: '45 (default)', + }, + { + id: 'transition', + title: 'Transition', + description: 'Transition in time in seconds (default 3)', + type: 'number', + placeholder: '3 (default)', + }, + { + id: 'delay', + title: 'Delay', + description: 'Delay between transition in and out in seconds (default 3)', + type: 'number', + placeholder: '3 (default)', + }, + { + id: 'key', + title: 'Key Colour', + description: 'Colour of the background', + prefix: '#', + type: 'string', + placeholder: 'ffffffff (default)', + }, + { + id: 'line-colour', + title: 'Line Colour', + description: 'Colour of the line', + prefix: '#', + type: 'string', + placeholder: 'ff0000ff (default)', + }, + ]; +}; diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 673dca07ca..2a6f23e316 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -2,7 +2,6 @@ import { useSearchParams } from 'react-router-dom'; import { Playback, TimerPhase, TimerType, ViewSettings } from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/constants'; -import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -11,6 +10,8 @@ import { OverridableOptions } from '../../../common/models/View.types'; import { useTranslation } from '../../../translation/TranslationProvider'; import { getFormattedTimer, getTimerByType, isStringBoolean } from '../common/viewUtils'; +import { MINIMAL_TIMER_OPTIONS } from './minimalTimer.options'; + import './MinimalTimer.scss'; interface MinimalTimerProps { @@ -161,7 +162,7 @@ export default function MinimalTimer(props: MinimalTimerProps) { }} data-testid='minimal-timer' > - + {showEndMessage ? (
{viewSettings.endMessage}
) : ( diff --git a/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts b/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts new file mode 100644 index 0000000000..224bf33646 --- /dev/null +++ b/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts @@ -0,0 +1,91 @@ +import { hideTimerSeconds } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/types'; + +export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [ + { section: 'Timer Options' }, + hideTimerSeconds, + { section: 'Element visibility' }, + { + id: 'hideovertime', + title: 'Hide Overtime', + description: 'Whether to suppress overtime styles (red borders and red text)', + type: 'boolean', + defaultValue: false, + }, + { + id: 'hideendmessage', + title: 'Hide End Message', + description: 'Whether to hide end message and continue showing the clock if timer is in overtime', + type: 'boolean', + defaultValue: false, + }, + { section: 'View style override' }, + { + id: 'key', + title: 'Key Colour', + description: 'Background colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'text', + title: 'Text Colour', + description: 'Text colour in hexadecimal', + prefix: '#', + type: 'string', + placeholder: 'fffff (default)', + }, + { + id: 'textbg', + title: 'Text Background', + description: 'Colour of text background in hexadecimal', + prefix: '#', + type: 'string', + placeholder: '00000000 (default)', + }, + { + id: 'font', + title: 'Font', + description: 'Font family, will use the fonts available in the system', + type: 'string', + placeholder: 'Arial Black (default)', + }, + { + id: 'size', + title: 'Text Size', + description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)', + type: 'number', + placeholder: '1 (default)', + }, + { + id: 'alignx', + title: 'Align Horizontal', + description: 'Moves the horizontally in page to start = left | center | end = right', + type: 'option', + values: { start: 'Start', center: 'Center', end: 'End' }, + defaultValue: 'center', + }, + { + id: 'offsetx', + title: 'Offset Horizontal', + description: 'Offsets the timer horizontal position by a given amount in pixels', + type: 'number', + placeholder: '0 (default)', + }, + { + id: 'aligny', + title: 'Align Vertical', + description: 'Moves the vertically in page to start = left | center | end = right', + type: 'option', + values: { start: 'Start', center: 'Center', end: 'End' }, + defaultValue: 'center', + }, + { + id: 'offsety', + title: 'Offset Vertical', + description: 'Offsets the timer vertical position by a given amount in pixels', + type: 'number', + placeholder: '0 (default)', + }, +]; diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx index 4d3a2a6273..d88c811ce9 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/features/viewers/public/Public.tsx @@ -8,7 +8,6 @@ import Schedule from '../../../common/components/schedule/Schedule'; import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext'; import ScheduleNav from '../../../common/components/schedule/ScheduleNav'; import TitleCard from '../../../common/components/title-card/TitleCard'; -import { getPublicOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -19,6 +18,8 @@ import { titleVariants } from '../common/animation'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getPropertyValue } from '../common/viewUtils'; +import { getPublicOptions } from './public.options'; + import './Public.scss'; export const MotionTitleCard = motion(TitleCard); @@ -73,7 +74,7 @@ export default function Public(props: BackstageProps) { return (
- +
{general.title}
diff --git a/apps/client/src/features/viewers/public/public.options.ts b/apps/client/src/features/viewers/public/public.options.ts new file mode 100644 index 0000000000..02f1da9bec --- /dev/null +++ b/apps/client/src/features/viewers/public/public.options.ts @@ -0,0 +1,44 @@ +import { CustomFields } from 'ontime-types'; + +import { getTimeOption, makeOptionsFromCustomFields } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/types'; + +export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { + const secondaryOptions = makeOptionsFromCustomFields(customFields); + + return [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Data sources' }, + { + id: 'secondary-src', + title: 'Event secondary text', + description: 'Select the data source for auxiliary text shown in now and next cards', + type: 'option', + values: secondaryOptions, + defaultValue: '', + }, + { section: 'Schedule options' }, + { + id: 'eventsPerPage', + title: 'Events per page', + description: 'Sets the number of events on the page, can cause overflow', + type: 'number', + placeholder: '8 (default)', + }, + { + id: 'hidePast', + title: 'Hide past events', + description: 'Scheduler will only show upcoming events', + type: 'boolean', + defaultValue: false, + }, + { + id: 'stopCycle', + title: 'Stop cycling through event pages', + description: 'Schedule will not auto-cycle through events', + type: 'boolean', + defaultValue: false, + }, + ]; +}; diff --git a/apps/client/src/features/viewers/studio/StudioClock.tsx b/apps/client/src/features/viewers/studio/StudioClock.tsx index 17bce990b6..b3be821dcc 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.tsx +++ b/apps/client/src/features/viewers/studio/StudioClock.tsx @@ -4,7 +4,6 @@ import { isOntimeEvent, Playback } from 'ontime-types'; import { millisToString, removeSeconds } from 'ontime-utils'; import { overrideStylesURL } from '../../../common/api/constants'; -import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import useFitText from '../../../common/hooks/useFitText'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; @@ -14,6 +13,7 @@ import { formatTime, getDefaultFormat } from '../../../common/utils/time'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { isStringBoolean } from '../common/viewUtils'; +import { getStudioClockOptions } from './studioClock.options'; import { secondsInMillis, trimRundown } from './studioClock.utils'; import './StudioClock.scss'; @@ -75,7 +75,7 @@ export default function StudioClock(props: StudioClockProps) { return (
- +
{hasAmPm &&
{hasAmPm}
}
{clock}
diff --git a/apps/client/src/features/viewers/studio/studioClock.options.ts b/apps/client/src/features/viewers/studio/studioClock.options.ts new file mode 100644 index 0000000000..23ac6c0acb --- /dev/null +++ b/apps/client/src/features/viewers/studio/studioClock.options.ts @@ -0,0 +1,9 @@ +import { getTimeOption, hideTimerSeconds } from '../../../common/components/view-params-editor/constants'; +import type { ViewOption } from '../../../common/components/view-params-editor/types'; + +export const getStudioClockOptions = (timeFormat: string): ViewOption[] => [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Timer Options' }, + hideTimerSeconds, +]; diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index dc7e767b50..6edba99d96 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -15,7 +15,6 @@ import { import { overrideStylesURL } from '../../../common/api/constants'; import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar'; import TitleCard from '../../../common/components/title-card/TitleCard'; -import { getTimerOptions } from '../../../common/components/view-params-editor/constants'; import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; @@ -25,6 +24,8 @@ import { useTranslation } from '../../../translation/TranslationProvider'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getFormattedTimer, getPropertyValue, getTimerByType, isStringBoolean } from '../common/viewUtils'; +import { getTimerOptions } from './timer.options'; + import './Timer.scss'; // motion @@ -158,7 +159,7 @@ export default function Timer(props: TimerProps) { return (
- +
{!userOptions.hideMessage && (
diff --git a/apps/client/src/features/viewers/timer/timer.options.ts b/apps/client/src/features/viewers/timer/timer.options.ts new file mode 100644 index 0000000000..9a0cc39eab --- /dev/null +++ b/apps/client/src/features/viewers/timer/timer.options.ts @@ -0,0 +1,74 @@ +import { CustomFields } from 'ontime-types'; + +import { + getTimeOption, + hideTimerSeconds, + makeOptionsFromCustomFields, + showLeadingZeros, +} from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/types'; + +export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { + const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title' }); + const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); + return [ + { section: 'Clock Options' }, + getTimeOption(timeFormat), + { section: 'Timer Options' }, + hideTimerSeconds, + showLeadingZeros, + { section: 'Data sources' }, + { + id: 'main', + title: 'Main text', + description: 'Select the data source for the main text', + type: 'option', + values: mainOptions, + defaultValue: 'Title', + }, + { + id: 'secondary-src', + title: 'Secondary text', + description: 'Select the data source for the secondary text', + type: 'option', + values: secondaryOptions, + defaultValue: '', + }, + { section: 'Element visibility' }, + { + id: 'hideClock', + title: 'Hide Time Now', + description: 'Hides the Time Now field', + type: 'boolean', + defaultValue: false, + }, + { + id: 'hideCards', + title: 'Hide Cards', + description: 'Hides the Now and Next cards', + type: 'boolean', + defaultValue: false, + }, + { + id: 'hideProgress', + title: 'Hide progress bar', + description: 'Hides the progress bar', + type: 'boolean', + defaultValue: false, + }, + { + id: 'hideMessage', + title: 'Hide Presenter Message', + description: 'Prevents the screen from displaying messages from the presenter', + type: 'boolean', + defaultValue: false, + }, + { + id: 'hideExternal', + title: 'Hide External', + description: 'Prevents the screen from displaying the external field', + type: 'boolean', + defaultValue: false, + }, + ]; +}; From e4d7040141b9de8ff96674eeade2911b28cff1db Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 9 Jul 2024 09:42:13 +0200 Subject: [PATCH 5/5] refactor: simplify logic dynamic menus --- .../components/view-params-editor/constants.ts | 13 ++++++++----- .../src/features/operator/operator.options.tsx | 7 +++++-- 2 files changed, 13 insertions(+), 7 deletions(-) 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 be4a2cc70b..626b824ee8 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -2,11 +2,14 @@ import { CustomFields } from 'ontime-types'; import type { ParamField } from './types'; -export const makeOptionsFromCustomFields = (customFields: CustomFields, additionalOptions?: Record) => { - const customFieldOptions = Object.entries(customFields).reduce((acc, [key, value]) => { - return { ...acc, [`custom-${key}`]: `Custom: ${value.label}` }; - }, additionalOptions ?? {}); - return customFieldOptions; +export const makeOptionsFromCustomFields = ( + customFields: CustomFields, + additionalOptions: Record = {}, +) => { + return Object.entries(customFields).reduce((options, [key, value]) => { + options[`custom-${key}`] = `Custom: ${value.label}`; + return options; + }, additionalOptions); }; export const getTimeOption = (timeFormat: string): ParamField => { diff --git a/apps/client/src/features/operator/operator.options.tsx b/apps/client/src/features/operator/operator.options.tsx index df32091360..203bddcd8e 100644 --- a/apps/client/src/features/operator/operator.options.tsx +++ b/apps/client/src/features/operator/operator.options.tsx @@ -6,8 +6,11 @@ import { ViewOption } from '../../common/components/view-params-editor/types'; export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => { const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); - const customFieldSelect = Object.entries(customFields).reduce((acc, [key, field]) => { - return { ...acc, [key]: { value: key, label: field.label, colour: field.colour } }; + const customFieldSelect = Object.entries(customFields).reduce< + Record + >((acc, [key, field]) => { + acc[key] = { value: key, label: field.label, colour: field.colour }; + return acc; }, {}); return [