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:
Carlos Valente
2024-02-29 18:19:58 +01:00
committed by GitHub
parent d7392b93d2
commit 8df419d835
51 changed files with 735 additions and 731 deletions
@@ -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';
}
@@ -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,
-16
View File
@@ -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
View File
@@ -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
View File
@@ -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": {
-2
View File
@@ -25,8 +25,6 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
'Colour',
'Cue',
'Title',
'Subtitle',
'Presenter',
'Note',
'Is Public? (x)',
'Skip?',
+56 -42
View File
@@ -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',
+41
View File
@@ -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": []
}
}