mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +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
|
For the backstage views
|
||||||
-------------------------------------------------------------
|
-------------------------------------------------------------
|
||||||
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
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/backstage > Stage Manager / Backstage view
|
||||||
IP.ADDRESS:4001/countdown > Countdown to anything
|
IP.ADDRESS:4001/countdown > Countdown to anything
|
||||||
IP.ADDRESS:4001/studio > Studio Clock
|
IP.ADDRESS:4001/studio > Studio Clock
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<style>
|
<style>
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
background-color: #101010 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -23,23 +23,17 @@ const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
|||||||
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
||||||
|
|
||||||
const TimerView = React.lazy(() => import('./views/timer/Timer'));
|
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 Countdown = React.lazy(() => import('./views/countdown/Countdown'));
|
||||||
|
|
||||||
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
|
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
|
||||||
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
|
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 StudioClock = React.lazy(() => import('./views/studio/Studio'));
|
||||||
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
||||||
|
|
||||||
const STimer = withPreset(withData(TimerView));
|
const STimer = withPreset(withData(TimerView));
|
||||||
const SMinimalTimer = withPreset(withData(MinimalTimerView));
|
|
||||||
const SClock = withPreset(withData(ClockView));
|
|
||||||
const SCountdown = withPreset(withData(Countdown));
|
const SCountdown = withPreset(withData(Countdown));
|
||||||
const SBackstage = withPreset(withData(Backstage));
|
const SBackstage = withPreset(withData(Backstage));
|
||||||
const SProjectInfo = withPreset(ProjectInfo); // NOTE: ProjectInfo does not use the viewWrapper since it has no options
|
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 SStudio = withPreset(withData(StudioClock));
|
||||||
const STimeline = withPreset(withData(Timeline));
|
const STimeline = withPreset(withData(Timeline));
|
||||||
const PCuesheet = withPreset(Cuesheet);
|
const PCuesheet = withPreset(Cuesheet);
|
||||||
@@ -86,22 +80,6 @@ export default function AppRouter() {
|
|||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/minimal'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<SMinimalTimer />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path='/clock'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<SClock />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/countdown'
|
path='/countdown'
|
||||||
element={
|
element={
|
||||||
@@ -126,8 +104,6 @@ export default function AppRouter() {
|
|||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/*/!* Lower third cannot have a loading screen *!/*/}
|
|
||||||
<Route path='/lower' element={<SLowerThird />} />
|
|
||||||
<Route
|
<Route
|
||||||
path='/timeline'
|
path='/timeline'
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
@import './BaseButtonStyles.module.scss';
|
@import './BaseButtonStyles.module.scss';
|
||||||
|
|
||||||
.baseButton {
|
.baseButton {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
@import './BaseButtonStyles.module.scss';
|
@import './BaseButtonStyles.module.scss';
|
||||||
|
|
||||||
.baseIconButton {
|
.baseIconButton {
|
||||||
|
|||||||
@@ -91,12 +91,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
<span className={style.label}>Enter custom path</span>
|
<span className={style.label}>Enter custom path</span>
|
||||||
<label className={style.textEntry}>
|
<label className={style.textEntry}>
|
||||||
{origin}
|
{origin}
|
||||||
<Input
|
<Input placeholder='eg. /timer' fluid value={path} onChange={(event) => setPath(event.target.value)} />
|
||||||
placeholder='eg. /minimal?key=0000ffff'
|
|
||||||
fluid
|
|
||||||
value={path}
|
|
||||||
onChange={(event) => setPath(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$progress-bar-size: 1.5rem;
|
$progress-bar-size: 1.5rem;
|
||||||
$progress-bar-br: 3px;
|
$progress-bar-br: 3px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$progress-bar-size: 12px;
|
$progress-bar-size: 12px;
|
||||||
$progress-bar-br: 3px;
|
$progress-bar-br: 3px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
/* share the same style as a page layout */
|
/* share the same style as a page layout */
|
||||||
.page {
|
.page {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.title-card {
|
.title-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
import { IoChevronDown } from 'react-icons/io5';
|
import { IoChevronDown } from 'react-icons/io5';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ interface ViewParamsSectionProps {
|
|||||||
options: ParamField[];
|
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 [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
|
||||||
|
|
||||||
const handleCollapse = () => {
|
const handleCollapse = () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
grid-area: 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: 'timer', label: 'Timer' },
|
||||||
{ url: 'backstage', label: 'Backstage' },
|
{ url: 'backstage', label: 'Backstage' },
|
||||||
{ url: 'timeline', label: 'Timeline (beta)' },
|
{ url: 'timeline', label: 'Timeline (beta)' },
|
||||||
{ url: 'lower', label: 'Lower Thirds' },
|
|
||||||
{ url: 'studio', label: 'Studio Clock' },
|
{ url: 'studio', label: 'Studio Clock' },
|
||||||
{ url: 'countdown', label: 'Countdown' },
|
{ url: 'countdown', label: 'Countdown' },
|
||||||
{ url: 'info', label: 'Project Info' },
|
{ url: 'info', label: 'Project Info' },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$dot-size: 0.5rem;
|
$dot-size: 0.5rem;
|
||||||
$dot-spacing: 1.5rem;
|
$dot-spacing: 1.5rem;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.backstage {
|
.backstage {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$circle-size: clamp(8px, 0.75vw, 12px);
|
$circle-size: clamp(8px, 0.75vw, 12px);
|
||||||
$indeterminate-width: clamp(32px, 3vw, 48px);
|
$indeterminate-width: clamp(32px, 3vw, 48px);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$item-height: 3.5rem;
|
$item-height: 3.5rem;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.project {
|
.project {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.studio {
|
.studio {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { MessageState, OntimeEvent, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
|
import { MessageState, OntimeEvent, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
@@ -43,7 +44,7 @@ export default function Studio({
|
|||||||
|
|
||||||
// gather option data
|
// gather option data
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const studioOptions = getStudioOptions(defaultFormat);
|
const studioOptions = useMemo(() => getStudioOptions(defaultFormat), [defaultFormat]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(['studio', isMirrored && 'mirror'])} data-testid='studio-view'>
|
<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-red: $red-600;
|
||||||
$studio-idle: $red-1300;
|
$studio-idle: $red-1300;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.studio__timers {
|
.studio__timers {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
$timeline-height: 1rem;
|
$timeline-height: 1rem;
|
||||||
$timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-override, $viewer-background-color));
|
$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 {
|
.timeline {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ function makeViewMenu(clientUrl) {
|
|||||||
makeItemOpenInBrowser('Timer', `${clientUrl}/timer`),
|
makeItemOpenInBrowser('Timer', `${clientUrl}/timer`),
|
||||||
makeItemOpenInBrowser('Backstage', `${clientUrl}/backstage`),
|
makeItemOpenInBrowser('Backstage', `${clientUrl}/backstage`),
|
||||||
makeItemOpenInBrowser('Timeline (beta)', `${clientUrl}/timeline`),
|
makeItemOpenInBrowser('Timeline (beta)', `${clientUrl}/timeline`),
|
||||||
makeItemOpenInBrowser('Lower Thirds', `${clientUrl}/lower`),
|
|
||||||
makeItemOpenInBrowser('Studio Clock', `${clientUrl}/studio`),
|
makeItemOpenInBrowser('Studio Clock', `${clientUrl}/studio`),
|
||||||
makeItemOpenInBrowser('Countdown', `${clientUrl}/countdown`),
|
makeItemOpenInBrowser('Countdown', `${clientUrl}/countdown`),
|
||||||
makeItemOpenInBrowser('Project info', `${clientUrl}/info`),
|
makeItemOpenInBrowser('Project info', `${clientUrl}/info`),
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
alias: 'test',
|
alias: 'test',
|
||||||
pathAndParams: 'lower?bg=ff2&text=f00&size=0.6&transition=5',
|
pathAndParams: 'timer?timerType=clock',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
automation: {
|
automation: {
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ export const defaultCss = `
|
|||||||
/** Font used for all text in views */
|
/** Font used for all text in views */
|
||||||
--font-family-override: 'Open Sans';
|
--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 */
|
/** Colour used for external message and aux timer in /timer */
|
||||||
--external-color-override: #161616;
|
--external-color-override: #161616;
|
||||||
|
|
||||||
@@ -58,15 +55,6 @@ export const defaultCss = `
|
|||||||
--studio-active-label: #101010;
|
--studio-active-label: #101010;
|
||||||
--studio-idle-label: #595959;
|
--studio-idle-label: #595959;
|
||||||
--studio-overtime: #101010;
|
--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 used for all text in views */
|
||||||
--font-family-override: 'Open Sans';
|
--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 */
|
/** Colour used for external message and aux timer in /timer */
|
||||||
--external-color-override: #161616;
|
--external-color-override: #161616;
|
||||||
|
|
||||||
@@ -58,15 +55,6 @@
|
|||||||
--studio-active-label: #101010;
|
--studio-active-label: #101010;
|
||||||
--studio-idle-label: #595959;
|
--studio-idle-label: #595959;
|
||||||
--studio-overtime: #101010;
|
--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": [
|
"urlPresets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alias": "test",
|
"alias": "clock",
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "timer?timerType=clock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"automation": {
|
"automation": {
|
||||||
|
|||||||
@@ -20,14 +20,6 @@ test.describe('test view navigation feature', () => {
|
|||||||
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
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 }) => {
|
test('Studio Clock', async ({ page }) => {
|
||||||
await openNavigationMenu(page);
|
await openNavigationMenu(page);
|
||||||
await page.getByRole('button', { name: 'Studio Clock' }).click();
|
await page.getByRole('button', { name: 'Studio Clock' }).click();
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ test('redirect', async ({ context }) => {
|
|||||||
await remotePage.goto('http://localhost:4001/timer');
|
await remotePage.goto('http://localhost:4001/timer');
|
||||||
|
|
||||||
await controllerPage.getByTestId('not-self-redirect').click();
|
await controllerPage.getByTestId('not-self-redirect').click();
|
||||||
await controllerPage.getByPlaceholder('minimal?key=0000ffff').click();
|
await controllerPage.getByRole('textbox', { name: 'http://localhost:' }).click();
|
||||||
await controllerPage.getByPlaceholder('minimal?key=0000ffff').fill('clock');
|
await controllerPage.getByRole('textbox', { name: 'http://localhost:' }).fill('studio');
|
||||||
await controllerPage.getByLabel('Redirect', { exact: true }).click();
|
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 }) => {
|
test('identify', async ({ context }) => {
|
||||||
|
|||||||
Vendored
+2
-2
@@ -504,8 +504,8 @@
|
|||||||
"urlPresets": [
|
"urlPresets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alias": "test",
|
"alias": "clock",
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "timer?timerType=clock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"automation": {
|
"automation": {
|
||||||
|
|||||||
Reference in New Issue
Block a user