): string {
- return Component.displayName || Component.name || 'Component';
-}
-
-const withData = (Component: ComponentType
) => {
- const WithDataComponent = (props: P) => {
- // persisted app state
- const isMirrored = useViewOptionsStore((state) => state.mirror);
-
- // HTTP API data
- const { data: rundownData } = useFlatRundown();
- const { data: project } = useProjectData();
- const { data: viewSettings } = useViewSettings();
- const { data: settings } = useSettings();
- const { data: customFields } = useCustomFields();
-
- // websocket data
- const { clock, timer, message, onAir, eventNext, eventNow, runtime, auxtimer1 } = useStore(runtimeStore);
- const selectedId = eventNow?.id ?? null;
- const nextId = eventNext?.id ?? null;
-
- /**
- * Contains an extended timer object with properties from the current event
- */
- const timeManagerType: ViewExtendedTimer = {
- ...timer,
- clock,
- timerType: eventNow?.timerType ?? TimerType.CountDown,
- countToEnd: eventNow?.countToEnd ?? false,
- };
-
- return (
- <>
-
-
- >
- );
- };
-
- WithDataComponent.displayName = `WithData(${getDisplayName(Component)})`;
- return WithDataComponent;
-};
-
-export default withData;
diff --git a/apps/client/src/features/viewers/common/viewUtils.ts b/apps/client/src/features/viewers/common/viewUtils.ts
index 7ce663658..29a9e00f2 100644
--- a/apps/client/src/features/viewers/common/viewUtils.ts
+++ b/apps/client/src/features/viewers/common/viewUtils.ts
@@ -1,24 +1,28 @@
-import { MaybeNumber, MaybeString, OntimeEvent, TimerType } from 'ontime-types';
+import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types';
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
-import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time';
-type TimerTypeParams = Pick;
-
+/**
+ * Gathers all options that affect which timer is displayed and selects the correct data source to display
+ * it also handles edge cases such as freezing on end
+ */
export function getTimerByType(
freezeEnd: boolean,
- timerObject?: TimerTypeParams,
+ timerTypeNow: TimerType,
+ countToEndNow: boolean,
+ clock: number,
+ timerObject: Pick,
timerTypeOverride?: TimerType,
): number | null {
if (!timerObject) {
return null;
}
- const viewTimerType = timerTypeOverride ?? timerObject.timerType;
+ const viewTimerType = timerTypeOverride ?? timerTypeNow;
- if (timerObject.countToEnd) {
+ if (countToEndNow) {
if (timerObject.current === null) {
return null;
}
@@ -34,7 +38,7 @@ export function getTimerByType(
case TimerType.CountUp:
return Math.abs(timerObject.elapsed ?? 0);
case TimerType.Clock:
- return timerObject.clock;
+ return clock;
case TimerType.None:
return null;
default: {
diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx
index e3141744c..d9a9e659e 100644
--- a/apps/client/src/views/timer/Timer.tsx
+++ b/apps/client/src/views/timer/Timer.tsx
@@ -1,19 +1,20 @@
import { useMemo } from 'react';
-import { CustomFields, MessageState, OntimeEvent, OntimeView, ProjectData, Settings, ViewSettings } from 'ontime-types';
+import { OntimeView } from 'ontime-types';
import { FitText } from '../../common/components/fit-text/FitText';
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
+import EmptyPage from '../../common/components/state/EmptyPage';
import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
-import { useAuxTimersTime } from '../../common/hooks/useSocket';
+import { useTimerSocket } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
-import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
import { cx } from '../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
import { getFormattedTimer, getTimerByType } from '../../features/viewers/common/viewUtils';
import { useTranslation } from '../../translation/TranslationProvider';
+import Loader from '../common/loader/Loader';
import { getTimerColour } from '../utils/presentation.utils';
import { getTimerOptions, useTimerOptions } from './timer.options';
@@ -28,33 +29,28 @@ import {
getShowProgressBar,
getTotalTime,
} from './timer.utils';
+import { TimerData, useTimerData } from './useTimerData';
import './Timer.scss';
-interface TimerProps {
- customFields: CustomFields;
- eventNext: OntimeEvent | null;
- eventNow: OntimeEvent | null;
- general: ProjectData;
- isMirrored: boolean;
- message: MessageState;
- settings: Settings | undefined;
- time: ViewExtendedTimer;
- viewSettings: ViewSettings;
+export default function TimerLoader() {
+ const { data, status } = useTimerData();
+
+ useWindowTitle('Timer');
+
+ if (status === 'pending') {
+ return ;
+ }
+
+ if (status === 'error') {
+ return ;
+ }
+
+ return ;
}
-export default function Timer({
- customFields,
- eventNow,
- eventNext,
- general,
- isMirrored,
- message,
- settings,
- time,
- viewSettings,
-}: TimerProps) {
- const auxTimer = useAuxTimersTime();
+function Timer({ customFields, projectData, isMirrored, settings, viewSettings }: TimerData) {
+ const { eventNext, eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket();
const {
hideClock,
hideCards,
@@ -78,14 +74,12 @@ export default function Timer({
const { getLocalizedString } = useTranslation();
const localisedMinutes = getLocalizedString('common.minutes');
- useWindowTitle('Timer');
-
// gather modifiers
- const viewTimerType = timerType ?? time.timerType;
+ const viewTimerType = timerType ?? timerTypeNow;
const showOverlay = getShowMessage(message.timer);
const { showEndMessage, showFinished, showWarning, showDanger } = getShowModifiers(
- time.timerType,
- time.countToEnd,
+ timerTypeNow,
+ countToEndNow,
time.phase,
freezeOvertime,
freezeMessage,
@@ -107,8 +101,8 @@ export default function Timer({
// gather timer data
const totalTime = getTotalTime(time.duration, time.addedTime);
- const clock = formatTime(time.clock);
- const stageTimer = getTimerByType(freezeOvertime, time, timerType);
+ const formattedClock = formatTime(clock);
+ const stageTimer = getTimerByType(freezeOvertime, timerTypeNow, countToEndNow, clock, time, timerType);
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
removeSeconds: hideTimerSeconds,
removeLeadingZero: removeLeadingZeros,
@@ -155,7 +149,7 @@ export default function Timer({
className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])}
style={userStyles}
>
- {!hideLogo && general?.logo && }
+ {!hideLogo && projectData?.logo && }
@@ -172,7 +166,7 @@ export default function Timer({
{showClock && (
{getLocalizedString('common.time_now')}
-
+
)}
diff --git a/apps/client/src/views/timer/useTimerData.ts b/apps/client/src/views/timer/useTimerData.ts
new file mode 100644
index 000000000..2f7425236
--- /dev/null
+++ b/apps/client/src/views/timer/useTimerData.ts
@@ -0,0 +1,38 @@
+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';
+import { useViewOptionsStore } from '../../common/stores/viewOptions';
+import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
+
+export interface TimerData {
+ customFields: CustomFields;
+ projectData: ProjectData;
+ isMirrored: boolean;
+ settings: Settings;
+ viewSettings: ViewSettings;
+}
+
+export function useTimerData(): ViewData {
+ // persisted app state
+ const isMirrored = useViewOptionsStore((state) => state.mirror);
+
+ // HTTP API data
+ 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, customFieldsStatus]),
+ };
+}
diff --git a/apps/client/src/views/utils/viewLoader.utils.ts b/apps/client/src/views/utils/viewLoader.utils.ts
index 410f04b0f..4aae1d1ce 100644
--- a/apps/client/src/views/utils/viewLoader.utils.ts
+++ b/apps/client/src/views/utils/viewLoader.utils.ts
@@ -4,3 +4,22 @@ export type ViewData = {
data: T;
status: QueryStatus;
};
+
+/**
+ * Aggregates a loading status from multiple query statuses.
+ * If all statuses are 'pending', returns 'pending'.
+ * If all statuses are 'success', returns 'success'.
+ * If any status is 'error', returns 'error'.
+ */
+export function aggregateQueryStatus(statuses: QueryStatus[]): QueryStatus {
+ if (statuses.every((status) => status === 'pending')) {
+ return 'pending';
+ }
+ if (statuses.every((status) => status === 'success')) {
+ return 'success';
+ }
+ if (statuses.some((status) => status === 'error')) {
+ return 'error';
+ }
+ return 'pending';
+}