refactor: remove deprecated views

This commit is contained in:
Carlos Valente
2025-07-12 12:22:45 +02:00
parent 1c6adbd2fd
commit 2aac4cb06d
41 changed files with 32 additions and 1153 deletions
-2
View File
@@ -95,8 +95,6 @@ You can access the different views by reaching the ontime server, in your browse
For the backstage views
-------------------------------------------------------------
IP.ADDRESS:4001/timer > Presenter / Stage timer view
IP.ADDRESS:4001/minimal > Simple timer view
IP.ADDRESS:4001/clock > Simple clock view
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
IP.ADDRESS:4001/countdown > Countdown to anything
IP.ADDRESS:4001/studio > Studio Clock
+1 -1
View File
@@ -16,7 +16,7 @@
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
background-color: #101010 !important;
}
</style>
</head>
-24
View File
@@ -23,23 +23,17 @@ const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
const TimerView = React.lazy(() => import('./views/timer/Timer'));
const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
const ClockView = React.lazy(() => import('./features/viewers/clock/Clock'));
const Countdown = React.lazy(() => import('./views/countdown/Countdown'));
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
const StudioClock = React.lazy(() => import('./views/studio/Studio'));
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
const STimer = withPreset(withData(TimerView));
const SMinimalTimer = withPreset(withData(MinimalTimerView));
const SClock = withPreset(withData(ClockView));
const SCountdown = withPreset(withData(Countdown));
const SBackstage = withPreset(withData(Backstage));
const SProjectInfo = withPreset(ProjectInfo); // NOTE: ProjectInfo does not use the viewWrapper since it has no options
const SLowerThird = withPreset(withData(Lower));
const SStudio = withPreset(withData(StudioClock));
const STimeline = withPreset(withData(Timeline));
const PCuesheet = withPreset(Cuesheet);
@@ -86,22 +80,6 @@ export default function AppRouter() {
</ViewLoader>
}
/>
<Route
path='/minimal'
element={
<ViewLoader>
<SMinimalTimer />
</ViewLoader>
}
/>
<Route
path='/clock'
element={
<ViewLoader>
<SClock />
</ViewLoader>
}
/>
<Route
path='/countdown'
element={
@@ -126,8 +104,6 @@ export default function AppRouter() {
</ViewLoader>
}
/>
{/*/!* Lower third cannot have a loading screen *!/*/}
<Route path='/lower' element={<SLowerThird />} />
<Route
path='/timeline'
element={
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
@import './BaseButtonStyles.module.scss';
.baseButton {
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
@import './BaseButtonStyles.module.scss';
.baseIconButton {
@@ -91,12 +91,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
<span className={style.label}>Enter custom path</span>
<label className={style.textEntry}>
{origin}
<Input
placeholder='eg. /minimal?key=0000ffff'
fluid
value={path}
onChange={(event) => setPath(event.target.value)}
/>
<Input placeholder='eg. /timer' fluid value={path} onChange={(event) => setPath(event.target.value)} />
</label>
<Button
variant='primary'
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$progress-bar-size: 1.5rem;
$progress-bar-br: 3px;
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$progress-bar-size: 12px;
$progress-bar-br: 3px;
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
/* share the same style as a page layout */
.page {
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.title-card {
position: relative;
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { IoChevronDown } from 'react-icons/io5';
import { useLocalStorage } from '@mantine/hooks';
@@ -15,7 +16,8 @@ interface ViewParamsSectionProps {
options: ParamField[];
}
export default function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) {
export default memo(ViewParamsSection);
function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) {
const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
const handleCollapse = () => {
@@ -1,4 +1,4 @@
@use '../../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.timer {
grid-area: timer;
@@ -1,38 +0,0 @@
@use '../../../theme/viewerDefs' as *;
.clock-view {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
display: grid;
place-content: center;
.clock {
font-family: var(--font-family-bold-override, $timer-bold-font-family);
font-size: 20vw;
position: relative;
color: var(--timer-color-override, $timer-color);
letter-spacing: 0.05em;
}
.logo {
position: absolute;
top: 2vw;
left: 2vw;
max-width: min(200px, 20vw);
}
}
/* =================== MOBILE ===================*/
@media screen and (max-width: 768px) {
.clock-view {
.logo img {
height: min(50px, 10vh);
}
}
}
@@ -1,144 +0,0 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ProjectData, Settings } from 'ontime-types';
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 { OverridableOptions } from '../../../common/models/View.types';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import { getClockOptions } from './clock.options';
import './Clock.scss';
interface ClockProps {
general: ProjectData;
isMirrored: boolean;
time: ViewExtendedTimer;
settings: Settings | undefined;
}
export default function Clock(props: ClockProps) {
const { general, isMirrored, time, settings } = props;
const [searchParams] = useSearchParams();
useWindowTitle('Clock');
// get config from url: key, text, font, size, hidenav
// eg. http://localhost:3000/clock?key=f00&text=fff
// Check for user options
const userOptions: OverridableOptions = {
size: 1,
};
// key: string
// Should be a hex string '#00FF00' with key colour
const key = searchParams.get('key');
if (key) {
userOptions.keyColour = `#${key}`;
}
// textColour: string
// Should be a hex string '#ffffff'
const textColour = searchParams.get('text');
if (textColour) {
userOptions.textColour = `#${textColour}`;
}
// textBackground: string
// Should be a hex string '#ffffff'
const textBackground = searchParams.get('textbg');
if (textBackground) {
userOptions.textBackground = `#${textBackground}`;
}
// font: string
// Should be a string with a font name 'arial'
const font = searchParams.get('font');
if (font) {
userOptions.font = font;
}
// size: multiplier
// Should be a number 0.0-n
const size = searchParams.get('size');
if (size !== null && typeof size !== 'undefined') {
if (!Number.isNaN(Number(size))) {
userOptions.size = Number(size);
}
}
// alignX: flex justification
// start | center | end
const alignX = searchParams.get('alignx');
if (alignX) {
if (alignX === 'start' || alignX === 'center' || alignX === 'end') {
userOptions.justifyContent = alignX;
}
}
// alignX: flex alignment
// start | center | end
const alignY = searchParams.get('aligny');
if (alignY) {
if (alignY === 'start' || alignY === 'center' || alignY === 'end') {
userOptions.alignItems = alignY;
}
}
// offsetX: position in pixels
// Should be a number 0 - 1920
const offsetX = searchParams.get('offsetx');
if (offsetX) {
const pixels = Number(offsetX);
if (!isNaN(pixels)) {
userOptions.left = `${pixels}px`;
}
}
// offsetX: position in pixels
// Should be a number 0 - 1920
const offsetY = searchParams.get('offsety');
if (offsetY) {
const pixels = Number(offsetY);
if (!isNaN(pixels)) {
userOptions.top = `${pixels}px`;
}
}
const clock = formatTime(time.clock);
const clean = clock.replace('/:/g', '');
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const clockOptions = useMemo(() => getClockOptions(defaultFormat), [defaultFormat]);
return (
<div
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
style={{
backgroundColor: userOptions.keyColour,
justifyContent: userOptions.justifyContent,
alignContent: userOptions.alignItems,
}}
data-testid='clock-view'
>
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
<ViewParamsEditor viewOptions={clockOptions} />
<SuperscriptTime
time={clock}
className='clock'
style={{
color: userOptions.textColour,
fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`,
fontFamily: userOptions.font,
top: userOptions.top,
left: userOptions.left,
backgroundColor: userOptions.textBackground,
}}
/>
</div>
);
}
@@ -1,86 +0,0 @@
import { getTimeOption } from '../../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
export const getClockOptions = (timeFormat: string): ViewOption[] => [
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
{
title: OptionTitle.ClockOptions,
collapsible: true,
options: [
{
id: 'key',
title: 'Key Colour',
description: 'Background or key colour for entire view. Default: #000000',
type: 'colour',
defaultValue: '000000',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour. Default: #FFFFFF',
type: 'colour',
defaultValue: 'FFFFFF',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Background colour for timer text. Default: #FFF0 (transparent)',
type: 'colour',
defaultValue: 'FFF0',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
placeholder: '1 (default)',
},
{
id: 'alignx',
title: 'Align Horizontal',
description: 'Moves the horizontally in page to start = left | center | end = right',
type: 'option',
values: [
{ value: 'start', label: 'Start' },
{ value: 'center', label: 'Center' },
{ value: 'end', label: 'End' },
],
defaultValue: 'center',
},
{
id: 'offsetx',
title: 'Offset Horizontal',
description: 'Offsets the timer horizontal position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
{
id: 'aligny',
title: 'Align Vertical',
description: 'Moves the vertically in page to start = left | center | end = right',
type: 'option',
values: [
{ value: 'start', label: 'Start' },
{ value: 'center', label: 'Center' },
{ value: 'end', label: 'End' },
],
defaultValue: 'center',
},
{
id: 'offsety',
title: 'Offset Vertical',
description: 'Offsets the timer vertical position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
],
},
];
@@ -1,60 +0,0 @@
.lower-third {
margin: 0;
width: 100%;
height: 100%;
.container {
position: absolute;
display: block;
top: 75vh;
line-height: normal;
.line {
width: 100%;
height: var(--lowerThird-line-height-override, 0.5vh);
}
.clip {
overflow: hidden;
}
.data-bottom,
.data-top {
padding: 0 3vw;
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
text-align: var(--lowerThird-text-align-override, right);
white-space: nowrap;
@include ellipsis-text();
&::after {
content: '\200b';
}
}
.data-top {
font-weight: var(--lowerThird-top-font-weight-override, 600);
font-style: var(--lowerThird-top-font-style-override, normal);
}
.data-bottom {
font-weight: var(--lowerThird-bottom-font-weight-override, 540);
font-style: var(--lowerThird-bottom-font-style-override, normal);
}
&--in {
transition-timing-function: cubic-bezier(0.25, 0.5, 0.5, 1);
}
&--out {
transition-timing-function: cubic-bezier(0.5, 0, 0.75, 0.5);
transform: translateX(-100%);
opacity: 0;
.data-top {
transform: translateY(100%);
}
.data-bottom {
transform: translateY(-100%);
}
}
}
}
@@ -1,149 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CustomFields, OntimeEvent, ViewSettings } from 'ontime-types';
import { isPlaybackActive, MILLIS_PER_SECOND } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { getPropertyValue } from '../common/viewUtils';
import { getLowerThirdOptions, useLowerOptions } from './lowerThird.options';
import './LowerThird.scss';
interface LowerProps {
customFields: CustomFields;
eventNow: OntimeEvent | null;
viewSettings: ViewSettings;
time: ViewExtendedTimer;
}
export default function LowerThird(props: LowerProps) {
const { customFields, eventNow, viewSettings, time } = props;
const previousId = useRef<string>();
const animationTimeout = useRef<NodeJS.Timeout>();
const [playState, setPlayState] = useState<boolean>(false);
const [textValue, setTextValue] = useState<{ top: string; bottom: string }>({ top: '', bottom: '' });
useRuntimeStylesheet(viewSettings?.overrideStyles ? overrideStylesURL : undefined);
const options = useLowerOptions();
const { playback } = time;
useWindowTitle('Lower Third');
// on unmount, cancel any ongoing animations
useEffect(() => {
// if hold is negative then force animate in
if (options.hold < 0) {
clearTimeout(animationTimeout.current);
setTextValue({
top: getPropertyValue(eventNow, options.topSrc) ?? '',
bottom: getPropertyValue(eventNow, options.bottomSrc) ?? '',
});
setPlayState(true);
return;
}
}, [eventNow, options.bottomSrc, options.hold, options.topSrc]);
const animateIn = useCallback(() => {
// if hold skip
if (options.hold < 0) return;
//clear any pending timeouts
clearTimeout(animationTimeout.current);
// set the values
setTextValue({
top: getPropertyValue(eventNow, options.topSrc) ?? '',
bottom: getPropertyValue(eventNow, options.bottomSrc) ?? '',
});
// start animation
setPlayState(true);
// reschedule out animation, should animate out after the in animation time + hold time
setTimeout(() => setPlayState(false), (options.hold + options.transitionIn) * MILLIS_PER_SECOND);
}, [eventNow, options.bottomSrc, options.hold, options.topSrc, options.transitionIn]);
const animateOut = useCallback(() => {
if (options.hold < 0) return; // if hold is negative then we never animate out
//clear any pending timeouts
clearTimeout(animationTimeout.current);
// start animation
setPlayState(false);
}, [options.hold]);
// check if playback has changed and schedule animations
useEffect(() => {
if (isPlaybackActive(playback)) {
animateIn();
} else {
animateOut();
}
}, [animateIn, animateOut, playback]);
// check if data has changed and schedule animations
useEffect(() => {
const hasChanged = eventNow?.id !== previousId.current;
if (hasChanged) {
previousId.current = eventNow?.id;
if (eventNow?.id) animateIn();
}
}, [animateIn, eventNow?.id]);
const boxDuration = playState ? `${options.transitionIn * 0.5}s` : `${options.transitionOut * 0.5}s`;
const boxDelay = playState ? `${options.delay}s` : `${options.transitionOut * 0.5}s`;
const textDuration = playState ? `${options.transitionIn * 0.5}s` : `${options.transitionOut * 0.5}s`;
const textDelay = playState ? `${options.delay + options.transitionIn * 0.5}s` : '0s';
// gather option data
const lowerThirdOptions = useMemo(() => getLowerThirdOptions(customFields), [customFields]);
return (
<div className='lower-third' style={{ backgroundColor: `#${options.key}` }}>
<ViewParamsEditor viewOptions={lowerThirdOptions} />
<div
className={`container ${playState ? 'container--in' : 'container--out'}`}
style={{
minWidth: `${options.width}vw`,
transitionDuration: boxDuration,
transitionDelay: boxDelay,
}}
>
<div className='clip'>
<div
className='data-top'
style={{
transitionDuration: textDuration,
transitionDelay: textDelay,
color: `#${options.topColour}`,
backgroundColor: `#${options.topBg}`,
fontSize: `${options.topSize}em`,
}}
>
{textValue.top}
</div>
</div>
<div
className='line'
style={{
backgroundColor: `#${options.lineColour}`,
}}
/>
<div className='clip'>
<div
className='data-bottom'
style={{
transitionDuration: textDuration,
transitionDelay: textDelay,
color: `#${options.bottomColour}`,
backgroundColor: `#${options.bottomBg}`,
fontSize: `${options.bottomSize}em`,
}}
>
{textValue.bottom}
</div>
</div>
</div>
</div>
);
}
@@ -1,221 +0,0 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { CustomFields } from 'ontime-types';
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../../common/components/view-params-editor/viewParams.utils';
import safeParseNumber from '../../../common/utils/safeParseNumber';
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
const topSourceOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' },
]);
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' },
{ value: 'none', label: 'None' },
]);
return [
{
title: OptionTitle.DataSources,
collapsible: true,
options: [
{
id: 'top-src',
title: 'Top Text',
description: '',
type: 'option',
values: topSourceOptions,
defaultValue: 'title',
},
{
id: 'bottom-src',
title: 'Bottom Text',
description: 'Select the data source for the bottom element',
type: 'option',
values: bottomSourceOptions,
defaultValue: 'none',
},
],
},
{
title: OptionTitle.Animation,
collapsible: true,
options: [
{
id: 'transition-in',
title: 'Transition In',
description: 'Transition in time (default 3 seconds)',
type: 'number',
placeholder: '3 (default)',
},
{
id: 'transition-out',
title: 'Transition Out',
description: 'Transition out time (default 3 seconds)',
type: 'number',
placeholder: '3 (default)',
},
{
id: 'hold',
title: 'Hold',
description: 'Time on screen before transition out. Set to -1 to stop transition (default 3 seconds) ',
type: 'number',
placeholder: '3 (default)',
},
{
id: 'delay',
title: 'Delay',
description: 'Delay between trigger and transition in (default 0 seconds)',
type: 'number',
placeholder: '0 (default)',
},
],
},
{
title: OptionTitle.StyleOverride,
collapsible: true,
options: [
{
id: 'top-size',
title: 'Top Text Size',
description: 'Font size of the top text',
type: 'string',
placeholder: '5em',
},
{
id: 'bottom-size',
title: 'Bottom Text Size',
description: 'Font size of the bottom text',
type: 'string',
placeholder: '4em',
},
{
id: 'width',
title: 'Minimum Width',
description: 'Minimum Width of the element (percentage)',
type: 'number',
placeholder: '45 (default %)',
},
{
id: 'key',
title: 'Key Colour',
description: 'Colour of the background. Default: #FFF0 (transparent)',
type: 'colour',
defaultValue: 'FFF0',
},
{
id: 'top-colour',
title: 'Top Text Colour',
description: 'Top text colour. Default: #000000',
type: 'colour',
defaultValue: '000000',
},
{
id: 'bottom-colour',
title: 'Bottom Text Colour',
description: 'Bottom text colour. Default: #000000',
type: 'colour',
defaultValue: '000000',
},
{
id: 'top-bg',
title: 'Top Background Colour',
description: 'Top text background colour. Default: #FFF0 (transparent)',
type: 'colour',
defaultValue: 'FFF0',
},
{
id: 'bottom-bg',
title: 'Bottom Background Colour',
description: 'Bottom text background colour. Default: #FFF0 (transparent)',
type: 'colour',
defaultValue: 'FFF0',
},
{
id: 'line-colour',
title: 'Line Colour',
description: 'Colour of the line. Default: #FF0000',
type: 'colour',
defaultValue: 'FF0000',
},
],
},
];
};
type LowerOptions = {
width: number;
topSrc: string;
bottomSrc: string;
topColour: string;
bottomColour: string;
topBg: string;
bottomBg: string;
topSize: number;
bottomSize: number;
transitionIn: number;
transitionOut: number;
hold: number;
delay: number;
key: string;
lineColour: string;
};
const defaultOptions: Readonly<LowerOptions> = {
width: 45,
topSrc: 'title',
bottomSrc: 'lowerMsg',
topColour: '000000',
bottomColour: '000000',
topBg: 'FFF0',
bottomBg: 'FFF0',
topSize: 5,
bottomSize: 4,
transitionIn: 3,
transitionOut: 3,
hold: 3,
delay: 0,
key: 'FFF0',
lineColour: 'FF0000',
};
/**
* Utility extract the view options from URL Params
* the names and fallbacks are manually matched with defaultOptions
*/
function getOptionsFromParams(searchParams: URLSearchParams): LowerOptions {
// we manually make an object that matches the key above
return {
width: safeParseNumber(searchParams.get('width'), defaultOptions.width),
topSrc: searchParams.get('top-src') ?? defaultOptions.topSrc,
bottomSrc: searchParams.get('bottom-src') ?? defaultOptions.bottomSrc,
topColour: searchParams.get('top-colour') ?? defaultOptions.topColour,
bottomColour: searchParams.get('bottom-colour') ?? defaultOptions.bottomColour,
topBg: searchParams.get('top-bg') ?? defaultOptions.topBg,
bottomBg: searchParams.get('bottom-bg') ?? defaultOptions.bottomBg,
topSize: safeParseNumber(searchParams.get('top-size'), defaultOptions.topSize),
bottomSize: safeParseNumber(searchParams.get('bottom-size'), defaultOptions.bottomSize),
transitionIn: safeParseNumber(searchParams.get('transition-in'), defaultOptions.transitionIn),
transitionOut: safeParseNumber(searchParams.get('transition-out'), defaultOptions.transitionOut),
hold: safeParseNumber(searchParams.get('hold'), defaultOptions.hold),
delay: safeParseNumber(searchParams.get('hold'), defaultOptions.delay),
key: searchParams.get('key') ?? defaultOptions.key,
lineColour: searchParams.get('line-colour') ?? defaultOptions.lineColour,
};
}
/**
* Hook exposes the timer view options
*/
export function useLowerOptions(): LowerOptions {
const [searchParams] = useSearchParams();
const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
return options;
}
@@ -1,70 +0,0 @@
@use '../../../theme/viewerDefs' as *;
.minimal-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;
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
display: grid;
place-content: center;
&--finished {
outline: clamp(4px, 1vw, 16px) solid $timer-finished-color;
outline-offset: calc(clamp(4px, 1vw, 16px) * -1);
transition: $viewer-transition-time;
}
.timer {
opacity: 1;
font-family: var(--font-family-bold-override, $timer-bold-font-family);
font-size: 20vw;
position: relative;
color: var(--timer-color-override, var(--phase-color));
transition: $viewer-transition-time;
transition-property: opacity;
background-color: transparent;
letter-spacing: 0.05em;
&--paused {
opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time;
}
&--finished {
color: $timer-finished-color;
}
}
/* =================== OVERLAY ===================*/
.end-message {
text-align: center;
font-size: 12vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}
.logo {
position: absolute;
top: 2vw;
left: 2vw;
max-width: min(200px, 20vw);
}
}
/* =================== MOBILE ===================*/
@media screen and (max-width: 768px) {
.minimal-timer {
.logo img {
height: min(50px, 10vh);
}
}
}
@@ -1,185 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { Playback, ProjectData, TimerPhase, TimerType, ViewSettings } from 'ontime-types';
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 { OverridableOptions } from '../../../common/models/View.types';
import { useTranslation } from '../../../translation/TranslationProvider';
import { getFormattedTimer, getTimerByType, isStringBoolean } from '../common/viewUtils';
import { MINIMAL_TIMER_OPTIONS } from './minimalTimer.options';
import './MinimalTimer.scss';
interface MinimalTimerProps {
general: ProjectData;
isMirrored: boolean;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
}
export default function MinimalTimer(props: MinimalTimerProps) {
const { general, isMirrored, time, viewSettings } = props;
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Minimal Timer');
// TODO: this should be tied to the params
// USER OPTIONS
const userOptions: OverridableOptions = {
size: 1,
};
// key: string
// Should be a hex string '#00FF00' with key colour
const key = searchParams.get('key');
if (key) {
userOptions.keyColour = `#${key}`;
}
// textColour: string
// Should be a hex string '#ffffff'
const textColour = searchParams.get('text');
if (textColour) {
userOptions.textColour = `#${textColour}`;
}
// textBackground: string
// Should be a hex string '#ffffff'
const textBackground = searchParams.get('textbg');
if (textBackground) {
userOptions.textBackground = `#${textBackground}`;
}
// font: string
// Should be a string with a font name 'arial'
const font = searchParams.get('font');
if (font) {
userOptions.font = font;
}
// size: multiplier
// Should be a number 0.0-n
const size = searchParams.get('size');
if (size !== null && typeof size !== 'undefined') {
if (!Number.isNaN(Number(size))) {
userOptions.size = Number(size);
}
}
// alignX: flex justification
// start | center | end
const alignX = searchParams.get('alignx');
if (alignX) {
if (alignX === 'start' || alignX === 'center' || alignX === 'end') {
userOptions.justifyContent = alignX;
}
}
// alignX: flex alignment
// start | center | end
const alignY = searchParams.get('aligny');
if (alignY) {
if (alignY === 'start' || alignY === 'center' || alignY === 'end') {
userOptions.alignItems = alignY;
}
}
// offsetX: position in pixels
// Should be a number 0 - 1920
const offsetX = searchParams.get('offsetx');
if (offsetX) {
const pixels = Number(offsetX);
if (!isNaN(pixels)) {
userOptions.left = `${pixels}px`;
}
}
// offsetX: position in pixels
// Should be a number 0 - 1920
const offsetY = searchParams.get('offsety');
if (offsetY) {
const pixels = Number(offsetY);
if (!isNaN(pixels)) {
userOptions.top = `${pixels}px`;
}
}
const hideOvertime = searchParams.get('hideovertime');
userOptions.hideOvertime = isStringBoolean(hideOvertime);
const hideEndMessage = searchParams.get('hideendmessage');
userOptions.hideEndMessage = isStringBoolean(hideEndMessage);
const hideTimerSeconds = searchParams.get('hideTimerSeconds');
userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds);
const showLeadingZeros = searchParams.get('showLeadingZeros');
userOptions.removeLeadingZeros = !isStringBoolean(showLeadingZeros);
const timerIsTimeOfDay = time.timerType === TimerType.Clock;
const isPlaying = time.playback !== Playback.Pause;
const shouldShowModifiers = time.timerType === TimerType.CountDown || time.countToEnd;
const finished = time.phase === TimerPhase.Overtime;
const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage;
const showFinished =
shouldShowModifiers && finished && !userOptions?.hideOvertime && (shouldShowModifiers || showEndMessage);
const showProgress = time.playback !== Playback.Stop;
const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning;
const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger;
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 timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1);
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`;
return (
<div
className={showFinished ? `${baseClasses} minimal-timer--finished` : baseClasses}
style={{
backgroundColor: userOptions.keyColour,
justifyContent: userOptions.justifyContent,
alignContent: userOptions.alignItems,
}}
data-testid='minimal-timer'
>
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
<ViewParamsEditor viewOptions={MINIMAL_TIMER_OPTIONS} />
{showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
style={{
color: userOptions.textColour,
fontSize: `${timerFontSize}vw`,
fontFamily: userOptions.font,
top: userOptions.top,
left: userOptions.left,
backgroundColor: userOptions.textBackground,
'--phase-color': timerColor,
}}
>
{display}
</div>
)}
</div>
);
}
@@ -1,106 +0,0 @@
import { hideTimerSeconds, showLeadingZeros } from '../../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
{ title: OptionTitle.TimerOptions, collapsible: true, options: [hideTimerSeconds, showLeadingZeros] },
{
title: OptionTitle.ElementVisibility,
collapsible: true,
options: [
{
id: 'hideovertime',
title: 'Hide Overtime',
description: 'Whether to suppress overtime styles (red borders and red text)',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideendmessage',
title: 'Hide End Message',
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
type: 'boolean',
defaultValue: false,
},
],
},
{
title: OptionTitle.StyleOverride,
collapsible: true,
options: [
{
id: 'key',
title: 'Key Colour',
description: 'Background or key colour for entire view. Default: #000000',
type: 'colour',
defaultValue: '000000',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour. Default: #FFFFFF',
type: 'colour',
defaultValue: 'FFFFFF',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Background colour for timer text. Default: #FFF0 (transparent)',
type: 'colour',
defaultValue: 'FFF0',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
placeholder: '1 (default)',
},
{
id: 'alignx',
title: 'Align Horizontal',
description: 'Moves the horizontally in page to start = left | center | end = right',
type: 'option',
values: [
{ value: 'start', label: 'Start' },
{ value: 'center', label: 'Center' },
{ value: 'end', label: 'End' },
],
defaultValue: 'center',
},
{
id: 'offsetx',
title: 'Offset Horizontal',
description: 'Offsets the timer horizontal position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
{
id: 'aligny',
title: 'Align Vertical',
description: 'Moves the vertically in page to start = left | center | end = right',
type: 'option',
values: [
{ value: 'start', label: 'Start' },
{ value: 'center', label: 'Center' },
{ value: 'end', label: 'End' },
],
defaultValue: 'center',
},
{
id: 'offsety',
title: 'Offset Vertical',
description: 'Offsets the timer vertical position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
],
},
];
-1
View File
@@ -2,7 +2,6 @@ export const navigatorConstants = [
{ url: 'timer', label: 'Timer' },
{ url: 'backstage', label: 'Backstage' },
{ url: 'timeline', label: 'Timeline (beta)' },
{ url: 'lower', label: 'Lower Thirds' },
{ url: 'studio', label: 'Studio Clock' },
{ url: 'countdown', label: 'Countdown' },
{ url: 'info', label: 'Project Info' },
+1 -1
View File
@@ -1,4 +1,4 @@
@use '../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$dot-size: 0.5rem;
$dot-spacing: 1.5rem;
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.backstage {
margin: 0;
@@ -1,4 +1,4 @@
@use '../../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$circle-size: clamp(8px, 0.75vw, 12px);
$indeterminate-width: clamp(32px, 3vw, 48px);
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$item-height: 3.5rem;
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.project {
margin: 0;
+1 -1
View File
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.studio {
margin: 0;
+2 -1
View File
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { MessageState, OntimeEvent, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
@@ -43,7 +44,7 @@ export default function Studio({
// gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const studioOptions = getStudioOptions(defaultFormat);
const studioOptions = useMemo(() => getStudioOptions(defaultFormat), [defaultFormat]);
return (
<div className={cx(['studio', isMirrored && 'mirror'])} data-testid='studio-view'>
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$studio-red: $red-600;
$studio-idle: $red-1300;
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.studio__timers {
display: flex;
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
$timeline-height: 1rem;
$timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-override, $viewer-background-color));
@@ -1,4 +1,4 @@
@use '../../theme/viewerDefs' as *;
@use '@/theme/viewerDefs' as *;
.timeline {
width: 100vw;
@@ -155,7 +155,6 @@ function makeViewMenu(clientUrl) {
makeItemOpenInBrowser('Timer', `${clientUrl}/timer`),
makeItemOpenInBrowser('Backstage', `${clientUrl}/backstage`),
makeItemOpenInBrowser('Timeline (beta)', `${clientUrl}/timeline`),
makeItemOpenInBrowser('Lower Thirds', `${clientUrl}/lower`),
makeItemOpenInBrowser('Studio Clock', `${clientUrl}/studio`),
makeItemOpenInBrowser('Countdown', `${clientUrl}/countdown`),
makeItemOpenInBrowser('Project info', `${clientUrl}/info`),
+1 -1
View File
@@ -539,7 +539,7 @@ export const demoDb: DatabaseModel = {
{
enabled: true,
alias: 'test',
pathAndParams: 'lower?bg=ff2&text=f00&size=0.6&transition=5',
pathAndParams: 'timer?timerType=clock',
},
],
automation: {
-12
View File
@@ -34,9 +34,6 @@ export const defaultCss = `
/** Font used for all text in views */
--font-family-override: 'Open Sans';
/** Font used for clock in /minimal and /clock views */
--font-family-bold-override: 'Arial Black';
/** Colour used for external message and aux timer in /timer */
--external-color-override: #161616;
@@ -58,15 +55,6 @@ export const defaultCss = `
--studio-active-label: #101010;
--studio-idle-label: #595959;
--studio-overtime: #101010;
/** View specific features: /lower */
--lowerThird-font-family-override: 'Courier New';
--lowerThird-top-font-weight-override: bold;
--lowerThird-bottom-font-weight-override: bold;
--lowerThird-top-font-style-override: normal;
--lowerThird-bottom-font-style-override: italic;
--lowerThird-line-height-override: 1vh;
--lowerThird-text-align-override: end;
}
/**
-12
View File
@@ -34,9 +34,6 @@
/** Font used for all text in views */
--font-family-override: 'Open Sans';
/** Font used for clock in /minimal and /clock views */
--font-family-bold-override: 'Arial Black';
/** Colour used for external message and aux timer in /timer */
--external-color-override: #161616;
@@ -58,15 +55,6 @@
--studio-active-label: #101010;
--studio-idle-label: #595959;
--studio-overtime: #101010;
/** View specific features: /lower */
--lowerThird-font-family-override: 'Courier New';
--lowerThird-top-font-weight-override: bold;
--lowerThird-bottom-font-weight-override: bold;
--lowerThird-top-font-style-override: normal;
--lowerThird-bottom-font-style-override: italic;
--lowerThird-line-height-override: 1vh;
--lowerThird-text-align-override: end;
}
/**
+2 -2
View File
@@ -487,8 +487,8 @@
"urlPresets": [
{
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
"alias": "clock",
"pathAndParams": "timer?timerType=clock"
}
],
"automation": {
-8
View File
@@ -20,14 +20,6 @@ test.describe('test view navigation feature', () => {
await expect(page).toHaveURL('http://localhost:4001/backstage');
});
test('Lower Thirds', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('button', { name: 'Lower Thirds' }).click();
await expect(page).toHaveURL('http://localhost:4001/lower');
const errorBoundary = page.locator('data-testid=error-container');
await expect(errorBoundary).toHaveCount(0);
});
test('Studio Clock', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('button', { name: 'Studio Clock' }).click();
+3 -3
View File
@@ -8,11 +8,11 @@ test('redirect', async ({ context }) => {
await remotePage.goto('http://localhost:4001/timer');
await controllerPage.getByTestId('not-self-redirect').click();
await controllerPage.getByPlaceholder('minimal?key=0000ffff').click();
await controllerPage.getByPlaceholder('minimal?key=0000ffff').fill('clock');
await controllerPage.getByRole('textbox', { name: 'http://localhost:' }).click();
await controllerPage.getByRole('textbox', { name: 'http://localhost:' }).fill('studio');
await controllerPage.getByLabel('Redirect', { exact: true }).click();
await expect(remotePage.getByTestId('clock-view')).toBeVisible();
await expect(remotePage.getByTestId('studio-view')).toBeVisible();
});
test('identify', async ({ context }) => {
+2 -2
View File
@@ -504,8 +504,8 @@
"urlPresets": [
{
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
"alias": "clock",
"pathAndParams": "timer?timerType=clock"
}
],
"automation": {