mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 12:29:10 +00:00
Alpha revision (#811)
* style: less space for direction button * chore: cleanup unused * refactor: prevent CSS pollution * refactor: force render on swatch * refactor: force render on swatch * refactor: improve import of blocks and skip import * chore: remove unused * chore: clarify effect * chore: updata dataset * fix: integrations not triggering * chore: remove unused * fix: onair derives from playback * fix: gap day after
This commit is contained in:
@@ -21,6 +21,7 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
// avoid wrong submit on cancel
|
// avoid wrong submit on cancel
|
||||||
let ignoreChange = false;
|
let ignoreChange = false;
|
||||||
|
|
||||||
|
// set internal value on duration change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof duration === 'undefined') {
|
if (typeof duration === 'undefined') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ export const setMessage = {
|
|||||||
publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }),
|
publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }),
|
||||||
lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
|
lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
|
||||||
lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: payload } }),
|
lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: payload } }),
|
||||||
externalText: (payload: string) => socketSendJson('message', { external: { visible: payload } }),
|
|
||||||
externalVisible: (payload: boolean) => socketSendJson('message', { external: { visible: payload } }),
|
|
||||||
onAir: (payload: boolean) => socketSendJson('onAir', payload),
|
|
||||||
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
||||||
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, Input } from '@chakra-ui/react';
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
import { CustomField } from 'ontime-types';
|
import { CustomField } from 'ontime-types';
|
||||||
@@ -19,6 +19,8 @@ interface CustomFieldsFormProps {
|
|||||||
|
|
||||||
export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||||
const { onSubmit, onCancel, initialColour, initialLabel } = props;
|
const { onSubmit, onCancel, initialColour, initialLabel } = props;
|
||||||
|
// we use this to force an update
|
||||||
|
const [_, setColour] = useState(initialColour || '');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -55,6 +57,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
}, [setFocus]);
|
}, [setFocus]);
|
||||||
|
|
||||||
const handleSelectColour = (colour: string) => {
|
const handleSelectColour = (colour: string) => {
|
||||||
|
setColour(colour);
|
||||||
setValue('colour', colour, { shouldDirty: true });
|
setValue('colour', colour, { shouldDirty: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function ExtraTimer() {
|
|||||||
return (
|
return (
|
||||||
<div className={style.extraRow}>
|
<div className={style.extraRow}>
|
||||||
<ExtraTimeInput />
|
<ExtraTimeInput />
|
||||||
<TapButton onClick={toggleDirection}>
|
<TapButton onClick={toggleDirection} aspect='tight'>
|
||||||
{direction === SimpleDirection.CountDown && <IoArrowDown />}
|
{direction === SimpleDirection.CountDown && <IoArrowDown />}
|
||||||
{direction === SimpleDirection.CountUp && <IoArrowUp />}
|
{direction === SimpleDirection.CountUp && <IoArrowUp />}
|
||||||
</TapButton>
|
</TapButton>
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ $button-color-white: $gray-50;
|
|||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tapButton.tight {
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
width: fit-content
|
||||||
|
}
|
||||||
|
|
||||||
.tapButton.fill {
|
.tapButton.fill {
|
||||||
aspect-ratio: unset;
|
aspect-ratio: unset;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import style from './TapButton.module.scss';
|
|||||||
|
|
||||||
interface TapButtonProps {
|
interface TapButtonProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
aspect?: 'normal' | 'square' | 'fill';
|
aspect?: 'normal' | 'square' | 'fill' | 'tight';
|
||||||
free?: boolean;
|
free?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
theme?: Playback | 'neutral';
|
theme?: Playback | 'neutral';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { lazy, useEffect } from 'react';
|
import { lazy, useEffect } from 'react';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
@@ -21,8 +20,6 @@ export default function Editor() {
|
|||||||
setShowSettings(newTab);
|
setShowSettings(newTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Editor';
|
document.title = 'ontime - Editor';
|
||||||
@@ -33,13 +30,7 @@ export default function Editor() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MenuBar
|
<MenuBar openSettings={handleSettings} isSettingsOpen={isSettingsOpen} />
|
||||||
isOldSettingsOpen={isOldSettingsOpen}
|
|
||||||
onSettingsOpen={onSettingsOpen}
|
|
||||||
onSettingsClose={onSettingsClose}
|
|
||||||
openSettings={handleSettings}
|
|
||||||
isSettingsOpen={isSettingsOpen}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
{showSettings ? (
|
{showSettings ? (
|
||||||
<AppSettings />
|
<AppSettings />
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import { cx } from '../../common/utils/styleUtils';
|
|||||||
import style from './MenuBar.module.scss';
|
import style from './MenuBar.module.scss';
|
||||||
|
|
||||||
interface MenuBarProps {
|
interface MenuBarProps {
|
||||||
isOldSettingsOpen: boolean;
|
|
||||||
onSettingsOpen: () => void;
|
|
||||||
onSettingsClose: () => void;
|
|
||||||
openSettings: (newTab?: string) => void;
|
openSettings: (newTab?: string) => void;
|
||||||
isSettingsOpen: boolean;
|
isSettingsOpen: boolean;
|
||||||
}
|
}
|
||||||
@@ -29,7 +26,7 @@ const buttonStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MenuBar = (props: MenuBarProps) => {
|
const MenuBar = (props: MenuBarProps) => {
|
||||||
const { onSettingsOpen, onSettingsClose, openSettings, isSettingsOpen } = props;
|
const { openSettings, isSettingsOpen } = props;
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|
||||||
const sendShutdown = () => {
|
const sendShutdown = () => {
|
||||||
@@ -48,14 +45,13 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
// ctrl + , (settings)
|
// ctrl + , (settings)
|
||||||
if (event.key === ',') {
|
if (event.key === ',') {
|
||||||
// open if not open
|
openSettings();
|
||||||
isSettingsOpen ? onSettingsClose() : onSettingsOpen();
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSettingsOpen, onSettingsClose, onSettingsOpen],
|
[openSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -209,9 +209,11 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
|
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previousStart: MaybeNumber = null;
|
||||||
let previousEnd: MaybeNumber = null;
|
let previousEnd: MaybeNumber = null;
|
||||||
let previousEventId: string | undefined;
|
let previousEventId: string | undefined;
|
||||||
let thisEnd: MaybeNumber = previousEnd;
|
let thisStart: MaybeNumber = null;
|
||||||
|
let thisEnd: MaybeNumber = null;
|
||||||
let thisId = previousEventId;
|
let thisId = previousEventId;
|
||||||
|
|
||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
@@ -237,10 +239,12 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
if (isOntimeEvent(event)) {
|
if (isOntimeEvent(event)) {
|
||||||
// event indexes are 1 based in frontend
|
// event indexes are 1 based in frontend
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
|
previousStart = thisStart;
|
||||||
previousEnd = thisEnd;
|
previousEnd = thisEnd;
|
||||||
previousEventId = thisId;
|
previousEventId = thisId;
|
||||||
|
|
||||||
if (!event.skip) {
|
if (!event.skip) {
|
||||||
|
thisStart = event.timeStart;
|
||||||
thisEnd = event.timeEnd;
|
thisEnd = event.timeEnd;
|
||||||
thisId = eventId;
|
thisId = eventId;
|
||||||
}
|
}
|
||||||
@@ -266,6 +270,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
loaded={isLoaded}
|
loaded={isLoaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
|
previousStart={previousStart}
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
previousEventId={previousEventId}
|
previousEventId={previousEventId}
|
||||||
playback={isLoaded ? featureData.playback : undefined}
|
playback={isLoaded ? featureData.playback : undefined}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface RundownEntryProps {
|
|||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
|
previousStart: MaybeNumber;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd: MaybeNumber;
|
||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
playback?: Playback; // we only care about this if this event is playing
|
||||||
@@ -30,8 +31,19 @@ interface RundownEntryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
const { isPast, data, loaded, hasCursor, isNext, previousEnd, previousEventId, playback, isRolling, eventIndex } =
|
const {
|
||||||
props;
|
isPast,
|
||||||
|
data,
|
||||||
|
loaded,
|
||||||
|
hasCursor,
|
||||||
|
isNext,
|
||||||
|
previousStart,
|
||||||
|
previousEnd,
|
||||||
|
previousEventId,
|
||||||
|
playback,
|
||||||
|
isRolling,
|
||||||
|
eventIndex,
|
||||||
|
} = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
||||||
const { cursor } = useAppMode();
|
const { cursor } = useAppMode();
|
||||||
@@ -135,6 +147,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
title={data.title}
|
title={data.title}
|
||||||
note={data.note}
|
note={data.note}
|
||||||
delay={data.delay ?? 0}
|
delay={data.delay ?? 0}
|
||||||
|
previousStart={previousStart}
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
colour={data.colour}
|
colour={data.colour}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface EventBlockProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
|
previousStart: MaybeNumber;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd: MaybeNumber;
|
||||||
colour: string;
|
colour: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
@@ -73,6 +74,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
title,
|
title,
|
||||||
note,
|
note,
|
||||||
delay,
|
delay,
|
||||||
|
previousStart,
|
||||||
previousEnd,
|
previousEnd,
|
||||||
colour,
|
colour,
|
||||||
isPast,
|
isPast,
|
||||||
@@ -244,7 +246,13 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
id='event-block'
|
id='event-block'
|
||||||
>
|
>
|
||||||
<RundownIndicators timeStart={timeStart} previousEnd={previousEnd} delay={delay} />
|
<RundownIndicators
|
||||||
|
timeStart={timeStart}
|
||||||
|
timeEnd={timeEnd}
|
||||||
|
previousStart={previousStart}
|
||||||
|
previousEnd={previousEnd}
|
||||||
|
delay={delay}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
import { millisToString, removeLeadingZero, removeTrailingZero } from 'ontime-utils';
|
||||||
|
|
||||||
|
export function formatDelay(timeStart: number, delay: number): string | undefined {
|
||||||
|
if (!delay) return;
|
||||||
|
|
||||||
|
const delayedStart = Math.max(0, timeStart + delay);
|
||||||
|
|
||||||
|
const timeTag = removeTrailingZero(millisToString(delayedStart));
|
||||||
|
return `New start ${timeTag}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOverlap(
|
||||||
|
previousStart: MaybeNumber,
|
||||||
|
previousEnd: MaybeNumber,
|
||||||
|
timeStart: number,
|
||||||
|
timeEnd: number,
|
||||||
|
): string | undefined {
|
||||||
|
if (previousEnd === null) return;
|
||||||
|
|
||||||
|
const overlap = previousEnd - timeStart;
|
||||||
|
if (overlap === 0) return;
|
||||||
|
|
||||||
|
if (previousStart && timeStart < previousEnd) {
|
||||||
|
const overlap = timeEnd - previousStart;
|
||||||
|
if (overlap <= 0) return;
|
||||||
|
|
||||||
|
const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
|
||||||
|
return `Overlap ${overlapString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
|
||||||
|
return `${overlap > 0 ? 'Overlap' : 'Gap'} ${overlapString}`;
|
||||||
|
}
|
||||||
@@ -1,36 +1,21 @@
|
|||||||
import { millisToString, removeLeadingZero, removeTrailingZero } from 'ontime-utils';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
|
import { formatDelay, formatOverlap } from './EventBlock.utils';
|
||||||
|
|
||||||
import style from './RundownIndicators.module.scss';
|
import style from './RundownIndicators.module.scss';
|
||||||
|
|
||||||
interface RundownIndicatorProps {
|
interface RundownIndicatorProps {
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
previousEnd: number | null;
|
timeEnd: number;
|
||||||
|
previousStart: MaybeNumber;
|
||||||
|
previousEnd: MaybeNumber;
|
||||||
delay: number;
|
delay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDelay(timeStart: number, delay: number): string | undefined {
|
|
||||||
if (!delay) return;
|
|
||||||
|
|
||||||
const delayedStart = Math.max(0, timeStart + delay);
|
|
||||||
const timeTag = removeTrailingZero(millisToString(delayedStart));
|
|
||||||
return `New start ${timeTag}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatOverlap(previousEnd: number | null, timeStart: number): string | undefined {
|
|
||||||
if (previousEnd === null) return;
|
|
||||||
|
|
||||||
const overlap = previousEnd - timeStart;
|
|
||||||
if (overlap === 0) return;
|
|
||||||
|
|
||||||
const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
|
|
||||||
|
|
||||||
return `${overlap > 0 ? 'Overlap' : 'Gap'} ${overlapString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RundownIndicators(props: RundownIndicatorProps) {
|
export default function RundownIndicators(props: RundownIndicatorProps) {
|
||||||
const { timeStart, previousEnd, delay } = props;
|
const { timeStart, timeEnd, previousStart, previousEnd, delay } = props;
|
||||||
|
|
||||||
const hasOverlap = formatOverlap(previousEnd, timeStart);
|
const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||||
const hasDelay = formatDelay(timeStart, delay);
|
const hasDelay = formatDelay(timeStart, delay);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { formatDelay, formatOverlap } from '../EventBlock.utils';
|
||||||
|
|
||||||
|
describe('formatDelay()', () => {
|
||||||
|
it('adds a given delay to the start time', () => {
|
||||||
|
const timeStart = 60000; // 1 min
|
||||||
|
const delay = 60000; // 1 min
|
||||||
|
const result = formatDelay(timeStart, delay);
|
||||||
|
expect(result).toEqual('New start 00:02');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatOverlap()', () => {
|
||||||
|
it('recognises an overlap between two times', () => {
|
||||||
|
const previousStart = 0;
|
||||||
|
const previousEnd = 60000; // 1 min
|
||||||
|
const timeStart = 30000; // 30 sec
|
||||||
|
const timeEnd = 90000; // 1:30 min
|
||||||
|
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||||
|
expect(result).toEqual('Overlap 0:30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, without overlap', () => {
|
||||||
|
const previousStart = new Date(0).setUTCHours(11).valueOf();
|
||||||
|
const previousEnd = new Date(0).setUTCHours(12).valueOf();
|
||||||
|
const timeStart = new Date(0).setUTCHours(6).valueOf();
|
||||||
|
const timeEnd = new Date(0).setUTCHours(10).valueOf();
|
||||||
|
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles events the day after, with overlap', () => {
|
||||||
|
const previousStart = new Date(0).setUTCHours(9).valueOf();
|
||||||
|
const previousEnd = new Date(0).setUTCHours(10).valueOf();
|
||||||
|
const timeStart = new Date(0).setUTCHours(6).valueOf();
|
||||||
|
const timeEnd = new Date(0).setUTCHours(11).valueOf();
|
||||||
|
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||||
|
expect(result).toBe('Overlap 02:00:00');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -41,161 +41,161 @@ $orange-active: #f60;
|
|||||||
gap: 2.5vw;
|
gap: 2.5vw;
|
||||||
grid-template-areas: 'clock schedule';
|
grid-template-areas: 'clock schedule';
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
|
||||||
|
|
||||||
.clock-container {
|
.clock-container {
|
||||||
grid-area: clock;
|
grid-area: clock;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
padding-top: 3em;
|
padding-top: 3em;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: seven-seg;
|
font-family: seven-seg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock-indicators {
|
.clock-indicators {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.min,
|
|
||||||
.hours {
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--studio-idle, $red-idle);
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&--active {
|
.min,
|
||||||
background: var(--studio-active, $red-active);
|
.hours {
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
background: var(--studio-idle, $red-idle);
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: var(--studio-active, $red-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.min {
|
||||||
|
min-height: $size-min;
|
||||||
|
width: $size-min;
|
||||||
|
top: calc(50% - #{$half_min});
|
||||||
|
left: calc(50% - #{$half_min});
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours {
|
||||||
|
min-height: $size-hours;
|
||||||
|
width: $size-hours;
|
||||||
|
top: calc(50% - #{$half_hours});
|
||||||
|
left: calc(50% - #{$half_hours});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.min {
|
.studio-timer {
|
||||||
min-height: $size-min;
|
font-size: calc(var(--clock-size) / 5);
|
||||||
width: $size-min;
|
line-height: 1em;
|
||||||
top: calc(50% - #{$half_min});
|
|
||||||
left: calc(50% - #{$half_min});
|
color: var(--studio-active, $red-active);
|
||||||
|
|
||||||
|
&--with-seconds {
|
||||||
|
font-size: calc(var(--clock-size) / 6.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '88:88';
|
||||||
|
mix-blend-mode: hard-light;
|
||||||
|
opacity: 0.1;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hours {
|
.clock__ampm {
|
||||||
min-height: $size-hours;
|
font-family: monospace;
|
||||||
width: $size-hours;
|
font-size: calc(var(--clock-size) / 20);
|
||||||
top: calc(50% - #{$half_hours});
|
position: relative;
|
||||||
left: calc(50% - #{$half_hours});
|
top: -2em;
|
||||||
}
|
margin-bottom: -2em;
|
||||||
}
|
|
||||||
|
|
||||||
.studio-timer {
|
|
||||||
font-size: calc(var(--clock-size) / 5);
|
|
||||||
line-height: 1em;
|
|
||||||
|
|
||||||
color: var(--studio-active, $red-active);
|
|
||||||
|
|
||||||
&--with-seconds {
|
|
||||||
font-size: calc(var(--clock-size) / 6.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '88:88';
|
|
||||||
mix-blend-mode: hard-light;
|
|
||||||
opacity: 0.1;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock__ampm {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: calc(var(--clock-size) / 20);
|
|
||||||
position: relative;
|
|
||||||
top: -2em;
|
|
||||||
margin-bottom: -2em;
|
|
||||||
color: var(--studio-active, $red-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-title {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--studio-active-label, $cyan-active);
|
|
||||||
font-size: calc(var(--clock-size) / 10);
|
|
||||||
text-align: center;
|
|
||||||
overflow: clip hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-countdown {
|
|
||||||
color: var(--studio-active, $red-active);
|
|
||||||
font-size: calc(var(--clock-size) / 10);
|
|
||||||
line-height: 1em;
|
|
||||||
|
|
||||||
&--overtime {
|
|
||||||
color: var(--studio-active, $red-active);
|
color: var(--studio-active, $red-active);
|
||||||
}
|
}
|
||||||
&--paused {
|
|
||||||
color: var(--studio-overtime, $orange-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-container {
|
.next-title {
|
||||||
grid-area: schedule;
|
font-family: monospace;
|
||||||
font-family: monospace;
|
font-weight: 400;
|
||||||
margin-right: 2vw;
|
color: var(--studio-active-label, $cyan-active);
|
||||||
}
|
font-size: calc(var(--clock-size) / 10);
|
||||||
|
text-align: center;
|
||||||
.onAir {
|
overflow: clip hidden;
|
||||||
font-size: 13vh;
|
white-space: nowrap;
|
||||||
padding-top: 6vh;
|
|
||||||
padding-bottom: 0.25em;
|
|
||||||
line-height: 0.8em;
|
|
||||||
color: var(--studio-active, $red-active);
|
|
||||||
|
|
||||||
&--idle {
|
|
||||||
color: var(--studio-idle, $red-idle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule {
|
|
||||||
color: var(--studio-idle-label, $red-idle);
|
|
||||||
font-size: 3.5vh;
|
|
||||||
line-height: 1.25em;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule__item {
|
|
||||||
margin-bottom: 1.5vh;
|
|
||||||
padding-left: 0.25em;
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
gap: 0.5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&--now {
|
|
||||||
color: var(--studio-active-label, $red-active);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--next {
|
.next-countdown {
|
||||||
color: var(--studio-active, $cyan-active);
|
color: var(--studio-active, $red-active);
|
||||||
padding-bottom: 1em;
|
font-size: calc(var(--clock-size) / 10);
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
&--overtime {
|
||||||
|
color: var(--studio-active, $red-active);
|
||||||
|
}
|
||||||
|
&--paused {
|
||||||
|
color: var(--studio-overtime, $orange-active);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--future {
|
.schedule-container {
|
||||||
color: var(--studio-idle-label, $cyan-idle);
|
grid-area: schedule;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-right: 2vw;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.event {
|
.onAir {
|
||||||
display: flex;
|
font-size: 13vh;
|
||||||
align-items: center;
|
padding-top: 6vh;
|
||||||
gap: 0.25em;
|
padding-bottom: 0.25em;
|
||||||
|
line-height: 0.8em;
|
||||||
|
color: var(--studio-active, $red-active);
|
||||||
|
|
||||||
&__colour {
|
&--idle {
|
||||||
width: 0.35em;
|
color: var(--studio-idle, $red-idle);
|
||||||
height: 0.35em;
|
}
|
||||||
aspect-ratio: 1;
|
}
|
||||||
border-radius: 0.35em;
|
|
||||||
background-color: var(--studio-idle, $red-idle);
|
.schedule {
|
||||||
|
color: var(--studio-idle-label, $red-idle);
|
||||||
|
font-size: 3.5vh;
|
||||||
|
line-height: 1.25em;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__item {
|
||||||
|
margin-bottom: 1.5vh;
|
||||||
|
padding-left: 0.25em;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
gap: 0.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--now {
|
||||||
|
color: var(--studio-active-label, $red-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--next {
|
||||||
|
color: var(--studio-active, $cyan-active);
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--future {
|
||||||
|
color: var(--studio-idle-label, $cyan-idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
&__colour {
|
||||||
|
width: 0.35em;
|
||||||
|
height: 0.35em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 0.35em;
|
||||||
|
background-color: var(--studio-idle, $red-idle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OntimeEvent, RuntimeStore } from 'ontime-types';
|
import { OntimeEvent, Playback, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { deepEqual } from 'fast-equals';
|
import { deepEqual } from 'fast-equals';
|
||||||
|
|
||||||
@@ -149,7 +149,10 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback;
|
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback;
|
||||||
|
|
||||||
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
|
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
|
||||||
eventStore.set('timer', state.timer);
|
eventStore.batchSet({
|
||||||
|
timer: state.timer,
|
||||||
|
onAir: state.timer.playback !== Playback.Stop,
|
||||||
|
});
|
||||||
TimerService.previousState.timer = { ...state.timer };
|
TimerService.previousState.timer = { ...state.timer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1518,7 +1518,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
_timer: { pausedAt: null, secondaryTarget: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
@@ -1570,7 +1570,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
_timer: { pausedAt: null, secondaryTarget: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
@@ -1622,7 +1622,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: 78000000,
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
_timer: { pausedAt: null, secondaryTarget: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
@@ -1674,7 +1674,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: 78000000,
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
_timer: { pausedAt: null, secondaryTarget: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class HttpIntegration implements IIntegration<HttpSubscription> {
|
|||||||
|
|
||||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||||
// noop
|
// noop
|
||||||
if (!this.enabled || !action) {
|
if (!this.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class OscIntegration implements IIntegration<OscSubscription> {
|
|||||||
|
|
||||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||||
// noop
|
// noop
|
||||||
if (!this.oscClient || !action) {
|
if (!this.oscClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class RuntimeService {
|
|||||||
/** Checks result of an update and notifies integrations as needed */
|
/** Checks result of an update and notifies integrations as needed */
|
||||||
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
|
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
|
||||||
const newState = runtimeState.getState();
|
const newState = runtimeState.getState();
|
||||||
|
|
||||||
if (hasTimerFinished) {
|
if (hasTimerFinished) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||||
|
|
||||||
@@ -37,11 +36,11 @@ class RuntimeService {
|
|||||||
// actions are added to the queue stack to ensure that the order of operations is maintained
|
// actions are added to the queue stack to ensure that the order of operations is maintained
|
||||||
if (newState.timer.playback === Playback.Play && newState.eventNow) {
|
if (newState.timer.playback === Playback.Play && newState.eventNow) {
|
||||||
if (newState.eventNow.endAction === EndAction.Stop) {
|
if (newState.eventNow.endAction === EndAction.Stop) {
|
||||||
setTimeout(this.stop, 0);
|
setTimeout(this.stop.bind(this), 0);
|
||||||
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
|
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
|
||||||
setTimeout(this.loadNext, 0);
|
setTimeout(this.loadNext.bind(this), 0);
|
||||||
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
|
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
|
||||||
setTimeout(this.startNext, 0);
|
setTimeout(this.startNext.bind(this), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export type RuntimeState = {
|
|||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: MaybeNumber;
|
pausedAt: MaybeNumber;
|
||||||
finishedNow: boolean;
|
|
||||||
secondaryTarget: MaybeNumber;
|
secondaryTarget: MaybeNumber;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -63,9 +62,6 @@ const runtimeState: RuntimeState = {
|
|||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
get finishedNow() {
|
|
||||||
return this.current <= 0 && this.finishedAt === null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +88,6 @@ export function clear() {
|
|||||||
runtimeState._timer = {
|
runtimeState._timer = {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
finishedNow: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,8 +406,9 @@ export function update(): UpdateResult {
|
|||||||
function onPlayUpdate() {
|
function onPlayUpdate() {
|
||||||
let isFinished = false;
|
let isFinished = false;
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
|
const finishedNow = runtimeState.timer.current <= 0 && runtimeState.timer.finishedAt === null;
|
||||||
|
|
||||||
if (runtimeState.timer.playback === Playback.Play && runtimeState._timer.finishedNow) {
|
if (runtimeState.timer.playback === Playback.Play && finishedNow) {
|
||||||
runtimeState.timer.finishedAt = runtimeState.clock;
|
runtimeState.timer.finishedAt = runtimeState.clock;
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1106,5 +1106,261 @@ describe('parseExcel()', () => {
|
|||||||
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.todo('imports events and blocks, ignores otherwise', () => {});
|
it('ignores unknown event types', () => {
|
||||||
|
const testdata = [
|
||||||
|
[
|
||||||
|
'Time Start',
|
||||||
|
'Time End',
|
||||||
|
'Title',
|
||||||
|
'End Action',
|
||||||
|
'Timer type',
|
||||||
|
'Public',
|
||||||
|
'Skip',
|
||||||
|
'Notes',
|
||||||
|
'test0',
|
||||||
|
'test1',
|
||||||
|
'test2',
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
'test5',
|
||||||
|
'test6',
|
||||||
|
'test7',
|
||||||
|
'test8',
|
||||||
|
'test9',
|
||||||
|
'Colour',
|
||||||
|
'cue',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'1899-12-30T07:00:00.000Z',
|
||||||
|
'1899-12-30T08:00:10.000Z',
|
||||||
|
'Guest Welcome',
|
||||||
|
'',
|
||||||
|
'skip',
|
||||||
|
'x',
|
||||||
|
'',
|
||||||
|
'Ballyhoo',
|
||||||
|
'a0',
|
||||||
|
'a1',
|
||||||
|
'a2',
|
||||||
|
'a3',
|
||||||
|
'a4',
|
||||||
|
'a5',
|
||||||
|
'a6',
|
||||||
|
'a7',
|
||||||
|
'a8',
|
||||||
|
'a9',
|
||||||
|
'red',
|
||||||
|
101,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'1899-12-30T08:00:00.000Z',
|
||||||
|
'1899-12-30T08:30:00.000Z',
|
||||||
|
'A song from the hearth',
|
||||||
|
'load-next',
|
||||||
|
'clock',
|
||||||
|
'',
|
||||||
|
'x',
|
||||||
|
'Rainbow chase',
|
||||||
|
'b0',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'b5',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'#F00',
|
||||||
|
102,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
const importMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {},
|
||||||
|
};
|
||||||
|
const result = parseExcel(testdata, importMap);
|
||||||
|
expect(result.rundown.length).toBe(1);
|
||||||
|
expect((result.rundown.at(0) as OntimeEvent).title).toBe('A song from the hearth');
|
||||||
|
});
|
||||||
|
it('imports blocks', () => {
|
||||||
|
const testdata = [
|
||||||
|
[
|
||||||
|
'Time Start',
|
||||||
|
'Time End',
|
||||||
|
'Title',
|
||||||
|
'End Action',
|
||||||
|
'Timer type',
|
||||||
|
'Public',
|
||||||
|
'Skip',
|
||||||
|
'Notes',
|
||||||
|
'test0',
|
||||||
|
'test1',
|
||||||
|
'test2',
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
'test5',
|
||||||
|
'test6',
|
||||||
|
'test7',
|
||||||
|
'test8',
|
||||||
|
'test9',
|
||||||
|
'Colour',
|
||||||
|
'cue',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'block',
|
||||||
|
'x',
|
||||||
|
'',
|
||||||
|
'Ballyhoo',
|
||||||
|
'a0',
|
||||||
|
'a1',
|
||||||
|
'a2',
|
||||||
|
'a3',
|
||||||
|
'a4',
|
||||||
|
'a5',
|
||||||
|
'a6',
|
||||||
|
'a7',
|
||||||
|
'a8',
|
||||||
|
'a9',
|
||||||
|
'red',
|
||||||
|
101,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'1899-12-30T08:00:00.000Z',
|
||||||
|
'1899-12-30T08:30:00.000Z',
|
||||||
|
'A song from the hearth',
|
||||||
|
'load-next',
|
||||||
|
'clock',
|
||||||
|
'',
|
||||||
|
'x',
|
||||||
|
'Rainbow chase',
|
||||||
|
'b0',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'b5',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'#F00',
|
||||||
|
102,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
const importMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {},
|
||||||
|
};
|
||||||
|
const result = parseExcel(testdata, importMap);
|
||||||
|
expect(result.rundown.length).toBe(2);
|
||||||
|
expect(result.rundown.at(0).type).toBe(SupportedEvent.Block);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports as events if there is no timer type column', () => {
|
||||||
|
const testdata = [
|
||||||
|
[
|
||||||
|
'Time Start',
|
||||||
|
'Time End',
|
||||||
|
'Title',
|
||||||
|
'End Action',
|
||||||
|
'Public',
|
||||||
|
'Skip',
|
||||||
|
'Notes',
|
||||||
|
'test0',
|
||||||
|
'test1',
|
||||||
|
'test2',
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
'test5',
|
||||||
|
'test6',
|
||||||
|
'test7',
|
||||||
|
'test8',
|
||||||
|
'test9',
|
||||||
|
'Colour',
|
||||||
|
'cue',
|
||||||
|
],
|
||||||
|
['', '', '', '', 'x', '', 'Ballyhoo', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'red', 101],
|
||||||
|
[
|
||||||
|
'1899-12-30T08:00:00.000Z',
|
||||||
|
'1899-12-30T08:30:00.000Z',
|
||||||
|
'A song from the hearth',
|
||||||
|
'load-next',
|
||||||
|
'',
|
||||||
|
'x',
|
||||||
|
'Rainbow chase',
|
||||||
|
'b0',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'b5',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'#F00',
|
||||||
|
102,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
const importMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {},
|
||||||
|
};
|
||||||
|
const result = parseExcel(testdata, importMap);
|
||||||
|
expect(result.rundown.length).toBe(2);
|
||||||
|
expect(result.rundown.at(0).type).toBe(SupportedEvent.Event);
|
||||||
|
expect((result.rundown.at(0) as OntimeEvent).timerType).toBe(TimerType.CountDown);
|
||||||
|
expect(result.rundown.at(1).type).toBe(SupportedEvent.Event);
|
||||||
|
expect((result.rundown.at(1) as OntimeEvent).timerType).toBe(TimerType.CountDown);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
EventCustomFields,
|
EventCustomFields,
|
||||||
|
TimerType,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
@@ -176,23 +177,24 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
|||||||
const event: any = {};
|
const event: any = {};
|
||||||
const eventCustomFields: EventCustomFields = {};
|
const eventCustomFields: EventCustomFields = {};
|
||||||
|
|
||||||
row.forEach((column, j) => {
|
for (let j = 0; j < row.length; j++) {
|
||||||
|
const column = row[j];
|
||||||
// 1. we check if we have set a flag for a known field
|
// 1. we check if we have set a flag for a known field
|
||||||
if (j === timerTypeIndex) {
|
if (j === timerTypeIndex) {
|
||||||
if (column === 'block') {
|
if (column === 'block') {
|
||||||
event.type = SupportedEvent.Block;
|
event.type = SupportedEvent.Block;
|
||||||
}
|
} else if (column === '' || isKnownTimerType(column)) {
|
||||||
if (column === '' || isKnownTimerType(column)) {
|
|
||||||
event.type = SupportedEvent.Event;
|
event.type = SupportedEvent.Event;
|
||||||
event.timerType = validateTimerType(column);
|
event.timerType = validateTimerType(column);
|
||||||
|
} else {
|
||||||
|
// if it is not a block or a known type, we dont import it
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// if it is not a block or a known type, we dont import it
|
|
||||||
return;
|
|
||||||
} else if (j === titleIndex) {
|
} else if (j === titleIndex) {
|
||||||
event.title = makeString(column, '');
|
event.title = makeString(column, '');
|
||||||
// if this is a block, we have nothing else to import
|
// if this is a block, we have nothing else to import
|
||||||
if (event.type === SupportedEvent.Block) {
|
if (event.type === SupportedEvent.Block) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (j === timeStartIndex) {
|
} else if (j === timeStartIndex) {
|
||||||
event.timeStart = parseExcelDate(column);
|
event.timeStart = parseExcelDate(column);
|
||||||
@@ -225,7 +227,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
|||||||
if (typeof column === 'string') {
|
if (typeof column === 'string') {
|
||||||
// we cant deal with empty content
|
// we cant deal with empty content
|
||||||
if (column.length === 0) {
|
if (column.length === 0) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
const columnText = column.toLowerCase();
|
const columnText = column.toLowerCase();
|
||||||
|
|
||||||
@@ -243,11 +245,15 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>)
|
|||||||
// just ignore it
|
// just ignore it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// if any data was found in row, push to array
|
// if any data was found in row, push to array
|
||||||
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
|
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
|
||||||
if (keysFound > 0) {
|
if (keysFound > 0) {
|
||||||
|
if (timerTypeIndex === null) {
|
||||||
|
event.timerType = TimerType.CountDown;
|
||||||
|
event.type = SupportedEvent.Event;
|
||||||
|
}
|
||||||
rundown.push({ ...event, custom: { ...eventCustomFields } });
|
rundown.push({ ...event, custom: { ...eventCustomFields } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -317,7 +317,7 @@
|
|||||||
"dangerThreshold": 60000,
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [
|
"urlPresets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alias": "test",
|
"alias": "test",
|
||||||
|
|||||||
+1
-1
@@ -317,7 +317,7 @@
|
|||||||
"dangerThreshold": 60000,
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [
|
"urlPresets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alias": "test",
|
"alias": "test",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -317,7 +317,7 @@
|
|||||||
"dangerThreshold": 60000,
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [
|
"urlPresets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alias": "test",
|
"alias": "test",
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
"dangerColor": "#ED3333",
|
"dangerColor": "#ED3333",
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [],
|
"urlPresets": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"osc": {
|
"osc": {
|
||||||
"portIn": 8888,
|
"portIn": 8888,
|
||||||
|
|||||||
Reference in New Issue
Block a user