refactor: extract and simplify logic

This commit is contained in:
Carlos Valente
2025-01-18 22:02:47 +01:00
committed by Carlos Valente
parent 49cae647d6
commit 3f35b5e297
4 changed files with 275 additions and 142 deletions
+78 -137
View File
@@ -1,57 +1,42 @@
import { useSearchParams } from 'react-router-dom'; import { AnimatePresence } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { import {
CustomFields, CustomFields,
MessageState, MessageState,
OntimeEvent, OntimeEvent,
Playback,
ProjectData, ProjectData,
Settings, Settings,
SimpleTimerState, SimpleTimerState,
TimerPhase,
TimerType,
ViewSettings, ViewSettings,
} from 'ontime-types'; } 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 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 { useWindowTitle } from '../../common/hooks/useWindowTitle'; import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
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 { import { getFormattedTimer, getPropertyValue, getTimerByType } from '../../features/viewers/common/viewUtils';
getFormattedTimer,
getPropertyValue,
getTimerByType,
isStringBoolean,
} from '../../features/viewers/common/viewUtils';
import { useTranslation } from '../../translation/TranslationProvider'; import { useTranslation } from '../../translation/TranslationProvider';
import { getTimerOptions } from './timer.options'; import { MotionTitleCard, titleVariants } from './timer.animations';
import { getTimerOptions, useTimerOptions } from './timer.options';
import {
getEstimatedFontSize,
getIsPlaying,
getSecondaryDisplay,
getShowClock,
getShowMessage,
getShowModifiers,
getShowProgressBar,
getTimerColour,
getTotalTime,
} from './timer.utils';
import './Timer.scss'; import './Timer.scss';
// motion
const titleVariants = {
hidden: {
x: -2500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -2500,
},
};
export const MotionTitleCard = motion(TitleCard);
interface TimerProps { interface TimerProps {
auxTimer: SimpleTimerState; auxTimer: SimpleTimerState;
customFields: CustomFields; customFields: CustomFields;
@@ -69,170 +54,126 @@ export default function Timer(props: TimerProps) {
const { auxTimer, customFields, eventNow, eventNext, general, isMirrored, message, settings, time, viewSettings } = const { auxTimer, customFields, eventNow, eventNext, general, isMirrored, message, settings, time, viewSettings } =
props; props;
const {
hideClock,
hideCards,
hideProgress,
hideMessage,
hideTimerSeconds,
removeLeadingZeros,
mainSource,
secondarySource,
} = useTimerOptions();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Timer'); useWindowTitle('Timer');
// USER OPTIONS // gather modifiers
const userOptions = { const showOverlay = getShowMessage(message.timer);
hideClock: false, const { showEndMessage, showFinished, showWarning, showDanger } = getShowModifiers(
hideCards: false, time.timerType,
hideProgress: false, time.countToEnd,
hideMessage: false, time.phase,
hideTimerSeconds: false, viewSettings,
hideClockSeconds: false, );
removeLeadingZeros: true, const isPlaying = getIsPlaying(time.playback);
}; const showClock = !hideClock && getShowClock(time.timerType);
const showProgressBar = !hideProgress && getShowProgressBar(time.timerType);
const hideClock = searchParams.get('hideClock'); // gather card data
userOptions.hideClock = isStringBoolean(hideClock); const mainFieldNow = getPropertyValue(eventNow, mainSource) ?? eventNow?.title ?? '';
const mainFieldNext = getPropertyValue(eventNext, mainSource) ?? eventNext?.title ?? '';
const hideCards = searchParams.get('hideCards');
userOptions.hideCards = isStringBoolean(hideCards);
const hideProgress = searchParams.get('hideProgress');
userOptions.hideProgress = isStringBoolean(hideProgress);
const hideMessage = searchParams.get('hideMessage');
userOptions.hideMessage = isStringBoolean(hideMessage);
const hideClockSeconds = searchParams.get('hideClockSeconds');
userOptions.hideClockSeconds = isStringBoolean(hideClockSeconds);
const clock = formatTime(time.clock);
const hideTimerSeconds = searchParams.get('hideTimerSeconds');
userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds);
const showLeadingZeros = searchParams.get('showLeadingZeros');
userOptions.removeLeadingZeros = !isStringBoolean(showLeadingZeros);
const secondarySource = searchParams.get('secondary-src');
const secondaryTextNow = getPropertyValue(eventNow, secondarySource); const secondaryTextNow = getPropertyValue(eventNow, secondarySource);
const secondaryTextNext = getPropertyValue(eventNext, secondarySource); const secondaryTextNext = getPropertyValue(eventNext, secondarySource);
const main = searchParams.get('main'); // gather timer data
const mainFieldNow = (main ? getPropertyValue(eventNow, main) : eventNow?.title) ?? ''; const totalTime = getTotalTime(time.duration, time.addedTime);
const mainFieldNext = (main ? getPropertyValue(eventNext, main) : eventNext?.title) ?? ''; const clock = formatTime(time.clock);
const showOverlay = message.timer.text !== '' && message.timer.visible;
const isPlaying = time.playback !== Playback.Pause;
const timerIsTimeOfDay = time.timerType === TimerType.Clock;
const finished = time.phase === TimerPhase.Overtime;
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.countToEnd;
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage;
const showProgress =
eventNow !== null &&
time.timerType !== TimerType.None &&
time.timerType !== TimerType.Clock &&
time.playback !== Playback.Stop;
const showFinished = shouldShowModifiers && finished && (shouldShowModifiers || showEndMessage);
const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning;
const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger;
const showClock = time.timerType !== TimerType.Clock;
const secondaryContent = ((): string | undefined => {
if (message.timer.secondarySource === 'aux') {
return getFormattedTimer(auxTimer.current, TimerType.CountDown, getLocalizedString('common.minutes'), {
removeSeconds: userOptions.hideTimerSeconds,
removeLeadingZero: userOptions.removeLeadingZeros,
});
}
if (message.timer.secondarySource === 'external' && message.external) {
return message.external;
}
return;
})();
let timerColor = viewSettings.normalColor;
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor;
const stageTimer = getTimerByType(viewSettings.freezeEnd, time); const stageTimer = getTimerByType(viewSettings.freezeEnd, time);
const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), { const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), {
removeSeconds: userOptions.hideTimerSeconds, removeSeconds: hideTimerSeconds,
removeLeadingZero: userOptions.removeLeadingZeros, removeLeadingZero: removeLeadingZeros,
}); });
const stageTimerCharacters = display.replace('/:/g', '').length; const secondaryContent = getSecondaryDisplay(
message,
auxTimer.current,
getLocalizedString('common.minutes'),
hideTimerSeconds,
removeLeadingZeros,
);
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`; // gather presentation styles
const timerColour = getTimerColour(viewSettings, showWarning, showDanger);
let timerFontSize = 89 / (stageTimerCharacters - 1); const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, Boolean(secondaryContent));
// we need to shrink the timer if the external is going to be there
if (secondaryContent) {
timerFontSize *= 0.8;
}
const externalFontSize = timerFontSize * 0.4;
const timerContainerClasses = `timer-container ${message.timer.blink ? (showOverlay ? '' : 'blink') : ''}`;
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
// gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat); const defaultFormat = getDefaultFormat(settings?.timeFormat);
const timerOptions = getTimerOptions(defaultFormat, customFields); const timerOptions = getTimerOptions(defaultFormat, customFields);
const disableProgress = timerIsTimeOfDay || time.timerType === TimerType.None;
return ( return (
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'> <div
className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])}
data-testid='timer-view'
>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />}
<ViewParamsEditor viewOptions={timerOptions} /> <ViewParamsEditor viewOptions={timerOptions} />
<div className={message.timer.blackout ? 'blackout blackout--active' : 'blackout'} />
{!userOptions.hideMessage && ( <div className={cx(['blackout', message.timer.blackout && 'blackout--active'])} />
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
<FitText mode='multi' min={32} max={256} className={`message ${message.timer.blink ? 'blink' : ''}`}> {!hideMessage && (
<div className={cx(['message-overlay', showOverlay && ' message-overlay--active'])}>
<FitText mode='multi' min={32} max={256} className={cx(['message', message.timer.blink && 'blink'])}>
{message.timer.text} {message.timer.text}
</FitText> </FitText>
</div> </div>
)} )}
{!userOptions.hideClock && ( {showClock && (
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}> <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={clock} className='clock' />
</div> </div>
)} )}
<div className={timerContainerClasses}> <div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}>
{showEndMessage ? ( {showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div> <div className='end-message'>{viewSettings.endMessage}</div>
) : ( ) : (
<div <div
className={timerClasses} className={cx(['timer', !isPlaying && 'timer--paused', showFinished && 'timer--finished'])}
style={{ style={{
fontSize: `${timerFontSize}vw`, fontSize: `${timerFontSize}vw`,
'--phase-color': timerColor, '--phase-color': timerColour,
}} }}
> >
{display} {display}
</div> </div>
)} )}
<div <div
className={`secondary${secondaryContent ? '' : ' secondary--hidden'}`} className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
style={{ fontSize: `${externalFontSize}vw` }} style={{ fontSize: `${externalFontSize}vw` }}
> >
{secondaryContent} {secondaryContent}
</div> </div>
</div> </div>
{!userOptions.hideProgress && ( {showProgressBar && (
<MultiPartProgressBar <MultiPartProgressBar
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'} className={cx(['progress-container', !isPlaying && 'progress-container--paused'])}
now={disableProgress ? null : time.current} now={time.current}
complete={totalTime} complete={totalTime}
normalColor={viewSettings.normalColor} normalColor={viewSettings.normalColor}
warning={eventNow?.timeWarning} warning={eventNow?.timeWarning}
warningColor={viewSettings.warningColor} warningColor={viewSettings.warningColor}
danger={eventNow?.timeDanger} danger={eventNow?.timeDanger}
dangerColor={viewSettings.dangerColor} dangerColor={viewSettings.dangerColor}
hidden={!showProgress}
/> />
)} )}
{!userOptions.hideCards && ( {!hideCards && (
<> <>
<AnimatePresence> <AnimatePresence>
{eventNow?.title && ( {eventNow?.title && (
@@ -253,6 +194,7 @@ export default function Timer(props: TimerProps) {
<AnimatePresence> <AnimatePresence>
{eventNext?.title && ( {eventNext?.title && (
<MotionTitleCard <MotionTitleCard
className='event next'
key='next' key='next'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
@@ -261,7 +203,6 @@ export default function Timer(props: TimerProps) {
label='next' label='next'
title={mainFieldNext} title={mainFieldNext}
secondary={secondaryTextNext} secondary={secondaryTextNext}
className='event next'
/> />
)} )}
</AnimatePresence> </AnimatePresence>
@@ -0,0 +1,20 @@
import { motion } from 'framer-motion';
import TitleCard from '../../common/components/title-card/TitleCard';
export const titleVariants = {
hidden: {
x: -2500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -2500,
},
};
export const MotionTitleCard = motion(TitleCard);
+49 -5
View File
@@ -1,4 +1,6 @@
import { CustomFields } from 'ontime-types'; import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { CustomFields, OntimeEvent } from 'ontime-types';
import { import {
getTimeOption, getTimeOption,
@@ -7,6 +9,7 @@ import {
showLeadingZeros, showLeadingZeros,
} from '../../common/components/view-params-editor/constants'; } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types'; import { ViewOption } from '../../common/components/view-params-editor/types';
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
@@ -59,17 +62,58 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
}, },
{ {
id: 'hideMessage', id: 'hideMessage',
title: 'Hide Presenter Message', title: 'Hide Timer Message',
description: 'Prevents the screen from displaying messages from the presenter', description: 'Prevents displaying fullscreen messages in the timer',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}, },
{ {
id: 'hideExternal', id: 'hideExternal',
title: 'Hide External', title: 'Hide Auxiliary timer / External message',
description: 'Prevents the screen from displaying the external field', description: 'Prevents the screen from displaying the secondary timer field',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}, },
]; ];
}; };
type TimerOptions = {
hideClock: boolean;
hideCards: boolean;
hideProgress: boolean;
hideMessage: boolean;
hideExternal: boolean;
hideTimerSeconds: boolean;
removeLeadingZeros: boolean;
mainSource: keyof OntimeEvent | null;
secondarySource: keyof OntimeEvent | null;
};
/**
* Utility extract the view options from URL Params
* the names and fallbacks are manually matched with timerOptions
*/
function getOptionsFromParams(searchParams: URLSearchParams): TimerOptions {
// we manually make an object that matches the key above
return {
hideClock: isStringBoolean(searchParams.get('hideClock')),
hideCards: isStringBoolean(searchParams.get('hideCards')),
hideProgress: isStringBoolean(searchParams.get('hideProgress')),
hideMessage: isStringBoolean(searchParams.get('hideMessage')),
hideExternal: isStringBoolean(searchParams.get('hideExternal')),
hideTimerSeconds: isStringBoolean(searchParams.get('hideTimerSeconds')),
removeLeadingZeros: !isStringBoolean(searchParams.get('showLeadingZeros')),
mainSource: searchParams.get('main') as keyof OntimeEvent | null,
secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null,
};
}
/**
* Hook exposes the timer view options
*/
export function useTimerOptions(): TimerOptions {
const [searchParams] = useSearchParams();
const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
return options;
}
+128
View File
@@ -0,0 +1,128 @@
import { MaybeNumber, MessageState, Playback, TimerMessage, TimerPhase, TimerType, ViewSettings } from 'ontime-types';
import { getFormattedTimer } from '../../features/viewers/common/viewUtils';
/**
* Whether a message should be shown
*/
export function getShowMessage(message: TimerMessage): boolean {
return message.text !== '' && message.visible;
}
/**
* Whether the playback is playing
*/
export function getIsPlaying(playback: Playback): boolean {
return playback === Playback.Play || playback === Playback.Roll;
}
/**
* Gets the total time from the duration and added time of an event
*/
export function getTotalTime(duration: MaybeNumber, addedTime: MaybeNumber): number {
return (duration ?? 0) + (addedTime ?? 0);
}
/**
* Whether the progress bar should be shown for this timer type
*/
export function getShowProgressBar(timerType: TimerType) {
return timerType !== TimerType.None && timerType !== TimerType.Clock;
}
/**
* Whether the clock should be shown with this timer type
*/
export function getShowClock(timerType: TimerType) {
return timerType !== TimerType.Clock;
}
const fontSizeMap: { [key: number]: number } = {
4: 28, // 9:01
5: 28, // -9:01, 10:01, 9 min
6: 25, // -10:01, 10 min
8: 20, // 23:01:01
9: 20, // -23:01:01
};
/**
* Finds a font size that fits the timer in the screen
* Unfortunately hand tweaked
*/
export function getEstimatedFontSize(stageTimer: string, secondaryContent?: string) {
const stageTimerCharacters = stageTimer.length;
let timerFontSize = (100 / (stageTimerCharacters - 1)) * 1.25;
if (fontSizeMap[stageTimerCharacters]) {
timerFontSize = fontSizeMap[stageTimerCharacters];
}
let externalFontSize = timerFontSize * 0.2;
if (secondaryContent) {
// we need to shrink the timer if the external is going to be there
// this number has been tweaked to fit in a landscape mobile screen
timerFontSize *= 0.6;
if (secondaryContent.length > 25) {
externalFontSize = (100 / (secondaryContent.length - 1)) * 1.8;
}
}
return {
timerFontSize,
externalFontSize,
};
}
/**
* which, if any, modifier should be shown at any time
*/
export function getShowModifiers(
timerType: TimerType,
countToEnd: boolean,
phase: TimerPhase,
viewSettings: ViewSettings,
) {
const showModifiers = timerType === TimerType.CountDown || countToEnd;
const finished = phase === TimerPhase.Overtime;
return {
showEndMessage: showModifiers && finished && viewSettings.endMessage,
showFinished: showModifiers && finished, // ????
showWarning: showModifiers && phase === TimerPhase.Warning,
showDanger: showModifiers && phase === TimerPhase.Danger,
};
}
/**
* Which colour should the timer have at a given moment
*/
export function getTimerColour(viewSettings: ViewSettings, showWarning: boolean, showDanger: boolean) {
if (showWarning) return viewSettings.warningColor;
if (showDanger) return viewSettings.dangerColor;
return viewSettings.normalColor;
}
/**
* What, if anything, should be displayed in the secondary field
*/
export function getSecondaryDisplay(
message: MessageState,
currentAux: MaybeNumber,
localisedMinutes: string,
removeSeconds: boolean,
removeLeadingZero: boolean,
hideExternal: boolean,
): string | undefined {
if (hideExternal) {
return;
}
if (message.timer.secondarySource === 'aux') {
return getFormattedTimer(currentAux, TimerType.CountDown, localisedMinutes, {
removeSeconds,
removeLeadingZero,
});
}
if (message.timer.secondarySource === 'external' && message.external) {
return message.external;
}
return;
}