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, motion } from 'framer-motion';
import { AnimatePresence } from 'framer-motion';
import {
CustomFields,
MessageState,
OntimeEvent,
Playback,
ProjectData,
Settings,
SimpleTimerState,
TimerPhase,
TimerType,
ViewSettings,
} from 'ontime-types';
import { FitText } from '../../common/components/fit-text/FitText';
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 ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
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,
getPropertyValue,
getTimerByType,
isStringBoolean,
} from '../../features/viewers/common/viewUtils';
import { getFormattedTimer, getPropertyValue, getTimerByType } from '../../features/viewers/common/viewUtils';
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';
// motion
const titleVariants = {
hidden: {
x: -2500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -2500,
},
};
export const MotionTitleCard = motion(TitleCard);
interface TimerProps {
auxTimer: SimpleTimerState;
customFields: CustomFields;
@@ -69,170 +54,126 @@ export default function Timer(props: TimerProps) {
const { auxTimer, customFields, eventNow, eventNext, general, isMirrored, message, settings, time, viewSettings } =
props;
const {
hideClock,
hideCards,
hideProgress,
hideMessage,
hideTimerSeconds,
removeLeadingZeros,
mainSource,
secondarySource,
} = useTimerOptions();
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Timer');
// USER OPTIONS
const userOptions = {
hideClock: false,
hideCards: false,
hideProgress: false,
hideMessage: false,
hideTimerSeconds: false,
hideClockSeconds: false,
removeLeadingZeros: true,
};
// gather modifiers
const showOverlay = getShowMessage(message.timer);
const { showEndMessage, showFinished, showWarning, showDanger } = getShowModifiers(
time.timerType,
time.countToEnd,
time.phase,
viewSettings,
);
const isPlaying = getIsPlaying(time.playback);
const showClock = !hideClock && getShowClock(time.timerType);
const showProgressBar = !hideProgress && getShowProgressBar(time.timerType);
const hideClock = searchParams.get('hideClock');
userOptions.hideClock = isStringBoolean(hideClock);
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');
// gather card data
const mainFieldNow = getPropertyValue(eventNow, mainSource) ?? eventNow?.title ?? '';
const mainFieldNext = getPropertyValue(eventNext, mainSource) ?? eventNext?.title ?? '';
const secondaryTextNow = getPropertyValue(eventNow, secondarySource);
const secondaryTextNext = getPropertyValue(eventNext, secondarySource);
const main = searchParams.get('main');
const mainFieldNow = (main ? getPropertyValue(eventNow, main) : eventNow?.title) ?? '';
const mainFieldNext = (main ? getPropertyValue(eventNext, main) : eventNext?.title) ?? '';
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;
// gather timer data
const totalTime = getTotalTime(time.duration, time.addedTime);
const clock = formatTime(time.clock);
const stageTimer = getTimerByType(viewSettings.freezeEnd, time);
const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), {
removeSeconds: userOptions.hideTimerSeconds,
removeLeadingZero: userOptions.removeLeadingZeros,
removeSeconds: hideTimerSeconds,
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' : ''}`;
let timerFontSize = 89 / (stageTimerCharacters - 1);
// 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 presentation styles
const timerColour = getTimerColour(viewSettings, showWarning, showDanger);
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, Boolean(secondaryContent));
// gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const timerOptions = getTimerOptions(defaultFormat, customFields);
const disableProgress = timerIsTimeOfDay || time.timerType === TimerType.None;
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' />}
<ViewParamsEditor viewOptions={timerOptions} />
<div className={message.timer.blackout ? 'blackout blackout--active' : 'blackout'} />
{!userOptions.hideMessage && (
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
<FitText mode='multi' min={32} max={256} className={`message ${message.timer.blink ? 'blink' : ''}`}>
<div className={cx(['blackout', message.timer.blackout && 'blackout--active'])} />
{!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}
</FitText>
</div>
)}
{!userOptions.hideClock && (
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
{showClock && (
<div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={clock} className='clock' />
</div>
)}
<div className={timerContainerClasses}>
<div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}>
{showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
className={cx(['timer', !isPlaying && 'timer--paused', showFinished && 'timer--finished'])}
style={{
fontSize: `${timerFontSize}vw`,
'--phase-color': timerColor,
'--phase-color': timerColour,
}}
>
{display}
</div>
)}
<div
className={`secondary${secondaryContent ? '' : ' secondary--hidden'}`}
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
style={{ fontSize: `${externalFontSize}vw` }}
>
{secondaryContent}
</div>
</div>
{!userOptions.hideProgress && (
{showProgressBar && (
<MultiPartProgressBar
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
now={disableProgress ? null : time.current}
className={cx(['progress-container', !isPlaying && 'progress-container--paused'])}
now={time.current}
complete={totalTime}
normalColor={viewSettings.normalColor}
warning={eventNow?.timeWarning}
warningColor={viewSettings.warningColor}
danger={eventNow?.timeDanger}
dangerColor={viewSettings.dangerColor}
hidden={!showProgress}
/>
)}
{!userOptions.hideCards && (
{!hideCards && (
<>
<AnimatePresence>
{eventNow?.title && (
@@ -253,6 +194,7 @@ export default function Timer(props: TimerProps) {
<AnimatePresence>
{eventNext?.title && (
<MotionTitleCard
className='event next'
key='next'
variants={titleVariants}
initial='hidden'
@@ -261,7 +203,6 @@ export default function Timer(props: TimerProps) {
label='next'
title={mainFieldNext}
secondary={secondaryTextNext}
className='event next'
/>
)}
</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 {
getTimeOption,
@@ -7,6 +9,7 @@ import {
showLeadingZeros,
} from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types';
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
@@ -59,17 +62,58 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
},
{
id: 'hideMessage',
title: 'Hide Presenter Message',
description: 'Prevents the screen from displaying messages from the presenter',
title: 'Hide Timer Message',
description: 'Prevents displaying fullscreen messages in the timer',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideExternal',
title: 'Hide External',
description: 'Prevents the screen from displaying the external field',
title: 'Hide Auxiliary timer / External message',
description: 'Prevents the screen from displaying the secondary timer field',
type: 'boolean',
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;
}