mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 15:33:59 +00:00
refactor: deprecate presenter and subtitle (#795)
* refactor: deprecate presenter and subtitle * style: reduce spacing between related sections * refactor: optional secondary field * refactor: remove secondary field * refactor: dynamic data source
This commit is contained in:
@@ -31,6 +31,12 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entry-secondary {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
@@ -68,4 +74,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createContext, PropsWithChildren, useContext, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
import { useInterval } from '../../hooks/useInterval';
|
||||
import { isStringBoolean } from '../../utils/viewUtils';
|
||||
|
||||
interface ScheduleContextState {
|
||||
events: OntimeEvent[];
|
||||
|
||||
@@ -13,14 +13,13 @@ interface ScheduleItemProps {
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
title: string;
|
||||
presenter?: string;
|
||||
backstageEvent: boolean;
|
||||
colour: string;
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
export default function ScheduleItem(props: ScheduleItemProps) {
|
||||
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props;
|
||||
|
||||
const start = formatTime(timeStart, formatOptions);
|
||||
const end = formatTime(timeEnd, formatOptions);
|
||||
@@ -39,7 +38,6 @@ export default function ScheduleItem(props: ScheduleItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
{presenter && <div className='entry-presenter'>{presenter}</div>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,34 +3,38 @@
|
||||
.title-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
}
|
||||
.inline {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: clamp(32px, 3.5vw, 50px);
|
||||
color: var(--color-override, $viewer-color);
|
||||
line-height: 1.1em;
|
||||
}
|
||||
.title-card__title {
|
||||
font-weight: 600;
|
||||
font-size: clamp(32px, 3.5vw, 50px);
|
||||
color: var(--color-override, $viewer-color);
|
||||
line-height: 1.1em;
|
||||
}
|
||||
|
||||
.subtitle, .presenter {
|
||||
font-size: clamp(24px, 2vw, 35px);
|
||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||
line-height: 1.1em;
|
||||
}
|
||||
.title-card__label {
|
||||
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||
font-weight: 400;
|
||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
|
||||
.label {
|
||||
font-size: clamp(16px, 1.5vw, 24px);
|
||||
font-weight: 400;
|
||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.accent {
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
&--accent {
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.title-card__secondary {
|
||||
font-size: clamp(1.5rem, 2vw, 2.25rem);
|
||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||
line-height: 1.1em;
|
||||
|
||||
&::after {
|
||||
content: '\200b';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import './TitleCard.scss';
|
||||
|
||||
interface TitleCardProps {
|
||||
label: 'now' | 'next';
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
presenter: string | null;
|
||||
title: string;
|
||||
secondary?: string;
|
||||
}
|
||||
|
||||
export default function TitleCard(props: TitleCardProps) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
const { label, title, secondary } = props;
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
const accent = label === 'now';
|
||||
@@ -18,11 +17,12 @@ export default function TitleCard(props: TitleCardProps) {
|
||||
return (
|
||||
<div className='title-card'>
|
||||
<div className='inline'>
|
||||
<span className='presenter'>{presenter}</span>
|
||||
<span className={accent ? 'label accent' : 'label'}>{getLocalizedString(`common.${label}`)}</span>
|
||||
<span className='title-card__title'>{title}</span>
|
||||
<span className={accent ? 'title-card__label title-card__label--accent' : 'title-card__label'}>
|
||||
{getLocalizedString(`common.${label}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='title'>{title}</div>
|
||||
<div className='subtitle'>{subtitle}</div>
|
||||
<div className='title-card__secondary'>{secondary}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Input, InputGroup, InputLeftElement, Select, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { isStringBoolean } from '../../utils/viewUtils';
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
|
||||
import { ParamField } from './types';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@extend .drawerContent;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: $element-spacing;
|
||||
gap: $section-spacing;
|
||||
|
||||
button[type='reset'] {
|
||||
padding: 0 2em;
|
||||
@@ -30,9 +30,9 @@
|
||||
|
||||
.columnSection {
|
||||
display: flex;
|
||||
padding: $element-spacing;
|
||||
padding: $section-spacing 0;
|
||||
flex-direction: column;
|
||||
gap: $element-inner-spacing;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { CustomFields } from 'ontime-types';
|
||||
|
||||
import { capitaliseFirstLetter } from '../../../features/viewers/common/viewUtils';
|
||||
|
||||
import { ParamField } from './types';
|
||||
|
||||
const makeOptionsFromCustomFields = (customFields: CustomFields, additionalOptions?: Record<string, string>) => {
|
||||
const customFieldOptions = Object.keys(customFields).reduce((acc, key) => {
|
||||
return { ...acc, [`custom-${key}`]: `Custom: ${capitaliseFirstLetter(key)}` };
|
||||
}, additionalOptions ?? {});
|
||||
return customFieldOptions;
|
||||
};
|
||||
|
||||
const getTimeOption = (timeFormat: string): ParamField => {
|
||||
const placeholder = `${timeFormat} (default)`;
|
||||
return {
|
||||
@@ -93,45 +102,56 @@ export const getClockOptions = (timeFormat: string): ParamField[] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const getTimerOptions = (timeFormat: string): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideTimerSeconds,
|
||||
{
|
||||
id: 'hideClock',
|
||||
title: 'Hide Time Now',
|
||||
description: 'Hides the Time Now field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideCards',
|
||||
title: 'Hide Cards',
|
||||
description: 'Hides the Now and Next cards',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideProgress',
|
||||
title: 'Hide progress bar',
|
||||
description: 'Hides the progress bar',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideMessage',
|
||||
title: 'Hide Presenter Message',
|
||||
description: 'Prevents the screen from displaying messages from the presenter',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideExternal',
|
||||
title: 'Hide External',
|
||||
description: 'Prevents the screen from displaying the external field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => {
|
||||
const secondaryOptions = makeOptionsFromCustomFields(customFields);
|
||||
return [
|
||||
getTimeOption(timeFormat),
|
||||
hideTimerSeconds,
|
||||
{
|
||||
id: 'hideClock',
|
||||
title: 'Hide Time Now',
|
||||
description: 'Hides the Time Now field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'secondary-src',
|
||||
title: 'Secondary text',
|
||||
description: 'Select the data source for the secondary text',
|
||||
type: 'option',
|
||||
values: secondaryOptions,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'hideCards',
|
||||
title: 'Hide Cards',
|
||||
description: 'Hides the Now and Next cards',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideProgress',
|
||||
title: 'Hide progress bar',
|
||||
description: 'Hides the progress bar',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideMessage',
|
||||
title: 'Hide Presenter Message',
|
||||
description: 'Prevents the screen from displaying messages from the presenter',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideExternal',
|
||||
title: 'Hide External',
|
||||
description: 'Prevents the screen from displaying the external field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
hideTimerSeconds,
|
||||
@@ -226,187 +246,216 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const LOWER_THIRD_OPTIONS: ParamField[] = [
|
||||
{
|
||||
id: 'trigger',
|
||||
title: 'Animation Trigger',
|
||||
description: '',
|
||||
type: 'option',
|
||||
values: {
|
||||
event: 'Event Load',
|
||||
manual: 'Manual',
|
||||
},
|
||||
defaultValue: 'event',
|
||||
},
|
||||
{
|
||||
id: 'top-src',
|
||||
title: 'Top Text',
|
||||
description: '',
|
||||
type: 'option',
|
||||
values: {
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
presenter: 'Presenter',
|
||||
lowerMsg: 'Lower Third Message',
|
||||
},
|
||||
defaultValue: 'title',
|
||||
},
|
||||
{
|
||||
id: 'bottom-src',
|
||||
title: 'Bottom Text',
|
||||
description: 'Select the text source for the bottom element',
|
||||
type: 'option',
|
||||
values: {
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
presenter: 'Presenter',
|
||||
lowerMsg: 'Lower Third Message',
|
||||
},
|
||||
defaultValue: 'subtitle',
|
||||
},
|
||||
{
|
||||
id: 'top-colour',
|
||||
title: 'Top Text Colour',
|
||||
description: 'Top text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '0000ff (default)',
|
||||
},
|
||||
{
|
||||
id: 'bottom-colour',
|
||||
title: 'Bottom Text Colour',
|
||||
description: 'Bottom text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '0000ff (default)',
|
||||
},
|
||||
{
|
||||
id: 'top-bg',
|
||||
title: 'Top Background Colour',
|
||||
description: 'Top text background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'bottom-bg',
|
||||
title: 'Bottom Background Colour',
|
||||
description: 'Bottom text background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'top-size',
|
||||
title: 'Top Text Size',
|
||||
description: 'Font size of the top text',
|
||||
type: 'string',
|
||||
placeholder: '65px',
|
||||
},
|
||||
{
|
||||
id: 'bottom-size',
|
||||
title: 'Bottom Text Size',
|
||||
description: 'Font size of the bottom text',
|
||||
type: 'string',
|
||||
placeholder: '64px',
|
||||
},
|
||||
{
|
||||
id: 'width',
|
||||
title: 'Minimum Width',
|
||||
description: 'Minimum Width of the element',
|
||||
type: 'number',
|
||||
prefix: '%',
|
||||
placeholder: '45 (default)',
|
||||
},
|
||||
{
|
||||
id: 'transition',
|
||||
title: 'Transition',
|
||||
description: 'Transition in time in seconds (default 3)',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
{
|
||||
id: 'delay',
|
||||
title: 'Delay',
|
||||
description: 'Delay between transition in and out in seconds (default 3)',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Colour of the background',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'ffffffff (default)',
|
||||
},
|
||||
{
|
||||
id: 'line-colour',
|
||||
title: 'Line Colour',
|
||||
description: 'Colour of the line',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'ff0000ff (default)',
|
||||
},
|
||||
];
|
||||
export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => {
|
||||
const topSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||
title: 'Title',
|
||||
lowerMsg: 'Lower Third Message',
|
||||
});
|
||||
|
||||
export const getBackstageOptions = (timeFormat: string): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
];
|
||||
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||
title: 'Title',
|
||||
lowerMsg: 'Lower Third Message',
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'trigger',
|
||||
title: 'Animation Trigger',
|
||||
description: '',
|
||||
type: 'option',
|
||||
values: {
|
||||
event: 'Event Load',
|
||||
manual: 'Manual',
|
||||
},
|
||||
defaultValue: 'event',
|
||||
},
|
||||
{
|
||||
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: 'lowerMsg',
|
||||
},
|
||||
{
|
||||
id: 'top-colour',
|
||||
title: 'Top Text Colour',
|
||||
description: 'Top text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '0000ff (default)',
|
||||
},
|
||||
{
|
||||
id: 'bottom-colour',
|
||||
title: 'Bottom Text Colour',
|
||||
description: 'Bottom text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '0000ff (default)',
|
||||
},
|
||||
{
|
||||
id: 'top-bg',
|
||||
title: 'Top Background Colour',
|
||||
description: 'Top text background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'bottom-bg',
|
||||
title: 'Bottom Background Colour',
|
||||
description: 'Bottom text background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'top-size',
|
||||
title: 'Top Text Size',
|
||||
description: 'Font size of the top text',
|
||||
type: 'string',
|
||||
placeholder: '65px',
|
||||
},
|
||||
{
|
||||
id: 'bottom-size',
|
||||
title: 'Bottom Text Size',
|
||||
description: 'Font size of the bottom text',
|
||||
type: 'string',
|
||||
placeholder: '64px',
|
||||
},
|
||||
{
|
||||
id: 'width',
|
||||
title: 'Minimum Width',
|
||||
description: 'Minimum Width of the element',
|
||||
type: 'number',
|
||||
prefix: '%',
|
||||
placeholder: '45 (default)',
|
||||
},
|
||||
{
|
||||
id: 'transition',
|
||||
title: 'Transition',
|
||||
description: 'Transition in time in seconds (default 3)',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
{
|
||||
id: 'delay',
|
||||
title: 'Delay',
|
||||
description: 'Delay between transition in and out in seconds (default 3)',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Colour of the background',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'ffffffff (default)',
|
||||
},
|
||||
{
|
||||
id: 'line-colour',
|
||||
title: 'Line Colour',
|
||||
description: 'Colour of the line',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'ff0000ff (default)',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => {
|
||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
|
||||
|
||||
return [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
{
|
||||
id: 'secondary-src',
|
||||
title: 'Event secondary text',
|
||||
description: 'Select the data source for auxiliary text shown in now and next cards',
|
||||
type: 'option',
|
||||
values: secondaryOptions,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ParamField[] => {
|
||||
const secondaryOptions = makeOptionsFromCustomFields(customFields);
|
||||
|
||||
return [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
{
|
||||
id: 'secondary-src',
|
||||
title: 'Event secondary text',
|
||||
description: 'Select the data source for auxiliary text shown in now and next cards',
|
||||
type: 'option',
|
||||
values: secondaryOptions,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getPublicOptions = (timeFormat: string): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
];
|
||||
export const getStudioClockOptions = (timeFormat: string): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideTimerSeconds,
|
||||
];
|
||||
|
||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ParamField[] => {
|
||||
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
||||
|
||||
const customFieldSelect = Object.keys(customFields).reduce((acc, key) => {
|
||||
return { ...acc, [key]: key };
|
||||
return { ...acc, [key]: `Custom: ${capitaliseFirstLetter(key)}` };
|
||||
}, {});
|
||||
|
||||
return [
|
||||
@@ -423,22 +472,16 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin
|
||||
title: 'Main data field',
|
||||
description: 'Field to be shown in the first line of text',
|
||||
type: 'option',
|
||||
values: {
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
presenter: 'Presenter',
|
||||
},
|
||||
values: fieldOptions,
|
||||
defaultValue: 'title',
|
||||
},
|
||||
{
|
||||
id: 'secondary',
|
||||
title: 'Secondary data field',
|
||||
description: 'Field to be shown in the second line of text',
|
||||
type: 'option',
|
||||
values: {
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
presenter: 'Presenter',
|
||||
},
|
||||
values: fieldOptions,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'subscribe',
|
||||
@@ -446,6 +489,7 @@ export const getOperatorOptions = (customFields: CustomFields, timeFormat: strin
|
||||
description: 'Choose a custom field to highlight',
|
||||
type: 'option',
|
||||
values: customFieldSelect,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'shouldEdit',
|
||||
|
||||
@@ -9,8 +9,6 @@ describe('cloneEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
title: 'title',
|
||||
cue: 'cue',
|
||||
subtitle: 'subtitle',
|
||||
presenter: 'presenter',
|
||||
note: 'note',
|
||||
timeStart: 0,
|
||||
duration: 10,
|
||||
@@ -33,8 +31,6 @@ describe('cloneEvent()', () => {
|
||||
// @ts-expect-error -- safeguarding this
|
||||
expect(cloned?.id).toBe(undefined);
|
||||
expect(cloned.title).toBe(original.title);
|
||||
expect(cloned.subtitle).toBe(original.subtitle);
|
||||
expect(cloned.presenter).toBe(original.presenter);
|
||||
expect(cloned.note).toBe(original.note);
|
||||
expect(cloned.endAction).toBe(original.endAction);
|
||||
expect(cloned.timerType).toBe(original.timerType);
|
||||
|
||||
@@ -11,8 +11,6 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEvent.Event,
|
||||
title: event.title,
|
||||
subtitle: event.subtitle,
|
||||
presenter: event.presenter,
|
||||
note: event.note,
|
||||
timeStart: event.timeStart,
|
||||
duration: event.duration,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export function isStringBoolean(text: string | null) {
|
||||
if (text === null) {
|
||||
return false;
|
||||
}
|
||||
return text?.toLowerCase() === 'true' || text === '1';
|
||||
}
|
||||
-2
@@ -11,8 +11,6 @@ describe('convertToImportMap', () => {
|
||||
Duration: 'duration',
|
||||
Cue: 'cue',
|
||||
Title: 'title',
|
||||
Presenter: 'presenter',
|
||||
Subtitle: 'subtitle',
|
||||
'Is Public': 'public',
|
||||
Skip: 'skip',
|
||||
Note: 'notes',
|
||||
|
||||
@@ -10,8 +10,6 @@ export const namedImportMap = {
|
||||
Duration: 'duration',
|
||||
Cue: 'cue',
|
||||
Title: 'title',
|
||||
Presenter: 'presenter',
|
||||
Subtitle: 'subtitle',
|
||||
'Is Public': 'public',
|
||||
Skip: 'skip',
|
||||
Note: 'notes',
|
||||
@@ -38,8 +36,6 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
||||
duration: namedImportMap.Duration,
|
||||
cue: namedImportMap.Cue,
|
||||
title: namedImportMap.Title,
|
||||
presenter: namedImportMap.Presenter,
|
||||
subtitle: namedImportMap.Subtitle,
|
||||
isPublic: namedImportMap['Is Public'],
|
||||
skip: namedImportMap.Skip,
|
||||
note: namedImportMap.Note,
|
||||
|
||||
@@ -33,8 +33,6 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
<th>Type</th>
|
||||
<th>Cue</th>
|
||||
<th>Title</th>
|
||||
<th>Subtitle</th>
|
||||
<th>Presenter</th>
|
||||
<th>Time Start</th>
|
||||
<th>Time End</th>
|
||||
<th>Duration</th>
|
||||
@@ -85,8 +83,6 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
</td>
|
||||
<td className={style.nowrap}>{event.cue}</td>
|
||||
<td>{event.title}</td>
|
||||
<td>{event.subtitle}</td>
|
||||
<td>{event.presenter}</td>
|
||||
<td>{millisToString(event.timeStart)}</td>
|
||||
<td>{millisToString(event.timeEnd)}</td>
|
||||
<td>{millisToString(event.duration)}</td>
|
||||
|
||||
@@ -19,8 +19,6 @@ exports[`makeTable() > returns array of arrays with given fields 1`] = `
|
||||
"Colour",
|
||||
"Cue",
|
||||
"Title",
|
||||
"Subtitle",
|
||||
"Presenter",
|
||||
"Note",
|
||||
"Is Public? (x)",
|
||||
"Skip?",
|
||||
@@ -35,8 +33,6 @@ exports[`makeTable() > returns array of arrays with given fields 1`] = `
|
||||
"",
|
||||
"test title 1",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"x",
|
||||
"",
|
||||
"",
|
||||
|
||||
@@ -27,15 +27,13 @@ describe('parseField()', () => {
|
||||
});
|
||||
|
||||
it('returns an empty string on undefined fields', () => {
|
||||
expect(parseField('presenter')).toBe('');
|
||||
expect(parseField('title')).toBe('');
|
||||
});
|
||||
|
||||
describe('simply returns any other value in any other field', () => {
|
||||
const testFields = [
|
||||
{ field: 'nothing', value: '123' },
|
||||
{ field: 'title', value: 'test' },
|
||||
{ field: 'presenter', value: 'test' },
|
||||
{ field: 'subtitle', value: 'test' },
|
||||
{ field: 'note', value: 'test' },
|
||||
{ field: 'colour', value: 'test' },
|
||||
];
|
||||
@@ -57,7 +55,6 @@ describe('makeTable()', () => {
|
||||
const tableData = [
|
||||
{
|
||||
title: 'test title 1',
|
||||
presenter: '',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
isPublic: 'x',
|
||||
|
||||
@@ -61,6 +61,7 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
header: customFields[key].label,
|
||||
meta: { colour: customFields[key].colour },
|
||||
cell: MakeCustomField,
|
||||
size: 250,
|
||||
}));
|
||||
|
||||
return [
|
||||
@@ -104,24 +105,14 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
id: 'title',
|
||||
header: 'Title',
|
||||
cell: (row) => row.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'subtitle',
|
||||
id: 'subtitle',
|
||||
header: 'Subtitle',
|
||||
cell: (row) => row.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'presenter',
|
||||
id: 'presenter',
|
||||
header: 'Presenter',
|
||||
cell: (row) => row.getValue(),
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
accessorKey: 'note',
|
||||
id: 'note',
|
||||
header: 'Note',
|
||||
cell: (row) => row.getValue(),
|
||||
size: 250,
|
||||
},
|
||||
...dynamicCustomFields,
|
||||
];
|
||||
|
||||
@@ -56,8 +56,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
||||
'colour',
|
||||
'cue',
|
||||
'title',
|
||||
'subtitle',
|
||||
'presenter',
|
||||
'note',
|
||||
'isPublic',
|
||||
'skip',
|
||||
@@ -72,8 +70,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
||||
'Colour',
|
||||
'Cue',
|
||||
'Title',
|
||||
'Subtitle',
|
||||
'Presenter',
|
||||
'Note',
|
||||
'Is Public? (x)',
|
||||
'Skip?',
|
||||
|
||||
@@ -10,8 +10,6 @@ export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
||||
'timeEnd',
|
||||
'duration',
|
||||
'title',
|
||||
'subtitle',
|
||||
'presenter',
|
||||
'note',
|
||||
];
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import { debounce } from '../../common/utils/debounce';
|
||||
import { getDefaultFormat } from '../../common/utils/time';
|
||||
import { isStringBoolean } from '../../common/utils/viewUtils';
|
||||
import { getPropertyValue, isStringBoolean } from '../viewers/common/viewUtils';
|
||||
|
||||
import EditModal from './edit-modal/EditModal';
|
||||
import FollowButton from './follow-button/FollowButton';
|
||||
@@ -27,7 +27,7 @@ import style from './Operator.module.scss';
|
||||
|
||||
const selectedOffset = 50;
|
||||
|
||||
type TitleFields = Pick<OntimeEvent, 'title' | 'subtitle' | 'presenter'>;
|
||||
type TitleFields = Pick<OntimeEvent, 'title'>;
|
||||
export type EditEvent = Pick<OntimeEvent, 'id' | 'cue'> & { fieldLabel?: string; fieldValue: string };
|
||||
export type PartialEdit = EditEvent & {
|
||||
field: keyof CustomFields;
|
||||
@@ -129,7 +129,7 @@ export default function Operator() {
|
||||
const canEdit = shouldEdit && subscribe;
|
||||
|
||||
const main = searchParams.get('main') as keyof TitleFields | null;
|
||||
const secondary = searchParams.get('secondary') as keyof TitleFields | null;
|
||||
const secondary = searchParams.get('secondary');
|
||||
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
const operatorOptions = getOperatorOptions(customFields, defaultFormat);
|
||||
@@ -176,7 +176,7 @@ export default function Operator() {
|
||||
}
|
||||
|
||||
const mainField = main ? entry?.[main] || entry.title : entry.title;
|
||||
const secondaryField = secondary ? entry?.[secondary] || entry.subtitle : entry.subtitle;
|
||||
const secondaryField = getPropertyValue(entry, secondary) ?? '';
|
||||
const subscribedData = entry.custom[subscribe]?.value;
|
||||
|
||||
return (
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $orange-500
|
||||
color: $orange-500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import style from './EventEditor.module.scss';
|
||||
|
||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'presenter' | 'subtitle' | 'note' | 'colour' | CustomFieldLabel; // TODO: keyof customFields
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
||||
|
||||
export default function EventEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
@@ -90,8 +90,6 @@ export default function EventEditor() {
|
||||
eventId={event.id}
|
||||
cue={event.cue}
|
||||
title={event.title}
|
||||
presenter={event.presenter}
|
||||
subtitle={event.subtitle}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
handleSubmit={handleSubmit}
|
||||
|
||||
@@ -70,7 +70,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
: '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.column}>
|
||||
<div>
|
||||
<div className={style.inputLabel}>Event schedule</div>
|
||||
<div className={style.inline}>
|
||||
@@ -139,7 +139,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,19 +10,17 @@ import EventTextInput from './EventTextInput';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorLeftProps {
|
||||
interface EventEditorTitlesProps {
|
||||
eventId: string;
|
||||
cue: string;
|
||||
title: string;
|
||||
presenter: string;
|
||||
subtitle: string;
|
||||
note: string;
|
||||
colour: string;
|
||||
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
const EventEditorTitles = (props: EventEditorLeftProps) => {
|
||||
const { eventId, cue, title, presenter, subtitle, note, colour, handleSubmit } = props;
|
||||
const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
||||
const { eventId, cue, title, note, colour, handleSubmit } = props;
|
||||
|
||||
const cueSubmitHandler = (_field: string, newValue: string) => {
|
||||
handleSubmit('cue', sanitiseCue(newValue));
|
||||
@@ -51,8 +49,6 @@ const EventEditorTitles = (props: EventEditorLeftProps) => {
|
||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||
<EventTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
||||
<EventTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { ComponentType, useMemo } from 'react';
|
||||
import { ViewExtendedTimer } from 'common/models/TimeManager.type';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
|
||||
import {
|
||||
CustomFields,
|
||||
Message,
|
||||
OntimeEvent,
|
||||
ProjectData,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimerMessage,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
@@ -11,25 +21,26 @@ import { runtimeStore } from '../../common/stores/runtime';
|
||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
|
||||
type WithDataProps = {
|
||||
backstageEvents: OntimeEvent[];
|
||||
customFields: CustomFields;
|
||||
eventNext: OntimeEvent | null;
|
||||
eventNow: OntimeEvent | null;
|
||||
events: OntimeEvent[];
|
||||
external: Message;
|
||||
general: ProjectData;
|
||||
isMirrored: boolean;
|
||||
lower: Message;
|
||||
nextId: string | null;
|
||||
onAir: boolean;
|
||||
pres: TimerMessage;
|
||||
publ: Message;
|
||||
lower: Message;
|
||||
external: Message;
|
||||
eventNow: OntimeEvent | null;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
time: ViewExtendedTimer;
|
||||
events: OntimeEvent[];
|
||||
backstageEvents: OntimeEvent[];
|
||||
selectedId: string | null;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
publicSelectedId: string | null;
|
||||
nextId: string | null;
|
||||
general: ProjectData;
|
||||
viewSettings: ViewSettings;
|
||||
selectedId: string | null;
|
||||
settings: Settings | undefined;
|
||||
onAir: boolean;
|
||||
time: ViewExtendedTimer;
|
||||
viewSettings: ViewSettings;
|
||||
};
|
||||
|
||||
function getDisplayName(Component: React.ComponentType<any>): string {
|
||||
@@ -46,6 +57,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
const { data: project } = useProjectData();
|
||||
const { data: viewSettings } = useViewSettings();
|
||||
const { data: settings } = useSettings();
|
||||
const { data: customFields } = useCustomFields();
|
||||
|
||||
const publicEvents = useMemo(() => {
|
||||
if (Array.isArray(rundownData)) {
|
||||
@@ -83,25 +95,26 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
backstageEvents={rundownData}
|
||||
customFields={customFields}
|
||||
eventNext={eventNext}
|
||||
eventNow={eventNow}
|
||||
events={publicEvents}
|
||||
external={message.external}
|
||||
general={project}
|
||||
isMirrored={isMirrored}
|
||||
lower={message.lower}
|
||||
nextId={nextId}
|
||||
onAir={onAir}
|
||||
pres={message.timer}
|
||||
publ={message.public}
|
||||
lower={message.lower}
|
||||
external={message.external}
|
||||
eventNow={eventNow}
|
||||
publicEventNow={publicEventNow}
|
||||
eventNext={eventNext}
|
||||
publicEventNext={publicEventNext}
|
||||
time={TimeManagerType}
|
||||
events={publicEvents}
|
||||
backstageEvents={rundownData}
|
||||
selectedId={selectedId}
|
||||
publicEventNow={publicEventNow}
|
||||
publicSelectedId={publicSelectedId}
|
||||
viewSettings={viewSettings}
|
||||
selectedId={selectedId}
|
||||
settings={settings}
|
||||
nextId={nextId}
|
||||
general={project}
|
||||
onAir={onAir}
|
||||
time={TimeManagerType}
|
||||
viewSettings={viewSettings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -19,10 +20,12 @@ import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
import './Backstage.scss';
|
||||
|
||||
interface BackstageProps {
|
||||
customFields: CustomFields;
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
eventNow: OntimeEvent | null;
|
||||
@@ -36,12 +39,24 @@ interface BackstageProps {
|
||||
}
|
||||
|
||||
export default function Backstage(props: BackstageProps) {
|
||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings, settings } =
|
||||
props;
|
||||
const {
|
||||
customFields,
|
||||
isMirrored,
|
||||
publ,
|
||||
eventNow,
|
||||
eventNext,
|
||||
time,
|
||||
backstageEvents,
|
||||
selectedId,
|
||||
general,
|
||||
viewSettings,
|
||||
settings,
|
||||
} = props;
|
||||
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [blinkClass, setBlinkClass] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -74,12 +89,16 @@ export default function Backstage(props: BackstageProps) {
|
||||
const showPublicMessage = publ.text && publ.visible;
|
||||
const showProgress = time.playback !== 'stop';
|
||||
|
||||
const secondarySource = searchParams.get('secondary-src');
|
||||
const secondaryTextNext = getPropertyValue(eventNext, secondarySource);
|
||||
const secondaryTextNow = getPropertyValue(eventNow, secondarySource);
|
||||
|
||||
let stageTimer = millisToString(time.current, { fallback: '- - : - -' });
|
||||
stageTimer = removeLeadingZero(stageTimer);
|
||||
|
||||
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
const backstageOptions = getBackstageOptions(defaultFormat);
|
||||
const backstageOptions = getBackstageOptions(defaultFormat, customFields);
|
||||
|
||||
return (
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||
@@ -111,12 +130,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='now'
|
||||
title={eventNow.title}
|
||||
subtitle={eventNow.subtitle}
|
||||
presenter={eventNow.presenter}
|
||||
/>
|
||||
<TitleCard label='now' title={eventNow.title} secondary={secondaryTextNow} />
|
||||
<div className='timer-group'>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
||||
@@ -151,12 +165,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={eventNext.title}
|
||||
subtitle={eventNext.subtitle}
|
||||
presenter={eventNext.presenter}
|
||||
/>
|
||||
<TitleCard label='next' title={eventNext.title} secondary={secondaryTextNext} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { MaybeString, OntimeEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
|
||||
type TimerTypeParams = Pick<ViewExtendedTimer, 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
||||
|
||||
export function getTimerByType(timerObject?: TimerTypeParams): number | null {
|
||||
if (!timerObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (timerObject.timerType) {
|
||||
case TimerType.CountDown:
|
||||
case TimerType.TimeToEnd:
|
||||
return timerObject.current;
|
||||
case TimerType.CountUp:
|
||||
return Math.abs(timerObject.elapsed ?? 0);
|
||||
case TimerType.Clock:
|
||||
return timerObject.clock;
|
||||
default: {
|
||||
const exhaustiveCheck: never = timerObject.timerType;
|
||||
return exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isStringBoolean(text: string | null) {
|
||||
if (text === null) {
|
||||
return false;
|
||||
}
|
||||
return text?.toLowerCase() === 'true' || text === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a dynamic property from an event
|
||||
* Considers custom fields
|
||||
*/
|
||||
export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined {
|
||||
if (!event) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof property !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (property.startsWith('custom-')) {
|
||||
const field = property.split('custom-')[1];
|
||||
return event.custom?.[field]?.value;
|
||||
}
|
||||
|
||||
return event[property as keyof OntimeEvent] as string;
|
||||
}
|
||||
|
||||
export function capitaliseFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { TimerType } from 'ontime-types';
|
||||
|
||||
import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
|
||||
type TimerTypeParams = Pick<ViewExtendedTimer, 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
||||
|
||||
export function getTimerByType(timerObject?: TimerTypeParams): number | null {
|
||||
if (!timerObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (timerObject.timerType) {
|
||||
case TimerType.CountDown:
|
||||
case TimerType.TimeToEnd:
|
||||
return timerObject.current;
|
||||
case TimerType.CountUp:
|
||||
return Math.abs(timerObject.elapsed ?? 0);
|
||||
case TimerType.Clock:
|
||||
return timerObject.clock;
|
||||
default: {
|
||||
const exhaustiveCheck: never = timerObject.timerType;
|
||||
return exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
.data-bottom,
|
||||
.data-top {
|
||||
padding: 0 3vw;
|
||||
font-family: var(--lowerThird-font-family-override, 'Lato');
|
||||
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
|
||||
text-align: var(--lowerThird-text-align-override, left);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { LOWER_THIRD_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
import './LowerThird.scss';
|
||||
|
||||
enum SrcKeys {
|
||||
Title = 'title',
|
||||
Subtitle = 'subtitle',
|
||||
Presenter = 'presenter',
|
||||
LowerMsg = 'lowerMsg',
|
||||
}
|
||||
|
||||
enum TriggerType {
|
||||
Event = 'event',
|
||||
Manual = 'manual',
|
||||
@@ -25,8 +19,8 @@ enum TriggerType {
|
||||
type LowerOptions = {
|
||||
width: number;
|
||||
trigger: TriggerType;
|
||||
topSrc: SrcKeys;
|
||||
bottomSrc: SrcKeys;
|
||||
topSrc: string;
|
||||
bottomSrc: string;
|
||||
topColour: string;
|
||||
bottomColour: string;
|
||||
topBg: string;
|
||||
@@ -41,6 +35,7 @@ type LowerOptions = {
|
||||
};
|
||||
|
||||
interface LowerProps {
|
||||
customFields: CustomFields;
|
||||
eventNow: OntimeEvent | null;
|
||||
viewSettings: ViewSettings;
|
||||
lower: Message;
|
||||
@@ -49,8 +44,8 @@ interface LowerProps {
|
||||
const defaultOptions: Readonly<LowerOptions> = {
|
||||
width: 45,
|
||||
trigger: TriggerType.Event,
|
||||
topSrc: SrcKeys.Title,
|
||||
bottomSrc: SrcKeys.Subtitle,
|
||||
topSrc: 'title',
|
||||
bottomSrc: 'lowerMsg',
|
||||
topColour: '000000ff',
|
||||
bottomColour: '000000ff',
|
||||
topBg: '00000000',
|
||||
@@ -65,7 +60,7 @@ const defaultOptions: Readonly<LowerOptions> = {
|
||||
};
|
||||
|
||||
export default function LowerThird(props: LowerProps) {
|
||||
const { eventNow, lower, viewSettings } = props;
|
||||
const { customFields, eventNow, lower, viewSettings } = props;
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const options = useMemo(() => {
|
||||
@@ -81,12 +76,12 @@ export default function LowerThird(props: LowerProps) {
|
||||
newOptions.trigger = trigger;
|
||||
}
|
||||
|
||||
const topSrc = Object.values(SrcKeys).find((s) => s === searchParams.get('top-src'));
|
||||
const topSrc = searchParams.get('top-src');
|
||||
if (topSrc) {
|
||||
newOptions.topSrc = topSrc;
|
||||
}
|
||||
|
||||
const bottomSrc = Object.values(SrcKeys).find((s) => s === searchParams.get('bottom-src'));
|
||||
const bottomSrc = searchParams.get('bottom-src');
|
||||
if (bottomSrc) {
|
||||
newOptions.bottomSrc = bottomSrc;
|
||||
}
|
||||
@@ -144,34 +139,14 @@ export default function LowerThird(props: LowerProps) {
|
||||
return newOptions;
|
||||
}, [searchParams]);
|
||||
|
||||
const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre');
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
|
||||
const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre');
|
||||
|
||||
// set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Lower Third';
|
||||
}, []);
|
||||
|
||||
const topText = useMemo(() => {
|
||||
if (options.topSrc === SrcKeys.LowerMsg) {
|
||||
return lower.text;
|
||||
} else if (eventNow) {
|
||||
return eventNow[options.topSrc];
|
||||
}
|
||||
return '';
|
||||
}, [eventNow, lower.text, options]);
|
||||
|
||||
const bottomText = useMemo(() => {
|
||||
if (options.bottomSrc === SrcKeys.LowerMsg) {
|
||||
return lower.text;
|
||||
} else if (eventNow) {
|
||||
return eventNow[options.bottomSrc];
|
||||
}
|
||||
return '';
|
||||
}, [eventNow, lower.text, options]);
|
||||
|
||||
const transition = `${options.transition}s`;
|
||||
|
||||
const trigger = useMemo(() => {
|
||||
if (options.trigger === TriggerType.Event) {
|
||||
return eventNow?.id;
|
||||
@@ -181,6 +156,7 @@ export default function LowerThird(props: LowerProps) {
|
||||
return false;
|
||||
}, [eventNow?.id, lower.visible, options.trigger]);
|
||||
|
||||
// coordinate load-unload of lower third
|
||||
useEffect(() => {
|
||||
if (options.trigger === TriggerType.Event && trigger) {
|
||||
setPlayState('in');
|
||||
@@ -197,10 +173,26 @@ export default function LowerThird(props: LowerProps) {
|
||||
return () => null;
|
||||
}, [options.delay, options.transition, options.trigger, trigger]);
|
||||
|
||||
const topText = useMemo(() => {
|
||||
if (options.topSrc === 'lowerMsg') {
|
||||
return lower.text;
|
||||
}
|
||||
return getPropertyValue(eventNow, options.topSrc) ?? '';
|
||||
}, [eventNow, lower.text, options]);
|
||||
|
||||
const bottomText = useMemo(() => {
|
||||
if (options.bottomSrc === 'lowerMsg') {
|
||||
return lower.text;
|
||||
}
|
||||
return getPropertyValue(eventNow, options.bottomSrc) ?? '';
|
||||
}, [eventNow, lower.text, options]);
|
||||
|
||||
const transition = `${options.transition}s`;
|
||||
|
||||
return (
|
||||
<div className='lower-third' style={{ backgroundColor: `#${options.key}` }}>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={LOWER_THIRD_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={getLowerThirdOptions(customFields)} />
|
||||
<div
|
||||
className={`container container--${playState}`}
|
||||
style={{ minWidth: `${options.width}vw`, animationDuration: transition }}
|
||||
|
||||
@@ -11,9 +11,8 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { OverridableOptions } from '../../../common/models/View.types';
|
||||
import { timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { getTimerByType } from '../common/viewerUtils';
|
||||
import { getTimerByType, isStringBoolean } from '../common/viewUtils';
|
||||
|
||||
import './MinimalTimer.scss';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
@@ -17,10 +18,12 @@ import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
import './Public.scss';
|
||||
|
||||
interface BackstageProps {
|
||||
customFields: CustomFields;
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
@@ -35,6 +38,7 @@ interface BackstageProps {
|
||||
|
||||
export default function Public(props: BackstageProps) {
|
||||
const {
|
||||
customFields,
|
||||
isMirrored,
|
||||
publ,
|
||||
publicEventNow,
|
||||
@@ -49,7 +53,9 @@ export default function Public(props: BackstageProps) {
|
||||
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Public Screen';
|
||||
}, []);
|
||||
@@ -64,7 +70,11 @@ export default function Public(props: BackstageProps) {
|
||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
const publicOptions = getPublicOptions(defaultFormat);
|
||||
const publicOptions = getPublicOptions(defaultFormat, customFields);
|
||||
|
||||
const secondarySource = searchParams.get('secondary-src');
|
||||
const secondaryTextNext = getPropertyValue(publicEventNext, secondarySource);
|
||||
const secondaryTextNow = getPropertyValue(publicEventNow, secondarySource);
|
||||
|
||||
return (
|
||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||
@@ -89,12 +99,7 @@ export default function Public(props: BackstageProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='now'
|
||||
title={publicEventNow.title}
|
||||
subtitle={publicEventNow.subtitle}
|
||||
presenter={publicEventNow.presenter}
|
||||
/>
|
||||
<TitleCard label='now' title={publicEventNow.title} secondary={secondaryTextNow} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -109,12 +114,7 @@ export default function Public(props: BackstageProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={publicEventNext.title}
|
||||
subtitle={publicEventNext.subtitle}
|
||||
presenter={publicEventNext.presenter}
|
||||
/>
|
||||
<TitleCard label='next' title={publicEventNext.title} secondary={secondaryTextNext} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -12,8 +12,8 @@ import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { isStringBoolean } from '../common/viewUtils';
|
||||
|
||||
import { secondsInMillis, trimRundown } from './studioClock.utils';
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, Playback, Settings, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||
import {
|
||||
CustomFields,
|
||||
Message,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
Settings,
|
||||
TimerMessage,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -14,10 +23,9 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
import { timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { getTimerByType } from '../common/viewerUtils';
|
||||
import { getPropertyValue, getTimerByType, isStringBoolean } from '../common/viewUtils';
|
||||
|
||||
import './Timer.scss';
|
||||
|
||||
@@ -38,18 +46,19 @@ const titleVariants = {
|
||||
};
|
||||
|
||||
interface TimerProps {
|
||||
customFields: CustomFields;
|
||||
eventNext: OntimeEvent | null;
|
||||
eventNow: OntimeEvent | null;
|
||||
external: Message;
|
||||
isMirrored: boolean;
|
||||
pres: TimerMessage;
|
||||
external: Message;
|
||||
eventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
settings: Settings | undefined;
|
||||
time: ViewExtendedTimer;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function Timer(props: TimerProps) {
|
||||
const { isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
|
||||
const { customFields, isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -92,6 +101,10 @@ export default function Timer(props: TimerProps) {
|
||||
const hideTimerSeconds = searchParams.get('hideTimerSeconds');
|
||||
userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds);
|
||||
|
||||
const secondarySource = searchParams.get('secondary-src');
|
||||
const secondaryTextNow = getPropertyValue(eventNow, secondarySource);
|
||||
const secondaryTextNext = getPropertyValue(eventNext, secondarySource);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playback !== Playback.Pause;
|
||||
|
||||
@@ -143,7 +156,7 @@ export default function Timer(props: TimerProps) {
|
||||
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
|
||||
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
const timerOptions = getTimerOptions(defaultFormat);
|
||||
const timerOptions = getTimerOptions(defaultFormat, customFields);
|
||||
|
||||
return (
|
||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||
@@ -201,7 +214,7 @@ export default function Timer(props: TimerProps) {
|
||||
{!userOptions.hideCards && (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{eventNow && !finished && (
|
||||
{eventNow && (
|
||||
<motion.div
|
||||
className='event now'
|
||||
key='now'
|
||||
@@ -210,12 +223,7 @@ export default function Timer(props: TimerProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='now'
|
||||
title={eventNow.title}
|
||||
subtitle={eventNow.subtitle}
|
||||
presenter={eventNow.presenter}
|
||||
/>
|
||||
<TitleCard label='now' title={eventNow.title} secondary={secondaryTextNow} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -230,12 +238,7 @@ export default function Timer(props: TimerProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={eventNext.title}
|
||||
subtitle={eventNext.subtitle}
|
||||
presenter={eventNext.presenter}
|
||||
/>
|
||||
<TitleCard label='next' title={eventNext.title} secondary={secondaryTextNext} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -6,8 +6,6 @@ import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../util
|
||||
// TODO: handle custom fields
|
||||
const whitelistedPayload = {
|
||||
title: coerceString,
|
||||
subtitle: coerceString,
|
||||
presenter: coerceString,
|
||||
note: coerceString,
|
||||
cue: coerceString,
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
|
||||
export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
|
||||
@@ -395,8 +395,6 @@ describe('calculateRuntimeDelays', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -423,8 +421,6 @@ describe('calculateRuntimeDelays', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -451,8 +447,6 @@ describe('calculateRuntimeDelays', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -479,8 +473,6 @@ describe('calculateRuntimeDelays', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -516,8 +508,6 @@ describe('getDelayAt()', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -545,8 +535,6 @@ describe('getDelayAt()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -574,8 +562,6 @@ describe('getDelayAt()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -603,8 +589,6 @@ describe('getDelayAt()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -658,8 +642,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -687,8 +669,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -716,8 +696,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
@@ -745,8 +723,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
|
||||
@@ -24,8 +24,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -47,8 +45,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
@@ -72,8 +68,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -95,8 +89,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
@@ -121,8 +113,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -144,8 +134,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
@@ -170,8 +158,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -192,12 +178,10 @@ describe('cellRequestFromEvent()', () => {
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 0 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[6].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells.rows[0].values[10].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
@@ -205,8 +189,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -227,13 +209,11 @@ describe('cellRequestFromEvent()', () => {
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 5 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[1].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
@@ -241,8 +221,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
@@ -263,7 +241,6 @@ describe('cellRequestFromEvent()', () => {
|
||||
const metadata = {
|
||||
cue: { row: 10, col: 5 },
|
||||
title: { row: 10, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
};
|
||||
const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||
|
||||
@@ -28,8 +28,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Guest Welcoming',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Guest Welcoming',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.Clock,
|
||||
@@ -51,8 +49,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Good Morning',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Good Morning',
|
||||
subtitle: 'Days schedule',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.CountUp,
|
||||
@@ -74,8 +70,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Stage 2 setup',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Stage 2 setup',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: 'wrong action' as EndAction, // testing
|
||||
timerType: TimerType.Clock,
|
||||
@@ -98,8 +92,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Working Procedures',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Working Procedures',
|
||||
subtitle: '',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
@@ -118,8 +110,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Lunch',
|
||||
title: 'Lunch',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
@@ -141,8 +131,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'A day being carlos',
|
||||
title: 'A day being carlos',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: 'My life in a song',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
@@ -165,8 +153,6 @@ describe('test json parser with valid def', () => {
|
||||
cue: 'Hamburgers and Cheese',
|
||||
title: 'Hamburgers and Cheese',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '... and other life questions',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
@@ -470,8 +456,6 @@ describe('test event validator', () => {
|
||||
expect(validated).toEqual(
|
||||
expect.objectContaining({
|
||||
title: expect.any(String),
|
||||
subtitle: expect.any(String),
|
||||
presenter: expect.any(String),
|
||||
note: expect.any(String),
|
||||
timeStart: expect.any(Number),
|
||||
timeEnd: expect.any(Number),
|
||||
@@ -496,15 +480,11 @@ describe('test event validator', () => {
|
||||
it('makes objects strings', () => {
|
||||
const event = {
|
||||
title: 2,
|
||||
subtitle: true,
|
||||
presenter: 3.2,
|
||||
note: '1899-12-30T08:00:10.000Z',
|
||||
};
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const validated = createEvent(event, 'not-used');
|
||||
expect(typeof validated.title).toEqual('string');
|
||||
expect(typeof validated.subtitle).toEqual('string');
|
||||
expect(typeof validated.presenter).toEqual('string');
|
||||
expect(typeof validated.note).toEqual('string');
|
||||
});
|
||||
|
||||
@@ -697,8 +677,6 @@ describe('test import of v2 datamodel', () => {
|
||||
id: expect.any(String),
|
||||
cue: expect.any(String),
|
||||
title: expect.any(String),
|
||||
subtitle: expect.any(String),
|
||||
presenter: expect.any(String),
|
||||
note: expect.any(String),
|
||||
endAction: expect.any(String),
|
||||
timerType: expect.any(String),
|
||||
@@ -757,8 +735,6 @@ describe('getCustomFieldData()', () => {
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
@@ -811,8 +787,6 @@ describe('parseExcel()', () => {
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Title',
|
||||
'Presenter',
|
||||
'Subtitle',
|
||||
'End Action',
|
||||
'Timer type',
|
||||
'Public',
|
||||
@@ -835,8 +809,6 @@ describe('parseExcel()', () => {
|
||||
'1899-12-30T07:00:00.000Z',
|
||||
'1899-12-30T08:00:10.000Z',
|
||||
'Guest Welcome',
|
||||
'Carlos',
|
||||
'Getting things started',
|
||||
'',
|
||||
'',
|
||||
'x',
|
||||
@@ -859,8 +831,6 @@ describe('parseExcel()', () => {
|
||||
'1899-12-30T08:00:00.000Z',
|
||||
'1899-12-30T08:30:00.000Z',
|
||||
'A song from the hearth',
|
||||
'Still Carlos',
|
||||
'Derailing early',
|
||||
'load-next',
|
||||
'clock',
|
||||
'',
|
||||
@@ -904,8 +874,6 @@ describe('parseExcel()', () => {
|
||||
//timeStart: 28800000,
|
||||
//timeEnd: 32410000,
|
||||
title: 'Guest Welcome',
|
||||
presenter: 'Carlos',
|
||||
subtitle: 'Getting things started',
|
||||
timerType: 'count-down',
|
||||
endAction: 'none',
|
||||
isPublic: true,
|
||||
@@ -931,8 +899,6 @@ describe('parseExcel()', () => {
|
||||
//timeStart: 32400000,
|
||||
//timeEnd: 34200000,
|
||||
title: 'A song from the hearth',
|
||||
presenter: 'Still Carlos',
|
||||
subtitle: 'Derailing early',
|
||||
timerType: 'clock',
|
||||
endAction: 'load-next',
|
||||
isPublic: false,
|
||||
@@ -1014,8 +980,6 @@ describe('parseExcel()', () => {
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Title',
|
||||
'Presenter',
|
||||
'Subtitle',
|
||||
'End Action',
|
||||
'Timer type',
|
||||
'Public',
|
||||
@@ -1038,8 +1002,6 @@ describe('parseExcel()', () => {
|
||||
'1899-12-30T07:00:00.000Z',
|
||||
'1899-12-30T08:00:10.000Z',
|
||||
'Guest Welcome',
|
||||
'Carlos',
|
||||
'Getting things started',
|
||||
'',
|
||||
'',
|
||||
'x',
|
||||
@@ -1062,8 +1024,6 @@ describe('parseExcel()', () => {
|
||||
'1899-12-30T08:00:00.000Z',
|
||||
'1899-12-30T08:30:00.000Z',
|
||||
'A song from the hearth',
|
||||
'Still Carlos',
|
||||
'Derailing early',
|
||||
'load-next',
|
||||
'clock',
|
||||
'',
|
||||
@@ -1099,8 +1059,6 @@ describe('parseExcel()', () => {
|
||||
//timeStart: 28800000,
|
||||
//timeEnd: 32410000,
|
||||
title: 'Guest Welcome',
|
||||
presenter: 'Carlos',
|
||||
subtitle: 'Getting things started',
|
||||
timerType: 'count-down',
|
||||
endAction: 'none',
|
||||
isPublic: true,
|
||||
@@ -1115,8 +1073,6 @@ describe('parseExcel()', () => {
|
||||
//timeStart: 32400000,
|
||||
//timeEnd: 34200000,
|
||||
title: 'A song from the hearth',
|
||||
presenter: 'Still Carlos',
|
||||
subtitle: 'Derailing early',
|
||||
timerType: 'clock',
|
||||
endAction: 'load-next',
|
||||
isPublic: false,
|
||||
|
||||
@@ -91,8 +91,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
||||
// title stuff: strings
|
||||
let titleIndex: number | null = null;
|
||||
let cueIndex: number | null = null;
|
||||
let presenterIndex: number | null = null;
|
||||
let subtitleIndex: number | null = null;
|
||||
let notesIndex: number | null = null;
|
||||
let colourIndex: number | null = null;
|
||||
|
||||
@@ -142,14 +140,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
||||
titleIndex = col;
|
||||
rundownMetadata['title'] = { row, col };
|
||||
},
|
||||
[importMap.presenter]: (row: number, col: number) => {
|
||||
presenterIndex = col;
|
||||
rundownMetadata['presenter'] = { row, col };
|
||||
},
|
||||
[importMap.subtitle]: (row: number, col: number) => {
|
||||
subtitleIndex = col;
|
||||
rundownMetadata['subtitle'] = { row, col };
|
||||
},
|
||||
[importMap.isPublic]: (row: number, col: number) => {
|
||||
isPublicIndex = col;
|
||||
rundownMetadata['isPublic'] = { row, col };
|
||||
@@ -217,10 +207,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = column == 'x' ? true : coerceBoolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
@@ -337,8 +323,6 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
||||
id: originalEvent.id,
|
||||
type: SupportedEvent.Event,
|
||||
title: makeString(patchEvent.title, originalEvent.title),
|
||||
subtitle: makeString(patchEvent.subtitle, originalEvent.subtitle),
|
||||
presenter: makeString(patchEvent.presenter, originalEvent.presenter),
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
|
||||
+59
-51
@@ -2,8 +2,6 @@
|
||||
"rundown": [
|
||||
{
|
||||
"title": "Albania",
|
||||
"subtitle": "Sekret",
|
||||
"presenter": "Ronela Hajati",
|
||||
"note": "SF1.01",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -17,12 +15,13 @@
|
||||
"revision": 0,
|
||||
"id": "32d31",
|
||||
"cue": "SF1.01",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Sekret",
|
||||
"artist": "Ronela Hajati"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Latvia",
|
||||
"subtitle": "Eat Your Salad",
|
||||
"presenter": "Citi Zeni",
|
||||
"note": "SF1.02",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -36,12 +35,13 @@
|
||||
"revision": 0,
|
||||
"id": "21cd2",
|
||||
"cue": "SF1.02",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Eat Your Salad",
|
||||
"artist": "Citi Zeni"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lithuania",
|
||||
"subtitle": "Sentimentai",
|
||||
"presenter": "Monika Liu",
|
||||
"note": "SF1.03",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -54,12 +54,14 @@
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "0b371",
|
||||
"cue": "SF1.03"
|
||||
"cue": "SF1.03",
|
||||
"custom": {
|
||||
"song": "Sentimentai",
|
||||
"artist": "Monika Liu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Switzerland",
|
||||
"subtitle": "Boys Do Cry",
|
||||
"presenter": "Marius Bear",
|
||||
"note": "SF1.04",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -73,12 +75,13 @@
|
||||
"revision": 0,
|
||||
"id": "3cd28",
|
||||
"cue": "SF1.04",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Boys Do Cry",
|
||||
"artist": "Marius Bear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Slovenia",
|
||||
"subtitle": "Disko",
|
||||
"presenter": "LPS",
|
||||
"note": "SF1.05",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -92,7 +95,10 @@
|
||||
"revision": 0,
|
||||
"id": "e457f",
|
||||
"cue": "SF1.05",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Disko",
|
||||
"artist": "LPS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lunch break",
|
||||
@@ -101,8 +107,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Ukraine",
|
||||
"subtitle": "Stefania",
|
||||
"presenter": "Kalush Orchestra",
|
||||
"note": "SF1.06",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -116,12 +120,13 @@
|
||||
"revision": 0,
|
||||
"id": "1c420",
|
||||
"cue": "SF1.06",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Stefania",
|
||||
"artist": "Kalush Orchestra"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Bulgaria",
|
||||
"subtitle": "Intention",
|
||||
"presenter": "Intelligent Music Project",
|
||||
"note": "SF1.07",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -135,12 +140,13 @@
|
||||
"revision": 0,
|
||||
"id": "b7737",
|
||||
"cue": "SF1.07",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Intention",
|
||||
"artist": "Intelligent Music Project"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Netherlands",
|
||||
"subtitle": "De Diepte",
|
||||
"presenter": "S10",
|
||||
"note": "SF1.08",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -154,12 +160,13 @@
|
||||
"revision": 0,
|
||||
"id": "d3a80",
|
||||
"cue": "SF1.08",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "De Diepte",
|
||||
"artist": "S10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Moldova",
|
||||
"subtitle": "Trenuletul",
|
||||
"presenter": "Zdob si Zdub",
|
||||
"note": "SF1.09",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -173,12 +180,13 @@
|
||||
"revision": 0,
|
||||
"id": "8276c",
|
||||
"cue": "SF1.09",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Trenuletul",
|
||||
"artist": "Zdob si Zdub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Portugal",
|
||||
"subtitle": "Saudade Saudade",
|
||||
"presenter": "Maro",
|
||||
"note": "SF1.10",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -192,7 +200,10 @@
|
||||
"revision": 0,
|
||||
"id": "2340b",
|
||||
"cue": "SF1.10",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Saudade Saudade",
|
||||
"artist": "Maro"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Afternoon break",
|
||||
@@ -201,8 +212,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Croatia",
|
||||
"subtitle": "Guilty Pleasure",
|
||||
"presenter": "Mia Dimsic",
|
||||
"note": "SF1.11",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -216,12 +225,13 @@
|
||||
"revision": 0,
|
||||
"id": "503c4",
|
||||
"cue": "SF1.11",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Guilty Pleasure",
|
||||
"artist": "Mia Dimsic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Denmark",
|
||||
"subtitle": "The Show",
|
||||
"presenter": "Reddi",
|
||||
"note": "SF1.12",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -235,12 +245,13 @@
|
||||
"revision": 0,
|
||||
"id": "5e965",
|
||||
"cue": "SF1.12",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "The Show",
|
||||
"artist": "Reddi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Austria",
|
||||
"subtitle": "Halo",
|
||||
"presenter": "LUM!X & Pia Maria",
|
||||
"note": "SF1.13",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -254,12 +265,13 @@
|
||||
"revision": 0,
|
||||
"id": "bab4a",
|
||||
"cue": "SF1.13",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Halo",
|
||||
"artist": "LUM!X & Pia Maria"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Greece",
|
||||
"subtitle": "Die Together",
|
||||
"presenter": "Amanda Tenfjord",
|
||||
"note": "SF1.14",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -273,7 +285,10 @@
|
||||
"revision": 0,
|
||||
"id": "d3eb1",
|
||||
"cue": "SF1.14",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Die Together",
|
||||
"artist": "Amanda Tenfjord"
|
||||
}
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
@@ -315,15 +330,8 @@
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabledIn": true,
|
||||
"enabledOut": true,
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": "10eea",
|
||||
"enabled": true,
|
||||
"cycle": "onUpdate",
|
||||
"message": "/ontime/update/{{timer.current}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subscriptions": []
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": []
|
||||
|
||||
+56
-42
@@ -2,8 +2,6 @@
|
||||
"rundown": [
|
||||
{
|
||||
"title": "Albania",
|
||||
"subtitle": "Sekret",
|
||||
"presenter": "Ronela Hajati",
|
||||
"note": "SF1.01",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -17,12 +15,13 @@
|
||||
"revision": 0,
|
||||
"id": "32d31",
|
||||
"cue": "SF1.01",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Sekret",
|
||||
"artist": "Ronela Hajati"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Latvia",
|
||||
"subtitle": "Eat Your Salad",
|
||||
"presenter": "Citi Zeni",
|
||||
"note": "SF1.02",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -36,12 +35,13 @@
|
||||
"revision": 0,
|
||||
"id": "21cd2",
|
||||
"cue": "SF1.02",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Eat Your Salad",
|
||||
"artist": "Citi Zeni"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lithuania",
|
||||
"subtitle": "Sentimentai",
|
||||
"presenter": "Monika Liu",
|
||||
"note": "SF1.03",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -55,12 +55,13 @@
|
||||
"revision": 0,
|
||||
"id": "0b371",
|
||||
"cue": "SF1.03",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Sentimentai",
|
||||
"artist": "Monika Liu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Switzerland",
|
||||
"subtitle": "Boys Do Cry",
|
||||
"presenter": "Marius Bear",
|
||||
"note": "SF1.04",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -74,12 +75,13 @@
|
||||
"revision": 0,
|
||||
"id": "3cd28",
|
||||
"cue": "SF1.04",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Boys Do Cry",
|
||||
"artist": "Marius Bear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Slovenia",
|
||||
"subtitle": "Disko",
|
||||
"presenter": "LPS",
|
||||
"note": "SF1.05",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -93,7 +95,10 @@
|
||||
"revision": 0,
|
||||
"id": "e457f",
|
||||
"cue": "SF1.05",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Disko",
|
||||
"artist": "LPS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lunch break",
|
||||
@@ -102,8 +107,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Ukraine",
|
||||
"subtitle": "Stefania",
|
||||
"presenter": "Kalush Orchestra",
|
||||
"note": "SF1.06",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -117,12 +120,13 @@
|
||||
"revision": 0,
|
||||
"id": "1c420",
|
||||
"cue": "SF1.06",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Stefania",
|
||||
"artist": "Kalush Orchestra"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Bulgaria",
|
||||
"subtitle": "Intention",
|
||||
"presenter": "Intelligent Music Project",
|
||||
"note": "SF1.07",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -136,12 +140,13 @@
|
||||
"revision": 0,
|
||||
"id": "b7737",
|
||||
"cue": "SF1.07",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Intention",
|
||||
"artist": "Intelligent Music Project"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Netherlands",
|
||||
"subtitle": "De Diepte",
|
||||
"presenter": "S10",
|
||||
"note": "SF1.08",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -155,12 +160,13 @@
|
||||
"revision": 0,
|
||||
"id": "d3a80",
|
||||
"cue": "SF1.08",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "De Diepte",
|
||||
"artist": "S10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Moldova",
|
||||
"subtitle": "Trenuletul",
|
||||
"presenter": "Zdob si Zdub",
|
||||
"note": "SF1.09",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -174,12 +180,13 @@
|
||||
"revision": 0,
|
||||
"id": "8276c",
|
||||
"cue": "SF1.09",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Trenuletul",
|
||||
"artist": "Zdob si Zdub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Portugal",
|
||||
"subtitle": "Saudade Saudade",
|
||||
"presenter": "Maro",
|
||||
"note": "SF1.10",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -193,7 +200,10 @@
|
||||
"revision": 0,
|
||||
"id": "2340b",
|
||||
"cue": "SF1.10",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Saudade Saudade",
|
||||
"artist": "Maro"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Afternoon break",
|
||||
@@ -202,8 +212,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Croatia",
|
||||
"subtitle": "Guilty Pleasure",
|
||||
"presenter": "Mia Dimsic",
|
||||
"note": "SF1.11",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -217,12 +225,13 @@
|
||||
"revision": 0,
|
||||
"id": "503c4",
|
||||
"cue": "SF1.11",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Guilty Pleasure",
|
||||
"artist": "Mia Dimsic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Denmark",
|
||||
"subtitle": "The Show",
|
||||
"presenter": "Reddi",
|
||||
"note": "SF1.12",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -236,12 +245,13 @@
|
||||
"revision": 0,
|
||||
"id": "5e965",
|
||||
"cue": "SF1.12",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "The Show",
|
||||
"artist": "Reddi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Austria",
|
||||
"subtitle": "Halo",
|
||||
"presenter": "LUM!X & Pia Maria",
|
||||
"note": "SF1.13",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -255,12 +265,13 @@
|
||||
"revision": 0,
|
||||
"id": "bab4a",
|
||||
"cue": "SF1.13",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Halo",
|
||||
"artist": "LUM!X & Pia Maria"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Greece",
|
||||
"subtitle": "Die Together",
|
||||
"presenter": "Amanda Tenfjord",
|
||||
"note": "SF1.14",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -274,7 +285,10 @@
|
||||
"revision": 0,
|
||||
"id": "d3eb1",
|
||||
"cue": "SF1.14",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Die Together",
|
||||
"artist": "Amanda Tenfjord"
|
||||
}
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
|
||||
@@ -25,8 +25,6 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||
'Colour',
|
||||
'Cue',
|
||||
'Title',
|
||||
'Subtitle',
|
||||
'Presenter',
|
||||
'Note',
|
||||
'Is Public? (x)',
|
||||
'Skip?',
|
||||
|
||||
Vendored
+56
-42
@@ -2,8 +2,6 @@
|
||||
"rundown": [
|
||||
{
|
||||
"title": "Albania",
|
||||
"subtitle": "Sekret",
|
||||
"presenter": "Ronela Hajati",
|
||||
"note": "SF1.01",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -17,12 +15,13 @@
|
||||
"revision": 0,
|
||||
"id": "32d31",
|
||||
"cue": "SF1.01",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Sekret",
|
||||
"artist": "Ronela Hajati"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Latvia",
|
||||
"subtitle": "Eat Your Salad",
|
||||
"presenter": "Citi Zeni",
|
||||
"note": "SF1.02",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -36,12 +35,13 @@
|
||||
"revision": 0,
|
||||
"id": "21cd2",
|
||||
"cue": "SF1.02",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Eat Your Salad",
|
||||
"artist": "Citi Zeni"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lithuania",
|
||||
"subtitle": "Sentimentai",
|
||||
"presenter": "Monika Liu",
|
||||
"note": "SF1.03",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -55,12 +55,13 @@
|
||||
"revision": 0,
|
||||
"id": "0b371",
|
||||
"cue": "SF1.03",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Sentimentai",
|
||||
"artist": "Monika Liu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Switzerland",
|
||||
"subtitle": "Boys Do Cry",
|
||||
"presenter": "Marius Bear",
|
||||
"note": "SF1.04",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -74,12 +75,13 @@
|
||||
"revision": 0,
|
||||
"id": "3cd28",
|
||||
"cue": "SF1.04",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Boys Do Cry",
|
||||
"artist": "Marius Bear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Slovenia",
|
||||
"subtitle": "Disko",
|
||||
"presenter": "LPS",
|
||||
"note": "SF1.05",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -93,7 +95,10 @@
|
||||
"revision": 0,
|
||||
"id": "e457f",
|
||||
"cue": "SF1.05",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Disko",
|
||||
"artist": "LPS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Lunch break",
|
||||
@@ -102,8 +107,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Ukraine",
|
||||
"subtitle": "Stefania",
|
||||
"presenter": "Kalush Orchestra",
|
||||
"note": "SF1.06",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -117,12 +120,13 @@
|
||||
"revision": 0,
|
||||
"id": "1c420",
|
||||
"cue": "SF1.06",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Stefania",
|
||||
"artist": "Kalush Orchestra"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Bulgaria",
|
||||
"subtitle": "Intention",
|
||||
"presenter": "Intelligent Music Project",
|
||||
"note": "SF1.07",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -136,12 +140,13 @@
|
||||
"revision": 0,
|
||||
"id": "b7737",
|
||||
"cue": "SF1.07",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Intention",
|
||||
"artist": "Intelligent Music Project"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Netherlands",
|
||||
"subtitle": "De Diepte",
|
||||
"presenter": "S10",
|
||||
"note": "SF1.08",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -155,12 +160,13 @@
|
||||
"revision": 0,
|
||||
"id": "d3a80",
|
||||
"cue": "SF1.08",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "De Diepte",
|
||||
"artist": "S10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Moldova",
|
||||
"subtitle": "Trenuletul",
|
||||
"presenter": "Zdob si Zdub",
|
||||
"note": "SF1.09",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -174,12 +180,13 @@
|
||||
"revision": 0,
|
||||
"id": "8276c",
|
||||
"cue": "SF1.09",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Trenuletul",
|
||||
"artist": "Zdob si Zdub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Portugal",
|
||||
"subtitle": "Saudade Saudade",
|
||||
"presenter": "Maro",
|
||||
"note": "SF1.10",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -193,7 +200,10 @@
|
||||
"revision": 0,
|
||||
"id": "2340b",
|
||||
"cue": "SF1.10",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Saudade Saudade",
|
||||
"artist": "Maro"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Afternoon break",
|
||||
@@ -202,8 +212,6 @@
|
||||
},
|
||||
{
|
||||
"title": "Croatia",
|
||||
"subtitle": "Guilty Pleasure",
|
||||
"presenter": "Mia Dimsic",
|
||||
"note": "SF1.11",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -217,12 +225,13 @@
|
||||
"revision": 0,
|
||||
"id": "503c4",
|
||||
"cue": "SF1.11",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Guilty Pleasure",
|
||||
"artist": "Mia Dimsic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Denmark",
|
||||
"subtitle": "The Show",
|
||||
"presenter": "Reddi",
|
||||
"note": "SF1.12",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -236,12 +245,13 @@
|
||||
"revision": 0,
|
||||
"id": "5e965",
|
||||
"cue": "SF1.12",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "The Show",
|
||||
"artist": "Reddi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Austria",
|
||||
"subtitle": "Halo",
|
||||
"presenter": "LUM!X & Pia Maria",
|
||||
"note": "SF1.13",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -255,12 +265,13 @@
|
||||
"revision": 0,
|
||||
"id": "bab4a",
|
||||
"cue": "SF1.13",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Halo",
|
||||
"artist": "LUM!X & Pia Maria"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Greece",
|
||||
"subtitle": "Die Together",
|
||||
"presenter": "Amanda Tenfjord",
|
||||
"note": "SF1.14",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
@@ -274,7 +285,10 @@
|
||||
"revision": 0,
|
||||
"id": "d3eb1",
|
||||
"cue": "SF1.14",
|
||||
"custom": {}
|
||||
"custom": {
|
||||
"song": "Die Together",
|
||||
"artist": "Amanda Tenfjord"
|
||||
}
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
|
||||
@@ -26,8 +26,6 @@ export type OntimeEvent = OntimeBaseEvent & {
|
||||
type: SupportedEvent.Event;
|
||||
cue: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
presenter: string;
|
||||
note: string;
|
||||
endAction: EndAction;
|
||||
timerType: TimerType;
|
||||
|
||||
@@ -9,8 +9,6 @@ describe('isImportMap()', () => {
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
@@ -33,8 +31,6 @@ describe('isImportMap()', () => {
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
|
||||
@@ -10,8 +10,6 @@ export const defaultImportMap = {
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"rundown": [],
|
||||
"project": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"publicUrl": "",
|
||||
"publicInfo": "",
|
||||
"backstageUrl": "",
|
||||
"backstageInfo": ""
|
||||
},
|
||||
"settings": {
|
||||
"app": "ontime",
|
||||
"version": "3.0.0-alpha",
|
||||
"serverPort": 4001,
|
||||
"editorKey": null,
|
||||
"operatorKey": null,
|
||||
"timeFormat": "24",
|
||||
"language": "en"
|
||||
},
|
||||
"viewSettings": {
|
||||
"overrideStyles": false,
|
||||
"normalColor": "#ffffffcc",
|
||||
"warningColor": "#FFAB33",
|
||||
"dangerColor": "#ED3333",
|
||||
"endMessage": ""
|
||||
},
|
||||
"aliases": [],
|
||||
"customFields": {},
|
||||
"osc": {
|
||||
"portIn": 8888,
|
||||
"portOut": 9999,
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabledIn": false,
|
||||
"enabledOut": false,
|
||||
"subscriptions": []
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": false,
|
||||
"subscriptions": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user