diff --git a/apps/client/src/common/utils/__tests__/rundownMetadata.test.ts b/apps/client/src/common/utils/__tests__/rundownMetadata.test.ts index 5151c82da..2b0e1fd28 100644 --- a/apps/client/src/common/utils/__tests__/rundownMetadata.test.ts +++ b/apps/client/src/common/utils/__tests__/rundownMetadata.test.ts @@ -173,7 +173,7 @@ describe('initRundownMetadata()', () => { isPast: false, isNextDay: false, totalGap: 10, - isLinkedToLoaded: false, + isLinkedToLoaded: true, isLoaded: true, groupId: 'group', groupColour: 'red', diff --git a/apps/client/src/common/utils/rundownMetadata.ts b/apps/client/src/common/utils/rundownMetadata.ts index 585799bf2..fbd41a4f8 100644 --- a/apps/client/src/common/utils/rundownMetadata.ts +++ b/apps/client/src/common/utils/rundownMetadata.ts @@ -125,6 +125,7 @@ function processEntry( if (entry.id === selectedEventId) { processedData.isLoaded = true; processedData.isPast = false; + processedData.isLinkedToLoaded = true; } if (isOntimeGroup(entry)) { @@ -158,7 +159,8 @@ function processEntry( * a) find an unlinked event * b) find a countToEnd event */ - processedData.isLinkedToLoaded = entry.linkStart && !processedData.previousEvent?.countToEnd; + processedData.isLinkedToLoaded = + entry.linkStart && !processedData.previousEvent?.countToEnd && processedData.isLinkedToLoaded; } if (isNewLatest(entry, processedData.latestEvent)) { diff --git a/apps/client/src/translation/languages/de.ts b/apps/client/src/translation/languages/de.ts index e23980f45..d1fd04533 100644 --- a/apps/client/src/translation/languages/de.ts +++ b/apps/client/src/translation/languages/de.ts @@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types'; export const langDe: TranslationObject = { 'common.expected_finish': 'Erwartetes Ende', 'common.minutes': 'min', + 'common.seconds': 'sek', 'common.now': 'Jetzt', 'common.next': 'Nächste', 'common.scheduled_start': 'Geplanter beginn', @@ -15,6 +16,7 @@ export const langDe: TranslationObject = { 'common.no_data': 'Keine Daten', 'countdown.ended': 'Veranstaltung endete um', 'countdown.running': 'Veranstaltung läuft', + 'countdown.loaded': 'Veranstaltung geladen', 'countdown.select_event': 'Wählen Sie eine Veranstaltung aus, um sie zu verfolgen', 'countdown.to_start': 'Zeit bis zum Start', 'countdown.waiting': 'Warten auf den Veranstaltungsbeginn', diff --git a/apps/client/src/translation/languages/es.ts b/apps/client/src/translation/languages/es.ts index dc6e9df7b..94850293d 100644 --- a/apps/client/src/translation/languages/es.ts +++ b/apps/client/src/translation/languages/es.ts @@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types'; export const langEs: TranslationObject = { 'common.expected_finish': 'Finalización esperada', 'common.minutes': 'min', + 'common.seconds': 'sec', 'common.now': 'Ahora', 'common.next': 'Siguiente', 'common.scheduled_start': 'Inicio programado', @@ -15,6 +16,7 @@ export const langEs: TranslationObject = { 'common.no_data': 'Sin datos', 'countdown.ended': 'Evento finalizado a las', 'countdown.running': 'Evento en curso', + 'countdown.loaded': 'Evento está cargado', //TODO: check translation 'countdown.select_event': 'Seleccionar un evento para seguir', 'countdown.to_start': 'Tiempo para comenzar', 'countdown.waiting': 'Esperando el inicio del evento', diff --git a/apps/client/src/translation/languages/fr.ts b/apps/client/src/translation/languages/fr.ts index 6c8a05bfb..903d4da1a 100644 --- a/apps/client/src/translation/languages/fr.ts +++ b/apps/client/src/translation/languages/fr.ts @@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types'; export const langFr: TranslationObject = { 'common.expected_finish': 'Fin estimée à', 'common.minutes': 'min', + 'common.seconds': 'sec', 'common.now': 'Maintenant', 'common.next': 'A suivre', 'common.scheduled_start': 'Début prévu', @@ -15,6 +16,7 @@ export const langFr: TranslationObject = { 'common.no_data': 'Aucune donnée', 'countdown.ended': 'Évènement terminé à', 'countdown.running': 'Évènement en cours', + 'countdown.loaded': 'Évènement chargé', 'countdown.select_event': 'Sélectionnez un évènement à suivre', 'countdown.to_start': 'Évènement commence dans', 'countdown.waiting': 'En attente du début de l’évènement', diff --git a/apps/client/src/translation/languages/it.ts b/apps/client/src/translation/languages/it.ts index c5a28f6f1..e2038b119 100644 --- a/apps/client/src/translation/languages/it.ts +++ b/apps/client/src/translation/languages/it.ts @@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types'; export const langIt: TranslationObject = { 'common.expected_finish': 'Fine Prevista', 'common.minutes': 'min', + 'common.seconds': 'sec', 'common.now': 'Adesso', 'common.next': 'Prossimo', 'common.scheduled_start': 'Inizio programmato', @@ -15,6 +16,7 @@ export const langIt: TranslationObject = { 'common.no_data': 'Nessun dato disponibile', 'countdown.ended': 'Evento finito alle', 'countdown.running': 'Evento in corso', + 'countdown.loaded': 'Evento caricato', //TODO: check translation 'countdown.select_event': 'Seleziona un evento da seguire', 'countdown.to_start': 'Tempo alla partenza', 'countdown.waiting': "In attesa dell'inizio dell'evento", diff --git a/apps/client/src/translation/languages/pt.ts b/apps/client/src/translation/languages/pt.ts index ad4924045..5b0cf41f3 100644 --- a/apps/client/src/translation/languages/pt.ts +++ b/apps/client/src/translation/languages/pt.ts @@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types'; export const langPt: TranslationObject = { 'common.expected_finish': 'Término esperado', 'common.minutes': 'min', + 'common.seconds': 'sec', 'common.now': 'Agora', 'common.next': 'Próximo', 'common.scheduled_start': 'Início programado', @@ -15,6 +16,7 @@ export const langPt: TranslationObject = { 'common.no_data': 'Sem dados', 'countdown.ended': 'Evento encerrado às', 'countdown.running': 'Evento em andamento', + 'countdown.loaded': 'Evento carregado', //TODO: check translation 'countdown.select_event': 'Selecione um evento para acompanhar', 'countdown.to_start': 'Tempo para iniciar', 'countdown.waiting': 'Aguardando o início do evento', diff --git a/apps/client/src/views/countdown/Countdown.scss b/apps/client/src/views/countdown/Countdown.scss index eb18d47af..0808440b8 100644 --- a/apps/client/src/views/countdown/Countdown.scss +++ b/apps/client/src/views/countdown/Countdown.scss @@ -118,7 +118,11 @@ $item-height: 3.5rem; } .sub--live { - background-color: $green-700; + background-color: $active-green; + } + + .sub--armed { + background-color: $gray-1325; } .sub__binder { @@ -140,12 +144,16 @@ $item-height: 3.5rem; color: $delay-color; } + .sub__schedule--strike { + text-decoration: line-through; + } + .sub__schedule--over { - color: $playback-over + color: $playback-over; } .sub__schedule--under { - color: $playback-under + color: $playback-under; } .sub__title { diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index 783da2d9d..e7d665c90 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -9,6 +9,7 @@ import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import { useClock } from '../../common/hooks/useSocket'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; +import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { formatTime, getDefaultFormat } from '../../common/utils/time'; import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; import { useTranslation } from '../../translation/TranslationProvider'; @@ -39,14 +40,14 @@ export default function CountdownLoader() { return ; } -function Countdown({ customFields, events, projectData, isMirrored, settings }: CountdownData) { +function Countdown({ customFields, rundownData, projectData, isMirrored, settings }: CountdownData) { const { getLocalizedString } = useTranslation(); const { subscriptions } = useCountdownOptions(); const [editMode, setEditMode] = useState(false); // gather rundown data - const playableEvents = events.filter((event) => isOntimeEvent(event) && isPlayableEvent(event)); + const playableEvents = rundownData.filter((entry) => isOntimeEvent(entry) && isPlayableEvent(entry)); // gather presentation data const hasEvents = playableEvents.length > 0; @@ -85,7 +86,7 @@ function Countdown({ customFields, events, projectData, isMirrored, settings }: } interface CountdownContentsProps { - playableEvents: OntimeEvent[]; + playableEvents: ExtendedEntry[]; subscriptions: EntryId[]; goToEditMode: () => void; } diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index 1c956af7d..b14f1041a 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -1,32 +1,37 @@ import { useEffect, useRef, useState } from 'react'; import { IoPencil } from 'react-icons/io5'; -import { EntryId, OntimeEvent } from 'ontime-types'; +import { MaybeNumber, OntimeEvent } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; import Button from '../../common/components/buttons/Button'; import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity'; import useFollowComponent from '../../common/hooks/useFollowComponent'; -import { - useCountdownSocket, - useCurrentDay, - usePlayback, - useRuntimeOffset, - useSelectedEventId, -} from '../../common/hooks/useSocket'; +import { useExpectedStartData, usePlayback, useSelectedEventId } from '../../common/hooks/useSocket'; +import useReport from '../../common/hooks-query/useReport'; import { getOffsetState } from '../../common/utils/offset'; +import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { cx } from '../../common/utils/styleUtils'; import { throttle } from '../../common/utils/throttle'; import FollowButton from '../../features/operator/follow-button/FollowButton'; import ClockTime from '../../features/viewers/common/clock-time/ClockTime'; +import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; import { getPropertyValue } from '../../features/viewers/common/viewUtils'; -import { useTranslation } from '../../translation/TranslationProvider'; import { useCountdownOptions } from './countdown.options'; -import { getIsLive, getSubscriptionDisplayData, sanitiseTitle, timerProgress } from './countdown.utils'; +import { + CountdownEvent, + extendEventData, + getIsLive, + isOutsideRange, + preferredFormat12, + preferredFormat24, + useSubscriptionDisplayData, +} from './countdown.utils'; import './Countdown.scss'; interface CountdownSubscriptionsProps { - subscribedEvents: OntimeEvent[]; + subscribedEvents: ExtendedEntry[]; goToEditMode: () => void; } @@ -36,6 +41,9 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode const { selectedEventId } = useSelectedEventId(); const showFab = useFadeOutOnInactivity(true); + const { data: reportData } = useReport(); + const { offset, currentDay, actualStart, plannedStart, mode } = useExpectedStartData(); + const timeoutId = useRef(null); const [lockAutoScroll, setLockAutoScroll] = useState(false); const selectedRef = useRef(null); @@ -92,23 +100,19 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode {subscribedEvents.map((event) => { const secondaryData = getPropertyValue(event, secondarySource); 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 return ( -
+
-
0 && 'sub__schedule--delayed'])}> - {showExpected ? ( - - ) : ( - <> - - → - - - )} -
- -
{sanitiseTitle(event.title)}
+ + +
{title}
{secondaryData &&
{secondaryData}
}
); @@ -123,63 +127,82 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode ); } -interface ExpectedScheduleProps { - timeStart: number; - timeEnd: number; - delay: number; -} -function ExpectedSchedule(props: ExpectedScheduleProps) { - const { timeStart, timeEnd, delay } = props; +type ScheduleTimeProps = { + event: CountdownEvent; + showExpected: boolean; +}; +//TODO: consider relative mode +export function ScheduleTime(props: ScheduleTimeProps) { + const { event, showExpected } = props; + const { timeStart, duration, delay, expectedStart, countToEnd } = event; - const { offset } = useRuntimeOffset(); + const plannedStart = timeStart + delay + event.dayOffset * dayInMs; - // offset is negative if we are ahead - const expectedOffset = offset - delay; - const expectedState = getOffsetState(expectedOffset); + // only show new exacted value if outside range of the planned value + const isExpectedValueShow = showExpected && isOutsideRange(plannedStart, expectedStart); + + const plannedStateClass = isExpectedValueShow ? 'sub__schedule--strike' : delay !== 0 ? 'sub__schedule--delayed' : ''; + + const expectedStateClass = `sub__schedule--${getOffsetState(expectedStart - plannedStart)}`; + const plannedEnd = plannedStart + duration + delay; + const expectedEnd = countToEnd ? Math.max(expectedStart + duration, plannedEnd) : expectedStart + duration; + const expectedEndClass = `sub__schedule--${getOffsetState(expectedEnd - plannedEnd)}`; return ( - <> +
- → - - + {!isExpectedValueShow && ( + <> + → + + + )} + {isExpectedValueShow && ( + <> + + → + + + )} +
); } interface SubscriptionStatusProps { - event: OntimeEvent; - selectedEventId: EntryId | null; + event: ExtendedEntry & { endedAt: MaybeNumber; expectedStart: number }; } -function SubscriptionStatus({ event, selectedEventId }: SubscriptionStatusProps) { - const { getLocalizedString } = useTranslation(); - const { currentDay } = useCurrentDay(); - const { offset } = useRuntimeOffset(); - const { showExpected } = useCountdownOptions(); - const { playback, current, clock } = useCountdownSocket(); - - // TODO: use reporter values as in the event block chip - const { status, timer } = getSubscriptionDisplayData( - current, - playback, - clock, - event, - selectedEventId, - offset, - currentDay, - getLocalizedString('common.minutes'), - showExpected, - ); +function SubscriptionStatus({ event }: SubscriptionStatusProps) { + const { status, statusDisplay, timeDisplay } = useSubscriptionDisplayData(event); return ( <> -
{getLocalizedString(timerProgress[status])}
-
{timer}
+
{statusDisplay}
+ {status === 'done' ? ( + + ) : ( +
{timeDisplay}
+ )} ); } diff --git a/apps/client/src/views/countdown/SingleEventCountdown.scss b/apps/client/src/views/countdown/SingleEventCountdown.scss index bea111232..ce13f9298 100644 --- a/apps/client/src/views/countdown/SingleEventCountdown.scss +++ b/apps/client/src/views/countdown/SingleEventCountdown.scss @@ -15,6 +15,14 @@ font-size: clamp(40px, 4.5vw, 80px); line-height: 1.1em; text-align: center; + border-left: 0.25em solid; + border-color: var(--card-background-color-override, $viewer-card-bg-color); + + .secondary { + text-align: left; + font-size: clamp(20px, 2vw, 40px); + + } } .event__status { diff --git a/apps/client/src/views/countdown/SingleEventCountdown.tsx b/apps/client/src/views/countdown/SingleEventCountdown.tsx index bd905d64e..cfa48d346 100644 --- a/apps/client/src/views/countdown/SingleEventCountdown.tsx +++ b/apps/client/src/views/countdown/SingleEventCountdown.tsx @@ -1,29 +1,57 @@ import { IoPencil } from 'react-icons/io5'; -import { OntimeEvent } from 'ontime-types'; +import { MaybeNumber, OntimeEvent } from 'ontime-types'; +import { getExpectedStart } from 'ontime-utils'; import Button from '../../common/components/buttons/Button'; import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity'; -import { useCountdownSocket, useCurrentDay, useRuntimeOffset, useSelectedEventId } from '../../common/hooks/useSocket'; +import { useExpectedStartData } from '../../common/hooks/useSocket'; +import useReport from '../../common/hooks-query/useReport'; +import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { cx } from '../../common/utils/styleUtils'; -import { useTranslation } from '../../translation/TranslationProvider'; +import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; +import { getPropertyValue } from '../../features/viewers/common/viewUtils'; import { useCountdownOptions } from './countdown.options'; -import { getSubscriptionDisplayData, timerProgress } from './countdown.utils'; +import { useSubscriptionDisplayData } from './countdown.utils'; +import { ScheduleTime } from './CountdownSubscriptions'; import './SingleEventCountdown.scss'; interface SingleEventCountdownProps { - subscribedEvent: OntimeEvent; + subscribedEvent: ExtendedEntry; goToEditMode: () => void; } export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) { + const { secondarySource, showExpected } = useCountdownOptions(); const showFab = useFadeOutOnInactivity(true); + const { data: reportData } = useReport(); + + const { offset, currentDay, actualStart, plannedStart, mode } = useExpectedStartData(); + const { totalGap, isLinkedToLoaded } = subscribedEvent; + const expectedStart = getExpectedStart(subscribedEvent, { + currentDay, + totalGap, + actualStart, + plannedStart, + isLinkedToLoaded, + offset, + mode, + }); + + 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 secondaryData = getPropertyValue(subscribedEvent, secondarySource); return (
- -
{subscribedEvent.title}
+ +
+ + {title} + {secondaryData &&
{secondaryData}
} +