mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
refactor: remove deprecated views
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
background-color: #101010 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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,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,4 +1,4 @@
|
||||
@use '../../theme/viewerDefs' as *;
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
|
||||
.studio {
|
||||
margin: 0;
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Vendored
+2
-2
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user