mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-02 04:49:05 +00:00
feat: propper timeuntil calc in countdown view (#1765)
* fix error with is linked to loaded metadata * pass down metadata * create expected schedule * expected start in sub info * for singel subscribtions * refactor * add color to single events * refactor: clean up subscribtion data * show schedule and secondray data for single events * improve subscribtion data display
This commit is contained in:
@@ -173,7 +173,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
totalGap: 10,
|
totalGap: 10,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: true,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
groupId: 'group',
|
groupId: 'group',
|
||||||
groupColour: 'red',
|
groupColour: 'red',
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ function processEntry(
|
|||||||
if (entry.id === selectedEventId) {
|
if (entry.id === selectedEventId) {
|
||||||
processedData.isLoaded = true;
|
processedData.isLoaded = true;
|
||||||
processedData.isPast = false;
|
processedData.isPast = false;
|
||||||
|
processedData.isLinkedToLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOntimeGroup(entry)) {
|
if (isOntimeGroup(entry)) {
|
||||||
@@ -158,7 +159,8 @@ function processEntry(
|
|||||||
* a) find an unlinked event
|
* a) find an unlinked event
|
||||||
* b) find a countToEnd 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)) {
|
if (isNewLatest(entry, processedData.latestEvent)) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
export const langDe: TranslationObject = {
|
export const langDe: TranslationObject = {
|
||||||
'common.expected_finish': 'Erwartetes Ende',
|
'common.expected_finish': 'Erwartetes Ende',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sek',
|
||||||
'common.now': 'Jetzt',
|
'common.now': 'Jetzt',
|
||||||
'common.next': 'Nächste',
|
'common.next': 'Nächste',
|
||||||
'common.scheduled_start': 'Geplanter beginn',
|
'common.scheduled_start': 'Geplanter beginn',
|
||||||
@@ -15,6 +16,7 @@ export const langDe: TranslationObject = {
|
|||||||
'common.no_data': 'Keine Daten',
|
'common.no_data': 'Keine Daten',
|
||||||
'countdown.ended': 'Veranstaltung endete um',
|
'countdown.ended': 'Veranstaltung endete um',
|
||||||
'countdown.running': 'Veranstaltung läuft',
|
'countdown.running': 'Veranstaltung läuft',
|
||||||
|
'countdown.loaded': 'Veranstaltung geladen',
|
||||||
'countdown.select_event': 'Wählen Sie eine Veranstaltung aus, um sie zu verfolgen',
|
'countdown.select_event': 'Wählen Sie eine Veranstaltung aus, um sie zu verfolgen',
|
||||||
'countdown.to_start': 'Zeit bis zum Start',
|
'countdown.to_start': 'Zeit bis zum Start',
|
||||||
'countdown.waiting': 'Warten auf den Veranstaltungsbeginn',
|
'countdown.waiting': 'Warten auf den Veranstaltungsbeginn',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
export const langEs: TranslationObject = {
|
export const langEs: TranslationObject = {
|
||||||
'common.expected_finish': 'Finalización esperada',
|
'common.expected_finish': 'Finalización esperada',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sec',
|
||||||
'common.now': 'Ahora',
|
'common.now': 'Ahora',
|
||||||
'common.next': 'Siguiente',
|
'common.next': 'Siguiente',
|
||||||
'common.scheduled_start': 'Inicio programado',
|
'common.scheduled_start': 'Inicio programado',
|
||||||
@@ -15,6 +16,7 @@ export const langEs: TranslationObject = {
|
|||||||
'common.no_data': 'Sin datos',
|
'common.no_data': 'Sin datos',
|
||||||
'countdown.ended': 'Evento finalizado a las',
|
'countdown.ended': 'Evento finalizado a las',
|
||||||
'countdown.running': 'Evento en curso',
|
'countdown.running': 'Evento en curso',
|
||||||
|
'countdown.loaded': 'Evento está cargado', //TODO: check translation
|
||||||
'countdown.select_event': 'Seleccionar un evento para seguir',
|
'countdown.select_event': 'Seleccionar un evento para seguir',
|
||||||
'countdown.to_start': 'Tiempo para comenzar',
|
'countdown.to_start': 'Tiempo para comenzar',
|
||||||
'countdown.waiting': 'Esperando el inicio del evento',
|
'countdown.waiting': 'Esperando el inicio del evento',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
export const langFr: TranslationObject = {
|
export const langFr: TranslationObject = {
|
||||||
'common.expected_finish': 'Fin estimée à',
|
'common.expected_finish': 'Fin estimée à',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sec',
|
||||||
'common.now': 'Maintenant',
|
'common.now': 'Maintenant',
|
||||||
'common.next': 'A suivre',
|
'common.next': 'A suivre',
|
||||||
'common.scheduled_start': 'Début prévu',
|
'common.scheduled_start': 'Début prévu',
|
||||||
@@ -15,6 +16,7 @@ export const langFr: TranslationObject = {
|
|||||||
'common.no_data': 'Aucune donnée',
|
'common.no_data': 'Aucune donnée',
|
||||||
'countdown.ended': 'Évènement terminé à',
|
'countdown.ended': 'Évènement terminé à',
|
||||||
'countdown.running': 'Évènement en cours',
|
'countdown.running': 'Évènement en cours',
|
||||||
|
'countdown.loaded': 'Évènement chargé',
|
||||||
'countdown.select_event': 'Sélectionnez un évènement à suivre',
|
'countdown.select_event': 'Sélectionnez un évènement à suivre',
|
||||||
'countdown.to_start': 'Évènement commence dans',
|
'countdown.to_start': 'Évènement commence dans',
|
||||||
'countdown.waiting': 'En attente du début de l’évènement',
|
'countdown.waiting': 'En attente du début de l’évènement',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
export const langIt: TranslationObject = {
|
export const langIt: TranslationObject = {
|
||||||
'common.expected_finish': 'Fine Prevista',
|
'common.expected_finish': 'Fine Prevista',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sec',
|
||||||
'common.now': 'Adesso',
|
'common.now': 'Adesso',
|
||||||
'common.next': 'Prossimo',
|
'common.next': 'Prossimo',
|
||||||
'common.scheduled_start': 'Inizio programmato',
|
'common.scheduled_start': 'Inizio programmato',
|
||||||
@@ -15,6 +16,7 @@ export const langIt: TranslationObject = {
|
|||||||
'common.no_data': 'Nessun dato disponibile',
|
'common.no_data': 'Nessun dato disponibile',
|
||||||
'countdown.ended': 'Evento finito alle',
|
'countdown.ended': 'Evento finito alle',
|
||||||
'countdown.running': 'Evento in corso',
|
'countdown.running': 'Evento in corso',
|
||||||
|
'countdown.loaded': 'Evento caricato', //TODO: check translation
|
||||||
'countdown.select_event': 'Seleziona un evento da seguire',
|
'countdown.select_event': 'Seleziona un evento da seguire',
|
||||||
'countdown.to_start': 'Tempo alla partenza',
|
'countdown.to_start': 'Tempo alla partenza',
|
||||||
'countdown.waiting': "In attesa dell'inizio dell'evento",
|
'countdown.waiting': "In attesa dell'inizio dell'evento",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
export const langPt: TranslationObject = {
|
export const langPt: TranslationObject = {
|
||||||
'common.expected_finish': 'Término esperado',
|
'common.expected_finish': 'Término esperado',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sec',
|
||||||
'common.now': 'Agora',
|
'common.now': 'Agora',
|
||||||
'common.next': 'Próximo',
|
'common.next': 'Próximo',
|
||||||
'common.scheduled_start': 'Início programado',
|
'common.scheduled_start': 'Início programado',
|
||||||
@@ -15,6 +16,7 @@ export const langPt: TranslationObject = {
|
|||||||
'common.no_data': 'Sem dados',
|
'common.no_data': 'Sem dados',
|
||||||
'countdown.ended': 'Evento encerrado às',
|
'countdown.ended': 'Evento encerrado às',
|
||||||
'countdown.running': 'Evento em andamento',
|
'countdown.running': 'Evento em andamento',
|
||||||
|
'countdown.loaded': 'Evento carregado', //TODO: check translation
|
||||||
'countdown.select_event': 'Selecione um evento para acompanhar',
|
'countdown.select_event': 'Selecione um evento para acompanhar',
|
||||||
'countdown.to_start': 'Tempo para iniciar',
|
'countdown.to_start': 'Tempo para iniciar',
|
||||||
'countdown.waiting': 'Aguardando o início do evento',
|
'countdown.waiting': 'Aguardando o início do evento',
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ $item-height: 3.5rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub--live {
|
.sub--live {
|
||||||
background-color: $green-700;
|
background-color: $active-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub--armed {
|
||||||
|
background-color: $gray-1325;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub__binder {
|
.sub__binder {
|
||||||
@@ -140,12 +144,16 @@ $item-height: 3.5rem;
|
|||||||
color: $delay-color;
|
color: $delay-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub__schedule--strike {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.sub__schedule--over {
|
.sub__schedule--over {
|
||||||
color: $playback-over
|
color: $playback-over;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub__schedule--under {
|
.sub__schedule--under {
|
||||||
color: $playback-under
|
color: $playback-under;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub__title {
|
.sub__title {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
|||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useClock } from '../../common/hooks/useSocket';
|
import { useClock } from '../../common/hooks/useSocket';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
@@ -39,14 +40,14 @@ export default function CountdownLoader() {
|
|||||||
return <Countdown {...data} />;
|
return <Countdown {...data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Countdown({ customFields, events, projectData, isMirrored, settings }: CountdownData) {
|
function Countdown({ customFields, rundownData, projectData, isMirrored, settings }: CountdownData) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { subscriptions } = useCountdownOptions();
|
const { subscriptions } = useCountdownOptions();
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
// gather rundown data
|
// gather rundown data
|
||||||
const playableEvents = events.filter((event) => isOntimeEvent(event) && isPlayableEvent(event));
|
const playableEvents = rundownData.filter((entry) => isOntimeEvent(entry) && isPlayableEvent(entry));
|
||||||
|
|
||||||
// gather presentation data
|
// gather presentation data
|
||||||
const hasEvents = playableEvents.length > 0;
|
const hasEvents = playableEvents.length > 0;
|
||||||
@@ -85,7 +86,7 @@ function Countdown({ customFields, events, projectData, isMirrored, settings }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CountdownContentsProps {
|
interface CountdownContentsProps {
|
||||||
playableEvents: OntimeEvent[];
|
playableEvents: ExtendedEntry<OntimeEvent>[];
|
||||||
subscriptions: EntryId[];
|
subscriptions: EntryId[];
|
||||||
goToEditMode: () => void;
|
goToEditMode: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { IoPencil } from 'react-icons/io5';
|
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 Button from '../../common/components/buttons/Button';
|
||||||
import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity';
|
import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import {
|
import { useExpectedStartData, usePlayback, useSelectedEventId } from '../../common/hooks/useSocket';
|
||||||
useCountdownSocket,
|
import useReport from '../../common/hooks-query/useReport';
|
||||||
useCurrentDay,
|
|
||||||
usePlayback,
|
|
||||||
useRuntimeOffset,
|
|
||||||
useSelectedEventId,
|
|
||||||
} from '../../common/hooks/useSocket';
|
|
||||||
import { getOffsetState } from '../../common/utils/offset';
|
import { getOffsetState } from '../../common/utils/offset';
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { throttle } from '../../common/utils/throttle';
|
import { throttle } from '../../common/utils/throttle';
|
||||||
import FollowButton from '../../features/operator/follow-button/FollowButton';
|
import FollowButton from '../../features/operator/follow-button/FollowButton';
|
||||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
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 { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
|
||||||
|
|
||||||
import { useCountdownOptions } from './countdown.options';
|
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';
|
import './Countdown.scss';
|
||||||
|
|
||||||
interface CountdownSubscriptionsProps {
|
interface CountdownSubscriptionsProps {
|
||||||
subscribedEvents: OntimeEvent[];
|
subscribedEvents: ExtendedEntry<OntimeEvent>[];
|
||||||
goToEditMode: () => void;
|
goToEditMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +41,9 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
const { selectedEventId } = useSelectedEventId();
|
const { selectedEventId } = useSelectedEventId();
|
||||||
const showFab = useFadeOutOnInactivity(true);
|
const showFab = useFadeOutOnInactivity(true);
|
||||||
|
|
||||||
|
const { data: reportData } = useReport();
|
||||||
|
const { offset, currentDay, actualStart, plannedStart, mode } = useExpectedStartData();
|
||||||
|
|
||||||
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [lockAutoScroll, setLockAutoScroll] = useState(false);
|
const [lockAutoScroll, setLockAutoScroll] = useState(false);
|
||||||
const selectedRef = useRef<HTMLDivElement | null>(null);
|
const selectedRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -92,23 +100,19 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
{subscribedEvents.map((event) => {
|
{subscribedEvents.map((event) => {
|
||||||
const secondaryData = getPropertyValue(event, secondarySource);
|
const secondaryData = getPropertyValue(event, secondarySource);
|
||||||
const isLive = getIsLive(event.id, selectedEventId, playback);
|
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 (
|
return (
|
||||||
<div key={event.id} ref={isLive ? selectedRef : undefined} className={cx(['sub', isLive && 'sub--live'])}>
|
<div
|
||||||
|
key={event.id}
|
||||||
|
ref={isLive ? selectedRef : undefined}
|
||||||
|
className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])}
|
||||||
|
>
|
||||||
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
||||||
<div className={cx(['sub__schedule', event.delay > 0 && 'sub__schedule--delayed'])}>
|
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
||||||
{showExpected ? (
|
<SubscriptionStatus event={countdownEvent} />
|
||||||
<ExpectedSchedule timeStart={event.timeStart} timeEnd={event.timeEnd} delay={event.delay} />
|
<div className={cx(['sub__title', !event.title && 'subdued'])}>{title}</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ClockTime value={event.timeStart + event.delay} preferredFormat12='h:mm' preferredFormat24='HH:mm' />
|
|
||||||
→
|
|
||||||
<ClockTime value={event.timeEnd + event.delay} preferredFormat12='h:mm' preferredFormat24='HH:mm' />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SubscriptionStatus key={event.id} event={event} selectedEventId={selectedEventId} />
|
|
||||||
<div className={cx(['sub__title', !event.title && 'subdued'])}>{sanitiseTitle(event.title)}</div>
|
|
||||||
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
|
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -123,63 +127,82 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpectedScheduleProps {
|
type ScheduleTimeProps = {
|
||||||
timeStart: number;
|
event: CountdownEvent;
|
||||||
timeEnd: number;
|
showExpected: boolean;
|
||||||
delay: number;
|
};
|
||||||
}
|
//TODO: consider relative mode
|
||||||
function ExpectedSchedule(props: ExpectedScheduleProps) {
|
export function ScheduleTime(props: ScheduleTimeProps) {
|
||||||
const { timeStart, timeEnd, delay } = props;
|
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
|
// only show new exacted value if outside range of the planned value
|
||||||
const expectedOffset = offset - delay;
|
const isExpectedValueShow = showExpected && isOutsideRange(plannedStart, expectedStart);
|
||||||
const expectedState = getOffsetState(expectedOffset);
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<div className='sub__schedule'>
|
||||||
<ClockTime
|
<ClockTime
|
||||||
value={timeStart - expectedOffset}
|
value={plannedStart}
|
||||||
className={`sub__schedule--${expectedState}`}
|
preferredFormat12={preferredFormat12}
|
||||||
preferredFormat12='h:mm'
|
preferredFormat24={preferredFormat24}
|
||||||
preferredFormat24='HH:mm'
|
className={plannedStateClass}
|
||||||
/>
|
/>
|
||||||
→
|
{!isExpectedValueShow && (
|
||||||
<ClockTime value={timeEnd - expectedOffset} preferredFormat12='h:mm' preferredFormat24='HH:mm' />
|
<>
|
||||||
</>
|
→
|
||||||
|
<ClockTime
|
||||||
|
value={plannedEnd}
|
||||||
|
preferredFormat12={preferredFormat12}
|
||||||
|
preferredFormat24={preferredFormat24}
|
||||||
|
className={plannedStateClass}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isExpectedValueShow && (
|
||||||
|
<>
|
||||||
|
<ClockTime
|
||||||
|
value={expectedStart}
|
||||||
|
className={expectedStateClass}
|
||||||
|
preferredFormat12={preferredFormat12}
|
||||||
|
preferredFormat24={preferredFormat24}
|
||||||
|
/>
|
||||||
|
→
|
||||||
|
<ClockTime
|
||||||
|
value={expectedEnd}
|
||||||
|
className={expectedEndClass}
|
||||||
|
preferredFormat12={preferredFormat12}
|
||||||
|
preferredFormat24={preferredFormat24}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionStatusProps {
|
interface SubscriptionStatusProps {
|
||||||
event: OntimeEvent;
|
event: ExtendedEntry<OntimeEvent> & { endedAt: MaybeNumber; expectedStart: number };
|
||||||
selectedEventId: EntryId | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubscriptionStatus({ event, selectedEventId }: SubscriptionStatusProps) {
|
function SubscriptionStatus({ event }: SubscriptionStatusProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { status, statusDisplay, timeDisplay } = useSubscriptionDisplayData(event);
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='sub__status'>{getLocalizedString(timerProgress[status])}</div>
|
<div className='sub__status'>{statusDisplay}</div>
|
||||||
<div className='sub__timer'>{timer}</div>
|
{status === 'done' ? (
|
||||||
|
<SuperscriptTime className='sub__timer' time={timeDisplay} />
|
||||||
|
) : (
|
||||||
|
<div className='sub__timer'>{timeDisplay}</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,14 @@
|
|||||||
font-size: clamp(40px, 4.5vw, 80px);
|
font-size: clamp(40px, 4.5vw, 80px);
|
||||||
line-height: 1.1em;
|
line-height: 1.1em;
|
||||||
text-align: center;
|
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 {
|
.event__status {
|
||||||
|
|||||||
@@ -1,29 +1,57 @@
|
|||||||
import { IoPencil } from 'react-icons/io5';
|
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 Button from '../../common/components/buttons/Button';
|
||||||
import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity';
|
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 { 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 { useCountdownOptions } from './countdown.options';
|
||||||
import { getSubscriptionDisplayData, timerProgress } from './countdown.utils';
|
import { useSubscriptionDisplayData } from './countdown.utils';
|
||||||
|
import { ScheduleTime } from './CountdownSubscriptions';
|
||||||
|
|
||||||
import './SingleEventCountdown.scss';
|
import './SingleEventCountdown.scss';
|
||||||
|
|
||||||
interface SingleEventCountdownProps {
|
interface SingleEventCountdownProps {
|
||||||
subscribedEvent: OntimeEvent;
|
subscribedEvent: ExtendedEntry<OntimeEvent>;
|
||||||
goToEditMode: () => void;
|
goToEditMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) {
|
export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) {
|
||||||
|
const { secondarySource, showExpected } = useCountdownOptions();
|
||||||
const showFab = useFadeOutOnInactivity(true);
|
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 (
|
return (
|
||||||
<div className='single-container' data-testid='countdown-event'>
|
<div className='single-container' data-testid='countdown-event'>
|
||||||
<SubscriptionStatus event={subscribedEvent} />
|
<SubscriptionStatus event={countdownEvent} />
|
||||||
<div className='event__title'>{subscribedEvent.title}</div>
|
<div className='event__title' style={{ borderColor: countdownEvent.colour }}>
|
||||||
|
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
||||||
|
{title}
|
||||||
|
{secondaryData && <div className='secondary'>{secondaryData}</div>}
|
||||||
|
</div>
|
||||||
<div className={cx(['fab-container', !showFab && 'fab-container--hidden'])}>
|
<div className={cx(['fab-container', !showFab && 'fab-container--hidden'])}>
|
||||||
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
||||||
<IoPencil /> Edit
|
<IoPencil /> Edit
|
||||||
@@ -34,34 +62,20 @@ export default function SingleEventCountdown({ subscribedEvent, goToEditMode }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionStatusProps {
|
interface SubscriptionStatusProps {
|
||||||
event: OntimeEvent;
|
event: ExtendedEntry<OntimeEvent> & { endedAt: MaybeNumber; expectedStart: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubscriptionStatus({ event }: SubscriptionStatusProps) {
|
function SubscriptionStatus({ event }: SubscriptionStatusProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { status, statusDisplay, timeDisplay } = useSubscriptionDisplayData(event);
|
||||||
const { selectedEventId } = useSelectedEventId();
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='event__status'>{getLocalizedString(timerProgress[status])}</div>
|
<div className='event__status'>{statusDisplay}</div>
|
||||||
<div className='event__timer'>{timer}</div>
|
{status === 'done' ? (
|
||||||
|
<SuperscriptTime className='event__timer' time={timeDisplay} />
|
||||||
|
) : (
|
||||||
|
<div className='event__timer'>{timeDisplay}</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { EntryId, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
import { EntryId, MaybeNumber, OffsetMode, OntimeEntry, OntimeEvent, OntimeReport, Playback } from 'ontime-types';
|
||||||
import { dayInMs } from 'ontime-utils';
|
import { getExpectedStart, MILLIS_PER_MINUTE, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { getFormattedTimer } from '../../features/viewers/common/viewUtils';
|
import { useCountdownSocket } from '../../common/hooks/useSocket';
|
||||||
import type { TranslationKey } from '../../translation/TranslationProvider';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
|
import { timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
|
import { formatDuration, formatTime } from '../../common/utils/time';
|
||||||
|
import { type TranslationKey, useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses string as a title
|
* Parses string as a title
|
||||||
@@ -11,6 +14,9 @@ export function sanitiseTitle(title: string | null) {
|
|||||||
return title ?? '{no title}';
|
return title ?? '{no title}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const preferredFormat12 = 'h:mm a';
|
||||||
|
export const preferredFormat24 = 'HH:mm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the current event is live
|
* Whether the current event is live
|
||||||
*/
|
*/
|
||||||
@@ -18,137 +24,96 @@ export function getIsLive(currentId: EntryId, selectedId: EntryId | null, playba
|
|||||||
return currentId === selectedId && playback !== Playback.Armed;
|
return currentId === selectedId && playback !== Playback.Armed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionTimerDisplayOptions = {
|
export type ProgressStatus = 'future' | 'due' | 'live' | 'done' | 'pending' | 'loaded';
|
||||||
removeSeconds: true,
|
type TimerMessage = Record<ProgressStatus, TranslationKey>;
|
||||||
removeLeadingZero: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const subscriptionScheduledTimeDisplayOptions = {
|
|
||||||
removeSeconds: true,
|
|
||||||
removeLeadingZero: false,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type TimerMessage = Record<string, TranslationKey>;
|
|
||||||
export type ProgressStatus = 'future' | 'due' | 'live' | 'done';
|
|
||||||
|
|
||||||
export const timerProgress: TimerMessage = {
|
export const timerProgress: TimerMessage = {
|
||||||
future: 'countdown.to_start',
|
future: 'countdown.to_start',
|
||||||
due: 'timeline.due',
|
due: 'timeline.due',
|
||||||
live: 'timeline.live',
|
live: 'countdown.running',
|
||||||
|
pending: 'countdown.waiting',
|
||||||
|
loaded: 'countdown.loaded',
|
||||||
done: 'countdown.ended',
|
done: 'countdown.ended',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getFormattedTime(
|
||||||
|
value: MaybeNumber,
|
||||||
|
status: ProgressStatus,
|
||||||
|
minText: string,
|
||||||
|
secText: string,
|
||||||
|
dueText: string,
|
||||||
|
) {
|
||||||
|
if (value === null) return timerPlaceholderMin;
|
||||||
|
if (status === 'future' || status === 'live') {
|
||||||
|
if (value <= 0) return dueText.toUpperCase();
|
||||||
|
return formatDuration(value, value > MILLIS_PER_MINUTE * 2)
|
||||||
|
.replace('m', `${minText} `)
|
||||||
|
.replace('s', secText);
|
||||||
|
}
|
||||||
|
return removeSeconds(formatTime(value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a parsed timer and relevant status message
|
* Returns a parsed timer and relevant status message
|
||||||
* Handles events in different days but disregards whether an event has actually played
|
* Handles events in different days but disregards whether an event has actually played
|
||||||
* TODO: get data from reporter and check if the event has played
|
|
||||||
* TODO: get timer data granularly
|
|
||||||
*/
|
*/
|
||||||
export function getSubscriptionDisplayData(
|
export function useSubscriptionDisplayData(
|
||||||
current: MaybeNumber,
|
subscribedEvent: ExtendedEntry<OntimeEvent> & { endedAt: MaybeNumber; expectedStart: number },
|
||||||
playback: Playback,
|
): { status: ProgressStatus; statusDisplay: string; timeDisplay: string } {
|
||||||
clock: number,
|
const { playback, current, clock } = useCountdownSocket();
|
||||||
subscribedEvent: OntimeEvent,
|
const { getLocalizedString } = useTranslation();
|
||||||
selectedId: EntryId | null,
|
|
||||||
offset: number,
|
|
||||||
currentDay: number,
|
|
||||||
minutesString: string,
|
|
||||||
showExpected = false,
|
|
||||||
): { status: ProgressStatus; timer: string } {
|
|
||||||
const offsetAndDelay = showExpected ? offset + subscribedEvent.delay : 0;
|
|
||||||
|
|
||||||
if (selectedId === subscribedEvent.id) {
|
const bigDuration = (value: number) => {
|
||||||
// 1. An event that is loaded but not running is {'due': <countdown | overtime>}
|
if (value <= 0) return getLocalizedString('countdown.overtime').toUpperCase();
|
||||||
|
return formatDuration(value, value > MILLIS_PER_MINUTE * 2)
|
||||||
|
.replace('m', `${getLocalizedString('common.minutes')} `)
|
||||||
|
.replace('s', getLocalizedString('common.seconds'));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subscribedEvent.isLoaded) {
|
||||||
if (playback === Playback.Armed) {
|
if (playback === Playback.Armed) {
|
||||||
// if we are following the event, but it is not running, we show the scheduled start
|
|
||||||
return {
|
return {
|
||||||
status: 'due',
|
status: 'loaded',
|
||||||
timer: getFormattedTimer(
|
statusDisplay: getLocalizedString(timerProgress['loaded']),
|
||||||
subscribedEvent.timeStart + offsetAndDelay,
|
timeDisplay: bigDuration(subscribedEvent.duration),
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionScheduledTimeDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. An event with a time-to-start lower than 0 is {'due': <countdown | scheduledStart>}, show the running timer
|
|
||||||
return {
|
return {
|
||||||
status: 'live',
|
status: 'live',
|
||||||
timer: getFormattedTimer(current, TimerType.CountDown, minutesString, subscriptionTimerDisplayOptions),
|
statusDisplay: getLocalizedString(timerProgress['live']),
|
||||||
|
timeDisplay: bigDuration(current ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (playback === Playback.Stop || playback === Playback.Armed) {
|
||||||
* If the running timer is not the one we are following
|
|
||||||
* we can be in future, due or have ended
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 3. event is the day after, we show a countdown to start
|
|
||||||
if (subscribedEvent.dayOffset > currentDay) {
|
|
||||||
const dayOffset = (subscribedEvent.dayOffset - currentDay) * dayInMs;
|
|
||||||
return {
|
return {
|
||||||
status: 'future',
|
status: 'pending',
|
||||||
timer: getFormattedTimer(
|
statusDisplay: getLocalizedString(timerProgress['pending']),
|
||||||
subscribedEvent.timeStart + dayOffset - clock - offsetAndDelay,
|
timeDisplay: ' ',
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionTimerDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. event is the before after, show the scheduled end
|
if (subscribedEvent.isPast) {
|
||||||
// TODO: get the time from the reporter
|
|
||||||
if (subscribedEvent.dayOffset < currentDay) {
|
|
||||||
return {
|
return {
|
||||||
status: 'done',
|
status: 'done',
|
||||||
timer: getFormattedTimer(
|
statusDisplay: getLocalizedString(timerProgress['done']),
|
||||||
subscribedEvent.timeEnd,
|
timeDisplay: formatTime(subscribedEvent.endedAt, { format12: preferredFormat12, format24: preferredFormat24 }),
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionScheduledTimeDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. if event is in future, we count to the scheduled start
|
if (subscribedEvent.expectedStart - clock <= 0) {
|
||||||
// TODO: get time until
|
|
||||||
if (clock < subscribedEvent.timeStart) {
|
|
||||||
return {
|
return {
|
||||||
status: 'future',
|
status: 'due',
|
||||||
timer: getFormattedTimer(
|
statusDisplay: getLocalizedString(timerProgress['future']), // We use future here on purpose for the look of it
|
||||||
subscribedEvent.timeStart - clock - offsetAndDelay,
|
timeDisplay: getLocalizedString(timerProgress['due']).toUpperCase(),
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionTimerDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. if event has ended, we show the scheduled end
|
|
||||||
// TODO: get the time from the reporter
|
|
||||||
if (clock > subscribedEvent.timeEnd) {
|
|
||||||
return {
|
|
||||||
status: 'done',
|
|
||||||
timer: getFormattedTimer(
|
|
||||||
subscribedEvent.timeEnd,
|
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionScheduledTimeDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// the event here has to be due, we show the countdown the expected start time
|
|
||||||
return {
|
return {
|
||||||
status: 'due',
|
status: 'future',
|
||||||
timer: getFormattedTimer(
|
statusDisplay: getLocalizedString(timerProgress['future']),
|
||||||
subscribedEvent.timeStart + offsetAndDelay,
|
timeDisplay: bigDuration(subscribedEvent.expectedStart - clock),
|
||||||
TimerType.CountDown,
|
|
||||||
minutesString,
|
|
||||||
subscriptionScheduledTimeDisplayOptions,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +146,7 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
|
|||||||
* Since the original array is already ordered, we simply filter out the events
|
* Since the original array is already ordered, we simply filter out the events
|
||||||
* which are not in the subscriptions list.
|
* which are not in the subscriptions list.
|
||||||
*/
|
*/
|
||||||
export function getOrderedSubscriptions(subscriptions: EntryId[], playableEvents: OntimeEvent[]): OntimeEvent[] {
|
export function getOrderedSubscriptions<T extends OntimeEntry>(subscriptions: EntryId[], playableEvents: T[]): T[] {
|
||||||
return playableEvents.filter((event) => subscriptions.includes(event.id));
|
return playableEvents.filter((event) => subscriptions.includes(event.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,3 +177,32 @@ export function isLinkedToLoadedEvent(events: OntimeEvent[], loadedId: EntryId |
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOutsideRange(a: number, b: number): boolean {
|
||||||
|
return Math.abs(a - b) > MILLIS_PER_MINUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CountdownEvent = ExtendedEntry<OntimeEvent> & { expectedStart: number; endedAt: MaybeNumber };
|
||||||
|
|
||||||
|
export function extendEventData(
|
||||||
|
event: ExtendedEntry<OntimeEvent>,
|
||||||
|
currentDay: number,
|
||||||
|
actualStart: MaybeNumber,
|
||||||
|
plannedStart: MaybeNumber,
|
||||||
|
offset: number,
|
||||||
|
mode: OffsetMode,
|
||||||
|
reportData: OntimeReport,
|
||||||
|
): CountdownEvent {
|
||||||
|
const { totalGap, isLinkedToLoaded } = event;
|
||||||
|
const expectedStart = getExpectedStart(event, {
|
||||||
|
currentDay,
|
||||||
|
totalGap,
|
||||||
|
actualStart,
|
||||||
|
plannedStart,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
offset,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
const { endedAt } = reportData[event.id] ?? { endedAt: null };
|
||||||
|
return { ...event, expectedStart, endedAt };
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { CustomFields, OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
|||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
||||||
|
|
||||||
export interface CountdownData {
|
export interface CountdownData {
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
events: OntimeEntry[];
|
rundownData: ExtendedEntry<OntimeEntry>[];
|
||||||
projectData: ProjectData;
|
projectData: ProjectData;
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@@ -20,7 +21,7 @@ export function useCountdownData(): ViewData<CountdownData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
@@ -28,7 +29,7 @@ export function useCountdownData(): ViewData<CountdownData> {
|
|||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
customFields,
|
customFields,
|
||||||
events: rundownData,
|
rundownData,
|
||||||
projectData,
|
projectData,
|
||||||
isMirrored,
|
isMirrored,
|
||||||
settings,
|
settings,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const langEn = {
|
export const langEn = {
|
||||||
'common.expected_finish': 'Expected Finish',
|
'common.expected_finish': 'Expected Finish',
|
||||||
'common.minutes': 'min',
|
'common.minutes': 'min',
|
||||||
|
'common.seconds': 'sec',
|
||||||
'common.now': 'Now',
|
'common.now': 'Now',
|
||||||
'common.next': 'Next',
|
'common.next': 'Next',
|
||||||
'common.scheduled_start': 'Scheduled start',
|
'common.scheduled_start': 'Scheduled start',
|
||||||
@@ -13,6 +14,7 @@ export const langEn = {
|
|||||||
'common.no_data': 'No data',
|
'common.no_data': 'No data',
|
||||||
'countdown.ended': 'Event ended at',
|
'countdown.ended': 'Event ended at',
|
||||||
'countdown.running': 'Event running',
|
'countdown.running': 'Event running',
|
||||||
|
'countdown.loaded': 'Event loaded',
|
||||||
'countdown.select_event': 'Select an event to follow',
|
'countdown.select_event': 'Select an event to follow',
|
||||||
'countdown.to_start': 'Time to start',
|
'countdown.to_start': 'Time to start',
|
||||||
'countdown.waiting': 'Waiting for event start',
|
'countdown.waiting': 'Waiting for event start',
|
||||||
|
|||||||
Reference in New Issue
Block a user