diff --git a/apps/client/src/common/components/title-card/TitleCard.scss b/apps/client/src/common/components/title-card/TitleCard.scss index 4bc059f81..e534aeb3a 100644 --- a/apps/client/src/common/components/title-card/TitleCard.scss +++ b/apps/client/src/common/components/title-card/TitleCard.scss @@ -16,6 +16,7 @@ .title-card__title { color: var(--color-override, $viewer-color); padding-right: 1em; + min-height: 1.2em; } .title-card__placeholder { diff --git a/apps/client/src/translation/languages/de.ts b/apps/client/src/translation/languages/de.ts index d1fd04533..82b811347 100644 --- a/apps/client/src/translation/languages/de.ts +++ b/apps/client/src/translation/languages/de.ts @@ -21,6 +21,7 @@ export const langDe: TranslationObject = { 'countdown.to_start': 'Zeit bis zum Start', 'countdown.waiting': 'Warten auf den Veranstaltungsbeginn', 'countdown.overtime': 'überfällig', + 'countdown.all_have_finished': 'Alle ausgewählten Veranstaltungen sind beendet', 'timeline.live': 'live', 'timeline.done': 'Beendet', 'timeline.due': 'fällig', diff --git a/apps/client/src/translation/languages/es.ts b/apps/client/src/translation/languages/es.ts index 94850293d..9beba854d 100644 --- a/apps/client/src/translation/languages/es.ts +++ b/apps/client/src/translation/languages/es.ts @@ -21,6 +21,7 @@ export const langEs: TranslationObject = { 'countdown.to_start': 'Tiempo para comenzar', 'countdown.waiting': 'Esperando el inicio del evento', 'countdown.overtime': 'en tiempo extra', + 'countdown.all_have_finished': 'Todos los eventos seleccionados han terminado', 'timeline.live': 'live', 'timeline.done': 'Terminado', 'timeline.due': 'pendiente', diff --git a/apps/client/src/translation/languages/fr.ts b/apps/client/src/translation/languages/fr.ts index 903d4da1a..de88b6d44 100644 --- a/apps/client/src/translation/languages/fr.ts +++ b/apps/client/src/translation/languages/fr.ts @@ -21,6 +21,7 @@ export const langFr: TranslationObject = { 'countdown.to_start': 'Évènement commence dans', 'countdown.waiting': 'En attente du début de l’évènement', 'countdown.overtime': 'en dépassement', + 'countdown.all_have_finished': 'Tous les évènements sélectionnés ont terminé', 'timeline.live': 'live', 'timeline.done': 'Terminé', 'timeline.due': 'dû', diff --git a/apps/client/src/translation/languages/it.ts b/apps/client/src/translation/languages/it.ts index e2038b119..3367a8b7b 100644 --- a/apps/client/src/translation/languages/it.ts +++ b/apps/client/src/translation/languages/it.ts @@ -21,6 +21,7 @@ export const langIt: TranslationObject = { 'countdown.to_start': 'Tempo alla partenza', 'countdown.waiting': "In attesa dell'inizio dell'evento", 'countdown.overtime': 'in ritardo', + 'countdown.all_have_finished': 'Tutti gli eventi selezionati sono finiti', 'timeline.live': 'live', 'timeline.done': 'Terminato', 'timeline.due': 'previsto', diff --git a/apps/client/src/translation/languages/pt.ts b/apps/client/src/translation/languages/pt.ts index 5b0cf41f3..a25b0b642 100644 --- a/apps/client/src/translation/languages/pt.ts +++ b/apps/client/src/translation/languages/pt.ts @@ -21,6 +21,7 @@ export const langPt: TranslationObject = { 'countdown.to_start': 'Tempo para iniciar', 'countdown.waiting': 'Aguardando o início do evento', 'countdown.overtime': 'em tempo extra', + 'countdown.all_have_finished': 'Todos os eventos selecionados terminaram', 'timeline.live': 'live', 'timeline.done': 'Concluído', 'timeline.due': 'Pendente', diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index b7c38e4f7..5f79c0a9c 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -43,7 +43,7 @@ export default function BackstageLoader() { function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) { const { getLocalizedString } = useTranslation(); - const { secondarySource, extraInfo } = useBackstageOptions(); + const { mainSource, secondarySource, extraInfo } = useBackstageOptions(); const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket(); const [blinkClass, setBlinkClass] = useState(false); const { height: screenHeight } = useViewportSize(); @@ -64,7 +64,7 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }: const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData( eventNow, eventNext, - 'title', + mainSource, secondarySource, time.playback, ); diff --git a/apps/client/src/views/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts index 9c1694291..a60545787 100644 --- a/apps/client/src/views/backstage/backstage.options.ts +++ b/apps/client/src/views/backstage/backstage.options.ts @@ -23,6 +23,7 @@ export const getBackstageOptions = ( { value: 'note', label: 'Note' }, ]); const projectDataOptions = makeProjectDataOptions(projectData, [{ value: 'none', label: 'None' }]); + const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]); return [ { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, @@ -30,6 +31,14 @@ export const getBackstageOptions = ( title: OptionTitle.DataSources, collapsible: true, options: [ + { + id: 'main', + title: 'Main text', + description: 'Select the data source for the main text', + type: 'option', + values: mainOptions, + defaultValue: 'title', + }, { id: 'secondary-src', title: 'Event secondary text', @@ -59,6 +68,7 @@ export const getBackstageOptions = ( }; type BackstageOptions = { + mainSource: keyof OntimeEvent | null; secondarySource: keyof OntimeEvent | null; extraInfo: string | null; }; @@ -72,6 +82,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key); return { + mainSource: getValue('main') as keyof OntimeEvent | null, secondarySource: getValue('secondary-src') as keyof OntimeEvent | null, extraInfo: getValue('extra-info'), }; diff --git a/apps/client/src/views/common/viewUtils.ts b/apps/client/src/views/common/viewUtils.ts index 9b6015ca6..a1d9c43a7 100644 --- a/apps/client/src/views/common/viewUtils.ts +++ b/apps/client/src/views/common/viewUtils.ts @@ -1,4 +1,12 @@ -import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types'; +import { + MaybeNumber, + MaybeString, + OntimeEvent, + OntimeGroup, + RundownEntries, + TimerState, + TimerType, +} from 'ontime-types'; import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils'; import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils'; @@ -66,12 +74,22 @@ export function makeColourString(hex: string | null): string | undefined { /** * Retrieves a dynamic property from an event * Considers custom fields + * if rundown entries are provided it can also find the parent title */ -export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined { +export function getPropertyValue( + event: OntimeEvent | null, + property: MaybeString, + entries?: RundownEntries, +): string | undefined { if (!event || typeof property !== 'string' || property === 'none') { return undefined; } + if (property === 'parent') { + if (!entries || !event.parent) return undefined; + return (entries[event.parent] as OntimeGroup)?.title; + } + if (property.startsWith('custom-')) { const field = property.split('custom-')[1]; return event.custom?.[field]; diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index 8e443bd28..4d523d7c2 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { IoAdd } from 'react-icons/io5'; -import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types'; +import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView, PlayableEvent } from 'ontime-types'; import Button from '../../common/components/buttons/Button'; import Empty from '../../common/components/state/Empty'; @@ -47,7 +47,9 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting const [editMode, setEditMode] = useState(false); // gather rundown data - const playableEvents = rundownData.filter((entry) => isOntimeEvent(entry) && isPlayableEvent(entry)); + const playableEvents = rundownData.filter((entry): entry is ExtendedEntry => { + return isOntimeEvent(entry) && isPlayableEvent(entry); + }); // gather presentation data const hasEvents = playableEvents.length > 0; @@ -93,6 +95,7 @@ interface CountdownContentsProps { function CountdownContents({ playableEvents, subscriptions, goToEditMode }: CountdownContentsProps) { const { getLocalizedString } = useTranslation(); + const { hidePast } = useCountdownOptions(); if (subscriptions.length === 0) { return ( @@ -106,6 +109,7 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun } const subscribedEvents = getOrderedSubscriptions(subscriptions, playableEvents); + const eventsToShow = !hidePast ? subscribedEvents : subscribedEvents.filter((event) => !event.isPast); if (subscribedEvents.length === 0) { return ( @@ -118,13 +122,21 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun ); } - if (subscribedEvents.length === 1) { + if (subscribedEvents.length === 1 && eventsToShow.length === 1) { const event = subscribedEvents.at(0); if (!event) return null; return ; } - return ; + if (eventsToShow.length === 0) { + return ( +
+ +
+ ); + } + + return ; } function CountdownClock() { diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index a53a3b789..e4a59d354 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -36,7 +36,7 @@ interface CountdownSubscriptionsProps { } export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) { - const { secondarySource, showExpected } = useCountdownOptions(); + const { mainSource, secondarySource, showExpected } = useCountdownOptions(); const { playback } = usePlayback(); const { selectedEventId } = useSelectedEventId(); const showFab = useFadeOutOnInactivity(true); @@ -103,7 +103,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode const isLive = getIsLive(event.id, selectedEventId, playback); const isArmed = !isLive && event.id === selectedEventId; const countdownEvent = extendEventData(event, currentDay, actualStart, plannedStart, offset, mode, reportData); - const title = event.title.length ? event.title : ' '; // insert utf-8 empty space to avoid the line collapsing + const displayTitle = getPropertyValue(event, mainSource ?? 'title'); return (
-
{title}
+
{displayTitle}
{secondaryData &&
{secondaryData}
}
); diff --git a/apps/client/src/views/countdown/SingleEventCountdown.tsx b/apps/client/src/views/countdown/SingleEventCountdown.tsx index 1f29e48d5..c793364ec 100644 --- a/apps/client/src/views/countdown/SingleEventCountdown.tsx +++ b/apps/client/src/views/countdown/SingleEventCountdown.tsx @@ -23,7 +23,7 @@ interface SingleEventCountdownProps { } export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) { - const { secondarySource, showExpected } = useCountdownOptions(); + const { mainSource, secondarySource, showExpected } = useCountdownOptions(); const showFab = useFadeOutOnInactivity(true); const { data: reportData } = useReport(); @@ -41,7 +41,8 @@ export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: const { endedAt } = reportData[subscribedEvent.id] ?? { endedAt: null }; const countdownEvent = { ...subscribedEvent, expectedStart, endedAt }; - const title = subscribedEvent.title.length ? subscribedEvent.title : ' '; // insert utf-8 empty space to avoid the line collapsing + const titleTmp = getPropertyValue(subscribedEvent, mainSource ?? 'title'); + const title = titleTmp?.length ? titleTmp : ' '; // insert utf-8 empty space to avoid the line collapsing; const secondaryData = getPropertyValue(subscribedEvent, secondarySource); return ( diff --git a/apps/client/src/views/countdown/countdown.options.ts b/apps/client/src/views/countdown/countdown.options.ts index 49d410abb..1ec21d4b0 100644 --- a/apps/client/src/views/countdown/countdown.options.ts +++ b/apps/client/src/views/countdown/countdown.options.ts @@ -14,6 +14,7 @@ export const getCountdownOptions = ( customFields: CustomFields, persistedSubscriptions: EntryId[], ): ViewOption[] => { + const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]); const secondaryOptions = makeOptionsFromCustomFields(customFields, [ { value: 'none', label: 'None' }, { value: 'note', label: 'Note' }, @@ -25,6 +26,14 @@ export const getCountdownOptions = ( title: OptionTitle.DataSources, collapsible: true, options: [ + { + id: 'main', + title: 'Main text', + description: 'Select the data source for the main text', + type: 'option', + values: mainOptions, + defaultValue: 'title', + }, { id: 'secondary-src', title: 'Event secondary text', @@ -48,6 +57,19 @@ export const getCountdownOptions = ( }, ], }, + { + title: OptionTitle.ElementVisibility, + collapsible: true, + options: [ + { + id: 'hidePast', + title: 'Hide Past Events', + description: 'Whether to hide events that have passed', + type: 'boolean', + defaultValue: false, + }, + ], + }, { title: OptionTitle.Hidden, options: [ @@ -65,8 +87,10 @@ export const getCountdownOptions = ( type CountdownOptions = { subscriptions: EntryId[]; + mainSource: keyof OntimeEvent | null; secondarySource: keyof OntimeEvent | null; showExpected: boolean; + hidePast: boolean; }; /** @@ -87,8 +111,10 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL return { subscriptions: getArrayValues('sub'), + mainSource: getValue('main') as keyof OntimeEvent | null, secondarySource: getValue('secondary-src') as keyof OntimeEvent | null, showExpected: isStringBoolean(getValue('showExpected')), + hidePast: isStringBoolean(getValue('hidePast')), }; } diff --git a/apps/client/src/views/studio/Studio.tsx b/apps/client/src/views/studio/Studio.tsx index b6f4c5e7c..fd0e8a033 100644 --- a/apps/client/src/views/studio/Studio.tsx +++ b/apps/client/src/views/studio/Studio.tsx @@ -32,12 +32,12 @@ export default function StudioLoader() { return ; } -function Studio({ projectData, isMirrored, settings, viewSettings }: StudioData) { +function Studio({ customFields, projectData, isMirrored, settings, viewSettings }: StudioData) { const { hideCards } = useStudioOptions(); // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); - const studioOptions = useMemo(() => getStudioOptions(defaultFormat), [defaultFormat]); + const studioOptions = useMemo(() => getStudioOptions(defaultFormat, customFields), [defaultFormat, customFields]); return (
diff --git a/apps/client/src/views/studio/StudioTimers.tsx b/apps/client/src/views/studio/StudioTimers.tsx index 88efd74a0..8f50a5733 100644 --- a/apps/client/src/views/studio/StudioTimers.tsx +++ b/apps/client/src/views/studio/StudioTimers.tsx @@ -5,8 +5,10 @@ import { useAuxTimersTime, useStudioTimersSocket } from '../../common/hooks/useS import { getOffsetState } from '../../common/utils/offset'; import { cx } from '../../common/utils/styleUtils'; import { useTranslation } from '../../translation/TranslationProvider'; +import { getPropertyValue } from '../common/viewUtils'; import { getTimerColour } from '../utils/presentation.utils'; +import { useStudioOptions } from './studio.options'; import { getFormattedEventData, getFormattedScheduleTimes } from './studioTimers.utils'; import './StudioTimers.scss'; @@ -17,6 +19,7 @@ interface StudioTimersProps { export default function StudioTimers({ viewSettings }: StudioTimersProps) { const { getLocalizedString } = useTranslation(); + const { mainSource } = useStudioOptions(); const { eventNow, eventNext, message, time, offset, rundown, expectedRundownEnd } = useStudioTimersSocket(); const schedule = getFormattedScheduleTimes({ @@ -24,8 +27,8 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) { actualStart: rundown.actualStart, expectedEnd: expectedRundownEnd, }); - const event = getFormattedEventData(eventNow, time); - const eventNextTitle = eventNext?.title || '-'; + const event = getFormattedEventData(eventNow, time, mainSource); + const eventNextTitle = getPropertyValue(eventNext, mainSource ?? 'title') || '-'; const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-'; const formattedSecondaryMessage = message.timer.secondarySource === 'secondary' ? message.secondary || '-' : '-'; diff --git a/apps/client/src/views/studio/studio.options.ts b/apps/client/src/views/studio/studio.options.ts index 843843834..5d39d2e0f 100644 --- a/apps/client/src/views/studio/studio.options.ts +++ b/apps/client/src/views/studio/studio.options.ts @@ -1,30 +1,51 @@ import { use, useMemo } from 'react'; import { useSearchParams } from 'react-router'; +import { CustomFields, OntimeEvent } from 'ontime-types'; import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { PresetContext } from '../../common/context/PresetContext'; import { isStringBoolean } from '../common/viewUtils'; -export const getStudioOptions = (timeFormat: string): ViewOption[] => [ - { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, - { - title: OptionTitle.ElementVisibility, - collapsible: true, - options: [ - { - id: 'hideCards', - title: 'Hide cards section', - description: 'Hides the card section with the timers', - type: 'boolean', - defaultValue: false, - }, - ], - }, -]; +export const getStudioOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { + const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]); + + return [ + { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, + { + title: OptionTitle.DataSources, + collapsible: true, + options: [ + { + id: 'main', + title: 'Main text', + description: 'Select the data source for the main text', + type: 'option', + values: mainOptions, + defaultValue: 'title', + }, + ], + }, + { + title: OptionTitle.ElementVisibility, + collapsible: true, + options: [ + { + id: 'hideCards', + title: 'Hide cards section', + description: 'Hides the card section with the timers', + type: 'boolean', + defaultValue: false, + }, + ], + }, + ]; +}; type StudioOptions = { + mainSource: keyof OntimeEvent | null; hideCards: boolean; }; @@ -37,6 +58,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key); return { + mainSource: getValue('main') as keyof OntimeEvent | null, hideCards: isStringBoolean(getValue('hideCards')), }; } diff --git a/apps/client/src/views/studio/studioTimers.utils.ts b/apps/client/src/views/studio/studioTimers.utils.ts index 953b53f9e..4bf7ceafc 100644 --- a/apps/client/src/views/studio/studioTimers.utils.ts +++ b/apps/client/src/views/studio/studioTimers.utils.ts @@ -17,9 +17,13 @@ export function getFormattedScheduleTimes(data: { }; } -export function getFormattedEventData(eventNow: OntimeEvent | null, timer: TimerState) { +export function getFormattedEventData( + eventNow: OntimeEvent | null, + timer: TimerState, + mainSource: keyof OntimeEvent | null, +) { return { - title: eventNow?.title || '-', + title: (eventNow?.[mainSource ?? 'title'] as string) || '-', startedAt: formatTime(timer.startedAt, timeFormat), expectedEnd: formatTime(timer.expectedFinish, timeFormat), timer: millisToString(timer.current), diff --git a/apps/client/src/views/studio/useStudioData.ts b/apps/client/src/views/studio/useStudioData.ts index 4065ef8ff..94a84d280 100644 --- a/apps/client/src/views/studio/useStudioData.ts +++ b/apps/client/src/views/studio/useStudioData.ts @@ -1,5 +1,6 @@ -import { ProjectData, Settings, ViewSettings } from 'ontime-types'; +import { CustomFields, ProjectData, Settings, ViewSettings } from 'ontime-types'; +import useCustomFields from '../../common/hooks-query/useCustomFields'; import useProjectData from '../../common/hooks-query/useProjectData'; import useSettings from '../../common/hooks-query/useSettings'; import useViewSettings from '../../common/hooks-query/useViewSettings'; @@ -7,6 +8,7 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions'; import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils'; export interface StudioData { + customFields: CustomFields; projectData: ProjectData; isMirrored: boolean; settings: Settings; @@ -21,14 +23,16 @@ export function useStudioData(): ViewData { const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); const { data: settings, status: settingsStatus } = useSettings(); + const { data: customFields, status: customFieldsStatus } = useCustomFields(); return { data: { + customFields, projectData, isMirrored, settings, viewSettings, }, - status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus]), + status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]), }; } diff --git a/apps/client/src/views/timeline/Timeline.tsx b/apps/client/src/views/timeline/Timeline.tsx index 80126ecb0..3bdbe8aa3 100644 --- a/apps/client/src/views/timeline/Timeline.tsx +++ b/apps/client/src/views/timeline/Timeline.tsx @@ -6,6 +6,7 @@ import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils'; import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent'; import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { cx } from '../../common/utils/styleUtils'; +import { getPropertyValue } from '../common/viewUtils'; import TimelineMarkers from './timeline-markers/TimelineMarkers'; import { useTimelineOptions } from './timeline.options'; @@ -24,7 +25,7 @@ interface TimelineProps { export default memo(Timeline); function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: TimelineProps) { const { width: screenWidth } = useViewportSize(); - const { hidePast, fixedSize } = useTimelineOptions(); + const { mainSource, hidePast, fixedSize } = useTimelineOptions(); const selectedRef = useRef(null); const scrollContainerRef = useRef(null); @@ -86,6 +87,8 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel const position = positions[index]; if (!position) return null; + const displayTitle = getPropertyValue(event, mainSource ?? 'title') || event.title; + return ( diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index e58acaa46..92dd08198 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -12,7 +12,7 @@ import Loader from '../common/loader/Loader'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import Timeline from './Timeline'; -import { getTimelineOptions } from './timeline.options'; +import { getTimelineOptions, useTimelineOptions } from './timeline.options'; import { getUpcomingEvents, useScopedRundown } from './timeline.utils'; import TimelineSections from './TimelineSections'; import { TimelineData, useTimelineData } from './useTimelineData'; @@ -35,8 +35,9 @@ export default function TimelinePageLoader() { return ; } -function TimelinePage({ events, projectData, settings }: TimelineData) { +function TimelinePage({ events, customFields, projectData, settings }: TimelineData) { const { selectedEventId } = useSelectedEventId(); + const { mainSource } = useTimelineOptions(); // holds copy of the rundown with only relevant events const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId); @@ -47,7 +48,7 @@ function TimelinePage({ events, projectData, settings }: TimelineData) { // populate options const defaultFormat = getDefaultFormat(settings?.timeFormat); - const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]); + const progressOptions = useMemo(() => getTimelineOptions(defaultFormat, customFields), [defaultFormat, customFields]); return (
@@ -58,7 +59,7 @@ function TimelinePage({ events, projectData, settings }: TimelineData) {
- + | null; next: ExtendedEntry | null; followedBy: ExtendedEntry | null; + mainSource: keyof OntimeEvent | null; } -export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) { +export default function TimelineSections({ now, next, followedBy, mainSource }: TimelineSectionsProps) { const { getLocalizedString } = useTranslation(); const state = useExpectedStartData(); // gather card data - const titleNow = now?.title ?? '-'; + const titleNow = getPropertyValue(now, mainSource ?? 'title') ?? '-'; const dueText = getLocalizedString('timeline.due').toUpperCase(); - const nextText = next !== null ? next.title : '-'; - const followedByText = followedBy !== null ? followedBy.title : '-'; + const nextText = getPropertyValue(next, mainSource ?? 'title') ?? '-'; + const followedByText = getPropertyValue(followedBy, mainSource ?? 'title') ?? '-'; let nextStatus: string | undefined; let followedByStatus: string | undefined; diff --git a/apps/client/src/views/timeline/timeline.options.ts b/apps/client/src/views/timeline/timeline.options.ts index 43740178b..540f7c611 100644 --- a/apps/client/src/views/timeline/timeline.options.ts +++ b/apps/client/src/views/timeline/timeline.options.ts @@ -1,15 +1,33 @@ import { use, useMemo } from 'react'; import { useSearchParams } from 'react-router'; +import { CustomFields, OntimeEvent } from 'ontime-types'; import { getTimeOption } from '../../common/components/view-params-editor/common.options'; import { OptionTitle } from '../../common/components/view-params-editor/constants'; import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { PresetContext } from '../../common/context/PresetContext'; import { isStringBoolean } from '../common/viewUtils'; -export const getTimelineOptions = (timeFormat: string): ViewOption[] => { +export const getTimelineOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { + const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]); + return [ { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, + { + title: OptionTitle.DataSources, + collapsible: true, + options: [ + { + id: 'main', + title: 'Main text', + description: 'Select the data source for the main text', + type: 'option', + values: mainOptions, + defaultValue: 'title', + }, + ], + }, { title: OptionTitle.ElementVisibility, collapsible: true, @@ -34,6 +52,7 @@ export const getTimelineOptions = (timeFormat: string): ViewOption[] => { }; type TimelineOptions = { + mainSource: keyof OntimeEvent | null; hidePast: boolean; fixedSize: boolean; }; @@ -47,6 +66,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key); return { + mainSource: getValue('main') as keyof OntimeEvent | null, hidePast: isStringBoolean(getValue('hidePast')), fixedSize: isStringBoolean(getValue('fixedSize')), }; diff --git a/apps/client/src/views/timeline/useTimelineData.ts b/apps/client/src/views/timeline/useTimelineData.ts index 1aa06422d..74675d3d4 100644 --- a/apps/client/src/views/timeline/useTimelineData.ts +++ b/apps/client/src/views/timeline/useTimelineData.ts @@ -1,5 +1,6 @@ -import { OntimeEntry, ProjectData, Settings } from 'ontime-types'; +import { CustomFields, OntimeEntry, ProjectData, Settings } from 'ontime-types'; +import useCustomFields from '../../common/hooks-query/useCustomFields'; import useProjectData from '../../common/hooks-query/useProjectData'; import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown'; import useSettings from '../../common/hooks-query/useSettings'; @@ -8,6 +9,7 @@ import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils'; export interface TimelineData { events: ExtendedEntry[]; + customFields: CustomFields; projectData: ProjectData; settings: Settings; } @@ -17,13 +19,15 @@ export function useTimelineData(): ViewData { const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(); const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: settings, status: settingsStatus } = useSettings(); + const { data: customFields, status: customFieldsStatus } = useCustomFields(); return { data: { events: rundownData, + customFields, projectData, settings, }, - status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus]), + status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), }; } diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index 568c7a18f..01e5c5883 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -49,7 +49,7 @@ export default function TimerLoader() { return ; } -function Timer({ customFields, projectData, isMirrored, settings, viewSettings }: TimerData) { +function Timer({ customFields, projectData, isMirrored, settings, viewSettings, entries }: TimerData) { const { eventNext, eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket(); const { hideClock, @@ -97,6 +97,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings } secondarySource, time.playback, time.phase, + entries, ); // gather timer data diff --git a/apps/client/src/views/timer/timer.options.ts b/apps/client/src/views/timer/timer.options.ts index cb0b18fe0..e939e3e5f 100644 --- a/apps/client/src/views/timer/timer.options.ts +++ b/apps/client/src/views/timer/timer.options.ts @@ -27,12 +27,12 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields): const mainOptions = makeOptionsFromCustomFields(customFields, [ { value: 'none', label: 'None' }, { value: 'title', label: 'Title' }, - { value: 'note', label: 'Note' }, ]); const secondaryOptions = makeOptionsFromCustomFields(customFields, [ { value: 'none', label: 'None' }, { value: 'title', label: 'Title' }, { value: 'note', label: 'Note' }, + { value: 'parent', label: 'Group Title' }, ]); return [ @@ -185,8 +185,8 @@ type TimerOptions = { hideLogo: boolean; hideTimerSeconds: boolean; removeLeadingZeros: boolean; - mainSource: keyof OntimeEvent | null; - secondarySource: keyof OntimeEvent | null; + mainSource: keyof OntimeEvent | null | 'none'; + secondarySource: keyof OntimeEvent | null | 'none'; timerType?: TimerType; freezeOvertime: boolean; freezeMessage: string; diff --git a/apps/client/src/views/timer/timer.utils.ts b/apps/client/src/views/timer/timer.utils.ts index c8adb31ec..de052fad7 100644 --- a/apps/client/src/views/timer/timer.utils.ts +++ b/apps/client/src/views/timer/timer.utils.ts @@ -1,4 +1,13 @@ -import { MaybeNumber, MessageState, OntimeEvent, Playback, TimerMessage, TimerPhase, TimerType } from 'ontime-types'; +import { + MaybeNumber, + MessageState, + OntimeEvent, + Playback, + RundownEntries, + TimerMessage, + TimerPhase, + TimerType, +} from 'ontime-types'; import { isPlaybackActive } from 'ontime-utils'; import { getFormattedTimer, getPropertyValue } from '../common/viewUtils'; @@ -141,10 +150,11 @@ export function getSecondaryDisplay( export function getCardData( eventNow: OntimeEvent | null, eventNext: OntimeEvent | null, - mainSource: keyof OntimeEvent | null, - secondarySource: keyof OntimeEvent | null, + mainSource: keyof OntimeEvent | null | 'none', + secondarySource: keyof OntimeEvent | null | 'none', playback: Playback, phase: TimerPhase, + entries: RundownEntries, ) { if (playback === Playback.Stop) { return { @@ -162,19 +172,19 @@ export function getCardData( // if we are loaded, we show the upcoming event as next const nowMain = hasActiveTimer ? getPropertyValue(eventNow, mainSource ?? 'title') : undefined; - const nowSecondary = hasActiveTimer ? getPropertyValue(eventNow, secondarySource) : undefined; + const nowSecondary = hasActiveTimer ? getPropertyValue(eventNow, secondarySource, entries) : undefined; const nextMain = hasActiveTimer ? getPropertyValue(eventNext, mainSource ?? 'title') : getPropertyValue(eventNow, mainSource ?? 'title'); const nextSecondary = hasActiveTimer - ? getPropertyValue(eventNext, secondarySource) - : getPropertyValue(eventNow, secondarySource); + ? getPropertyValue(eventNext, secondarySource, entries) + : getPropertyValue(eventNow, secondarySource, entries); return { - showNow: Boolean(nowMain) || Boolean(nowSecondary), + showNow: mainSource !== 'none' || Boolean(nowSecondary), nowMain, nowSecondary, - showNext: Boolean(nextMain) || Boolean(nextSecondary), + showNext: mainSource !== 'none' || Boolean(nextSecondary), nextMain, nextSecondary, }; diff --git a/apps/client/src/views/timer/useTimerData.ts b/apps/client/src/views/timer/useTimerData.ts index 2f7425236..9ec9c24a5 100644 --- a/apps/client/src/views/timer/useTimerData.ts +++ b/apps/client/src/views/timer/useTimerData.ts @@ -1,7 +1,8 @@ -import { CustomFields, ProjectData, Settings, ViewSettings } from 'ontime-types'; +import { CustomFields, ProjectData, RundownEntries, Settings, ViewSettings } from 'ontime-types'; import useCustomFields from '../../common/hooks-query/useCustomFields'; import useProjectData from '../../common/hooks-query/useProjectData'; +import useRundown from '../../common/hooks-query/useRundown'; import useSettings from '../../common/hooks-query/useSettings'; import useViewSettings from '../../common/hooks-query/useViewSettings'; import { useViewOptionsStore } from '../../common/stores/viewOptions'; @@ -13,6 +14,7 @@ export interface TimerData { isMirrored: boolean; settings: Settings; viewSettings: ViewSettings; + entries: RundownEntries; } export function useTimerData(): ViewData { @@ -24,6 +26,8 @@ export function useTimerData(): ViewData { const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); const { data: settings, status: settingsStatus } = useSettings(); const { data: customFields, status: customFieldsStatus } = useCustomFields(); + const { data: rundown, status: rundownStatus } = useRundown(); + const { entries } = rundown; return { data: { @@ -32,7 +36,14 @@ export function useTimerData(): ViewData { isMirrored, settings, viewSettings, + entries, }, - status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]), + status: aggregateQueryStatus([ + projectDataStatus, + viewSettingsStatus, + settingsStatus, + customFieldsStatus, + rundownStatus, + ]), }; } diff --git a/packages/types/src/translations/index.ts b/packages/types/src/translations/index.ts index 4a881868a..06af7f8d5 100644 --- a/packages/types/src/translations/index.ts +++ b/packages/types/src/translations/index.ts @@ -19,6 +19,7 @@ export const langEn = { 'countdown.to_start': 'Time to start', 'countdown.waiting': 'Waiting for event start', 'countdown.overtime': 'in overtime', + 'countdown.all_have_finished': 'All selected events have finished', 'timeline.live': 'live', 'timeline.done': 'done', 'timeline.due': 'due',