diff --git a/README.md b/README.md index c0fd400e2..4c6aa97e4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Ontime build v2](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml) -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Documentation in Gitbook](https://badges.aleen42.com/src/gitbook_2.svg)](https://ontime.gitbook.io) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) ## Download the latest releases here @@ -24,6 +24,8 @@ outputs. ![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/overview.png) +[Read the docs to learn more](https://docs.getontime.no) + ## Using Ontime Once installed and running, Ontime starts a background server that is the heart of all processes. @@ -59,7 +61,7 @@ IP.ADDRESS:4001/editor > the control interface, same as the app IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration ``` -More documentation is available [in our docs](https://ontime.gitbook.io) +More documentation is available [in our docs](https://docs.getontime.no) ## Feature List (in no specific order) @@ -84,7 +86,7 @@ More documentation is available [in our docs](https://ontime.gitbook.io) - WebSockets - [x] Roll mode: run standalone using the system clock - [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally -- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have +- [x] [Countdown to anything!](https://docs.getontime.no/features/count-to-anything/): have a countdown to any scheduled event - [x] Multi-platform (available on Windows, MacOS and Linux) - [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime) @@ -120,9 +122,14 @@ Ontime broadcasts its data over WebSockets. This allows you to consume its data Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
-See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on -how to get you started and read the docs about -the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-apis#osc-and-websocket-api) +We have prepared a few resources to help here: +- Shipped with Ontime there is a small clock to get you started, it is available at `http://localhost:4001/external/demo` and the [code can be found here](https://github.com/cpvalente/ontime/tree/master/apps/server/src/external/demo) +- See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a template on +how to get you started +- See information about the [Websocket API](https://docs.getontime.no/api/osc-and-ws/) +
+More information [in the docs](https://docs.getontime.no/features/custom-views/) + ### Headless runī¸ @@ -130,7 +137,7 @@ You can self-host and run Ontime in a docker image. The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime) -If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi) +If you want to run this image in a Raspberry Pi, please see [the docs](https://docs.getontime.no/additional-notes/use-with-rpi/) ## Roadmap @@ -192,7 +199,7 @@ Information about the project setup can be found in the [development documentati # Help -Help is underway! ... and can be found [here](https://ontime.gitbook.io) +Help is underway! ... and can be found [here](https://docs.getontime.no) # License diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index b9de8e013..0e8d41bdd 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -91,9 +91,6 @@ export default function TimeInput(props: TimeInputProps) { (event: KeyboardEvent) => { if (event.key === 'Enter') { inputRef.current?.blur(); - validateAndSubmit((event.target as HTMLInputElement).value); - } else if (event.key === 'Tab') { - validateAndSubmit((event.target as HTMLInputElement).value); } if (event.key === 'Escape') { ignoreChange.current = true; @@ -101,7 +98,7 @@ export default function TimeInput(props: TimeInputProps) { resetValue(); } }, - [resetValue, validateAndSubmit], + [resetValue], ); const onBlurHandler = useCallback( diff --git a/apps/client/src/common/components/state/Empty.module.scss b/apps/client/src/common/components/state/Empty.module.scss index 43f5c15af..193862ebc 100644 --- a/apps/client/src/common/components/state/Empty.module.scss +++ b/apps/client/src/common/components/state/Empty.module.scss @@ -1,7 +1,7 @@ .emptyContainer { width: 100%; text-align: center; - color: $gray-1350; + color: $white-10; .empty { width: 100%; diff --git a/apps/client/src/common/components/state/Empty.tsx b/apps/client/src/common/components/state/Empty.tsx index cd5f18873..e44146b84 100644 --- a/apps/client/src/common/components/state/Empty.tsx +++ b/apps/client/src/common/components/state/Empty.tsx @@ -5,7 +5,7 @@ import EmptyImage from '../../../assets/images/empty.svg?react'; import style from './Empty.module.scss'; interface EmptyProps { - text: string; + text?: string; style?: CSSProperties; } @@ -14,7 +14,7 @@ export default function Empty(props: EmptyProps) { return (
- {text} + {text && {text}}
); } diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 2a26fec96..42c0d88e2 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -14,6 +14,7 @@ describe('test forgivingStringToMillis()', () => { { value: '1h0m0s', expect: 1000 * 60 * 60 }, { value: '23h0m0s', expect: 1000 * 60 * 60 * 23 }, { value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, + { value: '12H12M12S', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, { value: '2m', expect: 2 * 60 * 1000 }, { value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 }, { value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 }, diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 7f16a2e1c..3738b9609 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -46,13 +46,13 @@ function checkAmPm(value: string) { * @param {string} value */ function checkMatchers(value: string) { - const hoursMatch = /(\d+)h/.exec(value); + const hoursMatch = /(\d+)h/i.exec(value); const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0; - const minutesMatch = /(\d+)m/.exec(value); + const minutesMatch = /(\d+)m/i.exec(value); const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0; - const secondsMatch = /(\d+)s/.exec(value); + const secondsMatch = /(\d+)s/i.exec(value); const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0; if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) { diff --git a/apps/client/src/externals.ts b/apps/client/src/externals.ts index 6dd7aec6b..ea971bfc9 100644 --- a/apps/client/src/externals.ts +++ b/apps/client/src/externals.ts @@ -2,4 +2,4 @@ export const githubUrl = 'https://www.github.com/cpvalente/ontime'; export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest'; export const websiteUrl = 'https://www.getontime.no'; -export const gitbookUrl = 'https://ontime.gitbook.io'; +export const documentationUrl = 'https://docs.getontime.no'; diff --git a/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx b/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx index b85dfba61..ab47622b4 100644 --- a/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx +++ b/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx @@ -1,6 +1,6 @@ import { version } from '../../../../../package.json'; import ExternalLink from '../../../../common/components/external-link/ExternalLink'; -import { gitbookUrl, githubUrl, websiteUrl } from '../../../../externals'; +import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals'; import * as Panel from '../PanelUtils'; import CheckUpdatesButton from './CheckUpdatesButton'; @@ -18,7 +18,7 @@ export default function AboutPanel() { Links - Read the docs over at GitBook + Read the docs Follow the project on GitHub diff --git a/apps/client/src/features/app-settings/panel/general-panel/ViewSettingsForm.tsx b/apps/client/src/features/app-settings/panel/general-panel/ViewSettingsForm.tsx index 9e842d04b..d3dbb08ae 100644 --- a/apps/client/src/features/app-settings/panel/general-panel/ViewSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/general-panel/ViewSettingsForm.tsx @@ -13,7 +13,7 @@ import * as Panel from '../PanelUtils'; import style from './GeneralPanel.module.scss'; -const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling'; +const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/'; export default function ViewSettingsForm() { const { data, status, refetch, isFetching } = useViewSettings(); diff --git a/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx b/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx index f78519645..8da16b5fa 100644 --- a/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx @@ -6,7 +6,7 @@ import * as Panel from '../PanelUtils'; import HttpIntegrations from './HttpIntegrations'; import OscIntegrations from './OscIntegrations'; -const integrationDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/integrations'; +const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/'; export default function IntegrationsPanel() { return ( diff --git a/apps/client/src/features/app-settings/panel/project-panel/ProjectCreateForm.tsx b/apps/client/src/features/app-settings/panel/project-panel/ProjectCreateForm.tsx index e4f3dbe16..cb9743297 100644 --- a/apps/client/src/features/app-settings/panel/project-panel/ProjectCreateForm.tsx +++ b/apps/client/src/features/app-settings/panel/project-panel/ProjectCreateForm.tsx @@ -147,7 +147,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) { diff --git a/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx b/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx index b2b770f9e..7dad84a75 100644 --- a/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx +++ b/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx @@ -142,7 +142,7 @@ export default function ProjectData() { diff --git a/apps/client/src/features/app-settings/panel/project-settings-panel/ProjectSettingsPanel.tsx b/apps/client/src/features/app-settings/panel/project-settings-panel/ProjectSettingsPanel.tsx index d72e311d9..b279a566d 100644 --- a/apps/client/src/features/app-settings/panel/project-settings-panel/ProjectSettingsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/project-settings-panel/ProjectSettingsPanel.tsx @@ -11,7 +11,7 @@ import * as Panel from '../PanelUtils'; import CustomFieldEntry from './CustomFieldEntry'; import CustomFieldForm from './CustomFieldForm'; -const customFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields'; +const customFieldsDocsUrl = 'https://docs.getontime.no/features/custom-fields/'; export default function ProjectSettingsPanel() { const { data, refetch } = useCustomFields(); diff --git a/apps/client/src/features/app-settings/panel/sources-panel/GSheetInfo.tsx b/apps/client/src/features/app-settings/panel/sources-panel/GSheetInfo.tsx index 959ad4cca..e05871a33 100644 --- a/apps/client/src/features/app-settings/panel/sources-panel/GSheetInfo.tsx +++ b/apps/client/src/features/app-settings/panel/sources-panel/GSheetInfo.tsx @@ -1,7 +1,7 @@ import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react'; import ExternalLink from '../../../../common/components/external-link/ExternalLink'; -const googleSheetDocsUrl = 'https://ontime.gitbook.io/v2/features/google-sheet'; +const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/'; export default function GSheetInfo() { return ( diff --git a/apps/client/src/features/control/playback/timer-display/TimerDisplay.module.scss b/apps/client/src/features/control/playback/timer-display/TimerDisplay.module.scss index d9c8d4b85..ee4613a55 100644 --- a/apps/client/src/features/control/playback/timer-display/TimerDisplay.module.scss +++ b/apps/client/src/features/control/playback/timer-display/TimerDisplay.module.scss @@ -4,7 +4,7 @@ grid-area: clk; white-space: nowrap; max-width: 18.75rem; - + min-width: 5em; font-family: var(--font-family-override, $viewer-font-family); color: var(--timer-color-override, $timer-color); diff --git a/apps/client/src/features/control/playback/timer-display/TimerDisplay.tsx b/apps/client/src/features/control/playback/timer-display/TimerDisplay.tsx index 066fe02c6..360b39960 100644 --- a/apps/client/src/features/control/playback/timer-display/TimerDisplay.tsx +++ b/apps/client/src/features/control/playback/timer-display/TimerDisplay.tsx @@ -11,7 +11,7 @@ interface TimerDisplayProps { /** * Displays time in ms in formatted timetag - * Typically used in production views + * Used in editor */ export default function TimerDisplay(props: TimerDisplayProps) { const { time } = props; diff --git a/apps/client/src/features/editors/Editor.module.scss b/apps/client/src/features/editors/Editor.module.scss index ebe5581b6..c39cf5e24 100644 --- a/apps/client/src/features/editors/Editor.module.scss +++ b/apps/client/src/features/editors/Editor.module.scss @@ -48,8 +48,7 @@ $panel-gap: 0.5rem; } .left { - flex-grow: 1; - flex-shrink: 1; + flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */ min-width: $min-playback-width; max-width: $max-playback-width; display: flex; diff --git a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx index 4b14bf64e..13a0aea5e 100644 --- a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx +++ b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx @@ -18,7 +18,7 @@ import OntimeModalFooter from '../OntimeModalFooter'; import style from './SettingsModal.module.scss'; -const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases'; +const aliasesDocsUrl = 'https://docs.getontime.no/features/url-presets/'; // we wrap the array in an object to be simplify react-hook-form type Aliases = { diff --git a/apps/client/src/features/modals/settings-modal/ProjectDataForm.tsx b/apps/client/src/features/modals/settings-modal/ProjectDataForm.tsx index 28f33b4da..744af1777 100644 --- a/apps/client/src/features/modals/settings-modal/ProjectDataForm.tsx +++ b/apps/client/src/features/modals/settings-modal/ProjectDataForm.tsx @@ -122,7 +122,7 @@ export default function ProjectDataForm() { {...inputProps} variant='ontime-filled-on-light' size='sm' - placeholder='www.ontime.gitbook.io' + placeholder='https://docs.getontime.no' isDisabled={disableInputs} {...register('backstageUrl')} /> diff --git a/apps/client/src/features/overview/Overview.tsx b/apps/client/src/features/overview/Overview.tsx index 1ce3047a8..16edfd8f3 100644 --- a/apps/client/src/features/overview/Overview.tsx +++ b/apps/client/src/features/overview/Overview.tsx @@ -15,7 +15,7 @@ import style from './Overview.module.scss'; * @param time * @returns */ -function formattedTime(time: MaybeNumber) { +function formatedTime(time: MaybeNumber) { return millisToString(time, { fallback: timerPlaceholder }); } @@ -27,13 +27,13 @@ export default function Overview() {
- - + +
- - + +
@@ -51,24 +51,32 @@ function TitlesOverview() { ); } +function getOffsetText(offset: MaybeNumber): string { + if (offset === null) { + return enDash; + } + const isAhead = offset <= 0; + let offsetText = millisToString(Math.abs(offset), { fallback: enDash }); + if (offsetText !== enDash) { + offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`; + } + return offsetText; +} + function RuntimeOverview() { const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview(); const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash; const ofTotal = numEvents || enDash; const progressText = numEvents ? `${current} of ${ofTotal}` : '-'; - - const isAhead = offset <= 0; - let offsetText = millisToString(Math.abs(offset), { fallback: enDash }); - if (offsetText !== enDash) { - offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`; - } + const offsetText = getOffsetText(offset); + const offsetClasses = offset === null ? undefined : offset > 0 ? style.behind : style.ahead; return ( <> - - + + ); } diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index b4e376c9e..98f00c8d5 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -254,7 +254,7 @@ export default function Rundown({ data }: RundownProps) { data={event} loaded={isLoaded} hasCursor={hasCursor} - next={isNext} + isNext={isNext} previousEnd={previousEnd} previousEventId={previousEventId} playback={isLoaded ? featureData.playback : undefined} diff --git a/apps/client/src/features/rundown/RundownEmpty.tsx b/apps/client/src/features/rundown/RundownEmpty.tsx index 6f4b20950..44fcec4c7 100644 --- a/apps/client/src/features/rundown/RundownEmpty.tsx +++ b/apps/client/src/features/rundown/RundownEmpty.tsx @@ -14,7 +14,7 @@ export default function RundownEmpty(props: RundownEmptyProps) { return (
- + diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index d9e802da8..e20f06875 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -22,7 +22,7 @@ interface RundownEntryProps { loaded: boolean; eventIndex: number; hasCursor: boolean; - next: boolean; + isNext: boolean; previousEnd: MaybeNumber; previousEventId?: string; playback?: Playback; // we only care about this if this event is playing @@ -30,7 +30,7 @@ interface RundownEntryProps { } export default function RundownEntry(props: RundownEntryProps) { - const { isPast, data, loaded, hasCursor, next, previousEnd, previousEventId, playback, isRolling, eventIndex } = + const { isPast, data, loaded, hasCursor, isNext, previousEnd, previousEventId, playback, isRolling, eventIndex } = props; const { emitError } = useEmitLog(); const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction(); @@ -138,7 +138,7 @@ export default function RundownEntry(props: RundownEntryProps) { previousEnd={previousEnd} colour={data.colour} isPast={isPast} - next={next} + isNext={isNext} skip={data.skip} loaded={loaded} hasCursor={hasCursor} diff --git a/apps/client/src/features/rundown/RundownExport.module.scss b/apps/client/src/features/rundown/RundownExport.module.scss index d2f5e3964..57f5f92b8 100644 --- a/apps/client/src/features/rundown/RundownExport.module.scss +++ b/apps/client/src/features/rundown/RundownExport.module.scss @@ -1,8 +1,8 @@ -@use '../editors//EditorMixin' as editor; +@use '../editors/EditorMixin' as editor; .rundownExport { height: 100%; - flex: 1; + flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */ &.extracted { .list { @@ -34,8 +34,7 @@ padding-left: 0; box-shadow: $box-shadow-right; - flex-grow: 1; - flex-shrink: 1; + flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */ min-width: 38rem; max-width: 45rem; } @@ -49,8 +48,7 @@ background-color: $gray-1325; border-radius: 0 8px 8px 0; - flex-grow: 1; - flex-shrink: 1; + flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */ min-width: 30rem; max-width: 45rem; } diff --git a/apps/client/src/features/rundown/common/EditableBlockTitle.tsx b/apps/client/src/features/rundown/common/EditableBlockTitle.tsx index 6a62bedd1..dbed42cdf 100644 --- a/apps/client/src/features/rundown/common/EditableBlockTitle.tsx +++ b/apps/client/src/features/rundown/common/EditableBlockTitle.tsx @@ -1,6 +1,7 @@ -import { useCallback, useEffect, useState } from 'react'; -import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react'; +import { useCallback } from 'react'; +import { Input } from '@chakra-ui/react'; +import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput'; import { useEventAction } from '../../../common/hooks/useEventAction'; import { cx } from '../../../common/utils/styleUtils'; @@ -15,39 +16,40 @@ interface TitleEditorProps { export default function EditableBlockTitle(props: TitleEditorProps) { const { title, eventId, placeholder, className } = props; - const [blockTitle, setBlockTitle] = useState(title || ''); const { updateEvent } = useEventAction(); - useEffect(() => { - setBlockTitle(title); - }, [title]); - - const handleTitle = useCallback( + const submitCallback = useCallback( (text: string) => { if (text === title) { return; } const cleanVal = text.trim(); - setBlockTitle(cleanVal); - updateEvent({ id: eventId, title: cleanVal }); }, [title, updateEvent, eventId], ); - const classes = cx([className, style.eventTitle, !blockTitle ? style.noTitle : null]); + const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(title, submitCallback, { + submitOnEnter: true, + }); + + const classes = cx([className, style.eventTitle, !value ? style.noTitle : null]); + return ( - setBlockTitle(value)} - onSubmit={(value) => handleTitle(value)} - > - - - + onChange={onChange} + onBlur={onBlur} + onKeyDown={onKeyDown} + autoComplete='off' + fontWeight='600' + letterSpacing='0.25px' + paddingLeft='0' + /> ); } diff --git a/apps/client/src/features/rundown/event-block/EventBlock.module.scss b/apps/client/src/features/rundown/event-block/EventBlock.module.scss index 277aa2bf1..5acd7bfaa 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.module.scss +++ b/apps/client/src/features/rundown/event-block/EventBlock.module.scss @@ -10,7 +10,7 @@ $skip-opacity: 0.1; grid-template-areas: 'binder ... ... ...' 'binder pb-actions times actions' - 'binder pb-actions title next' + 'binder pb-actions title title' 'binder pb-actions estatus estatus' 'binder ... ... ...'; @@ -127,15 +127,30 @@ $skip-opacity: 0.1; .eventTimers { grid-area: times; display: flex; + align-items: center; gap: $block-clearance; height: 100%; } -.eventTitle { +.titleSection { grid-area: title; - overflow: hidden; - max-height: calc(2.5em + 2px); - line-height: 1.25em; + display: flex; + align-items: center; + justify-content: space-between; + + .nextTag { + font-size: 1rem; + color: $orange-500; + letter-spacing: 0.03px; + font-weight: 600; + } + + .eventTitle { + overflow: hidden; + max-height: calc(2.5em + 2px); + line-height: 1.25em; + flex: 1; + } } .eventActions { @@ -146,10 +161,11 @@ $skip-opacity: 0.1; .progressBg { grid-area: progb; - border-radius: 2px; + border-radius: 1px; background-color: $gray-1100; opacity: 1; height: 100%; + overflow: hidden; /* clip foreground border radius*/ } .progressBg.hidden { @@ -184,15 +200,6 @@ $skip-opacity: 0.1; overflow-y: hidden; } -.nextTag { - grid-area: next; - font-size: 1rem; - color: $orange-500; - letter-spacing: 0.03px; - font-weight: 600; - text-align: right; -} - .eventStatus { grid-area: status; display: flex; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 2d7189e7b..7f30f6578 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -40,7 +40,7 @@ interface EventBlockProps { previousEnd: MaybeNumber; colour: string; isPast: boolean; - next: boolean; + isNext: boolean; skip: boolean; loaded: boolean; hasCursor: boolean; @@ -76,7 +76,7 @@ export default function EventBlock(props: EventBlockProps) { previousEnd, colour, isPast, - next, + isNext, skip = false, loaded, hasCursor, @@ -240,7 +240,7 @@ export default function EventBlock(props: EventBlockProps) { className={blockClasses} ref={setNodeRef} style={dragStyle} - onMouseDown={handleFocusClick} + onClick={handleFocusClick} onContextMenu={onContextMenu} id='event-block' > @@ -268,7 +268,7 @@ export default function EventBlock(props: EventBlockProps) { title={title} note={note} delay={delay} - next={next} + isNext={isNext} skip={skip} loaded={loaded} playback={playback} diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index fb7f766c1..6e17f7ded 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -41,7 +41,7 @@ interface EventBlockInnerProps { title: string; note: string; delay: number; - next: boolean; + isNext: boolean; skip: boolean; loaded: boolean; playback?: Playback; @@ -63,7 +63,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { title, note, delay, - next, + isNext, skip = false, loaded, playback, @@ -100,12 +100,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => { linkStart={linkStart} />
- - {next && ( - - UP NEXT - - )} +
+ + {isNext && ( + + UP NEXT + + )} +
{
{note}
- {loaded && } + {loaded && }
diff --git a/apps/client/src/features/rundown/event-block/RundownIndicators.module.scss b/apps/client/src/features/rundown/event-block/RundownIndicators.module.scss index 55bd51c07..f2d147f18 100644 --- a/apps/client/src/features/rundown/event-block/RundownIndicators.module.scss +++ b/apps/client/src/features/rundown/event-block/RundownIndicators.module.scss @@ -10,6 +10,7 @@ $gap-left: calc(2rem + 0.25rem + 3rem); display: flex; gap: 0.5rem; + font-weight: 700; } @mixin indicator($bg-colour) { diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx index e713a604e..bae3fd832 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, MouseEvent } from 'react'; import { IoPause } from '@react-icons/all-files/io5/IoPause'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoReload } from '@react-icons/all-files/io5/IoReload'; @@ -40,11 +40,13 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => { const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props; const { updateEvent } = useEventAction(); - const toggleSkip = () => { + const toggleSkip = (event: MouseEvent) => { + event.stopPropagation(); updateEvent({ id: eventId, skip: !skip }); }; - const actionHandler = () => { + const actionHandler = (event: MouseEvent) => { + event.stopPropagation(); // is playing -> pause // is paused -> continue // otherwise -> start @@ -57,6 +59,11 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => { } }; + const load = (event: MouseEvent) => { + event.stopPropagation(); + setEventPlayback.loadEvent(eventId); + }; + const buttonVariant: Partial = {}; if (isPaused) { @@ -103,7 +110,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => { isDisabled={disablePlayback} {...tooltipProps} {...blockBtnStyle} - clickHandler={() => setEventPlayback.loadEvent(eventId)} + clickHandler={load} tabIndex={-1} /> ; + return
; } diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss index f3d6a7d46..13b70b99d 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss @@ -63,6 +63,7 @@ font-size: calc(1rem - 3px); color: $label-gray; display: flex; + align-items: center; gap: 0.5rem; max-width: max-content; cursor: pointer; diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx index 0a16b4453..0d7fa853d 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx @@ -135,7 +135,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
Event Visibility
diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index b370e18f9..6875c1fb0 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -22,15 +22,16 @@ export default function RundownHeader() {
} clickHandler={setFreezeMode} tooltip='Freeze rundown' aria-label='Freeze rundown' + isDisabled /> } clickHandler={setRunMode} @@ -38,7 +39,7 @@ export default function RundownHeader() { aria-label='Run mode' /> } clickHandler={setEditMode} @@ -47,7 +48,7 @@ export default function RundownHeader() { /> - } aria-label='Rundown menu' variant='ontime-ghosted'> + } aria-label='Rundown menu' variant='ontime-outlined'> Rundown diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss index e4359d972..5d5fbb427 100644 --- a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss @@ -26,4 +26,5 @@ color: $blue-500; margin-right: 0.5rem; font-size: 1.5em; + display: grid; } diff --git a/apps/client/src/features/viewers/timer/Timer.scss b/apps/client/src/features/viewers/timer/Timer.scss index 7cd00751e..7eedbe2f1 100644 --- a/apps/client/src/features/viewers/timer/Timer.scss +++ b/apps/client/src/features/viewers/timer/Timer.scss @@ -30,6 +30,19 @@ transition: $viewer-transition-time; } + .blackout { + position: absolute; + width: 100vw; + height: 100vh; + background-color: #000; + z-index: 2; + opacity: 0; + transition: opacity $viewer-transition-time; + &--active { + opacity: 1; + } + } + /* =================== CLOCK ===================*/ .clock-container { diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index 8c59679ab..cf7c45cbe 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -145,7 +145,7 @@ export default function Timer(props: TimerProps) { } const stageTimerCharacters = display.replace('/:/g', '').length; - const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`; + const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`; let timerFontSize = 89 / (stageTimerCharacters - 1); // we need to shrink the timer if the external is going to be there if (showExternal) { @@ -162,6 +162,7 @@ export default function Timer(props: TimerProps) {
+
{!userOptions.hideMessage && (
{pres.text}
diff --git a/apps/client/src/index.scss b/apps/client/src/index.scss index 3e4ee9c74..1213ca8e1 100644 --- a/apps/client/src/index.scss +++ b/apps/client/src/index.scss @@ -33,8 +33,19 @@ html, background-color: $bg-container-l1; } -// workaround for chakra -// // https://github.com/chakra-ui/chakra-ui/issues/417 +/* smaller root size for MacOS laptops */ +@media (max-width: 1600px) { + body, + html, + .App { + font-size: 14px; + } +} + +/** + * workaround for chakra + * https://github.com/chakra-ui/chakra-ui/issues/417 + */ option { color: initial; } diff --git a/apps/client/src/theme/_viewerCommon.scss b/apps/client/src/theme/_viewerCommon.scss index d0822b85c..d14ad5915 100644 --- a/apps/client/src/theme/_viewerCommon.scss +++ b/apps/client/src/theme/_viewerCommon.scss @@ -1,7 +1,3 @@ .mirror { transform: rotate(180deg); } - -.blackout { - opacity: 0; -} \ No newline at end of file diff --git a/apps/client/src/theme/ontimeTextInputs.ts b/apps/client/src/theme/ontimeTextInputs.ts index 00e000762..edfab07f5 100644 --- a/apps/client/src/theme/ontimeTextInputs.ts +++ b/apps/client/src/theme/ontimeTextInputs.ts @@ -3,6 +3,7 @@ const commonStyles = { backgroundColor: '#262626', // $gray-1200 color: '#e2e2e2', // $gray-200 border: '1px solid transparent', + borderRadius: '3px', _hover: { backgroundColor: '#2d2d2d', // $gray-1100 }, @@ -25,6 +26,18 @@ export const ontimeInputFilled = { }, }; +export const ontimeInputGhosted = { + field: { + ...commonStyles, + backgroundColor: 'transparent', + color: '#f6f6f6', // $gray-50 + _hover: { + backgroundColor: 'transparent', // $gray-1100 + border: '1px solid #2B5ABC', // $blue-500 + }, + }, +}; + export const ontimeInputFilledOnLight = { field: { backgroundColor: 'white', diff --git a/apps/client/src/theme/theme.ts b/apps/client/src/theme/theme.ts index 00ddeb4e0..53dfd9a51 100644 --- a/apps/client/src/theme/theme.ts +++ b/apps/client/src/theme/theme.ts @@ -23,6 +23,7 @@ import { ontimeTab } from './ontimeTab'; import { ontimeInputFilled, ontimeInputFilledOnLight, + ontimeInputGhosted, ontimeTextAreaFilled, ontimeTextAreaFilledOnLight, ontimeTextAreaTransparent, @@ -71,6 +72,7 @@ const theme = extendTheme({ }, variants: { 'ontime-filled': { ...ontimeInputFilled }, + 'ontime-ghosted': { ...ontimeInputGhosted }, 'ontime-filled-on-light': { ...ontimeInputFilledOnLight }, }, }, diff --git a/apps/electron/package.json b/apps/electron/package.json index cef856577..a2a4c57dc 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,5 +1,5 @@ { - "name": "ontime", + "name": "ontime-prerelease", "version": "3.0.0-alpha", "author": "Carlos Valente", "description": "Time keeping for live events", @@ -29,8 +29,8 @@ "cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "build": { - "productName": "ontime", - "appId": "no.lightdev.ontime", + "productName": "ontime-prerelease", + "appId": "no.lightdev.ontime.prerelease", "asar": true, "dmg": { "artifactName": "ontime-macOS-${arch}.dmg", diff --git a/apps/electron/src/menu/applicationMenu.js b/apps/electron/src/menu/applicationMenu.js index b63fa5670..21adc1bdb 100644 --- a/apps/electron/src/menu/applicationMenu.js +++ b/apps/electron/src/menu/applicationMenu.js @@ -121,6 +121,12 @@ function getApplicationMenu(isMac, askToQuit) { await shell.openExternal('http://localhost:4001/cuesheet'); }, }, + { + label: 'Operator', + click: async () => { + await shell.openExternal('http://localhost:4001/operator'); + }, + }, ], }, { type: 'separator' }, @@ -153,7 +159,7 @@ function getApplicationMenu(isMac, askToQuit) { { label: 'Online documentation', click: async () => { - await shell.openExternal('https://ontime.gitbook.io/'); + await shell.openExternal('https://docs.getontime.no/'); }, }, ], diff --git a/apps/server/src/api-integration/integration.controller.ts b/apps/server/src/api-integration/integration.controller.ts index c23adc0c7..5577db1cd 100644 --- a/apps/server/src/api-integration/integration.controller.ts +++ b/apps/server/src/api-integration/integration.controller.ts @@ -85,13 +85,13 @@ const actionHandlers: Record = { return { payload: 'success' }; } if ('id' in payload) { - assert.isString(payload); - runtimeService.startById(payload); + assert.isString(payload.id); + runtimeService.startById(payload.id); return { payload: 'success' }; } if ('cue' in payload) { - assert.isString(payload); - runtimeService.startByCue(payload); + assert.isString(payload.cue); + runtimeService.startByCue(payload.cue); return { payload: 'success' }; } } diff --git a/apps/server/src/external/demo/app.js b/apps/server/src/external/demo/app.js index 0af53bb92..64858ba4f 100644 --- a/apps/server/src/external/demo/app.js +++ b/apps/server/src/external/demo/app.js @@ -48,7 +48,7 @@ const connectSocket = () => { // we only need to read message type of ontime if (type === 'ontime') { // destructure known data from ontime - // see https://cpvalente.gitbook.io/ontime/control-and-feedback/websocket-api + // see https://docs.getontime.no/api/osc-and-ws/ const { timer, playback } = payload; const timerElement = document.getElementById('timer'); if (playback == 'stop') { diff --git a/apps/server/src/external/demo/index.html b/apps/server/src/external/demo/index.html index 28b06716f..a469f4db5 100644 --- a/apps/server/src/external/demo/index.html +++ b/apps/server/src/external/demo/index.html @@ -10,5 +10,5 @@
- - \ No newline at end of file + + diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 5a6de5919..d9ff2cf8d 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { initAssets, startIntegrations, startOSCServer, startServer } from './app.js'; async function startOntime() { diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index cd941c703..b71f213b8 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -142,17 +142,18 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert const isTimeToUpdate = state.clock - TimerService.previousUpdate >= TimerService._updateInterval; // some changes need an immediate update + const hasNewLoaded = state.eventNow?.id !== TimerService.previousState?.eventNow?.id; const hasSkippedBack = state.clock < TimerService.previousUpdate; const justStarted = !TimerService.previousState?.timer; const hasChangedPlayback = TimerService.previousState.timer?.playback !== state.timer.playback; - const hasImmediateChanges = hasSkippedBack || justStarted || hasChangedPlayback; + const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback; if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) { eventStore.set('timer', state.timer); TimerService.previousState.timer = { ...state.timer }; } - if (isTimeToUpdate && !deepEqual(TimerService.previousState?.runtime, state.runtime)) { + if (hasChangedPlayback || (isTimeToUpdate && !deepEqual(TimerService.previousState?.runtime, state.runtime))) { eventStore.set('runtime', state.runtime); TimerService.previousState.runtime = { ...state.runtime }; } diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index f87cb895c..1a07ab792 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1387,6 +1387,9 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null, }, + runtime: { + actualStart: 150, + }, } as RuntimeState; const offset = getRuntimeOffset(state); @@ -1408,6 +1411,9 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: null, }, + runtime: { + actualStart: 100, + }, } as RuntimeState; const offset = getRuntimeOffset(state); @@ -1430,9 +1436,116 @@ describe('getRuntimeOffset()', () => { _timer: { pausedAt: 125, }, + runtime: { + actualStart: 100, + }, } as RuntimeState; const offset = getRuntimeOffset(state); expect(offset).toBe(25); }); + + it('can only count once started', () => { + const state = { + clock: 78480789, + eventNow: { + id: 'd6a2ce', + type: 'event', + title: '', + timeStart: 77400000, + timeEnd: 81000000, + duration: 3600000, + timeStrategy: 'lock-duration', + linkStart: null, + endAction: 'none', + timerType: 'count-down', + isPublic: true, + skip: false, + note: '', + colour: '', + cue: '1', + revision: 0, + timeWarning: 120000, + timeDanger: 60000, + custom: {}, + delay: 0, + }, + runtime: { + selectedEventIndex: 0, + numEvents: 2, + offset: -77400000, + plannedStart: 77400000, + plannedEnd: 84600000, + actualStart: null, + expectedEnd: null, + }, + timer: { + addedTime: 0, + current: 3600000, + duration: 3600000, + elapsed: null, + expectedFinish: null, + finishedAt: null, + playback: 'armed', + secondaryTimer: null, + startedAt: null, + }, + _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + } as RuntimeState; + + const offset = getRuntimeOffset(state); + expect(offset).toBe(null); + }); + + it('handles loaded event', () => { + const state = { + clock: 79521653, + eventNow: { + id: '835242', + type: 'event', + title: '', + timeStart: 81000000, + timeEnd: 84600000, + duration: 3600000, + timeStrategy: 'lock-duration', + linkStart: null, + endAction: 'none', + timerType: 'count-down', + isPublic: true, + skip: false, + note: '', + colour: '', + cue: '2', + revision: 0, + timeWarning: 120000, + timeDanger: 60000, + custom: {}, + delay: 0, + }, + runtime: { + selectedEventIndex: 1, + numEvents: 2, + offset: -81000000, + plannedStart: 77400000, + plannedEnd: 84600000, + actualStart: 79443403, + expectedEnd: null, + }, + timer: { + addedTime: 0, + current: 3600000, + duration: 3600000, + elapsed: null, + expectedFinish: null, + finishedAt: null, + playback: 'armed', + secondaryTimer: null, + startedAt: null, + }, + _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false }, + } as RuntimeState; + + const offset = getRuntimeOffset(state); + expect(offset).toBe(79521653 - 81000000); + }); }); diff --git a/apps/server/src/services/project-service/ProjectService.ts b/apps/server/src/services/project-service/ProjectService.ts index 989728091..80816b50b 100644 --- a/apps/server/src/services/project-service/ProjectService.ts +++ b/apps/server/src/services/project-service/ProjectService.ts @@ -14,6 +14,7 @@ import { appStateService } from '../app-state-service/AppStateService.js'; import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js'; import { dbModel } from '../../models/dataModel.js'; import { deleteFile } from '../../utils/parserUtils.js'; +import { switchDb } from '../../setup/loadDb.js'; // init dependencies init(); @@ -40,6 +41,9 @@ export async function applyProjectFile(filePath: string, options?: Options) { const newFilePath = join(resolveProjectsDirectory, filename); await rename(filePath, newFilePath); + // change LowDB to point to new file + await switchDb(filename); + // apply data model await applyDataModel(data, options); @@ -135,6 +139,9 @@ export async function createProjectFile(filename: string, projectData: ProjectDa const newFile = join(resolveProjectsDirectory, filename); await writeFile(newFile, JSON.stringify(data)); + // change LowDB to point to new file + await switchDb(filename); + // apply its data await applyDataModel(data); diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 6f07d8599..d51653206 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -191,10 +191,7 @@ class RuntimeService { } const timedEvents = getPlayableEvents(); - const state = runtimeState.getState(); - // TODO: return success boolean from runtimeState, when we work with optimising integrations - runtimeState.load(event, timedEvents); - const success = event.id === state.eventNow?.id; + const success = runtimeState.load(event, timedEvents); if (success) { integrationService.dispatch(TimerLifeCycle.onLoad); diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index d8098a608..3cbd4d2a7 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -292,20 +292,27 @@ export const updateRoll = (state: RuntimeState) => { /** * Calculates difference between the runtime and the schedule of an event + * Positive offset is a delay + * Negative offset is time ahead * @param state * @returns */ -export function getRuntimeOffset(state: RuntimeState): number { - if (state.eventNow === null) { - return 0; +export function getRuntimeOffset(state: RuntimeState): MaybeNumber { + if (state.runtime.actualStart === null) { + return null; } - + const { clock } = state; const { timeStart } = state.eventNow; const { addedTime, current, startedAt } = state.timer; + // if we havent started, the offset is the difference to the schedule + if (startedAt === null) { + return clock - timeStart; + } + const overtime = Math.min(current, 0); const startOffset = startedAt - timeStart; - const pausedTime = state._timer.pausedAt === null ? 0 : state.clock - state._timer.pausedAt; + const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt; return startOffset + addedTime + pausedTime + Math.abs(overtime); } diff --git a/apps/server/src/setup/index.ts b/apps/server/src/setup/index.ts index 68b49a3b7..a0b3241cd 100644 --- a/apps/server/src/setup/index.ts +++ b/apps/server/src/setup/index.ts @@ -97,7 +97,8 @@ const lastLoadedProject = isTest ? 'db.json' : getLastLoadedProject(); // path to public db export const resolveDbDirectory = join(testDbStartDirectory, isTest ? `../${config.database.testdb}` : config.projects); -export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename); +export const resolveDbName = lastLoadedProject ? lastLoadedProject : config.database.filename; +export const resolveDbPath = join(resolveDbDirectory, resolveDbName); export const pathToStartDb = isTest ? join(srcDirectory, '..', config.database.testdb, config.database.filename) diff --git a/apps/server/src/setup/loadDb.ts b/apps/server/src/setup/loadDb.ts index df74ad8a1..81112f15e 100644 --- a/apps/server/src/setup/loadDb.ts +++ b/apps/server/src/setup/loadDb.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import { ensureDirectory } from '../utils/fileManagement.js'; import { dbModel } from '../models/dataModel.js'; -import { pathToStartDb, resolveDbDirectory, resolveDbPath } from './index.js'; +import { pathToStartDb, resolveDbDirectory, resolveDbName } from './index.js'; import { parseProjectFile } from '../services/project-service/projectFileUtils.js'; import { parseJson } from '../utils/parser.js'; @@ -16,25 +16,25 @@ import { parseJson } from '../utils/parser.js'; * @description ensures directories exist and populates database * @return {string} - path to db file */ -const populateDb = (): string => { - // if everything goes well, the DB in disk is the one loaded - let dbInDisk = resolveDbPath; - ensureDirectory(resolveDbDirectory); +const populateDb = (directory: string, filename: string): string => { + ensureDirectory(directory); + let dbPath = join(directory, filename); + // if everything goes well, the DB in disk is the one loaded // if dbInDisk doesn't exist we want to use startup db - if (!existsSync(dbInDisk)) { + if (!existsSync(dbPath)) { try { const dbDirectory = resolveDbDirectory; const newFileDirectory = join(dbDirectory, pathToStartDb.split('/').pop()); copyFileSync(pathToStartDb, newFileDirectory); - dbInDisk = newFileDirectory; + dbPath = newFileDirectory; } catch (_) { /* we do not handle this */ } } - return dbInDisk; + return dbPath; }; /** @@ -55,10 +55,9 @@ const parseDatabase = async (fileToRead: string, adapterToUse: Low}>} */ -async function loadDb() { - const dbInDisk = populateDb(); +async function loadDb(directory: string, filename: string) { + const dbInDisk = populateDb(directory, filename); const adapter = new JSONFile(dbInDisk); const db = new Low(adapter, dbModel); @@ -72,12 +71,24 @@ async function loadDb() { export let db = {} as Low; export let data = {} as DatabaseModel; -export const dbLoadingProcess = loadDb(); +export const dbLoadingProcess = loadDb(resolveDbDirectory, resolveDbName); +/** + * Initialises database at known location + */ const init = async () => { const dbProvider = await dbLoadingProcess; db = dbProvider.db; data = dbProvider.data; }; +/** + * Allows to switch the database to a new file + */ +export const switchDb = async (newFileName: string) => { + const { db: newDb, data: newData } = await loadDb(resolveDbDirectory, newFileName); + db = newDb; + data = newData; +}; + init(); diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 636a90853..f9ae0c20b 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -177,7 +177,7 @@ describe('mutation on runtimeState', () => { stop(); newState = getState(); expect(newState.runtime.actualStart).toBeNull(); - expect(newState.runtime.offset).toBe(0); + expect(newState.runtime.offset).toBeNull(); expect(newState.runtime.expectedEnd).toBeNull(); }); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 2e9e17d2a..2998b10b7 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -17,7 +17,7 @@ import { timerConfig } from '../config/config.js'; const initialRuntime: Runtime = { selectedEventIndex: null, numEvents: 0, - offset: 0, + offset: null, plannedStart: 0, plannedEnd: 0, actualStart: null, @@ -128,7 +128,11 @@ export function updateRundownData(playableRundown: OntimeEvent[]) { * @param rundown * @param initialData */ -export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial) { +export function load( + event: OntimeEvent, + rundown: OntimeEvent[], + initialData?: Partial, +): boolean { clear(); updateRundownData(rundown); @@ -153,9 +157,11 @@ export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: P if (firstStart === null || typeof firstStart === 'number') { runtimeState.runtime.actualStart = firstStart; runtimeState.runtime.offset = getRuntimeOffset(runtimeState); - runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset; + runtimeState.runtime.expectedEnd = (runtimeState.runtime.plannedEnd + runtimeState.runtime.offset) % dayInMs; } } + + return event.id === runtimeState.eventNow?.id; } export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) { @@ -329,7 +335,9 @@ export function addTime(amount: number) { // update runtime delays: over - under runtimeState.runtime.offset = getRuntimeOffset(runtimeState); - runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset; + if (runtimeState.runtime.offset !== null) { + runtimeState.runtime.expectedEnd = (runtimeState.runtime.plannedEnd + runtimeState.runtime.offset) % dayInMs; + } return true; } diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 928d71d9b..c36292330 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -15,8 +15,6 @@ import { OntimeEvent, OntimeRundown, SupportedEvent, - EndAction, - TimerType, TimeStrategy, CustomFields, EventCustomFields, @@ -325,8 +323,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial { await page.getByRole('button', { name: 'close' }).click(); // asset test events - await page.getByText('Albania').click(); - await page.getByText('Latvia').click(); - await page.getByText('Lithuania').click(); + const firstTitle = page.getByTestId('entry-1').getByTestId('block__title') + await expect(firstTitle).toHaveValue('Albania'); + + const secondTitle = page.getByTestId('entry-2').getByTestId('block__title') + await expect(secondTitle).toHaveValue('Latvia'); + + const thirdTitle = page.getByTestId('entry-3').getByTestId('block__title') + await expect(thirdTitle).toHaveValue('Lithuania'); }); diff --git a/e2e/tests/features/203-delay-block.spec.ts b/e2e/tests/features/203-delay-block.spec.ts index 23bdf1187..54ad3d7a0 100644 --- a/e2e/tests/features/203-delay-block.spec.ts +++ b/e2e/tests/features/203-delay-block.spec.ts @@ -64,11 +64,11 @@ test('delays are show correctly', async ({ page }) => { await page.getByTestId('rundown').getByTestId('time-input-duration').click(); await page.getByTestId('rundown').getByTestId('time-input-duration').fill('10'); await page.getByTestId('rundown').getByTestId('time-input-duration').press('Enter'); - await page.getByText('Event title').click(); - await page.getByPlaceholder('Event title').fill('test'); - await page.getByPlaceholder('Event title').press('Enter'); + await page.getByTestId('block__title').click(); + await page.getByTestId('block__title').fill('test'); + await page.getByTestId('block__title').press('Enter'); - await page.getByTestId('entry-1').getByText('test').click({ button: 'right' }); + await page.locator('#event-block').getByText('1').click({ button: 'right' }); await page.getByRole('menuitem', { name: 'Toggle public' }).click(); // add a delay diff --git a/packages/types/src/definitions/runtime/Runtime.type.ts b/packages/types/src/definitions/runtime/Runtime.type.ts index 70606d622..3a1a5b153 100644 --- a/packages/types/src/definitions/runtime/Runtime.type.ts +++ b/packages/types/src/definitions/runtime/Runtime.type.ts @@ -3,7 +3,7 @@ import { MaybeNumber } from '../../utils/utils.type.js'; export type Runtime = { numEvents: number; selectedEventIndex: MaybeNumber; - offset: number; + offset: MaybeNumber; plannedStart: MaybeNumber; actualStart: MaybeNumber; plannedEnd: MaybeNumber;