mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-04 13:59:09 +00:00
refactor: extract timer data to hook
This commit is contained in:
committed by
Carlos Valente
parent
78184b23b7
commit
20ee391525
@@ -2,6 +2,7 @@ import { ComponentType, lazy, Suspense, useMemo } from 'react';
|
|||||||
import { Navigate, Route, useLocation } from 'react-router';
|
import { Navigate, Route, useLocation } from 'react-router';
|
||||||
import { OntimeView, OntimeViewPresettable } from 'ontime-types';
|
import { OntimeView, OntimeViewPresettable } from 'ontime-types';
|
||||||
|
|
||||||
|
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import { useClientPath } from './common/hooks/useClientPath';
|
import { useClientPath } from './common/hooks/useClientPath';
|
||||||
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
||||||
import Log from './features/log/Log';
|
import Log from './features/log/Log';
|
||||||
@@ -41,6 +42,7 @@ export default function AppRouter() {
|
|||||||
path='timer'
|
path='timer'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
|
<ViewNavigationMenu isLockable />
|
||||||
<Timer />
|
<Timer />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { useRuntimeStore } from '../stores/runtime';
|
import { useRuntimeStore } from '../stores/runtime';
|
||||||
import { sendSocket } from '../utils/socket';
|
import { sendSocket } from '../utils/socket';
|
||||||
@@ -225,3 +225,20 @@ export const usePlayback = () => {
|
|||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ======================= View specific subscriptions ======================= */
|
||||||
|
|
||||||
|
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
|
||||||
|
eventNext: state.eventNext,
|
||||||
|
eventNow: state.eventNow,
|
||||||
|
message: state.message,
|
||||||
|
time: state.timer,
|
||||||
|
clock: state.clock,
|
||||||
|
timerTypeNow: state.eventNow?.timerType ?? TimerType.CountDown,
|
||||||
|
countToEndNow: state.eventNow?.countToEnd ?? false,
|
||||||
|
auxTimer: {
|
||||||
|
aux1: state.auxtimer1.current,
|
||||||
|
aux2: state.auxtimer2.current,
|
||||||
|
aux3: state.auxtimer3.current,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { MaybeNumber, Playback, TimerPhase, TimerType } from 'ontime-types';
|
|
||||||
|
|
||||||
// first set extends TimerState
|
|
||||||
export type ViewExtendedTimer = {
|
|
||||||
addedTime: number;
|
|
||||||
current: MaybeNumber;
|
|
||||||
duration: MaybeNumber;
|
|
||||||
elapsed: MaybeNumber;
|
|
||||||
expectedFinish: MaybeNumber;
|
|
||||||
finishedAt: MaybeNumber;
|
|
||||||
phase: TimerPhase;
|
|
||||||
playback: Playback;
|
|
||||||
secondaryTimer: MaybeNumber;
|
|
||||||
startedAt: MaybeNumber;
|
|
||||||
|
|
||||||
clock: number;
|
|
||||||
timerType: TimerType;
|
|
||||||
countToEnd: boolean;
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { ComponentType } from 'react';
|
|
||||||
import { ViewExtendedTimer } from 'common/models/TimeManager.type';
|
|
||||||
import {
|
|
||||||
CustomFields,
|
|
||||||
MessageState,
|
|
||||||
OntimeEvent,
|
|
||||||
ProjectData,
|
|
||||||
Runtime,
|
|
||||||
Settings,
|
|
||||||
SimpleTimerState,
|
|
||||||
TimerType,
|
|
||||||
ViewSettings,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
|
|
||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
|
||||||
import { runtimeStore } from '../../common/stores/runtime';
|
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
|
||||||
|
|
||||||
type WithDataProps = {
|
|
||||||
auxTimer: SimpleTimerState;
|
|
||||||
events: OntimeEvent[];
|
|
||||||
customFields: CustomFields;
|
|
||||||
eventNext: OntimeEvent | null;
|
|
||||||
eventNow: OntimeEvent | null;
|
|
||||||
general: ProjectData;
|
|
||||||
isMirrored: boolean;
|
|
||||||
message: MessageState;
|
|
||||||
nextId: string | null;
|
|
||||||
onAir: boolean;
|
|
||||||
runtime: Runtime;
|
|
||||||
selectedId: string | null;
|
|
||||||
settings: Settings | undefined; // TODO: what is the case for this being undefined?
|
|
||||||
time: ViewExtendedTimer;
|
|
||||||
viewSettings: ViewSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDisplayName(Component: React.ComponentType<any>): string {
|
|
||||||
return Component.displayName || Component.name || 'Component';
|
|
||||||
}
|
|
||||||
|
|
||||||
const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<ViewNavigationMenu isLockable />
|
|
||||||
<Component
|
|
||||||
{...props}
|
|
||||||
auxTimer={auxtimer1}
|
|
||||||
events={rundownData}
|
|
||||||
customFields={customFields}
|
|
||||||
eventNext={eventNext}
|
|
||||||
eventNow={eventNow}
|
|
||||||
general={project}
|
|
||||||
isMirrored={isMirrored}
|
|
||||||
message={message}
|
|
||||||
nextId={nextId}
|
|
||||||
onAir={onAir}
|
|
||||||
runtime={runtime}
|
|
||||||
selectedId={selectedId}
|
|
||||||
settings={settings}
|
|
||||||
time={timeManagerType}
|
|
||||||
viewSettings={viewSettings}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
WithDataComponent.displayName = `WithData(${getDisplayName(Component)})`;
|
|
||||||
return WithDataComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withData;
|
|
||||||
@@ -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 { 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 { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
type TimerTypeParams = Pick<ViewExtendedTimer, 'countToEnd' | 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
/**
|
||||||
|
* 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(
|
export function getTimerByType(
|
||||||
freezeEnd: boolean,
|
freezeEnd: boolean,
|
||||||
timerObject?: TimerTypeParams,
|
timerTypeNow: TimerType,
|
||||||
|
countToEndNow: boolean,
|
||||||
|
clock: number,
|
||||||
|
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
||||||
timerTypeOverride?: TimerType,
|
timerTypeOverride?: TimerType,
|
||||||
): number | null {
|
): number | null {
|
||||||
if (!timerObject) {
|
if (!timerObject) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewTimerType = timerTypeOverride ?? timerObject.timerType;
|
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
||||||
|
|
||||||
if (timerObject.countToEnd) {
|
if (countToEndNow) {
|
||||||
if (timerObject.current === null) {
|
if (timerObject.current === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -34,7 +38,7 @@ export function getTimerByType(
|
|||||||
case TimerType.CountUp:
|
case TimerType.CountUp:
|
||||||
return Math.abs(timerObject.elapsed ?? 0);
|
return Math.abs(timerObject.elapsed ?? 0);
|
||||||
case TimerType.Clock:
|
case TimerType.Clock:
|
||||||
return timerObject.clock;
|
return clock;
|
||||||
case TimerType.None:
|
case TimerType.None:
|
||||||
return null;
|
return null;
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { useMemo } from 'react';
|
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 { FitText } from '../../common/components/fit-text/FitText';
|
||||||
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
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 TitleCard from '../../common/components/title-card/TitleCard';
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
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 { useAuxTimersTime } from '../../common/hooks/useSocket';
|
import { useTimerSocket } from '../../common/hooks/useSocket';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
|
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
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 { getFormattedTimer, getTimerByType } from '../../features/viewers/common/viewUtils';
|
import { getFormattedTimer, getTimerByType } from '../../features/viewers/common/viewUtils';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
import Loader from '../common/loader/Loader';
|
||||||
import { getTimerColour } from '../utils/presentation.utils';
|
import { getTimerColour } from '../utils/presentation.utils';
|
||||||
|
|
||||||
import { getTimerOptions, useTimerOptions } from './timer.options';
|
import { getTimerOptions, useTimerOptions } from './timer.options';
|
||||||
@@ -28,33 +29,28 @@ import {
|
|||||||
getShowProgressBar,
|
getShowProgressBar,
|
||||||
getTotalTime,
|
getTotalTime,
|
||||||
} from './timer.utils';
|
} from './timer.utils';
|
||||||
|
import { TimerData, useTimerData } from './useTimerData';
|
||||||
|
|
||||||
import './Timer.scss';
|
import './Timer.scss';
|
||||||
|
|
||||||
interface TimerProps {
|
export default function TimerLoader() {
|
||||||
customFields: CustomFields;
|
const { data, status } = useTimerData();
|
||||||
eventNext: OntimeEvent | null;
|
|
||||||
eventNow: OntimeEvent | null;
|
useWindowTitle('Timer');
|
||||||
general: ProjectData;
|
|
||||||
isMirrored: boolean;
|
if (status === 'pending') {
|
||||||
message: MessageState;
|
return <Loader />;
|
||||||
settings: Settings | undefined;
|
}
|
||||||
time: ViewExtendedTimer;
|
|
||||||
viewSettings: ViewSettings;
|
if (status === 'error') {
|
||||||
|
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Timer {...data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Timer({
|
function Timer({ customFields, projectData, isMirrored, settings, viewSettings }: TimerData) {
|
||||||
customFields,
|
const { eventNext, eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket();
|
||||||
eventNow,
|
|
||||||
eventNext,
|
|
||||||
general,
|
|
||||||
isMirrored,
|
|
||||||
message,
|
|
||||||
settings,
|
|
||||||
time,
|
|
||||||
viewSettings,
|
|
||||||
}: TimerProps) {
|
|
||||||
const auxTimer = useAuxTimersTime();
|
|
||||||
const {
|
const {
|
||||||
hideClock,
|
hideClock,
|
||||||
hideCards,
|
hideCards,
|
||||||
@@ -78,14 +74,12 @@ export default function Timer({
|
|||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const localisedMinutes = getLocalizedString('common.minutes');
|
const localisedMinutes = getLocalizedString('common.minutes');
|
||||||
|
|
||||||
useWindowTitle('Timer');
|
|
||||||
|
|
||||||
// gather modifiers
|
// gather modifiers
|
||||||
const viewTimerType = timerType ?? time.timerType;
|
const viewTimerType = timerType ?? timerTypeNow;
|
||||||
const showOverlay = getShowMessage(message.timer);
|
const showOverlay = getShowMessage(message.timer);
|
||||||
const { showEndMessage, showFinished, showWarning, showDanger } = getShowModifiers(
|
const { showEndMessage, showFinished, showWarning, showDanger } = getShowModifiers(
|
||||||
time.timerType,
|
timerTypeNow,
|
||||||
time.countToEnd,
|
countToEndNow,
|
||||||
time.phase,
|
time.phase,
|
||||||
freezeOvertime,
|
freezeOvertime,
|
||||||
freezeMessage,
|
freezeMessage,
|
||||||
@@ -107,8 +101,8 @@ export default function Timer({
|
|||||||
|
|
||||||
// gather timer data
|
// gather timer data
|
||||||
const totalTime = getTotalTime(time.duration, time.addedTime);
|
const totalTime = getTotalTime(time.duration, time.addedTime);
|
||||||
const clock = formatTime(time.clock);
|
const formattedClock = formatTime(clock);
|
||||||
const stageTimer = getTimerByType(freezeOvertime, time, timerType);
|
const stageTimer = getTimerByType(freezeOvertime, timerTypeNow, countToEndNow, clock, time, timerType);
|
||||||
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
|
const display = getFormattedTimer(stageTimer, viewTimerType, localisedMinutes, {
|
||||||
removeSeconds: hideTimerSeconds,
|
removeSeconds: hideTimerSeconds,
|
||||||
removeLeadingZero: removeLeadingZeros,
|
removeLeadingZero: removeLeadingZeros,
|
||||||
@@ -155,7 +149,7 @@ export default function Timer({
|
|||||||
className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])}
|
className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])}
|
||||||
style={userStyles}
|
style={userStyles}
|
||||||
>
|
>
|
||||||
{!hideLogo && general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
{!hideLogo && projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />}
|
||||||
|
|
||||||
<ViewParamsEditor target={OntimeView.Timer} viewOptions={timerOptions} />
|
<ViewParamsEditor target={OntimeView.Timer} viewOptions={timerOptions} />
|
||||||
|
|
||||||
@@ -172,7 +166,7 @@ export default function Timer({
|
|||||||
{showClock && (
|
{showClock && (
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
<SuperscriptTime time={clock} className='clock' />
|
<SuperscriptTime time={formattedClock} className='clock' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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<TimerData> {
|
||||||
|
// 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]),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,3 +4,22 @@ export type ViewData<T> = {
|
|||||||
data: T;
|
data: T;
|
||||||
status: QueryStatus;
|
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';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user