Compare commits

...

10 Commits

Author SHA1 Message Date
Carlos Valente 4f298e6fd9 version bump (#487) 2023-08-12 11:38:36 +02:00
Carlos Valente f3610059ef fix paused (#486)
* fix: prevent timer continue on paused count up

* refactor: throw if not found on delete
2023-08-12 11:24:35 +02:00
Carlos Valente 023aac4ead fix: midnight roll (#484)
* refactor: extract utilities and types

* refactor: extract and simplify progress bar logic

* refactor: duration validation handles midnight

* fix: prevent stale event loader

* refactor: consider next event in timer invalidation

* refactor: reload changes on changed roll target

* refactor: timer load accounts for midnight

* fix: issues with midnight on roll

* refactor: prevent stale secondary event
2023-08-12 10:42:43 +02:00
Carlos Valente 8c9cb908cf chore: improve demo file (#482) 2023-08-12 10:00:30 +02:00
Carlos Valente 839cee18f3 feat: add french translations (#483) 2023-08-12 10:00:14 +02:00
Olayinka Zadat O b83ebf53a9 Adding Alias as a Parameter (#471)
* redirect on alias to a new page

* move alias to a HOC wrapper
2023-08-11 14:53:36 +02:00
Carlos Valente 1b8392bba4 dx: improve local build command (#480) 2023-08-11 10:55:24 +02:00
Carlos Valente af1241bec7 feat: event description (#481)
* feat: enable configuration of <Info> panel

* feat: add description field to event data
2023-08-11 10:55:02 +02:00
Carlos Valente c361ff4b3f refactor: handle undefined (#479) 2023-08-11 10:54:24 +02:00
Cody Wilson b579a4c57e dist-mac: create separate dists for aarch64, x64 (#470)
* dist-mac: create separate dists for aarch64, x64
2023-08-11 10:51:00 +02:00
67 changed files with 1272 additions and 434 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ jobs:
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: './apps/electron/dist/ontime-macOS.dmg' files: |
'./apps/electron/dist/ontime-macOS-x64.dmg'
'./apps/electron/dist/ontime-macOS-arm64.dmg'
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "2.0.9", "version": "2.3.8",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.7.0", "@chakra-ui/react": "^2.7.0",
+11 -28
View File
@@ -1,8 +1,7 @@
import { lazy, Suspense, useEffect } from 'react'; import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import useAliases from './common/hooks-query/useAliases';
import withData from './features/viewers/ViewWrapper'; import withData from './features/viewers/ViewWrapper';
import withAlias from './features/AliasWrapper';
const Editor = lazy(() => import('./features/editors/ProtectedEditor')); const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet')); const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
@@ -17,14 +16,14 @@ const Public = lazy(() => import('./features/viewers/public/Public'));
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper')); const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock')); const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
const STimer = withData(TimerView); const STimer = withAlias(withData(TimerView));
const SMinimalTimer = withData(MinimalTimerView); const SMinimalTimer = withAlias(withData(MinimalTimerView));
const SClock = withData(ClockView); const SClock = withAlias(withData(ClockView));
const SCountdown = withData(Countdown); const SCountdown = withAlias(withData(Countdown));
const SBackstage = withData(Backstage); const SBackstage = withAlias(withData(Backstage));
const SPublic = withData(Public); const SPublic = withAlias(withData(Public));
const SLowerThird = withData(Lower); const SLowerThird = withAlias(withData(Lower));
const SStudio = withData(StudioClock); const SStudio = withAlias(withData(StudioClock));
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper')); const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
const RundownPanel = lazy(() => import('./features/rundown/RundownExport')); const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
@@ -33,22 +32,6 @@ const MessageControl = lazy(() => import('./features/control/message/MessageCont
const Info = lazy(() => import('./features/info/InfoExport')); const Info = lazy(() => import('./features/info/InfoExport'));
export default function AppRouter() { export default function AppRouter() {
const { data } = useAliases();
const location = useLocation();
const navigate = useNavigate();
// navigate if is alias route
useEffect(() => {
if (!data) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<Routes> <Routes>
+1 -1
View File
@@ -7,7 +7,7 @@ import { nowInMillis } from '../utils/time';
export function logAxiosError(prepend: string, error: unknown) { export function logAxiosError(prepend: string, error: unknown) {
const message = axios.isAxiosError(error) const message = axios.isAxiosError(error)
? `${prepend}, ${(error as AxiosError).response?.statusText}: ${(error as AxiosError).response?.data}` ? `${prepend} ${(error as AxiosError).response?.statusText ?? ''}: ${(error as AxiosError).response?.data ?? ''}`
: `${prepend}: ${error}`; : `${prepend}: ${error}`;
addLog({ addLog({
@@ -13,14 +13,14 @@ import {
requestPutEvent, requestPutEvent,
requestReorderEvent, requestReorderEvent,
} from '../api/eventsApi'; } from '../api/eventsApi';
import { useLocalEvent } from '../stores/localEvent'; import { useEditorSettings } from '../stores/editorSettings';
/** /**
* @description Set of utilities for events * @description Set of utilities for events
*/ */
export const useEventAction = () => { export const useEventAction = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const eventSettings = useLocalEvent((state) => state.eventSettings); const eventSettings = useEditorSettings((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -2,6 +2,7 @@ import { EventData } from 'ontime-types';
export const eventDataPlaceholder: EventData = { export const eventDataPlaceholder: EventData = {
title: '', title: '',
description: '',
publicUrl: '', publicUrl: '',
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
@@ -0,0 +1,67 @@
import { create } from 'zustand';
import { booleanFromLocalStorage } from '../utils/localStorage';
type EditorSettings = {
showQuickEntry: boolean;
startTimeIsLastEnd: boolean;
defaultPublic: boolean;
showNif: boolean;
};
type EditorSettingsStore = {
eventSettings: EditorSettings;
setLocalEventSettings: (newState: EditorSettings) => void;
setShowQuickEntry: (showQuickEntry: boolean) => void;
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
setDefaultPublic: (defaultPublic: boolean) => void;
setShowNif: (showNif: boolean) => void;
};
enum EditorSettingsKeys {
ShowQuickEntry = 'ontime-show-quick-entry',
StartTimeIsLastEnd = 'ontime-start-is-last-end',
DefaultPublic = 'ontime-default-public',
ShowNif = 'ontime-show-nif',
}
export const useEditorSettings = create<EditorSettingsStore>((set) => ({
eventSettings: {
showQuickEntry: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, false),
startTimeIsLastEnd: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
showNif: booleanFromLocalStorage(EditorSettingsKeys.ShowNif, true),
},
setLocalEventSettings: (value) =>
set(() => {
localStorage.setItem(EditorSettingsKeys.ShowQuickEntry, String(value.showQuickEntry));
localStorage.setItem(EditorSettingsKeys.StartTimeIsLastEnd, String(value.startTimeIsLastEnd));
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(value.defaultPublic));
return { eventSettings: value };
}),
setShowQuickEntry: (showQuickEntry) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.ShowQuickEntry, String(showQuickEntry));
return { eventSettings: { ...state.eventSettings, showQuickEntry } };
}),
setStartTimeIsLastEnd: (startTimeIsLastEnd) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.StartTimeIsLastEnd, String(startTimeIsLastEnd));
return { eventSettings: { ...state.eventSettings, startTimeIsLastEnd } };
}),
setDefaultPublic: (defaultPublic) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
return { eventSettings: { ...state.eventSettings, defaultPublic } };
}),
setShowNif: (showNif) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.ShowNif, String(showNif));
return { eventSettings: { ...state.eventSettings, showNif } };
}),
}));
@@ -1,57 +0,0 @@
import { create } from 'zustand';
import { booleanFromLocalStorage } from '../utils/localStorage';
type EventSettings = {
showQuickEntry: boolean;
startTimeIsLastEnd: boolean;
defaultPublic: boolean;
};
type LocalEventStore = {
eventSettings: EventSettings;
setLocalEventSettings: (newState: EventSettings) => void;
setShowQuickEntry: (showQuickEntry: boolean) => void;
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
setDefaultPublic: (defaultPublic: boolean) => void;
};
enum LocalEventKeys {
ShowQuickEntry = 'ontime-show-quick-entry',
StartTimeIsLastEnd = 'ontime-start-is-last-end',
DefaultPublic = 'ontime-default-public',
}
export const useLocalEvent = create<LocalEventStore>((set) => ({
eventSettings: {
showQuickEntry: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, false),
startTimeIsLastEnd: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
defaultPublic: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
},
setLocalEventSettings: (value) =>
set(() => {
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(value.showQuickEntry));
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(value.startTimeIsLastEnd));
localStorage.setItem(LocalEventKeys.DefaultPublic, String(value.defaultPublic));
return { eventSettings: value };
}),
setShowQuickEntry: (showQuickEntry) =>
set((state) => {
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(showQuickEntry));
return { eventSettings: { ...state.eventSettings, showQuickEntry } };
}),
setStartTimeIsLastEnd: (startTimeIsLastEnd) =>
set((state) => {
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(startTimeIsLastEnd));
return { eventSettings: { ...state.eventSettings, startTimeIsLastEnd } };
}),
setDefaultPublic: (defaultPublic) =>
set((state) => {
localStorage.setItem(LocalEventKeys.DefaultPublic, String(defaultPublic));
return { eventSettings: { ...state.eventSettings, defaultPublic } };
}),
}));
@@ -1,4 +1,5 @@
import { validateAlias } from '../aliases'; import { resolvePath } from 'react-router-dom';
import { validateAlias, generateURLFromAlias, getAliasRoute } from '../aliases';
describe('An alias fails if incorrect', () => { describe('An alias fails if incorrect', () => {
const testsToFail = [ const testsToFail = [
@@ -23,3 +24,79 @@ describe('An alias fails if incorrect', () => {
}), }),
); );
}); });
describe('generateURLFromAlias and getAliasRoute function', () => {
test('generate the expected url from an alias', () => {
const testData = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
const expected = [
{
url: '/timer?user=guest&alias=demopage',
},
];
expect(generateURLFromAlias(testData[0])).toStrictEqual(expected[0].url);
});
test('generate the url to redirect to when the current URL is just the alias', () => {
const aliases = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the alias
const location = resolvePath(aliases[0].alias);
const expected = [
{
url: '/timer?user=guest&alias=demopage',
},
];
expect(getAliasRoute(location, aliases, null)).toStrictEqual(expected[0].url);
});
test('generate the url to redirect to when the current URL the same url but with a change of params', () => {
const aliases = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the actual url with alias attached to it
const location = resolvePath(aliases[0].pathAndParams);
const urlSearchParams = new URLSearchParams(location.search);
urlSearchParams.append('alias', aliases[0].alias); //
// update current alias with extra param
aliases[0].pathAndParams += '&eventId=674';
const expected = [
{
url: '/timer?user=guest&eventId=674&alias=demopage',
},
];
expect(getAliasRoute(location, aliases, urlSearchParams)).toStrictEqual(expected[0].url);
});
test('generate no url to redirect to when the current URL the same url', () => {
const aliases = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the actual url with alias attached to it
const location = resolvePath(aliases[0].pathAndParams);
const urlSearchParams = new URLSearchParams(location.search);
urlSearchParams.append('alias', aliases[0].alias); //
expect(getAliasRoute(location, aliases, urlSearchParams)).toBeNull();
});
});
@@ -1,27 +0,0 @@
import { calculateDuration, DAY_TO_MS } from '../timesManager';
describe('calculateDuration()', () => {
describe('Given start and end values', () => {
it('calculates duration correctly', () => {
const testStart = 1;
const testEnd = 2;
const val = calculateDuration(testStart, testEnd);
expect(val).toBe(testEnd - testStart);
});
});
describe('Handles edge cases', () => {
it('when start is after end', () => {
const testStart = 3;
const testEnd = 2;
const val = calculateDuration(testStart, testEnd);
expect(val).toBe(testEnd + DAY_TO_MS - testStart);
});
it('when both are equal', () => {
const testStart = 1;
const testEnd = 1;
const val = calculateDuration(testStart, testEnd);
expect(val).toBe(testEnd - testStart);
});
});
});
+49 -1
View File
@@ -1,10 +1,13 @@
import { Alias } from 'ontime-types';
import isEqual from 'react-fast-compare';
import { Location, resolvePath } from 'react-router-dom';
/** /**
* Validates an alias against defined parameters * Validates an alias against defined parameters
* @param {string} alias * @param {string} alias
* @returns {{message: string, status: boolean}} * @returns {{message: string, status: boolean}}
*/ */
export const validateAlias = (alias: string) => { export const validateAlias = (alias: string) => {
const valid = { status: true, message: 'ok' }; const valid = { status: true, message: 'ok' };
if (alias === '' || alias == null) { if (alias === '' || alias == null) {
@@ -27,3 +30,48 @@ export const validateAlias = (alias: string) => {
return valid; return valid;
}; };
/**
* Gets the URL to send an alias to
* @param location
* @param data
* @param searchParams
*/
export const getAliasRoute = (location: Location, data: Alias[], searchParams: URLSearchParams) => {
const currentURL = location.pathname.substring(1);
// we need to check if the whole url here is an alias, so we can redirect
const foundAlias = data.filter((d) => d.alias === currentURL && d.enabled)[0];
if (foundAlias) {
return generateURLFromAlias(foundAlias);
}
const aliasOnPage = searchParams.get('alias');
for (const d of data) {
if (aliasOnPage) {
// if the alias fits the alias on this page, but the URL is diferent, we redirect user to the new URL
// if we have the same alias and its enabled and its not empty
if (d.alias !== '' && d.enabled && d.alias === aliasOnPage) {
const newAliasPath = resolvePath(d.pathAndParams);
const urlParams = new URLSearchParams(newAliasPath.search);
urlParams.set('alias', d.alias);
// we confirm either the url parameters does not match or the url path doesnt
if (!isEqual(urlParams, searchParams) || newAliasPath.pathname !== location.pathname) {
// we then redirect to the alias route, since the view listening to this alias has an outdated URL
return `${newAliasPath.pathname}?${urlParams}`;
}
}
}
}
return null;
};
/**
* Generate URL from an alias
* @param aliasData
*/
export const generateURLFromAlias = (aliasData: Alias) => {
const newAliasPath = resolvePath(aliasData.pathAndParams);
const urlParams = new URLSearchParams(newAliasPath.search);
urlParams.set('alias', aliasData.alias);
return `${newAliasPath.pathname}?${urlParams}`;
};
@@ -10,15 +10,8 @@ export const mts = 1000;
*/ */
export const mtm = 1000 * 60; export const mtm = 1000 * 60;
/** /**
* millis to hours * millis to hours
* @type {number} * @type {number}
*/ */
export const mth = 1000 * 60 * 60; export const mth = 1000 * 60 * 60;
/**
* milliseconds in a day
* @type {number}
*/
export const DAY_TO_MS = 86400000;
@@ -1,16 +1,5 @@
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride'; export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
/**
* @description Milliseconds in a day
*/
export const DAY_TO_MS = 86400000;
/**
* @description calculates duration from given values
*/
export const calculateDuration = (start: number, end: number): number =>
start > end ? end + DAY_TO_MS - start : end - start;
/** /**
* @description Checks which field the value relates to * @description Checks which field the value relates to
*/ */
+28
View File
@@ -0,0 +1,28 @@
/* eslint-disable react/display-name */
import useAliases from '../common/hooks-query/useAliases';
import { getAliasRoute } from '../common/utils/aliases';
import { ComponentType, useEffect } from 'react';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
const withAlias = <P extends object>(Component: ComponentType<P>) => {
return (props: Partial<P>) => {
const { data } = useAliases();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
// navigate if is alias route
useEffect(() => {
if (!data) return;
const url = getAliasRoute(location, data, searchParams);
// navigate to this route if its not empty
if (url) {
navigate(url);
}
}, [data, searchParams, navigate, location]);
return <Component {...props} />;
};
};
export default withAlias;
@@ -1,13 +1,13 @@
import { memo, useState } from 'react'; import { memo, useState } from 'react';
import { Select, Switch } from '@chakra-ui/react'; import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types'; import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils'; import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput'; import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../common/utils/dateConfig'; import { millisToDelayString } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils'; import { cx } from '../../../common/utils/styleUtils';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager'; import { TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss'; import style from '../EventEditor.module.scss';
@@ -19,6 +19,13 @@
} }
} }
.description {
color: $label-gray;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.labels { .labels {
font-size: $inner-section-text-size; font-size: $inner-section-text-size;
display: flex; display: flex;
+7 -3
View File
@@ -1,4 +1,5 @@
import { useInfoPanel } from '../../common/hooks/useSocket'; import { useInfoPanel } from '../../common/hooks/useSocket';
import { useEditorSettings } from '../../common/stores/editorSettings';
import InfoHeader from './info-header/InfoHeader'; import InfoHeader from './info-header/InfoHeader';
import CollapsableInfo from './CollapsableInfo'; import CollapsableInfo from './CollapsableInfo';
@@ -8,6 +9,7 @@ import InfoTitles from './InfoTitles';
export default function Info() { export default function Info() {
const data = useInfoPanel(); const data = useInfoPanel();
const showNif = useEditorSettings((state) => state.eventSettings.showNif);
const titlesNow = { const titlesNow = {
title: data.titles.titleNow || '', title: data.titles.titleNow || '',
@@ -32,9 +34,11 @@ export default function Info() {
return ( return (
<> <>
<InfoHeader selected={selected} /> <InfoHeader selected={selected} />
<CollapsableInfo title='Network Info'> {showNif && (
<InfoNif /> <CollapsableInfo title='Network Info'>
</CollapsableInfo> <InfoNif />
</CollapsableInfo>
)}
<CollapsableInfo title='Playing Now'> <CollapsableInfo title='Playing Now'>
<InfoTitles data={titlesNow} /> <InfoTitles data={titlesNow} />
</CollapsableInfo> </CollapsableInfo>
@@ -6,9 +6,12 @@ export default function InfoHeader({ selected }: { selected: string }) {
const { data } = useEventData(); const { data } = useEventData();
return ( return (
<div className={style.panelHeader}> <>
<span className={style.title}>{data?.title || ''}</span> <div className={style.panelHeader}>
<span className={style.selected}>{selected}</span> <span className={style.title}>{data?.title || ''}</span>
</div> <span className={style.selected}>{selected}</span>
</div>
<div className={style.description}>{data?.description || ''}</div>
</>
); );
} }
@@ -102,6 +102,18 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
/> />
</label> </label>
</div> </div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Event description
<Input
variant='ontime-filled-on-light'
size='sm'
maxLength={100}
placeholder='Euro Love, Malmö 2024'
{...register('description')}
/>
</label>
</div>
<div className={styles.entryRow}> <div className={styles.entryRow}>
<label className={styles.sectionTitle}> <label className={styles.sectionTitle}>
Public Info Public Info
@@ -131,7 +131,7 @@ export default function AliasesForm() {
isDisabled={disableInputs} isDisabled={disableInputs}
/> />
<TooltipActionBtn <TooltipActionBtn
clickHandler={(event) => handleLinks(event, alias.pathAndParams)} clickHandler={(event) => handleLinks(event, alias.alias)}
tooltip='Test alias' tooltip='Test alias'
aria-label='Test alias' aria-label='Test alias'
size='xs' size='xs'
@@ -118,6 +118,7 @@ export default function AppSettingsModal() {
> >
<Select backgroundColor='white' size='sm' width='auto' isDisabled={disableInputs} {...register('language')}> <Select backgroundColor='white' size='sm' width='auto' isDisabled={disableInputs} {...register('language')}>
<option value='en'>English</option> <option value='en'>English</option>
<option value='fr'>French</option>
<option value='de'>German</option> <option value='de'>German</option>
<option value='no'>Norwegian</option> <option value='no'>Norwegian</option>
<option value='pt'>Portuguese</option> <option value='pt'>Portuguese</option>
@@ -1,15 +1,16 @@
import { Switch } from '@chakra-ui/react'; import { Switch } from '@chakra-ui/react';
import { useLocalEvent } from '../../../common/stores/localEvent'; import { useEditorSettings } from '../../../common/stores/editorSettings';
import ModalSplitInput from '../ModalSplitInput'; import ModalSplitInput from '../ModalSplitInput';
import style from './SettingsModal.module.scss'; import style from './SettingsModal.module.scss';
export default function EditorSettings() { export default function EditorSettings() {
const eventSettings = useLocalEvent((state) => state.eventSettings); const eventSettings = useEditorSettings((state) => state.eventSettings);
const setShowQuickEntry = useLocalEvent((state) => state.setShowQuickEntry); const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry);
const setStartTimeIsLastEnd = useLocalEvent((state) => state.setStartTimeIsLastEnd); const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd);
const setDefaultPublic = useLocalEvent((state) => state.setDefaultPublic); const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
const setShowNif = useEditorSettings((state) => state.setShowNif);
return ( return (
<div className={style.sectionContainer}> <div className={style.sectionContainer}>
@@ -39,6 +40,18 @@ export default function EditorSettings() {
onChange={(event) => setDefaultPublic(event.target.checked)} onChange={(event) => setDefaultPublic(event.target.checked)}
/> />
</ModalSplitInput> </ModalSplitInput>
<span className={style.title}>Info settings</span>
<ModalSplitInput
field=''
title='Show network'
description='Whether to available show network interfaces in the panel'
>
<Switch
variant='ontime-on-light'
defaultChecked={eventSettings.showQuickEntry}
onChange={(event) => setShowNif(event.target.checked)}
/>
</ModalSplitInput>
</div> </div>
); );
} }
@@ -71,6 +71,21 @@ export default function EventDataForm() {
{...register('title')} {...register('title')}
/> />
</ModalInput> </ModalInput>
<ModalInput
field='description'
title='Event description'
description='Free field, shown in editor'
error={errors.description?.message}
>
<Input
{...inputProps}
variant='ontime-filled-on-light'
maxLength={100}
placeholder='Euro Love, Malmö 2024'
isDisabled={disableInputs}
{...register('description')}
/>
</ModalInput>
<div style={{ height: '16px' }} /> <div style={{ height: '16px' }} />
<ModalInput field='publicInfo' title='Public Info' description='Information shown in public screens'> <ModalInput field='publicInfo' title='Public Info' description='Information shown in public screens'>
<Textarea <Textarea
+2 -2
View File
@@ -6,7 +6,7 @@ import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket'; import { useRundownEditor } from '../../common/hooks/useSocket';
import { AppMode, useAppMode } from '../../common/stores/appModeStore'; import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import { useLocalEvent } from '../../common/stores/localEvent'; import { useEditorSettings } from '../../common/stores/editorSettings';
import { cloneEvent, getFirstEvent, getNextEvent, getPreviousEvent } from '../../common/utils/eventsManager'; import { cloneEvent, getFirstEvent, getNextEvent, getPreviousEvent } from '../../common/utils/eventsManager';
import QuickAddBlock from './quick-add-block/QuickAddBlock'; import QuickAddBlock from './quick-add-block/QuickAddBlock';
@@ -26,7 +26,7 @@ export default function Rundown(props: RundownProps) {
const featureData = useRundownEditor(); const featureData = useRundownEditor();
const { addEvent, reorderEvent } = useEventAction(); const { addEvent, reorderEvent } = useEventAction();
const eventSettings = useLocalEvent((state) => state.eventSettings); const eventSettings = useEditorSettings((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
const showQuickEntry = eventSettings.showQuickEntry; const showQuickEntry = eventSettings.showQuickEntry;
@@ -1,12 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import { calculateDuration } from 'ontime-utils';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import { useAppMode } from '../../common/stores/appModeStore'; import { useAppMode } from '../../common/stores/appModeStore';
import { useLocalEvent } from '../../common/stores/localEvent'; import { useEditorSettings } from '../../common/stores/editorSettings';
import { useEmitLog } from '../../common/stores/logger'; import { useEmitLog } from '../../common/stores/logger';
import { cloneEvent } from '../../common/utils/eventsManager'; import { cloneEvent } from '../../common/utils/eventsManager';
import { calculateDuration } from '../../common/utils/timesManager';
import BlockBlock from './block-block/BlockBlock'; import BlockBlock from './block-block/BlockBlock';
import DelayBlock from './delay-block/DelayBlock'; import DelayBlock from './delay-block/DelayBlock';
@@ -61,7 +61,7 @@ export default function RundownEntry(props: RundownEntryProps) {
} }
}, [cursor, data.id, openId, setCursor, setEditId]); }, [cursor, data.id, openId, setCursor, setEditId]);
const eventSettings = useLocalEvent((state) => state.eventSettings); const eventSettings = useEditorSettings((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -1,4 +1,4 @@
import { Playback } from 'ontime-types'; import { MaybeNumber, Playback } from 'ontime-types';
import { useTimer } from '../../../../common/hooks/useSocket'; import { useTimer } from '../../../../common/hooks/useSocket';
import { clamp } from '../../../../common/utils/math'; import { clamp } from '../../../../common/utils/math';
@@ -9,18 +9,26 @@ interface EventBlockProgressBarProps {
playback?: Playback; playback?: Playback;
} }
export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number {
if (remaining === null || total === null) {
return 0;
}
if (remaining <= 0) {
return 100;
}
if (remaining === total) {
return 0;
}
return clamp(100 - (remaining * 100) / total, 0, 100);
}
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) { export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
const { playback } = props; const { playback } = props;
const timer = useTimer(); const timer = useTimer();
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0)); const progress = `${getPercentComplete(timer.current, timer.duration)}%`;
const complete = (timer?.duration ?? 1) / 1000;
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
const progress = `${elapsed}%`;
if ((timer?.current ?? 0) < 0) {
return <div className={`${style.progressBar} ${style.overtime}`} style={{ width: '100%' }} />;
}
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />; return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
} }
@@ -1,11 +1,11 @@
import { memo, useCallback, useState } from 'react'; import { memo, useCallback, useState } from 'react';
import { OntimeEvent } from 'ontime-types'; import { OntimeEvent } from 'ontime-types';
import { millisToString } from 'ontime-utils'; import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction'; import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../../common/utils/dateConfig'; import { millisToDelayString } from '../../../../common/utils/dateConfig';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager'; import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss'; import style from '../EventBlock.module.scss';
@@ -0,0 +1,27 @@
import { dayInMs } from 'ontime-utils';
import { getPercentComplete } from '../EventBlockProgressBar';
describe('getPercentComplete()', () => {
describe('calculates progress in normal cases', () => {
const testScenarios = [
{ current: 0, duration: 0, expect: 100 },
{ current: 0, duration: 100, expect: 100 },
{ current: 0, duration: dayInMs, expect: 100 },
{ current: 10, duration: 100, expect: 90 },
{ current: 50, duration: 100, expect: 50 },
{ current: 100, duration: 100, expect: 0 },
];
testScenarios.forEach((testCase) => {
it(`handles ${testCase.current} / ${testCase.duration}`, () => {
const progress = getPercentComplete(testCase.current, testCase.duration);
expect(progress).toBe(testCase.expect);
});
});
});
it('is 0 if we dont have a current or duration', () => {
const progress = getPercentComplete(null, null);
expect(progress).toBe(0);
});
});
@@ -3,7 +3,7 @@ import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
import { SupportedEvent } from 'ontime-types'; import { SupportedEvent } from 'ontime-types';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
import { useLocalEvent } from '../../../common/stores/localEvent'; import { useEditorSettings } from '../../../common/stores/editorSettings';
import { useEmitLog } from '../../../common/stores/logger'; import { useEmitLog } from '../../../common/stores/logger';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
@@ -25,7 +25,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
const doStartTime = useRef<HTMLInputElement | null>(null); const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null); const doPublic = useRef<HTMLInputElement | null>(null);
const eventSettings = useLocalEvent((state) => state.eventSettings); const eventSettings = useEditorSettings((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -1,4 +1,5 @@
import { DAY_TO_MS } from '../../../../common/utils/timeConstants'; import { dayInMs } from 'ontime-utils';
import { fetchTimerData, sanitiseTitle, TimerMessage } from '../countdown.helpers'; import { fetchTimerData, sanitiseTitle, TimerMessage } from '../countdown.helpers';
describe('sanitiseTitle() function', () => { describe('sanitiseTitle() function', () => {
@@ -73,11 +74,11 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000; const timeNow = 15000;
const followId = 'testId'; const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(TimerMessage.waiting); expect(message).toBe(TimerMessage.waiting);
expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue); expect(timer).toBe(dayInMs + endMockValue - startMockValue);
}); });
it('handle an current event that finishes after midnight', () => { it('handle an current event that finishes after midnight', () => {
@@ -86,11 +87,11 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000; const timeNow = 15000;
const followId = 'testId'; const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, followId); const { message, timer } = fetchTimerData(time, follow, followId);
expect(message).toBe(TimerMessage.running); expect(message).toBe(TimerMessage.running);
expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue); expect(timer).toBe(dayInMs + endMockValue - startMockValue);
}); });
it('handle an event that finishes after midnight but hasnt started', () => { it('handle an event that finishes after midnight but hasnt started', () => {
@@ -99,7 +100,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 2000; const timeNow = 2000;
const followId = 'testId'; const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(TimerMessage.toStart); expect(message).toBe(TimerMessage.toStart);
@@ -5,6 +5,7 @@ import useSettings from '../common/hooks-query/useSettings';
import { langDe } from './languages/de'; import { langDe } from './languages/de';
import { langEn } from './languages/en'; import { langEn } from './languages/en';
import { langEs } from './languages/es'; import { langEs } from './languages/es';
import { langFr } from './languages/fr';
import { langNo } from './languages/no'; import { langNo } from './languages/no';
import { langPt } from './languages/pt'; import { langPt } from './languages/pt';
import { langSv } from './languages/sv'; import { langSv } from './languages/sv';
@@ -12,6 +13,7 @@ import { langSv } from './languages/sv';
const translationsList = { const translationsList = {
en: langEn, en: langEn,
es: langEs, es: langEs,
fr: langFr,
de: langDe, de: langDe,
no: langNo, no: langNo,
pt: langPt, pt: langPt,
@@ -0,0 +1,19 @@
import { TranslationObject } from './en';
export const langFr: TranslationObject = {
'common.end_time': 'Termine à',
'common.expected_finish': 'Fin estimée à',
'common.now': 'Maintenant',
'common.next': 'A suivre',
'common.public_message': 'Message public',
'common.start_time': 'Heure de début',
'common.stage_timer': 'Minuteur de scène',
'common.started_at': 'Commencé à',
'common.time_now': 'Heure',
'countdown.ended': 'Évènement terminé à',
'countdown.running': 'Évènement en cours',
'countdown.select_event': 'Sélectionnez un évènement à suivre',
'countdown.to_start': 'Évènement commence dans',
'countdown.waiting': 'En attente du début de l’évènement',
'countdown.overtime': 'en dépassement',
};
+1 -1
View File
@@ -14,6 +14,6 @@ export const langSv: TranslationObject = {
'countdown.running': 'Evenemang pågår', 'countdown.running': 'Evenemang pågår',
'countdown.select_event': 'Välj ett evenemang att följa', 'countdown.select_event': 'Välj ett evenemang att följa',
'countdown.to_start': 'Tid till start', 'countdown.to_start': 'Tid till start',
'countdown.waiting': '"Väntar på att evenemanget ska starta', 'countdown.waiting': 'Väntar på att evenemanget ska starta',
'countdown.overtime': 'i övertid', 'countdown.overtime': 'i övertid',
}; };
+10 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "2.0.9", "version": "2.3.8",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
@@ -22,7 +22,7 @@
"postinstall": "", "postinstall": "",
"dev:electron": "NODE_ENV=development electron .", "dev:electron": "NODE_ENV=development electron .",
"dist-win": "electron-builder --publish=never --x64 --win", "dist-win": "electron-builder --publish=never --x64 --win",
"dist-mac": "electron-builder --publish=never --x64 --mac", "dist-mac": "electron-builder --publish=never --mac",
"dist-linux": "electron-builder --publish=never --x64 --linux", "dist-linux": "electron-builder --publish=never --x64 --linux",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" "cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
}, },
@@ -31,11 +31,17 @@
"appId": "no.lightdev.ontime", "appId": "no.lightdev.ontime",
"asar": true, "asar": true,
"dmg": { "dmg": {
"artifactName": "ontime-macOS.dmg", "artifactName": "ontime-macOS-${arch}.dmg",
"icon": "icon.icns" "icon": "icon.icns"
}, },
"mac": { "mac": {
"target": "dmg", "target": {
"target": "dmg",
"arch": [
"x64",
"arm64"
]
},
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"icon": "icon.icns" "icon": "icon.icns"
}, },
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "2.0.9", "version": "2.3.8",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
@@ -48,7 +48,7 @@
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts", "dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
"prebuild": "pnpm setdb", "prebuild": "pnpm setdb",
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs", "build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs", "build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs", "build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs", "build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
"lint": "eslint .", "lint": "eslint .",
@@ -354,46 +354,83 @@ export class EventLoader {
* @private * @private
*/ */
private _loadThisTitles(event, type) { private _loadThisTitles(event, type) {
if (!event) { if (type === 'now') {
return; if (event === null) {
// public
this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null;
this.titlesPublic.presenterNow = null;
this.titlesPublic.noteNow = null;
this.loaded.selectedPublicEventId = null;
// private
this.titles.titleNow = null;
this.titles.subtitleNow = null;
this.titles.presenterNow = null;
this.titles.noteNow = null;
this.loaded.selectedEventId = null;
} else {
// public
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
// private
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
}
} else if (type === 'now-public') {
if (event === null) {
this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null;
this.titlesPublic.presenterNow = null;
this.titlesPublic.noteNow = null;
this.loaded.selectedPublicEventId = null;
} else {
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
}
} else if (type === 'now-private') {
if (event === null) {
this.titles.titleNow = null;
this.titles.subtitleNow = null;
this.titles.presenterNow = null;
this.titles.noteNow = null;
this.loaded.selectedEventId = null;
} else {
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
}
} }
switch (type) { // next, load to both public and private
// now, load to both public and private else if (type === 'next') {
case 'now': if (event === null) {
// public // public
this.titlesPublic.titleNow = event.title; this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNow = event.subtitle; this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNow = event.presenter; this.titlesPublic.presenterNext = null;
this.titlesPublic.noteNow = event.note; this.titlesPublic.noteNext = null;
this.loaded.selectedPublicEventId = event.id; this.loaded.nextPublicEventId = null;
// private // private
this.titles.titleNow = event.title; this.titles.titleNext = null;
this.titles.subtitleNow = event.subtitle; this.titles.subtitleNext = null;
this.titles.presenterNow = event.presenter; this.titles.presenterNext = null;
this.titles.noteNow = event.note; this.titles.noteNext = null;
this.loaded.selectedEventId = event.id; this.loaded.nextEventId = null;
break; } else {
case 'now-public':
this.titlesPublic.titleNow = event.title;
this.titlesPublic.subtitleNow = event.subtitle;
this.titlesPublic.presenterNow = event.presenter;
this.titlesPublic.noteNow = event.note;
this.loaded.selectedPublicEventId = event.id;
break;
case 'now-private':
this.titles.titleNow = event.title;
this.titles.subtitleNow = event.subtitle;
this.titles.presenterNow = event.presenter;
this.titles.noteNow = event.note;
this.loaded.selectedEventId = event.id;
break;
// next, load to both public and private
case 'next':
// public // public
this.titlesPublic.titleNext = event.title; this.titlesPublic.titleNext = event.title;
this.titlesPublic.subtitleNext = event.subtitle; this.titlesPublic.subtitleNext = event.subtitle;
@@ -407,26 +444,37 @@ export class EventLoader {
this.titles.presenterNext = event.presenter; this.titles.presenterNext = event.presenter;
this.titles.noteNext = event.note; this.titles.noteNext = event.note;
this.loaded.nextEventId = event.id; this.loaded.nextEventId = event.id;
break; }
} else if (type === 'next-public') {
case 'next-public': if (event === null) {
this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.titlesPublic.noteNext = null;
this.loaded.nextPublicEventId = null;
} else {
this.titlesPublic.titleNext = event.title; this.titlesPublic.titleNext = event.title;
this.titlesPublic.subtitleNext = event.subtitle; this.titlesPublic.subtitleNext = event.subtitle;
this.titlesPublic.presenterNext = event.presenter; this.titlesPublic.presenterNext = event.presenter;
this.titlesPublic.noteNext = event.note; this.titlesPublic.noteNext = event.note;
this.loaded.nextPublicEventId = event.id; this.loaded.nextPublicEventId = event.id;
break; }
} else if (type === 'next-private') {
case 'next-private': if (event === null) {
this.titles.titleNext = null;
this.titles.subtitleNext = null;
this.titles.presenterNext = null;
this.titles.noteNext = null;
this.loaded.nextEventId = null;
} else {
this.titles.titleNext = event.title; this.titles.titleNext = event.title;
this.titles.subtitleNext = event.subtitle; this.titles.subtitleNext = event.subtitle;
this.titles.presenterNext = event.presenter; this.titles.presenterNext = event.presenter;
this.titles.noteNext = event.note; this.titles.noteNext = event.note;
this.loaded.nextEventId = event.id; this.loaded.nextEventId = event.id;
break; }
} else {
default: throw new Error(`Unhandled title type: ${type}`);
throw new Error(`Unhandled title type: ${type}`);
} }
} }
} }
@@ -1,21 +1,26 @@
import { RequestHandler } from 'express';
import { EventData } from 'ontime-types';
import { removeUndefined } from '../utils/parserUtils.js'; import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js'; import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js'; import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event' // Create controller for GET request to 'event'
export const getEventData = async (req, res) => { export const getEventData: RequestHandler = async (req, res) => {
res.json(DataProvider.getEventData()); res.json(DataProvider.getEventData());
}; };
// Create controller for POST request to 'event' // Create controller for POST request to 'event'
export const postEventData = async (req, res) => { export const postEventData: RequestHandler = async (req, res) => {
if (failEmptyObjects(req.body, res)) { if (failEmptyObjects(req.body, res)) {
return; return;
} }
try { try {
const newEvent = removeUndefined({ const newEvent: Partial<EventData> = removeUndefined({
title: req.body?.title, title: req.body?.title,
description: req.body?.description,
publicUrl: req.body?.publicUrl, publicUrl: req.body?.publicUrl,
publicInfo: req.body?.publicInfo, publicInfo: req.body?.publicInfo,
backstageUrl: req.body?.backstageUrl, backstageUrl: req.body?.backstageUrl,
@@ -2,6 +2,7 @@ import { body, validationResult } from 'express-validator';
export const eventDataSanitizer = [ export const eventDataSanitizer = [
body('title').optional().isString().trim(), body('title').optional().isString().trim(),
body('description').optional().isString().trim(),
body('publicUrl').optional().isString().trim(), body('publicUrl').optional().isString().trim(),
body('publicInfo').optional().isString().trim(), body('publicInfo').optional().isString().trim(),
body('backstageUrl').optional().isString().trim(), body('backstageUrl').optional().isString().trim(),
@@ -1,5 +1,6 @@
import { Alias, EventData, LogOrigin } from 'ontime-types'; import { Alias, EventData, LogOrigin } from 'ontime-types';
import { RequestHandler } from 'express';
import fs from 'fs'; import fs from 'fs';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
@@ -325,10 +326,11 @@ export const dbUpload = async (req, res) => {
}; };
// Create controller for POST request to '/ontime/new' // Create controller for POST request to '/ontime/new'
export const postNew = async (req, res) => { export const postNew: RequestHandler = async (req, res) => {
try { try {
const newEventData: Omit<EventData, 'endMessage'> = { const newEventData: EventData = {
title: req.body?.title ?? '', title: req.body?.title ?? '',
description: req.body?.description ?? '',
publicUrl: req.body?.publicUrl ?? '', publicUrl: req.body?.publicUrl ?? '',
publicInfo: req.body?.publicInfo ?? '', publicInfo: req.body?.publicInfo ?? '',
backstageUrl: req.body?.backstageUrl ?? '', backstageUrl: req.body?.backstageUrl ?? '',
+1
View File
@@ -4,6 +4,7 @@ export const dbModel: DatabaseModel = {
rundown: [], rundown: [],
eventData: { eventData: {
title: '', title: '',
description: '',
publicUrl: '', publicUrl: '',
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
+1 -1
View File
@@ -1,4 +1,4 @@
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types'; import { LogOrigin, OntimeEvent } from 'ontime-types';
import { validatePlayback } from 'ontime-utils'; import { validatePlayback } from 'ontime-utils';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js'; import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
+20 -12
View File
@@ -1,11 +1,11 @@
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types'; import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
import { calculateDuration, dayInMs } from 'ontime-utils';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js'; import { PlaybackService } from './PlaybackService.js';
import { updateRoll } from './rollUtils.js'; import { updateRoll } from './rollUtils.js';
import { DAY_TO_MS } from '../utils/time.js';
import { integrationService } from './integration-service/IntegrationService.js'; import { integrationService } from './integration-service/IntegrationService.js';
import { getCurrent, getElapsed, getExpectedFinish } from './timerUtils.js'; import { getCurrent, getExpectedFinish } from './timerUtils.js';
import { clock } from './Clock.js'; import { clock } from './Clock.js';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
@@ -90,7 +90,7 @@ export class TimerService {
// TODO: check if any relevant information warrants update // TODO: check if any relevant information warrants update
// update relevant information and force update // update relevant information and force update
this.timer.duration = timer.duration; this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
this.timer.timerType = timer.timerType; this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction; this.timer.endAction = timer.endAction;
@@ -104,7 +104,7 @@ export class TimerService {
this.timer.addedTime, this.timer.addedTime,
); );
if (this.timer.startedAt === null) { if (this.timer.startedAt === null) {
this.timer.current = timer.duration; this.timer.current = this.timer.duration;
} }
this.update(true); this.update(true);
} }
@@ -112,6 +112,7 @@ export class TimerService {
/** /**
* Loads given timer to object * Loads given timer to object
* @param {object} timer * @param {object} timer
* @param initialData
* @param {number} timer.id * @param {number} timer.id
* @param {number} timer.timeStart * @param {number} timer.timeStart
* @param {number} timer.timeEnd * @param {number} timer.timeEnd
@@ -128,8 +129,8 @@ export class TimerService {
this._clear(); this._clear();
this.loadedTimerId = timer.id; this.loadedTimerId = timer.id;
this.timer.duration = timer.duration; this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
this.timer.current = timer.duration; this.timer.current = this.timer.duration;
this.playback = Playback.Armed; this.playback = Playback.Armed;
this.timer.timerType = timer.timerType; this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction; this.timer.endAction = timer.endAction;
@@ -168,6 +169,8 @@ export class TimerService {
} }
this.timer.clock = clock.timeNow(); this.timer.clock = clock.timeNow();
this.timer.secondaryTimer = null;
this.secondaryTarget = null;
// add paused time if it exists // add paused time if it exists
if (this.pausedTime) { if (this.pausedTime) {
@@ -267,7 +270,7 @@ export class TimerService {
_finishAt: _finishAt:
this.timer.expectedFinish >= this.timer.startedAt this.timer.expectedFinish >= this.timer.startedAt
? this.timer.expectedFinish ? this.timer.expectedFinish
: this.timer.expectedFinish + DAY_TO_MS, : this.timer.expectedFinish + dayInMs,
clock: this.timer.clock, clock: this.timer.clock,
secondaryTimer: this.timer.secondaryTimer, secondaryTimer: this.timer.secondaryTimer,
@@ -277,7 +280,7 @@ export class TimerService {
this.timer.current = updatedTimer; this.timer.current = updatedTimer;
this.timer.secondaryTimer = updatedSecondaryTimer; this.timer.secondaryTimer = updatedSecondaryTimer;
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock); this.timer.elapsed = this.timer.duration - this.timer.current;
if (isFinished) { if (isFinished) {
this.timer.selectedEventId = null; this.timer.selectedEventId = null;
@@ -296,7 +299,8 @@ export class TimerService {
this.pausedTime = this.timer.clock - this.pausedAt; this.pausedTime = this.timer.clock - this.pausedAt;
} }
if (this.playback === Playback.Play && this.timer.current <= 0 && this.timer.finishedAt === null) { const finishedNow = this.timer.current <= 0 && this.timer.finishedAt === null;
if (this.playback === Playback.Play && finishedNow) {
this.timer.finishedAt = this.timer.clock; this.timer.finishedAt = this.timer.clock;
this._onFinish(); this._onFinish();
} else { } else {
@@ -315,7 +319,7 @@ export class TimerService {
this.pausedTime, this.pausedTime,
this.timer.clock, this.timer.clock,
); );
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock); this.timer.elapsed = this.timer.duration - this.timer.current;
} }
update(force = false) { update(force = false) {
@@ -382,16 +386,20 @@ export class TimerService {
this.timer.secondaryTimer = null; this.timer.secondaryTimer = null;
this.secondaryTarget = null; this.secondaryTarget = null;
// account for event that finishes the day after
const endTime =
currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd;
// when we load a timer in roll, we do the same things as before // when we load a timer in roll, we do the same things as before
// but also pre-populate some data as to the running state // but also pre-populate some data as to the running state
this.load(currentEvent, { this.load(currentEvent, {
startedAt: currentEvent.timeStart, startedAt: currentEvent.timeStart,
expectedFinish: currentEvent.timeEnd, expectedFinish: currentEvent.timeEnd,
current: currentEvent.timeEnd - this.timer.clock, current: endTime - this.timer.clock,
}); });
} else if (nextEvent) { } else if (nextEvent) {
// account for day after // account for day after
const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + DAY_TO_MS : nextEvent.timeStart; const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart;
// nothing now, but something coming up // nothing now, but something coming up
this.timer.secondaryTimer = nextStart - this.timer.clock; this.timer.secondaryTimer = nextStart - this.timer.clock;
this.secondaryTarget = nextStart; this.secondaryTarget = nextStart;
@@ -1,10 +1,7 @@
import { import { OntimeEvent } from 'ontime-types';
DAY_TO_MS, import { dayInMs } from 'ontime-utils';
getRollTimers,
normaliseEndTime, import { getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.js';
sortArrayByProperty,
updateRoll,
} from '../rollUtils.ts';
// test sortArrayByProperty() // test sortArrayByProperty()
describe('sort simple arrays of objects', () => { describe('sort simple arrays of objects', () => {
@@ -43,51 +40,51 @@ describe('sort simple arrays of objects', () => {
// test getRollTimers() // test getRollTimers()
describe('test that roll loads selection in right order', () => { describe('test that roll loads selection in right order', () => {
const eventlist = [ const eventlist: Partial<OntimeEvent>[] = [
{ {
id: 1, id: '1',
timeStart: 5, timeStart: 5,
timeEnd: 10, timeEnd: 10,
isPublic: false, isPublic: false,
}, },
{ {
id: 2, id: '2',
timeStart: 10, timeStart: 10,
timeEnd: 20, timeEnd: 20,
isPublic: false, isPublic: false,
}, },
{ {
id: 3, id: '3',
timeStart: 20, timeStart: 20,
timeEnd: 30, timeEnd: 30,
isPublic: false, isPublic: false,
}, },
{ {
id: 4, id: '4',
timeStart: 30, timeStart: 30,
timeEnd: 40, timeEnd: 40,
isPublic: false, isPublic: false,
}, },
{ {
id: 5, id: '5',
timeStart: 40, timeStart: 40,
timeEnd: 50, timeEnd: 50,
isPublic: true, isPublic: true,
}, },
{ {
id: 6, id: '6',
timeStart: 50, timeStart: 50,
timeEnd: 60, timeEnd: 60,
isPublic: false, isPublic: false,
}, },
{ {
id: 7, id: '7',
timeStart: 60, timeStart: 60,
timeEnd: 70, timeEnd: 70,
isPublic: true, isPublic: true,
}, },
{ {
id: 8, id: '8',
timeStart: 70, timeStart: 70,
timeEnd: 80, timeEnd: 80,
isPublic: false, isPublic: false,
@@ -109,7 +106,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -128,7 +125,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -147,7 +144,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -166,7 +163,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -185,7 +182,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[4], currentPublicEvent: eventlist[4],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -204,7 +201,7 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[6], currentPublicEvent: eventlist[6],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -223,11 +220,11 @@ describe('test that roll loads selection in right order', () => {
currentPublicEvent: eventlist[6], currentPublicEvent: eventlist[6],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
it('if timer is at 100', () => { it('if timer is at 100 we roll to day after', () => {
const now = 100; const now = 100;
const expected = { const expected = {
nowIndex: null, nowIndex: null,
@@ -235,21 +232,21 @@ describe('test that roll loads selection in right order', () => {
publicIndex: null, publicIndex: null,
nextIndex: 0, nextIndex: 0,
publicNextIndex: 4, publicNextIndex: 4,
timeToNext: DAY_TO_MS - now + eventlist[0].timeStart, timeToNext: dayInMs - now + eventlist[0].timeStart,
nextEvent: eventlist[0], nextEvent: eventlist[0],
nextPublicEvent: eventlist[4], nextPublicEvent: eventlist[4],
currentEvent: null, currentEvent: null,
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
it('handles rolls to next day with real values', () => { it('handles rolls to next day with real values', () => {
const singleEventList = [ const singleEventList: Partial<OntimeEvent>[] = [
{ {
id: 1, id: '1',
timeStart: 36000000, // 10:00 timeStart: 36000000, // 10:00
timeEnd: 39600000, // 11:00 timeEnd: 39600000, // 11:00
isPublic: true, isPublic: true,
@@ -262,34 +259,111 @@ describe('test that roll loads selection in right order', () => {
publicIndex: null, publicIndex: null,
nextIndex: 0, nextIndex: 0,
publicNextIndex: 0, publicNextIndex: 0,
timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart, timeToNext: dayInMs - now + singleEventList[0].timeStart,
nextEvent: singleEventList[0], nextEvent: singleEventList[0],
nextPublicEvent: singleEventList[0], nextPublicEvent: singleEventList[0],
currentEvent: null, currentEvent: null,
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(singleEventList, now); const state = getRollTimers(singleEventList as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
it('handles rolls to next day with real values', () => {
const singleEventList: Partial<OntimeEvent>[] = [
{
id: '1',
timeStart: 36000000, // 10:00
timeEnd: 3600000, // 01:00
isPublic: true,
},
];
const now = 60000; // 00:01
const expected = {
nowIndex: 0,
nowId: singleEventList[0].id,
publicIndex: 0,
nextIndex: null,
publicNextIndex: null,
timeToNext: null,
nextEvent: null,
nextPublicEvent: null,
currentEvent: singleEventList[0],
currentPublicEvent: singleEventList[0],
};
const state = getRollTimers(singleEventList as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
it('handles rolls to next day with real values', () => {
const singleEventList: Partial<OntimeEvent>[] = [
{
id: '1',
timeStart: 36000000, // 10:00
timeEnd: 3600000, // 01:00
isPublic: true,
},
];
const now = 60000; // 00:01
const expected = {
nowIndex: 0,
nowId: singleEventList[0].id,
publicIndex: 0,
nextIndex: null,
publicNextIndex: null,
timeToNext: null,
nextEvent: null,
nextPublicEvent: null,
currentEvent: singleEventList[0],
currentPublicEvent: singleEventList[0],
};
const state = getRollTimers(singleEventList as OntimeEvent[], now);
expect(state).toStrictEqual(expected);
});
it('handles roll that goes over midnight', () => {
const singleEventList: Partial<OntimeEvent>[] = [
{
id: '1',
timeStart: 72000000, // 20:00
timeEnd: 60000, // 00:10
isPublic: true,
},
];
const now = 6000; // 00:01
const expected = {
nowIndex: 0,
nowId: singleEventList[0].id,
publicIndex: 0,
nextIndex: null,
publicNextIndex: null,
timeToNext: null,
nextEvent: null,
nextPublicEvent: null,
currentEvent: singleEventList[0],
currentPublicEvent: singleEventList[0],
};
const state = getRollTimers(singleEventList as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
}); });
// test getRollTimers() // test getRollTimers()
describe('test that roll behaviour with overlapping times', () => { describe('test that roll behaviour with overlapping times', () => {
const eventlist = [ const eventlist: Partial<OntimeEvent>[] = [
{ {
id: 1, id: '1',
timeStart: 10, timeStart: 10,
timeEnd: 10, timeEnd: 10,
isPublic: false, isPublic: false,
}, },
{ {
id: 2, id: '2',
timeStart: 10, timeStart: 10,
timeEnd: 20, timeEnd: 20,
isPublic: true, isPublic: true,
}, },
{ {
id: 3, id: '3',
timeStart: 10, timeStart: 10,
timeEnd: 30, timeEnd: 30,
isPublic: false, isPublic: false,
@@ -311,7 +385,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -330,7 +404,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1], currentPublicEvent: eventlist[1],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -349,7 +423,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1], currentPublicEvent: eventlist[1],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -368,7 +442,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1], currentPublicEvent: eventlist[1],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
@@ -387,7 +461,7 @@ describe('test that roll behaviour with overlapping times', () => {
currentPublicEvent: eventlist[1], currentPublicEvent: eventlist[1],
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
}); });
@@ -396,9 +470,9 @@ describe('test that roll behaviour with overlapping times', () => {
describe('test that roll behaviour multi day event edge cases', () => { describe('test that roll behaviour multi day event edge cases', () => {
it('if the start time is the day after end time, and start time is earlier than now', () => { it('if the start time is the day after end time, and start time is earlier than now', () => {
const now = 66600000; // 19:30 const now = 66600000; // 19:30
const eventlist = [ const eventlist: Partial<OntimeEvent>[] = [
{ {
id: 1, id: '1',
timeStart: 66000000, // 19:20 timeStart: 66000000, // 19:20
timeEnd: 54600000, // 16:10 timeEnd: 54600000, // 16:10
isPublic: false, isPublic: false,
@@ -406,7 +480,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
]; ];
const expected = { const expected = {
nowIndex: 0, nowIndex: 0,
nowId: 1, nowId: '1',
publicIndex: null, publicIndex: null,
nextIndex: null, nextIndex: null,
publicNextIndex: null, publicNextIndex: null,
@@ -417,34 +491,39 @@ describe('test that roll behaviour multi day event edge cases', () => {
currentPublicEvent: null, currentPublicEvent: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
it('if the start time is the day after end time, and both are later than now', () => { it('if the start time is the day after end time, and both are later than now', () => {
const now = 66840000; // 19:34 const now = 66840000; // 19:34
const eventlist = [ const eventlist: Partial<OntimeEvent>[] = [
{ {
id: 1, id: '1',
timeStart: 67200000, // 19:40 timeStart: 67200000, // 19:40
timeEnd: 66900000, // 19:35 timeEnd: 66900000, // 19:35
isPublic: false, isPublic: false,
}, },
]; ];
const expected = { const expected = {
nowIndex: null, currentEvent: {
nowId: null, id: '1',
publicIndex: null, isPublic: false,
nextIndex: 0, timeEnd: 66900000,
publicNextIndex: null, timeStart: 67200000,
timeToNext: eventlist[0].timeStart - now, },
nextEvent: eventlist[0],
nextPublicEvent: null,
currentEvent: null,
currentPublicEvent: null, currentPublicEvent: null,
nextEvent: null,
nextIndex: null,
nextPublicEvent: null,
nowId: '1',
nowIndex: 0,
publicIndex: null,
publicNextIndex: null,
timeToNext: null,
}; };
const state = getRollTimers(eventlist, now); const state = getRollTimers(eventlist as OntimeEvent[], now);
expect(state).toStrictEqual(expected); expect(state).toStrictEqual(expected);
}); });
}); });
@@ -460,10 +539,10 @@ test('test typical scenarios', () => {
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected); expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
const t2 = { const t2 = {
start: 10 + DAY_TO_MS, start: 10 + dayInMs,
end: 20, end: 20,
}; };
const t2_expected = 20 + DAY_TO_MS; const t2_expected = 20 + dayInMs;
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected); expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
@@ -480,7 +559,7 @@ test('test typical scenarios', () => {
describe('typical scenarios', () => { describe('typical scenarios', () => {
it('it updates running events correctly', () => { it('it updates running events correctly', () => {
const timers = { const timers = {
selectedEventId: 1, selectedEventId: '1',
current: 10, current: 10,
_finishAt: 15, _finishAt: 15,
clock: 11, clock: 11,
@@ -527,7 +606,7 @@ describe('typical scenarios', () => {
it('flags an event end', () => { it('flags an event end', () => {
const timers = { const timers = {
selectedEventId: 1, selectedEventId: '1',
current: 10, current: 10,
_finishAt: 11, _finishAt: 11,
clock: 12, clock: 12,
@@ -584,4 +663,44 @@ describe('typical scenarios', () => {
expect(updateRoll(timers)).toStrictEqual(expected); expect(updateRoll(timers)).toStrictEqual(expected);
}); });
it('counts over midnight', () => {
const timers = {
selectedEventId: '1',
current: 25,
_finishAt: 10 + dayInMs,
clock: dayInMs - 10,
secondaryTimer: null,
secondaryTarget: null,
};
const expected = {
updatedTimer: 20,
updatedSecondaryTimer: null,
doRollLoad: false,
isFinished: false,
};
expect(updateRoll(timers)).toStrictEqual(expected);
});
it('rolls over midnight', () => {
const timers = {
selectedEventId: '1',
current: dayInMs,
_finishAt: 10 + dayInMs,
clock: 10,
secondaryTimer: null,
secondaryTarget: null,
};
const expected = {
updatedTimer: dayInMs,
updatedSecondaryTimer: null,
doRollLoad: false,
isFinished: false,
};
expect(updateRoll(timers)).toStrictEqual(expected);
});
}); });
@@ -1,4 +1,6 @@
import { getCurrent, getElapsed, getExpectedFinish } from '../timerUtils.js'; import { dayInMs } from 'ontime-utils';
import { getCurrent, getExpectedFinish } from '../timerUtils.js';
describe('getExpectedFinish()', () => { describe('getExpectedFinish()', () => {
it('is null if we havent started', () => { it('is null if we havent started', () => {
@@ -64,6 +66,15 @@ describe('getExpectedFinish()', () => {
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(1); expect(calculatedFinish).toBe(1);
}); });
it('finish can be the day after', () => {
const startedAt = 10;
const finishedAt = null;
const duration = dayInMs;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(10);
});
}); });
describe('getCurrent()', () => { describe('getCurrent()', () => {
@@ -94,23 +105,36 @@ describe('getCurrent()', () => {
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(19); expect(current).toBe(19);
}); });
}); it('counts over midnight', () => {
describe('getElapsedTime()', () => {
it('time since we started', () => {
const startedAt = 0;
const clock = 5;
const elapsed = getElapsed(startedAt, clock);
expect(elapsed).toBe(5);
});
it('clock cannot be lower than started time', () => {
const startedAt = 10; const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 0;
const addedTime = 0;
const clock = 10;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(dayInMs + 10);
});
it('rolls over midnight', () => {
const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 0;
const addedTime = 0;
const clock = 5; const clock = 5;
expect(() => getElapsed(startedAt, clock)).toThrow(); const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(15);
});
it('midnight holds delays', () => {
const startedAt = 10;
const duration = dayInMs + 10;
const pausedTime = 10;
const addedTime = 10;
const clock = 5;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(35);
}); });
}); });
describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', () => { describe('getExpectedFinish() and getCurrentTime() combined', () => {
it('without added times, they combine to be duration', () => { it('without added times, they combine to be duration', () => {
const startedAt = 0; const startedAt = 0;
const duration = 10; const duration = 10;
@@ -119,8 +143,8 @@ describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', (
const addedTime = 0; const addedTime = 0;
const clock = 0; const clock = 0;
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
const elapsed = getElapsed(startedAt, clock);
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
const elapsed = duration - current;
expect(expectedFinish).toBe(10); expect(expectedFinish).toBe(10);
expect(elapsed).toBe(0); expect(elapsed).toBe(0);
expect(current).toBe(10); expect(current).toBe(10);
@@ -134,10 +158,10 @@ describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', (
const addedTime = 2; const addedTime = 2;
const clock = 5; const clock = 5;
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
const elapsed = getElapsed(startedAt, clock);
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
const elapsed = duration - current;
expect(expectedFinish).toBe(13); expect(expectedFinish).toBe(13);
expect(elapsed).toBe(5); expect(elapsed).toBe(2);
expect(current).toBe(8); expect(current).toBe(8);
}); });
}); });
+12 -15
View File
@@ -1,14 +1,10 @@
import { OntimeEvent } from 'ontime-types'; import { OntimeEvent } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
/**
* Utility variable: 24 hour in milliseconds .
*/
export const DAY_TO_MS = 86400000;
/** /**
* handle events that span over midnight * handle events that span over midnight
*/ */
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + DAY_TO_MS : end); export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
/** /**
* @description Sorts an array of objects by given property * @description Sorts an array of objects by given property
@@ -23,13 +19,6 @@ export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
}); });
}; };
type Timer = {
_startedAt: number;
_finishAt: number;
duration: number;
current: number;
};
/** /**
* Finds loading information given a current rundown and time * Finds loading information given a current rundown and time
* @param {OntimeEvent[]} rundown - List of playable events * @param {OntimeEvent[]} rundown - List of playable events
@@ -61,7 +50,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
const firstEvent = orderedEvents[0]; const firstEvent = orderedEvents[0];
nextIndex = 0; nextIndex = 0;
nextEvent = firstEvent; nextEvent = firstEvent;
timeToNext = firstEvent.timeStart + DAY_TO_MS - timeNow; timeToNext = firstEvent.timeStart + dayInMs - timeNow;
if (firstEvent.isPublic) { if (firstEvent.isPublic) {
nextPublicEvent = firstEvent; nextPublicEvent = firstEvent;
@@ -89,6 +78,10 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
// When does the event end (handle midnight) // When does the event end (handle midnight)
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd); const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
const hasNotEnded = normalEnd > timeNow;
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
if (normalEnd <= timeNow) { if (normalEnd <= timeNow) {
// event ran already // event ran already
@@ -98,7 +91,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
currentPublicEvent = event; currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
} }
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) { } else if (hasNotEnded && hasStarted && !nowFound) {
// event is running // event is running
currentEvent = event; currentEvent = event;
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
@@ -185,6 +178,10 @@ export const updateRoll = (currentTimers: CurrentTimers) => {
// if we have something selected and a timer, we are running // if we have something selected and a timer, we are running
updatedTimer = _finishAt - clock; updatedTimer = _finishAt - clock;
if (updatedTimer > dayInMs) {
updatedTimer -= dayInMs;
}
if (updatedTimer < 0) { if (updatedTimer < 0) {
isPrimaryFinished = true; isPrimaryFinished = true;
// we need a new event // we need a new event
@@ -5,11 +5,12 @@ import {
OntimeDelay, OntimeDelay,
OntimeEvent, OntimeEvent,
OntimeRundown, OntimeRundown,
Playback,
SupportedEvent, SupportedEvent,
} from 'ontime-types'; } from 'ontime-types';
import { generateId } from 'ontime-utils'; import { generateId } from 'ontime-utils';
import { DataProvider } from '../../classes/data-provider/DataProvider.js'; import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { block as blockDef, delay, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js'; import { block as blockDef, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
import { MAX_EVENTS } from '../../settings.js'; import { MAX_EVENTS } from '../../settings.js';
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js'; import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
import { eventTimer } from '../TimerService.js'; import { eventTimer } from '../TimerService.js';
@@ -17,14 +18,14 @@ import { sendRefetch } from '../../adapters/websocketAux.js';
import { runtimeCacheStore } from '../../stores/cachingStore.js'; import { runtimeCacheStore } from '../../stores/cachingStore.js';
import { import {
cachedAdd, cachedAdd,
cachedClear,
cachedDelete, cachedDelete,
cachedEdit, cachedEdit,
cachedReorder, cachedReorder,
calculateRuntimeDelaysFrom,
delayedRundownCacheKey, delayedRundownCacheKey,
getDelayedRundown,
} from './delayedRundown.utils.js'; } from './delayedRundown.utils.js';
import { logger } from '../../classes/Logger.js'; import { logger } from '../../classes/Logger.js';
import { clock } from '../Clock.js';
/** /**
* Forces rundown to be recalculated * Forces rundown to be recalculated
@@ -97,8 +98,9 @@ const isNewNext = () => {
*/ */
export function updateTimer(affectedIds?: string[]) { export function updateTimer(affectedIds?: string[]) {
const runningEventId = eventLoader.loaded.selectedEventId; const runningEventId = eventLoader.loaded.selectedEventId;
const nextEventId = eventLoader.loaded.nextEventId;
if (runningEventId === null) { if (runningEventId === null && nextEventId === null) {
return false; return false;
} }
@@ -119,11 +121,22 @@ export function updateTimer(affectedIds?: string[]) {
if (eventInMemory) { if (eventInMemory) {
eventLoader.reset(); eventLoader.reset();
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
if (!loadedEvent) { if (eventTimer.playback === Playback.Roll) {
eventTimer.stop(); const rollTimers = eventLoader.findRoll(clock.timeNow());
if (rollTimers === null) {
eventTimer.stop();
} else {
const { currentEvent, nextEvent } = rollTimers;
eventTimer.roll(currentEvent, nextEvent);
}
} else { } else {
eventTimer.hotReload(loadedEvent); const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
if (loadedEvent) {
eventTimer.hotReload(loadedEvent);
} else {
eventTimer.stop();
}
} }
return true; return true;
} }
@@ -215,9 +228,6 @@ export async function deleteEvent(eventId) {
// notify event loader that rundown size has changed // notify event loader that rundown size has changed
updateChangeNumEvents(); updateChangeNumEvents();
// invalidate cache
runtimeCacheStore.invalidate(delayedRundownCacheKey);
// advice socket subscribers of change // advice socket subscribers of change
sendRefetch(); sendRefetch();
} }
@@ -227,7 +237,9 @@ export async function deleteEvent(eventId) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function deleteAllEvents() { export async function deleteAllEvents() {
await DataProvider.clearRundown(); await cachedClear();
// notify timer service of changed events
updateTimer(); updateTimer();
forceReset(); forceReset();
} }
@@ -118,7 +118,7 @@ export async function cachedDelete(eventId: string) {
if (delayedRundown.findIndex((event) => event.id === eventId) >= 0) { if (delayedRundown.findIndex((event) => event.id === eventId) >= 0) {
invalidateFromError(); invalidateFromError();
} }
return; throw new Error(`Event with id ${eventId} not found`);
} }
let updatedRundown = DataProvider.getRundown(); let updatedRundown = DataProvider.getRundown();
@@ -171,6 +171,12 @@ export async function cachedReorder(eventId: string, from: number, to: number) {
return reorderedEvent; return reorderedEvent;
} }
export async function cachedClear() {
await DataProvider.clearRundown();
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
console.log(DataProvider.getRundown(), getDelayedRundown());
}
/** /**
* Calculates all delays in a given rundown * Calculates all delays in a given rundown
* @param rundown * @param rundown
+13 -12
View File
@@ -1,4 +1,5 @@
type MaybeNumber = number | null; import { MaybeNumber } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
/** /**
* Calculates expected finish time of a running timer * Calculates expected finish time of a running timer
@@ -18,7 +19,14 @@ export function getExpectedFinish(
return finishedAt; return finishedAt;
} }
return Math.max(startedAt + duration + pausedTime + addedTime, startedAt); // handle events that finish the day after
const expectedFinish = startedAt + duration + pausedTime + addedTime;
if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs;
}
// an event cannot finish before it started (user added too much negative time)
return Math.max(expectedFinish, startedAt);
} }
/** /**
@@ -34,15 +42,8 @@ export function getCurrent(
if (startedAt === null) { if (startedAt === null) {
return null; return null;
} }
if (startedAt > clock) {
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
}
return startedAt + duration + addedTime + pausedTime - clock; return startedAt + duration + addedTime + pausedTime - clock;
} }
/**
* Calculates elapsed time
*/
export function getElapsed(startedAt: number, clock: number) {
if (startedAt > clock) {
throw new Error('clock cannot be higher than startedAt');
}
return clock - startedAt;
}
+2 -32
View File
@@ -1,9 +1,10 @@
import { vi } from 'vitest'; import { vi } from 'vitest';
import { dbModel } from '../../models/dataModel.ts'; import { dbModel } from '../../models/dataModel.ts';
import { parseExcel, parseJson, validateEvent } from '../parser.ts'; import { parseExcel, parseJson, validateEvent } from '../parser.ts';
import { makeString, validateDuration } from '../parserUtils.js'; import { makeString } from '../parserUtils.ts';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts'; import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts';
import { EndAction, TimerType } from 'ontime-types'; import { EndAction, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
describe('test json parser with valid def', () => { describe('test json parser with valid def', () => {
const testData = { const testData = {
@@ -898,34 +899,3 @@ describe('test views import', () => {
expect(parsed).toStrictEqual(expectedParsedViewSettings); expect(parsed).toStrictEqual(expectedParsedViewSettings);
}); });
}); });
describe('test validateDuration()', () => {
describe('handles valid inputs', () => {
const valid = [
{ test: 'zero values', timeStart: 0, timeEnd: 0 },
{ test: 'end after start', timeStart: 0, timeEnd: 1 },
];
valid.forEach((t) => {
it(t.test, () => {
const d = validateDuration(t.timeStart, t.timeEnd);
expect(d).toBe(t.timeEnd - t.timeStart);
});
});
});
describe('handles edge cases', () => {
// edge cases
const testData = [
{ test: 'negative 0', timeStart: -0, timeEnd: -0, expected: 0 },
{ test: 'end before start', timeStart: 2, timeEnd: 1, expected: 0 },
];
testData.forEach((t) => {
it(t.test, () => {
const d = validateDuration(t.timeStart, t.timeEnd);
expect(d).toBe(t.expected);
});
});
});
});
@@ -5,9 +5,6 @@ describe('isEmptyObject()', () => {
const isEmpty = isEmptyObject({}); const isEmpty = isEmptyObject({});
expect(isEmpty).toBe(true); expect(isEmpty).toBe(true);
}); });
test('throws on other types', () => {
expect(() => isEmptyObject(12)).toThrow();
});
test('resolves an object with methods', () => { test('resolves an object with methods', () => {
const isEmpty = isEmptyObject({ test: 'yes' }); const isEmpty = isEmptyObject({ test: 'yes' });
expect(isEmpty).toBe(false); expect(isEmpty).toBe(false);
+3 -3
View File
@@ -3,11 +3,11 @@
import fs from 'fs'; import fs from 'fs';
import xlsx from 'node-xlsx'; import xlsx from 'node-xlsx';
import { generateId } from 'ontime-utils'; import { generateId, calculateDuration } from 'ontime-utils';
import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types'; import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
import { event as eventDef } from '../models/eventsDefinition.js'; import { event as eventDef } from '../models/eventsDefinition.js';
import { dbModel } from '../models/dataModel.js'; import { dbModel } from '../models/dataModel.js';
import { deleteFile, makeString, validateDuration } from './parserUtils.js'; import { deleteFile, makeString } from './parserUtils.js';
import { import {
parseAliases, parseAliases,
parseEventData, parseEventData,
@@ -322,7 +322,7 @@ export const validateEvent = (eventArgs) => {
timeEnd: end, timeEnd: end,
endAction: makeString(e.endAction, d.endAction), endAction: makeString(e.endAction, d.endAction),
timerType: makeString(e.timerType, d.timerType), timerType: makeString(e.timerType, d.timerType),
duration: validateDuration(start, end), duration: calculateDuration(start, end),
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
skip: typeof e.skip === 'boolean' ? e.skip : d.skip, skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
note: makeString(e.note, d.note), note: makeString(e.note, d.note),
+1
View File
@@ -96,6 +96,7 @@ export const parseEventData = (data, enforce): EventData => {
newEventData = { newEventData = {
...dbModel.eventData, ...dbModel.eventData,
title: e.title || dbModel.eventData.title, title: e.title || dbModel.eventData.title,
description: e.description || dbModel.eventData.description,
publicUrl: e.publicUrl || dbModel.eventData.publicUrl, publicUrl: e.publicUrl || dbModel.eventData.publicUrl,
publicInfo: e.publicInfo || dbModel.eventData.publicInfo, publicInfo: e.publicInfo || dbModel.eventData.publicInfo,
backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl, backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl,
@@ -1,4 +1,5 @@
import fs from 'fs'; import fs from 'fs';
import { dayInMs } from 'ontime-utils';
/** /**
* @description Ensures variable is string, it skips object types * @description Ensures variable is string, it skips object types
@@ -6,23 +7,12 @@ import fs from 'fs';
* @param {string} [fallback=''] - fallback value * @param {string} [fallback=''] - fallback value
* @returns {string} - value as string or fallback if not possible * @returns {string} - value as string or fallback if not possible
*/ */
export const makeString = (val, fallback = '') => { export const makeString = (val: any, fallback = ''): string => {
if (typeof val === 'string') return val; if (typeof val === 'string') return val;
else if (val == null || val.constructor === Object) return fallback; else if (val == null || val.constructor === Object) return fallback;
return val.toString(); return val.toString();
}; };
/**
* @description validates a duration value against options
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const validateDuration = (timeStart, timeEnd) => {
// Durations must be positive
return Math.max(timeEnd - timeStart, 0);
};
/** /**
* @description Delete file from system * @description Delete file from system
* @param {string} file - reference to file * @param {string} file - reference to file
@@ -54,7 +44,7 @@ export const validateFile = (file) => {
* @description Verifies if object is empty * @description Verifies if object is empty
* @param {object} obj * @param {object} obj
*/ */
export const isEmptyObject = (obj) => { export const isEmptyObject = (obj: object) => {
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) { if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
return Object.keys(obj).length === 0; return Object.keys(obj).length === 0;
} }
@@ -78,7 +68,7 @@ export const mergeObject = (a, b) => {
* @description Removes undefined * @description Removes undefined
* @param {object} obj * @param {object} obj
*/ */
export const removeUndefined = (obj) => { export const removeUndefined = (obj: object) => {
const patched = {}; const patched = {};
Object.keys({ ...obj }) Object.keys({ ...obj })
.filter((key) => typeof obj[key] !== 'undefined') .filter((key) => typeof obj[key] !== 'undefined')
-1
View File
@@ -6,7 +6,6 @@ const mth = 1000 * 60 * 60; // millis to hours
export const timeFormat = 'HH:mm'; export const timeFormat = 'HH:mm';
export const timeFormatSeconds = 'HH:mm:ss'; export const timeFormatSeconds = 'HH:mm:ss';
export const DAY_TO_MS = 86400000;
/** /**
* @description Converts an excel date to milliseconds * @description Converts an excel date to milliseconds
+1
View File
@@ -229,6 +229,7 @@
], ],
"eventData": { "eventData": {
"title": "All about Carlos demo event", "title": "All about Carlos demo event",
"description": "Demo event for Ontime",
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
+1
View File
@@ -95,6 +95,7 @@
], ],
"eventData": { "eventData": {
"title": "All about Carlos demo event", "title": "All about Carlos demo event",
"description": "Demo event for Ontime",
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
+375 -13
View File
@@ -1,15 +1,15 @@
{ {
"rundown": [ "rundown": [
{ {
"title": "Welcome to Ontime", "title": "Albania",
"subtitle": "", "subtitle": "Sekret",
"presenter": "", "presenter": "Ronela Hajati",
"note": "Ontime is an app for managing event rundowns", "note": "SF1.01",
"endAction": "none", "endAction": "none",
"timerType": "count-down", "timerType": "count-down",
"timeStart": 0, "timeStart": 36000000,
"timeEnd": 600000, "timeEnd": 37200000,
"duration": 600000, "duration": 1200000,
"isPublic": true, "isPublic": true,
"skip": false, "skip": false,
"colour": "", "colour": "",
@@ -24,16 +24,378 @@
"user8": "", "user8": "",
"user9": "", "user9": "",
"type": "event", "type": "event",
"revision": 1, "revision": 0,
"id": "5a6e1" "id": "32d31"
},
{
"title": "Latvia",
"subtitle": "Eat Your Salad",
"presenter": "Citi Zeni",
"note": "SF1.02",
"endAction": "none",
"timerType": "count-down",
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "21cd2"
},
{
"title": "Lithuania",
"subtitle": "Sentimentai",
"presenter": "Monika Liu",
"note": "SF1.03",
"endAction": "none",
"timerType": "count-down",
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "0b371"
},
{
"title": "Switzerland",
"subtitle": "Boys Do Cry",
"presenter": "Marius Bear",
"note": "SF1.04",
"endAction": "none",
"timerType": "count-down",
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "3cd28"
},
{
"title": "Slovenia",
"subtitle": "Disko",
"presenter": "LPS",
"note": "SF1.05",
"endAction": "none",
"timerType": "count-down",
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "e457f"
},
{
"title": "Lunch break",
"type": "block",
"id": "01e85"
},
{
"title": "Ukraine",
"subtitle": "Stefania",
"presenter": "Kalush Orchestra",
"note": "SF1.06",
"endAction": "none",
"timerType": "count-down",
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "1c420"
},
{
"title": "Bulgaria",
"subtitle": "Intention",
"presenter": "Intelligent Music Project",
"note": "SF1.07",
"endAction": "none",
"timerType": "count-down",
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "b7737"
},
{
"title": "Netherlands",
"subtitle": "De Diepte",
"presenter": "S10",
"note": "SF1.08",
"endAction": "none",
"timerType": "count-down",
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "d3a80"
},
{
"title": "Moldova",
"subtitle": "Trenuletul",
"presenter": "Zdob si Zdub",
"note": "SF1.09",
"endAction": "none",
"timerType": "count-down",
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "8276c"
},
{
"title": "Portugal",
"subtitle": "Saudade Saudade",
"presenter": "Maro",
"note": "SF1.10",
"endAction": "none",
"timerType": "count-down",
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "2340b"
},
{
"title": "Afternoon break",
"type": "block",
"id": "cb90b"
},
{
"title": "Croatia",
"subtitle": "Guilty Pleasure",
"presenter": "Mia Dimsic",
"note": "SF1.11",
"endAction": "none",
"timerType": "count-down",
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "503c4"
},
{
"title": "Denmark",
"subtitle": "The Show",
"presenter": "Reddi",
"note": "SF1.12",
"endAction": "none",
"timerType": "count-down",
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "5e965"
},
{
"title": "Austria",
"subtitle": "Halo",
"presenter": "LUM!X & Pia Maria",
"note": "SF1.13",
"endAction": "none",
"timerType": "count-down",
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "bab4a"
},
{
"title": "Greece",
"subtitle": "Die Together",
"presenter": "Amanda Tenfjord",
"note": "SF1.14",
"endAction": "none",
"timerType": "count-down",
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "d3eb1"
} }
], ],
"eventData": { "eventData": {
"title": "Ontime demo event", "title": "Eurovision Song Contest",
"description": "Turin 2022",
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "Rehearsal Schedule - Turin 2022",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.github.com/cpvalente/ontime",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject" "backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
+1
View File
@@ -95,6 +95,7 @@
], ],
"eventData": { "eventData": {
"title": "All about Carlos demo event", "title": "All about Carlos demo event",
"description": "Demo event for Ontime",
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "2.0.9", "version": "2.3.8",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"lighdev", "lighdev",
@@ -1,5 +1,6 @@
export type EventData = { export type EventData = {
title: string; title: string;
description: string;
publicUrl: string; publicUrl: string;
publicInfo: string; publicInfo: string;
backstageUrl: string; backstageUrl: string;
+4
View File
@@ -23,6 +23,7 @@ import { TimerType } from './definitions/TimerType.type.js';
import { TitleBlock } from './definitions/runtime/TitleBlock.type.js'; import { TitleBlock } from './definitions/runtime/TitleBlock.type.js';
import { UserFields } from './definitions/core/UserFields.type.js'; import { UserFields } from './definitions/core/UserFields.type.js';
import { ViewSettings } from './definitions/core/Views.type.js'; import { ViewSettings } from './definitions/core/Views.type.js';
import { MaybeNumber } from './utils/utils.type.js';
// DATA MODEL // DATA MODEL
export type { DatabaseModel }; export type { DatabaseModel };
@@ -69,3 +70,6 @@ export type { TimerState };
export type { TitleBlock }; export type { TitleBlock };
// CLIENT // CLIENT
// UTILITIES
export type { MaybeNumber };
+1
View File
@@ -0,0 +1 @@
export type MaybeNumber = number | null;
+11 -2
View File
@@ -1,6 +1,15 @@
// runtime utils
export { validatePlayback } from './src/validate-action/validatePlayback.js';
// rundown utils
export { generateId } from './src/generate-id/generateId.js';
export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
// format utils
export { formatDisplay } from './src/date-utils/formatDisplay.js'; export { formatDisplay } from './src/date-utils/formatDisplay.js';
export { formatFromMillis } from './src/date-utils/formatFromMillis.js'; export { formatFromMillis } from './src/date-utils/formatFromMillis.js';
export { isTimeString } from './src/date-utils/isTimeString.js'; export { isTimeString } from './src/date-utils/isTimeString.js';
export { millisToString } from './src/date-utils/millisToString.js'; export { millisToString } from './src/date-utils/millisToString.js';
export { generateId } from './src/generate-id/generateId.js';
export { validatePlayback } from './src/validate-action/validatePlayback.js'; // time utils
export { dayInMs, mts } from './src/timeConstants.js';
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-utils", "name": "ontime-utils",
"type": "module", "type": "module",
"exports": "./index.ts", "exports": "./index.ts",
"version": "2.0.9", "version": "2.3.8",
"private": true, "private": true,
"description": "shared logic for ontime", "description": "shared logic for ontime",
"scripts": { "scripts": {
@@ -0,0 +1,32 @@
import { dayInMs } from '../timeConstants.js';
import { calculateDuration } from './rundownUtils.js';
describe('calculateDuration()', () => {
describe('Given start and end values', () => {
it('is the difference between end and start', () => {
const duration = calculateDuration(10, 20);
expect(duration).toBe(10);
});
});
describe('Handles edge cases', () => {
it('handles events that go over midnight', () => {
const duration = calculateDuration(51, 50);
expect(duration).not.toBe(-50);
expect(duration).toBe(dayInMs - 1);
});
it('when both are equal', () => {
const testStart = 1;
const testEnd = 1;
const val = calculateDuration(testStart, testEnd);
expect(val).toBe(testEnd - testStart);
});
it('handles no difference', () => {
const duration1 = calculateDuration(0, 0);
const duration2 = calculateDuration(dayInMs, dayInMs);
expect(duration1).toBe(0);
expect(duration2).toBe(0);
});
});
});
@@ -0,0 +1,15 @@
import { dayInMs } from '../timeConstants.js';
/**
* @description calculates event duration considering midnight
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
// Durations must be positive
if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart;
}
return timeEnd - timeStart;
};
+9 -1
View File
@@ -1 +1,9 @@
export const mts = 1000; // millis to seconds /**
* Milliseconds in a second
*/
export const mts = 1000;
/**
* Milliseconds in a day
*/
export const dayInMs = 86400000;