mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +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:
-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>
|
||||
|
||||
Reference in New Issue
Block a user