chore: directory restructure

This commit is contained in:
Carlos Valente
2025-01-18 14:29:41 +01:00
committed by Carlos Valente
parent 0ea00ff6be
commit b5cf6c4bd1
4 changed files with 20 additions and 15 deletions
+205
View File
@@ -0,0 +1,205 @@
@use '../../theme/viewerDefs' as *;
.stage-timer {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
transition: opacity 0.5s ease-in-out;
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
gap: min(2vh, 16px);
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
display: grid;
grid-template-columns: 100%;
grid-template-rows: auto 1fr auto auto auto;
grid-template-areas:
'clock'
'timer'
'progress'
'now'
'next';
&--finished {
outline: clamp(4px, 1vw, 16px) solid $timer-finished-color;
outline-offset: calc(clamp(4px, 1vw, 16px) * -1);
transition: $viewer-transition-time;
}
.blackout {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000;
z-index: 2;
opacity: 0;
transition: opacity $viewer-transition-time;
&--active {
opacity: 1;
}
}
/* =================== CLOCK ===================*/
.clock-container {
grid-area: clock;
margin-left: auto;
font-weight: 600;
transition: opacity $viewer-transition-time;
.label {
font-size: clamp(16px, 1.5vw, 24px);
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
.clock {
font-size: clamp(32px, 3.5vw, 50px);
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
line-height: 0.95em;
}
&--hidden {
opacity: 0;
}
}
/* =================== TITLES ===================*/
.event {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
padding: 16px 24px;
border-radius: 8px;
&.now {
grid-area: now;
}
&.next {
grid-area: next;
}
}
/* =================== MAIN ===================*/
.timer-container {
grid-area: timer;
justify-self: center;
align-self: center;
width: 100%;
overflow: hidden;
.end-message {
text-align: center;
font-size: 11.5vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}
.timer {
opacity: 1;
font-family: var(--font-family-override, $viewer-font-family);
color: var(--timer-color-override, var(--phase-color));
line-height: 0.9em;
text-align: center;
letter-spacing: 0.05em;
font-weight: 600;
transition-property: font-size;
transition-duration: $viewer-transition-time;
&--paused {
opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time;
}
&--finished {
color: $timer-finished-color;
}
}
}
.secondary {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 0.25em;
padding-block: 0.25em;
font-weight: 600;
text-align: center;
color: var(--external-color-override, $external-color);
letter-spacing: 0.5px;
line-height: 0.9em;
transition-property: opacity, height;
transition-duration: $viewer-transition-time;
border-top: 1px solid color-mix(in srgb, var(--external-color-override, $external-color) 10%, transparent);
&--hidden {
opacity: 0;
height: 0;
}
}
.progress-container {
grid-area: progress;
width: 100%;
margin: 0 auto;
opacity: 1;
transition: $viewer-transition-time;
&--paused {
opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time;
}
}
/* =================== OVERLAY ===================*/
.message-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 2vw;
background: var(--background-color-override, $viewer-background-color);
opacity: 0;
transition: opacity $viewer-transition-time;
z-index: 2;
&--active {
opacity: 1;
}
}
.message {
display: grid;
place-content: center;
height: 100%;
width: 100%;
color: var(--color-override, $viewer-color);
text-align: center;
font-weight: 600;
}
.logo {
position: absolute;
top: 2vw;
left: 2vw;
max-width: min(200px, 20vw);
max-height: min(100px, 20vh);
}
}
+272
View File
@@ -0,0 +1,272 @@
import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } 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 { 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 { useTranslation } from '../../translation/TranslationProvider';
import { getTimerOptions } from './timer.options';
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;
eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null;
general: ProjectData;
isMirrored: boolean;
message: MessageState;
settings: Settings | undefined;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
}
export default function Timer(props: TimerProps) {
const { auxTimer, customFields, eventNow, eventNext, general, isMirrored, message, settings, time, viewSettings } =
props;
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,
};
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');
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;
const stageTimer = getTimerByType(viewSettings.freezeEnd, time);
const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), {
removeSeconds: userOptions.hideTimerSeconds,
removeLeadingZero: userOptions.removeLeadingZeros,
});
const stageTimerCharacters = display.replace('/:/g', '').length;
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' : ''}`;
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'>
{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' : ''}`}>
{message.timer.text}
</FitText>
</div>
)}
{!userOptions.hideClock && (
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={clock} className='clock' />
</div>
)}
<div className={timerContainerClasses}>
{showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
style={{
fontSize: `${timerFontSize}vw`,
'--phase-color': timerColor,
}}
>
{display}
</div>
)}
<div
className={`secondary${secondaryContent ? '' : ' secondary--hidden'}`}
style={{ fontSize: `${externalFontSize}vw` }}
>
{secondaryContent}
</div>
</div>
{!userOptions.hideProgress && (
<MultiPartProgressBar
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
now={disableProgress ? null : time.current}
complete={totalTime}
normalColor={viewSettings.normalColor}
warning={eventNow?.timeWarning}
warningColor={viewSettings.warningColor}
danger={eventNow?.timeDanger}
dangerColor={viewSettings.dangerColor}
hidden={!showProgress}
/>
)}
{!userOptions.hideCards && (
<>
<AnimatePresence>
{eventNow?.title && (
<MotionTitleCard
className='event now'
key='now'
variants={titleVariants}
initial='hidden'
animate='visible'
exit='exit'
label='now'
title={mainFieldNow}
secondary={secondaryTextNow}
/>
)}
</AnimatePresence>
<AnimatePresence>
{eventNext?.title && (
<MotionTitleCard
key='next'
variants={titleVariants}
initial='hidden'
animate='visible'
exit='exit'
label='next'
title={mainFieldNext}
secondary={secondaryTextNext}
className='event next'
/>
)}
</AnimatePresence>
</>
)}
</div>
);
}
@@ -0,0 +1,75 @@
import { CustomFields } from 'ontime-types';
import {
getTimeOption,
hideTimerSeconds,
makeOptionsFromCustomFields,
showLeadingZeros,
} from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types';
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
const secondaryOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
return [
{ section: 'Clock Options' },
getTimeOption(timeFormat),
{ section: 'Timer Options' },
hideTimerSeconds,
showLeadingZeros,
{ section: 'Data sources' },
{
id: 'main',
title: 'Main text',
description: 'Select the data source for the main text',
type: 'option',
values: mainOptions,
defaultValue: 'Title',
},
{
id: 'secondary-src',
title: 'Secondary text',
description: 'Select the data source for the secondary text',
type: 'option',
values: secondaryOptions,
defaultValue: '',
},
{ section: 'Element visibility' },
{
id: 'hideClock',
title: 'Hide Time Now',
description: 'Hides the Time Now field',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideCards',
title: 'Hide Cards',
description: 'Hides the Now and Next cards',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideProgress',
title: 'Hide progress bar',
description: 'Hides the progress bar',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideMessage',
title: 'Hide Presenter Message',
description: 'Prevents the screen from displaying messages from the presenter',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideExternal',
title: 'Hide External',
description: 'Prevents the screen from displaying the external field',
type: 'boolean',
defaultValue: false,
},
];
};