From 12d9e5922ceeef4d7406226491f2339e10647e8c Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:30:16 +0100 Subject: [PATCH] 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 --- .../input/delay-input/DelayInput.tsx | 1 + apps/client/src/common/hooks/useSocket.ts | 3 - .../CustomFieldForm.tsx | 5 +- .../playback/extra-timer/ExtraTimer.tsx | 2 +- .../playback/tap-button/TapButton.module.scss | 5 + .../control/playback/tap-button/TapButton.tsx | 2 +- apps/client/src/features/editors/Editor.tsx | 11 +- apps/client/src/features/menu/MenuBar.tsx | 10 +- apps/client/src/features/rundown/Rundown.tsx | 7 +- .../src/features/rundown/RundownEntry.tsx | 17 +- .../rundown/event-block/EventBlock.tsx | 10 +- .../rundown/event-block/EventBlock.utils.ts | 34 +++ .../rundown/event-block/RundownIndicators.tsx | 31 +- .../__tests__/EventBlock.utils.test.ts | 39 +++ .../features/viewers/studio/StudioClock.scss | 272 +++++++++--------- apps/server/src/services/TimerService.ts | 7 +- .../src/services/__tests__/timerUtils.test.ts | 8 +- .../integration-service/HttpIntegration.ts | 2 +- .../integration-service/OscIntegration.ts | 2 +- .../runtime-service/RuntimeService.ts | 7 +- apps/server/src/stores/runtimeState.ts | 8 +- .../server/src/utils/__tests__/parser.test.ts | 258 ++++++++++++++++- apps/server/src/utils/parser.ts | 22 +- apps/server/test-db/db.json | 2 +- demo-db/db.json | 2 +- e2e/tests/fixtures/test-db.json | 2 +- test-db/db.json | 2 +- 27 files changed, 554 insertions(+), 217 deletions(-) create mode 100644 apps/client/src/features/rundown/event-block/EventBlock.utils.ts create mode 100644 apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts diff --git a/apps/client/src/common/components/input/delay-input/DelayInput.tsx b/apps/client/src/common/components/input/delay-input/DelayInput.tsx index 77233a9bf..61bcb7e70 100644 --- a/apps/client/src/common/components/input/delay-input/DelayInput.tsx +++ b/apps/client/src/common/components/input/delay-input/DelayInput.tsx @@ -21,6 +21,7 @@ export default function DelayInput(props: DelayInputProps) { // avoid wrong submit on cancel let ignoreChange = false; + // set internal value on duration change useEffect(() => { if (typeof duration === 'undefined') { return; diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 6e2fe8af1..64bedc91d 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -41,9 +41,6 @@ export const setMessage = { publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }), lowerText: (payload: string) => socketSendJson('message', { lower: { text: 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 } }), timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }), }; diff --git a/apps/client/src/features/app-settings/panel/project-settings-panel/CustomFieldForm.tsx b/apps/client/src/features/app-settings/panel/project-settings-panel/CustomFieldForm.tsx index 081c62182..6e37abc8e 100644 --- a/apps/client/src/features/app-settings/panel/project-settings-panel/CustomFieldForm.tsx +++ b/apps/client/src/features/app-settings/panel/project-settings-panel/CustomFieldForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Button, Input } from '@chakra-ui/react'; import { CustomField } from 'ontime-types'; @@ -19,6 +19,8 @@ interface CustomFieldsFormProps { export default function CustomFieldForm(props: CustomFieldsFormProps) { const { onSubmit, onCancel, initialColour, initialLabel } = props; + // we use this to force an update + const [_, setColour] = useState(initialColour || ''); const { handleSubmit, @@ -55,6 +57,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) { }, [setFocus]); const handleSelectColour = (colour: string) => { + setColour(colour); setValue('colour', colour, { shouldDirty: true }); }; diff --git a/apps/client/src/features/control/playback/extra-timer/ExtraTimer.tsx b/apps/client/src/features/control/playback/extra-timer/ExtraTimer.tsx index 9f608f9cd..d1fa7fa7a 100644 --- a/apps/client/src/features/control/playback/extra-timer/ExtraTimer.tsx +++ b/apps/client/src/features/control/playback/extra-timer/ExtraTimer.tsx @@ -31,7 +31,7 @@ export function ExtraTimer() { return (
- + {direction === SimpleDirection.CountDown && } {direction === SimpleDirection.CountUp && } diff --git a/apps/client/src/features/control/playback/tap-button/TapButton.module.scss b/apps/client/src/features/control/playback/tap-button/TapButton.module.scss index 52e5678a4..38585de59 100644 --- a/apps/client/src/features/control/playback/tap-button/TapButton.module.scss +++ b/apps/client/src/features/control/playback/tap-button/TapButton.module.scss @@ -79,6 +79,11 @@ $button-color-white: $gray-50; font-size: calc(1rem - 2px); } +.tapButton.tight { + padding-inline: 0.5rem; + width: fit-content +} + .tapButton.fill { aspect-ratio: unset; height: 100%; diff --git a/apps/client/src/features/control/playback/tap-button/TapButton.tsx b/apps/client/src/features/control/playback/tap-button/TapButton.tsx index 54d6a1aac..267b8f963 100644 --- a/apps/client/src/features/control/playback/tap-button/TapButton.tsx +++ b/apps/client/src/features/control/playback/tap-button/TapButton.tsx @@ -7,7 +7,7 @@ import style from './TapButton.module.scss'; interface TapButtonProps { disabled?: boolean; - aspect?: 'normal' | 'square' | 'fill'; + aspect?: 'normal' | 'square' | 'fill' | 'tight'; free?: boolean; onClick: () => void; theme?: Playback | 'neutral'; diff --git a/apps/client/src/features/editors/Editor.tsx b/apps/client/src/features/editors/Editor.tsx index b830742f7..6ffda6552 100644 --- a/apps/client/src/features/editors/Editor.tsx +++ b/apps/client/src/features/editors/Editor.tsx @@ -1,5 +1,4 @@ import { lazy, useEffect } from 'react'; -import { useDisclosure } from '@chakra-ui/react'; import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'; import AppSettings from '../app-settings/AppSettings'; @@ -21,8 +20,6 @@ export default function Editor() { setShowSettings(newTab); }; - const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure(); - // Set window title useEffect(() => { document.title = 'ontime - Editor'; @@ -33,13 +30,7 @@ export default function Editor() { return (
- + {showSettings ? ( diff --git a/apps/client/src/features/menu/MenuBar.tsx b/apps/client/src/features/menu/MenuBar.tsx index 7accb71aa..a52240157 100644 --- a/apps/client/src/features/menu/MenuBar.tsx +++ b/apps/client/src/features/menu/MenuBar.tsx @@ -9,9 +9,6 @@ import { cx } from '../../common/utils/styleUtils'; import style from './MenuBar.module.scss'; interface MenuBarProps { - isOldSettingsOpen: boolean; - onSettingsOpen: () => void; - onSettingsClose: () => void; openSettings: (newTab?: string) => void; isSettingsOpen: boolean; } @@ -29,7 +26,7 @@ const buttonStyle = { }; const MenuBar = (props: MenuBarProps) => { - const { onSettingsOpen, onSettingsClose, openSettings, isSettingsOpen } = props; + const { openSettings, isSettingsOpen } = props; const { isElectron, sendToElectron } = useElectronEvent(); const sendShutdown = () => { @@ -48,14 +45,13 @@ const MenuBar = (props: MenuBarProps) => { if (event.ctrlKey || event.metaKey) { // ctrl + , (settings) if (event.key === ',') { - // open if not open - isSettingsOpen ? onSettingsClose() : onSettingsOpen(); + openSettings(); event.preventDefault(); event.stopPropagation(); } } }, - [isSettingsOpen, onSettingsClose, onSettingsOpen], + [openSettings], ); useEffect(() => { diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 7a30d056a..ab239c77d 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -209,9 +209,11 @@ export default function Rundown({ data }: RundownProps) { return insertAtCursor(SupportedEvent.Event, null)} />; } + let previousStart: MaybeNumber = null; let previousEnd: MaybeNumber = null; let previousEventId: string | undefined; - let thisEnd: MaybeNumber = previousEnd; + let thisStart: MaybeNumber = null; + let thisEnd: MaybeNumber = null; let thisId = previousEventId; let eventIndex = 0; @@ -237,10 +239,12 @@ export default function Rundown({ data }: RundownProps) { if (isOntimeEvent(event)) { // event indexes are 1 based in frontend eventIndex++; + previousStart = thisStart; previousEnd = thisEnd; previousEventId = thisId; if (!event.skip) { + thisStart = event.timeStart; thisEnd = event.timeEnd; thisId = eventId; } @@ -266,6 +270,7 @@ export default function Rundown({ data }: RundownProps) { loaded={isLoaded} hasCursor={hasCursor} isNext={isNext} + previousStart={previousStart} previousEnd={previousEnd} previousEventId={previousEventId} playback={isLoaded ? featureData.playback : undefined} diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index e20f06875..9babf3d49 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -23,6 +23,7 @@ interface RundownEntryProps { eventIndex: number; hasCursor: boolean; isNext: boolean; + previousStart: MaybeNumber; previousEnd: MaybeNumber; previousEventId?: string; playback?: Playback; // we only care about this if this event is playing @@ -30,8 +31,19 @@ interface RundownEntryProps { } export default function RundownEntry(props: RundownEntryProps) { - const { isPast, data, loaded, hasCursor, isNext, previousEnd, previousEventId, playback, isRolling, eventIndex } = - props; + const { + isPast, + data, + loaded, + hasCursor, + isNext, + previousStart, + previousEnd, + previousEventId, + playback, + isRolling, + eventIndex, + } = props; const { emitError } = useEmitLog(); const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction(); const { cursor } = useAppMode(); @@ -135,6 +147,7 @@ export default function RundownEntry(props: RundownEntryProps) { title={data.title} note={data.note} delay={data.delay ?? 0} + previousStart={previousStart} previousEnd={previousEnd} colour={data.colour} isPast={isPast} diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 7f30f6578..67a84ab3e 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -37,6 +37,7 @@ interface EventBlockProps { title: string; note: string; delay: number; + previousStart: MaybeNumber; previousEnd: MaybeNumber; colour: string; isPast: boolean; @@ -73,6 +74,7 @@ export default function EventBlock(props: EventBlockProps) { title, note, delay, + previousStart, previousEnd, colour, isPast, @@ -244,7 +246,13 @@ export default function EventBlock(props: EventBlockProps) { onContextMenu={onContextMenu} id='event-block' > - +
diff --git a/apps/client/src/features/rundown/event-block/EventBlock.utils.ts b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts new file mode 100644 index 000000000..bfb8a67ae --- /dev/null +++ b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts @@ -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}`; +} diff --git a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx index b557231ba..5f3120cbb 100644 --- a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx +++ b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx @@ -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'; interface RundownIndicatorProps { timeStart: number; - previousEnd: number | null; + timeEnd: number; + previousStart: MaybeNumber; + previousEnd: MaybeNumber; 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) { - 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); return ( diff --git a/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts b/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts new file mode 100644 index 000000000..1b77e7512 --- /dev/null +++ b/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts @@ -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'); + }); +}); diff --git a/apps/client/src/features/viewers/studio/StudioClock.scss b/apps/client/src/features/viewers/studio/StudioClock.scss index 56744eef3..c5c4b1084 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.scss +++ b/apps/client/src/features/viewers/studio/StudioClock.scss @@ -41,161 +41,161 @@ $orange-active: #f60; gap: 2.5vw; grid-template-areas: 'clock schedule'; text-transform: uppercase; -} -.clock-container { - grid-area: clock; - display: grid; - place-content: center; - justify-items: center; - padding-top: 3em; - gap: 0.5em; - width: 100%; - height: 100%; - position: relative; - font-family: seven-seg; -} + .clock-container { + grid-area: clock; + display: grid; + place-content: center; + justify-items: center; + padding-top: 3em; + gap: 0.5em; + width: 100%; + height: 100%; + position: relative; + font-family: seven-seg; + } -.clock-indicators { - position: absolute; - top: 0; - width: 100%; - height: 100%; - - .min, - .hours { - border-radius: 50%; + .clock-indicators { position: absolute; - background: var(--studio-idle, $red-idle); + top: 0; + width: 100%; + height: 100%; - &--active { - background: var(--studio-active, $red-active); + .min, + .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 { - min-height: $size-min; - width: $size-min; - top: calc(50% - #{$half_min}); - left: calc(50% - #{$half_min}); + .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; + } } - .hours { - min-height: $size-hours; - width: $size-hours; - top: calc(50% - #{$half_hours}); - left: calc(50% - #{$half_hours}); - } -} - -.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 { + .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); } - &--paused { - color: var(--studio-overtime, $orange-active); - } -} -.schedule-container { - grid-area: schedule; - font-family: monospace; - margin-right: 2vw; -} - -.onAir { - font-size: 13vh; - 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-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 { - color: var(--studio-active, $cyan-active); - padding-bottom: 1em; + .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); + } + &--paused { + color: var(--studio-overtime, $orange-active); + } } - &--future { - color: var(--studio-idle-label, $cyan-idle); + .schedule-container { + grid-area: schedule; + font-family: monospace; + margin-right: 2vw; } -} -.event { - display: flex; - align-items: center; - gap: 0.25em; + .onAir { + font-size: 13vh; + padding-top: 6vh; + padding-bottom: 0.25em; + line-height: 0.8em; + color: var(--studio-active, $red-active); - &__colour { - width: 0.35em; - height: 0.35em; - aspect-ratio: 1; - border-radius: 0.35em; - background-color: var(--studio-idle, $red-idle); + &--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 { + 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); + } } } diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index b71f213b8..993504bae 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -1,4 +1,4 @@ -import { OntimeEvent, RuntimeStore } from 'ontime-types'; +import { OntimeEvent, Playback, RuntimeStore } from 'ontime-types'; import { deepEqual } from 'fast-equals'; @@ -149,7 +149,10 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback; 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 }; } diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 5900d6a1f..089f8236a 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1518,7 +1518,7 @@ describe('getRuntimeOffset()', () => { secondaryTimer: null, startedAt: null, }, - _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + _timer: { pausedAt: null, secondaryTarget: null }, } as RuntimeState; const offset = getRuntimeOffset(state); @@ -1570,7 +1570,7 @@ describe('getRuntimeOffset()', () => { secondaryTimer: null, startedAt: null, }, - _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + _timer: { pausedAt: null, secondaryTarget: null }, } as RuntimeState; const offset = getRuntimeOffset(state); @@ -1622,7 +1622,7 @@ describe('getRuntimeOffset()', () => { secondaryTimer: null, startedAt: 78000000, }, - _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + _timer: { pausedAt: null, secondaryTarget: null }, } as RuntimeState; const offset = getRuntimeOffset(state); @@ -1674,7 +1674,7 @@ describe('getRuntimeOffset()', () => { secondaryTimer: null, startedAt: 78000000, }, - _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + _timer: { pausedAt: null, secondaryTarget: null }, } as RuntimeState; const offset = getRuntimeOffset(state); diff --git a/apps/server/src/services/integration-service/HttpIntegration.ts b/apps/server/src/services/integration-service/HttpIntegration.ts index 67f439d49..906702f6e 100644 --- a/apps/server/src/services/integration-service/HttpIntegration.ts +++ b/apps/server/src/services/integration-service/HttpIntegration.ts @@ -34,7 +34,7 @@ export class HttpIntegration implements IIntegration { dispatch(action: TimerLifeCycleKey, state?: object) { // noop - if (!this.enabled || !action) { + if (!this.enabled) { return; } diff --git a/apps/server/src/services/integration-service/OscIntegration.ts b/apps/server/src/services/integration-service/OscIntegration.ts index 62c440450..fd1d737f4 100644 --- a/apps/server/src/services/integration-service/OscIntegration.ts +++ b/apps/server/src/services/integration-service/OscIntegration.ts @@ -64,7 +64,7 @@ export class OscIntegration implements IIntegration { dispatch(action: TimerLifeCycleKey, state?: object) { // noop - if (!this.oscClient || !action) { + if (!this.oscClient) { return; } diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index e91e7b683..a8ee8f97e 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -29,7 +29,6 @@ class RuntimeService { /** Checks result of an update and notifies integrations as needed */ checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) { const newState = runtimeState.getState(); - if (hasTimerFinished) { 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 if (newState.timer.playback === Playback.Play && newState.eventNow) { if (newState.eventNow.endAction === EndAction.Stop) { - setTimeout(this.stop, 0); + setTimeout(this.stop.bind(this), 0); } else if (newState.eventNow.endAction === EndAction.LoadNext) { - setTimeout(this.loadNext, 0); + setTimeout(this.loadNext.bind(this), 0); } else if (newState.eventNow.endAction === EndAction.PlayNext) { - setTimeout(this.startNext, 0); + setTimeout(this.startNext.bind(this), 0); } } } diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index f3da864b1..f9860fd9d 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -47,7 +47,6 @@ export type RuntimeState = { // private properties of the timer calculations _timer: { pausedAt: MaybeNumber; - finishedNow: boolean; secondaryTarget: MaybeNumber; }; }; @@ -63,9 +62,6 @@ const runtimeState: RuntimeState = { _timer: { pausedAt: null, secondaryTarget: null, - get finishedNow() { - return this.current <= 0 && this.finishedAt === null; - }, }, }; @@ -92,7 +88,6 @@ export function clear() { runtimeState._timer = { pausedAt: null, secondaryTarget: null, - finishedNow: false, }; } @@ -411,8 +406,9 @@ export function update(): UpdateResult { function onPlayUpdate() { let isFinished = false; 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; isFinished = true; } else { diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts index f75f0e027..2fa338036 100644 --- a/apps/server/src/utils/__tests__/parser.test.ts +++ b/apps/server/src/utils/__tests__/parser.test.ts @@ -1106,5 +1106,261 @@ describe('parseExcel()', () => { 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); + }); }); diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index e1cdbbaf8..aac23bcc9 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -18,6 +18,7 @@ import { TimeStrategy, CustomFields, EventCustomFields, + TimerType, } from 'ontime-types'; import xlsx from 'node-xlsx'; @@ -176,23 +177,24 @@ export const parseExcel = (excelData: unknown[][], options?: Partial) const event: any = {}; 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 if (j === timerTypeIndex) { if (column === 'block') { event.type = SupportedEvent.Block; - } - if (column === '' || isKnownTimerType(column)) { + } else if (column === '' || isKnownTimerType(column)) { event.type = SupportedEvent.Event; 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) { event.title = makeString(column, ''); // if this is a block, we have nothing else to import if (event.type === SupportedEvent.Block) { - return; + continue; } } else if (j === timeStartIndex) { event.timeStart = parseExcelDate(column); @@ -225,7 +227,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial) if (typeof column === 'string') { // we cant deal with empty content if (column.length === 0) { - return; + continue; } const columnText = column.toLowerCase(); @@ -243,11 +245,15 @@ export const parseExcel = (excelData: unknown[][], options?: Partial) // just ignore it } } - }); + } // if any data was found in row, push to array const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length; if (keysFound > 0) { + if (timerTypeIndex === null) { + event.timerType = TimerType.CountDown; + event.type = SupportedEvent.Event; + } rundown.push({ ...event, custom: { ...eventCustomFields } }); } }); diff --git a/apps/server/test-db/db.json b/apps/server/test-db/db.json index 500ead203..ddf9e09be 100644 --- a/apps/server/test-db/db.json +++ b/apps/server/test-db/db.json @@ -317,7 +317,7 @@ "dangerThreshold": 60000, "endMessage": "" }, - "aliases": [ + "urlPresets": [ { "enabled": true, "alias": "test", diff --git a/demo-db/db.json b/demo-db/db.json index 500ead203..ddf9e09be 100644 --- a/demo-db/db.json +++ b/demo-db/db.json @@ -317,7 +317,7 @@ "dangerThreshold": 60000, "endMessage": "" }, - "aliases": [ + "urlPresets": [ { "enabled": true, "alias": "test", diff --git a/e2e/tests/fixtures/test-db.json b/e2e/tests/fixtures/test-db.json index 500ead203..ddf9e09be 100644 --- a/e2e/tests/fixtures/test-db.json +++ b/e2e/tests/fixtures/test-db.json @@ -317,7 +317,7 @@ "dangerThreshold": 60000, "endMessage": "" }, - "aliases": [ + "urlPresets": [ { "enabled": true, "alias": "test", diff --git a/test-db/db.json b/test-db/db.json index 1dd46eb8e..676c1e2da 100644 --- a/test-db/db.json +++ b/test-db/db.json @@ -24,7 +24,7 @@ "dangerColor": "#ED3333", "endMessage": "" }, - "aliases": [], + "urlPresets": [], "customFields": {}, "osc": { "portIn": 8888,