mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-31 13:08:00 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f298e6fd9 | |||
| f3610059ef | |||
| 023aac4ead | |||
| 8c9cb908cf | |||
| 839cee18f3 | |||
| b83ebf53a9 | |||
| 1b8392bba4 | |||
| af1241bec7 | |||
| c361ff4b3f | |||
| b579a4c57e |
@@ -35,7 +35,9 @@ jobs:
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: './apps/electron/dist/ontime-macOS.dmg'
|
||||
files: |
|
||||
'./apps/electron/dist/ontime-macOS-x64.dmg'
|
||||
'./apps/electron/dist/ontime-macOS-arm64.dmg'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "2.0.9",
|
||||
"version": "2.3.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.7.0",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import useAliases from './common/hooks-query/useAliases';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import withData from './features/viewers/ViewWrapper';
|
||||
import withAlias from './features/AliasWrapper';
|
||||
|
||||
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
||||
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 StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
|
||||
|
||||
const STimer = withData(TimerView);
|
||||
const SMinimalTimer = withData(MinimalTimerView);
|
||||
const SClock = withData(ClockView);
|
||||
const SCountdown = withData(Countdown);
|
||||
const SBackstage = withData(Backstage);
|
||||
const SPublic = withData(Public);
|
||||
const SLowerThird = withData(Lower);
|
||||
const SStudio = withData(StudioClock);
|
||||
const STimer = withAlias(withData(TimerView));
|
||||
const SMinimalTimer = withAlias(withData(MinimalTimerView));
|
||||
const SClock = withAlias(withData(ClockView));
|
||||
const SCountdown = withAlias(withData(Countdown));
|
||||
const SBackstage = withAlias(withData(Backstage));
|
||||
const SPublic = withAlias(withData(Public));
|
||||
const SLowerThird = withAlias(withData(Lower));
|
||||
const SStudio = withAlias(withData(StudioClock));
|
||||
|
||||
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
|
||||
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'));
|
||||
|
||||
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 (
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { nowInMillis } from '../utils/time';
|
||||
|
||||
export function logAxiosError(prepend: string, error: unknown) {
|
||||
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}`;
|
||||
|
||||
addLog({
|
||||
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
requestPutEvent,
|
||||
requestReorderEvent,
|
||||
} from '../api/eventsApi';
|
||||
import { useLocalEvent } from '../stores/localEvent';
|
||||
import { useEditorSettings } from '../stores/editorSettings';
|
||||
|
||||
/**
|
||||
* @description Set of utilities for events
|
||||
*/
|
||||
export const useEventAction = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const eventSettings = useLocalEvent((state) => state.eventSettings);
|
||||
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EventData } from 'ontime-types';
|
||||
|
||||
export const eventDataPlaceholder: EventData = {
|
||||
title: '',
|
||||
description: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
* @param {string} alias
|
||||
* @returns {{message: string, status: boolean}}
|
||||
*/
|
||||
export const validateAlias = (alias: string) => {
|
||||
|
||||
const valid = { status: true, message: 'ok' };
|
||||
|
||||
if (alias === '' || alias == null) {
|
||||
@@ -26,4 +29,49 @@ export const validateAlias = (alias: string) => {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* millis to hours
|
||||
* @type {number}
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
@@ -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 { Select, Switch } from '@chakra-ui/react';
|
||||
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 { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { millisToDelayString } from '../../../common/utils/dateConfig';
|
||||
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';
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: $label-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.labels {
|
||||
font-size: $inner-section-text-size;
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useInfoPanel } from '../../common/hooks/useSocket';
|
||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||
|
||||
import InfoHeader from './info-header/InfoHeader';
|
||||
import CollapsableInfo from './CollapsableInfo';
|
||||
@@ -8,6 +9,7 @@ import InfoTitles from './InfoTitles';
|
||||
|
||||
export default function Info() {
|
||||
const data = useInfoPanel();
|
||||
const showNif = useEditorSettings((state) => state.eventSettings.showNif);
|
||||
|
||||
const titlesNow = {
|
||||
title: data.titles.titleNow || '',
|
||||
@@ -32,9 +34,11 @@ export default function Info() {
|
||||
return (
|
||||
<>
|
||||
<InfoHeader selected={selected} />
|
||||
<CollapsableInfo title='Network Info'>
|
||||
<InfoNif />
|
||||
</CollapsableInfo>
|
||||
{showNif && (
|
||||
<CollapsableInfo title='Network Info'>
|
||||
<InfoNif />
|
||||
</CollapsableInfo>
|
||||
)}
|
||||
<CollapsableInfo title='Playing Now'>
|
||||
<InfoTitles data={titlesNow} />
|
||||
</CollapsableInfo>
|
||||
|
||||
@@ -6,9 +6,12 @@ export default function InfoHeader({ selected }: { selected: string }) {
|
||||
const { data } = useEventData();
|
||||
|
||||
return (
|
||||
<div className={style.panelHeader}>
|
||||
<span className={style.title}>{data?.title || ''}</span>
|
||||
<span className={style.selected}>{selected}</span>
|
||||
</div>
|
||||
<>
|
||||
<div className={style.panelHeader}>
|
||||
<span className={style.title}>{data?.title || ''}</span>
|
||||
<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>
|
||||
</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}>
|
||||
<label className={styles.sectionTitle}>
|
||||
Public Info
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function AliasesForm() {
|
||||
isDisabled={disableInputs}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
clickHandler={(event) => handleLinks(event, alias.pathAndParams)}
|
||||
clickHandler={(event) => handleLinks(event, alias.alias)}
|
||||
tooltip='Test alias'
|
||||
aria-label='Test alias'
|
||||
size='xs'
|
||||
|
||||
@@ -118,6 +118,7 @@ export default function AppSettingsModal() {
|
||||
>
|
||||
<Select backgroundColor='white' size='sm' width='auto' isDisabled={disableInputs} {...register('language')}>
|
||||
<option value='en'>English</option>
|
||||
<option value='fr'>French</option>
|
||||
<option value='de'>German</option>
|
||||
<option value='no'>Norwegian</option>
|
||||
<option value='pt'>Portuguese</option>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
|
||||
import { useLocalEvent } from '../../../common/stores/localEvent';
|
||||
import { useEditorSettings } from '../../../common/stores/editorSettings';
|
||||
import ModalSplitInput from '../ModalSplitInput';
|
||||
|
||||
import style from './SettingsModal.module.scss';
|
||||
|
||||
export default function EditorSettings() {
|
||||
const eventSettings = useLocalEvent((state) => state.eventSettings);
|
||||
const setShowQuickEntry = useLocalEvent((state) => state.setShowQuickEntry);
|
||||
const setStartTimeIsLastEnd = useLocalEvent((state) => state.setStartTimeIsLastEnd);
|
||||
const setDefaultPublic = useLocalEvent((state) => state.setDefaultPublic);
|
||||
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||
const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry);
|
||||
const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd);
|
||||
const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
|
||||
const setShowNif = useEditorSettings((state) => state.setShowNif);
|
||||
|
||||
return (
|
||||
<div className={style.sectionContainer}>
|
||||
@@ -39,6 +40,18 @@ export default function EditorSettings() {
|
||||
onChange={(event) => setDefaultPublic(event.target.checked)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,21 @@ export default function EventDataForm() {
|
||||
{...register('title')}
|
||||
/>
|
||||
</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' }} />
|
||||
<ModalInput field='publicInfo' title='Public Info' description='Information shown in public screens'>
|
||||
<Textarea
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
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 QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||
@@ -26,7 +26,7 @@ export default function Rundown(props: RundownProps) {
|
||||
|
||||
const featureData = useRundownEditor();
|
||||
const { addEvent, reorderEvent } = useEventAction();
|
||||
const eventSettings = useLocalEvent((state) => state.eventSettings);
|
||||
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
const showQuickEntry = eventSettings.showQuickEntry;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
import { calculateDuration } from 'ontime-utils';
|
||||
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
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 { cloneEvent } from '../../common/utils/eventsManager';
|
||||
import { calculateDuration } from '../../common/utils/timesManager';
|
||||
|
||||
import BlockBlock from './block-block/BlockBlock';
|
||||
import DelayBlock from './delay-block/DelayBlock';
|
||||
@@ -61,7 +61,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
}, [cursor, data.id, openId, setCursor, setEditId]);
|
||||
|
||||
const eventSettings = useLocalEvent((state) => state.eventSettings);
|
||||
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
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 { clamp } from '../../../../common/utils/math';
|
||||
@@ -9,18 +9,26 @@ interface EventBlockProgressBarProps {
|
||||
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) {
|
||||
const { playback } = props;
|
||||
const timer = useTimer();
|
||||
|
||||
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
|
||||
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%' }} />;
|
||||
}
|
||||
|
||||
const progress = `${getPercentComplete(timer.current, timer.duration)}%`;
|
||||
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
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 { useEventAction } from '../../../../common/hooks/useEventAction';
|
||||
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';
|
||||
|
||||
|
||||
+27
@@ -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 { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { useLocalEvent } from '../../../common/stores/localEvent';
|
||||
import { useEditorSettings } from '../../../common/stores/editorSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -25,7 +25,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
const doStartTime = 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 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';
|
||||
|
||||
describe('sanitiseTitle() function', () => {
|
||||
@@ -73,11 +74,11 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 15000;
|
||||
const followId = 'testId';
|
||||
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');
|
||||
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', () => {
|
||||
@@ -86,11 +87,11 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 15000;
|
||||
const followId = 'testId';
|
||||
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);
|
||||
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', () => {
|
||||
@@ -99,7 +100,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 2000;
|
||||
const followId = 'testId';
|
||||
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');
|
||||
expect(message).toBe(TimerMessage.toStart);
|
||||
|
||||
@@ -5,6 +5,7 @@ import useSettings from '../common/hooks-query/useSettings';
|
||||
import { langDe } from './languages/de';
|
||||
import { langEn } from './languages/en';
|
||||
import { langEs } from './languages/es';
|
||||
import { langFr } from './languages/fr';
|
||||
import { langNo } from './languages/no';
|
||||
import { langPt } from './languages/pt';
|
||||
import { langSv } from './languages/sv';
|
||||
@@ -12,6 +13,7 @@ import { langSv } from './languages/sv';
|
||||
const translationsList = {
|
||||
en: langEn,
|
||||
es: langEs,
|
||||
fr: langFr,
|
||||
de: langDe,
|
||||
no: langNo,
|
||||
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',
|
||||
};
|
||||
@@ -14,6 +14,6 @@ export const langSv: TranslationObject = {
|
||||
'countdown.running': 'Evenemang pågår',
|
||||
'countdown.select_event': 'Välj ett evenemang att följa',
|
||||
'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',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "2.0.9",
|
||||
"version": "2.3.8",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
@@ -22,7 +22,7 @@
|
||||
"postinstall": "",
|
||||
"dev:electron": "NODE_ENV=development electron .",
|
||||
"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",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
@@ -31,11 +31,17 @@
|
||||
"appId": "no.lightdev.ontime",
|
||||
"asar": true,
|
||||
"dmg": {
|
||||
"artifactName": "ontime-macOS.dmg",
|
||||
"artifactName": "ontime-macOS-${arch}.dmg",
|
||||
"icon": "icon.icns"
|
||||
},
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"target": {
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
"category": "public.app-category.productivity",
|
||||
"icon": "icon.icns"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "2.0.9",
|
||||
"version": "2.3.8",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
||||
"prebuild": "pnpm setdb",
|
||||
"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:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
|
||||
"lint": "eslint .",
|
||||
|
||||
@@ -354,46 +354,83 @@ export class EventLoader {
|
||||
* @private
|
||||
*/
|
||||
private _loadThisTitles(event, type) {
|
||||
if (!event) {
|
||||
return;
|
||||
if (type === 'now') {
|
||||
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) {
|
||||
// now, load to both public and private
|
||||
case 'now':
|
||||
// next, load to both public and private
|
||||
else if (type === 'next') {
|
||||
if (event === null) {
|
||||
// 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;
|
||||
this.titlesPublic.titleNext = null;
|
||||
this.titlesPublic.subtitleNext = null;
|
||||
this.titlesPublic.presenterNext = null;
|
||||
this.titlesPublic.noteNext = null;
|
||||
this.loaded.nextPublicEventId = null;
|
||||
|
||||
// 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;
|
||||
|
||||
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':
|
||||
this.titles.titleNext = null;
|
||||
this.titles.subtitleNext = null;
|
||||
this.titles.presenterNext = null;
|
||||
this.titles.noteNext = null;
|
||||
this.loaded.nextEventId = null;
|
||||
} else {
|
||||
// public
|
||||
this.titlesPublic.titleNext = event.title;
|
||||
this.titlesPublic.subtitleNext = event.subtitle;
|
||||
@@ -407,26 +444,37 @@ export class EventLoader {
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.loaded.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-public':
|
||||
}
|
||||
} else if (type === '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.subtitleNext = event.subtitle;
|
||||
this.titlesPublic.presenterNext = event.presenter;
|
||||
this.titlesPublic.noteNext = event.note;
|
||||
this.loaded.nextPublicEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-private':
|
||||
}
|
||||
} else if (type === '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.subtitleNext = event.subtitle;
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.loaded.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled title type: ${type}`);
|
||||
}
|
||||
} else {
|
||||
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 { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
|
||||
// Create controller for GET request to 'event'
|
||||
export const getEventData = async (req, res) => {
|
||||
export const getEventData: RequestHandler = async (req, res) => {
|
||||
res.json(DataProvider.getEventData());
|
||||
};
|
||||
|
||||
// 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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newEvent = removeUndefined({
|
||||
const newEvent: Partial<EventData> = removeUndefined({
|
||||
title: req.body?.title,
|
||||
description: req.body?.description,
|
||||
publicUrl: req.body?.publicUrl,
|
||||
publicInfo: req.body?.publicInfo,
|
||||
backstageUrl: req.body?.backstageUrl,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { body, validationResult } from 'express-validator';
|
||||
|
||||
export const eventDataSanitizer = [
|
||||
body('title').optional().isString().trim(),
|
||||
body('description').optional().isString().trim(),
|
||||
body('publicUrl').optional().isString().trim(),
|
||||
body('publicInfo').optional().isString().trim(),
|
||||
body('backstageUrl').optional().isString().trim(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Alias, EventData, LogOrigin } from 'ontime-types';
|
||||
|
||||
import { RequestHandler } from 'express';
|
||||
import fs from 'fs';
|
||||
import { networkInterfaces } from 'os';
|
||||
|
||||
@@ -325,10 +326,11 @@ export const dbUpload = async (req, res) => {
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/new'
|
||||
export const postNew = async (req, res) => {
|
||||
export const postNew: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const newEventData: Omit<EventData, 'endMessage'> = {
|
||||
const newEventData: EventData = {
|
||||
title: req.body?.title ?? '',
|
||||
description: req.body?.description ?? '',
|
||||
publicUrl: req.body?.publicUrl ?? '',
|
||||
publicInfo: req.body?.publicInfo ?? '',
|
||||
backstageUrl: req.body?.backstageUrl ?? '',
|
||||
|
||||
@@ -4,6 +4,7 @@ export const dbModel: DatabaseModel = {
|
||||
rundown: [],
|
||||
eventData: {
|
||||
title: '',
|
||||
description: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
backstageUrl: '',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
||||
import { LogOrigin, OntimeEvent } from 'ontime-types';
|
||||
import { validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
|
||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
import { updateRoll } from './rollUtils.js';
|
||||
import { DAY_TO_MS } from '../utils/time.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 { logger } from '../classes/Logger.js';
|
||||
|
||||
@@ -90,7 +90,7 @@ export class TimerService {
|
||||
// TODO: check if any relevant information warrants 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.endAction = timer.endAction;
|
||||
|
||||
@@ -104,7 +104,7 @@ export class TimerService {
|
||||
this.timer.addedTime,
|
||||
);
|
||||
if (this.timer.startedAt === null) {
|
||||
this.timer.current = timer.duration;
|
||||
this.timer.current = this.timer.duration;
|
||||
}
|
||||
this.update(true);
|
||||
}
|
||||
@@ -112,6 +112,7 @@ export class TimerService {
|
||||
/**
|
||||
* Loads given timer to object
|
||||
* @param {object} timer
|
||||
* @param initialData
|
||||
* @param {number} timer.id
|
||||
* @param {number} timer.timeStart
|
||||
* @param {number} timer.timeEnd
|
||||
@@ -128,8 +129,8 @@ export class TimerService {
|
||||
this._clear();
|
||||
|
||||
this.loadedTimerId = timer.id;
|
||||
this.timer.duration = timer.duration;
|
||||
this.timer.current = timer.duration;
|
||||
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
||||
this.timer.current = this.timer.duration;
|
||||
this.playback = Playback.Armed;
|
||||
this.timer.timerType = timer.timerType;
|
||||
this.timer.endAction = timer.endAction;
|
||||
@@ -168,6 +169,8 @@ export class TimerService {
|
||||
}
|
||||
|
||||
this.timer.clock = clock.timeNow();
|
||||
this.timer.secondaryTimer = null;
|
||||
this.secondaryTarget = null;
|
||||
|
||||
// add paused time if it exists
|
||||
if (this.pausedTime) {
|
||||
@@ -267,7 +270,7 @@ export class TimerService {
|
||||
_finishAt:
|
||||
this.timer.expectedFinish >= this.timer.startedAt
|
||||
? this.timer.expectedFinish
|
||||
: this.timer.expectedFinish + DAY_TO_MS,
|
||||
: this.timer.expectedFinish + dayInMs,
|
||||
|
||||
clock: this.timer.clock,
|
||||
secondaryTimer: this.timer.secondaryTimer,
|
||||
@@ -277,7 +280,7 @@ export class TimerService {
|
||||
|
||||
this.timer.current = updatedTimer;
|
||||
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) {
|
||||
this.timer.selectedEventId = null;
|
||||
@@ -296,7 +299,8 @@ export class TimerService {
|
||||
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._onFinish();
|
||||
} else {
|
||||
@@ -315,7 +319,7 @@ export class TimerService {
|
||||
this.pausedTime,
|
||||
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) {
|
||||
@@ -382,16 +386,20 @@ export class TimerService {
|
||||
this.timer.secondaryTimer = 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
|
||||
// but also pre-populate some data as to the running state
|
||||
this.load(currentEvent, {
|
||||
startedAt: currentEvent.timeStart,
|
||||
expectedFinish: currentEvent.timeEnd,
|
||||
current: currentEvent.timeEnd - this.timer.clock,
|
||||
current: endTime - this.timer.clock,
|
||||
});
|
||||
} else if (nextEvent) {
|
||||
// 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
|
||||
this.timer.secondaryTimer = nextStart - this.timer.clock;
|
||||
this.secondaryTarget = nextStart;
|
||||
|
||||
+178
-59
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
DAY_TO_MS,
|
||||
getRollTimers,
|
||||
normaliseEndTime,
|
||||
sortArrayByProperty,
|
||||
updateRoll,
|
||||
} from '../rollUtils.ts';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import { getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.js';
|
||||
|
||||
// test sortArrayByProperty()
|
||||
describe('sort simple arrays of objects', () => {
|
||||
@@ -43,51 +40,51 @@ describe('sort simple arrays of objects', () => {
|
||||
|
||||
// test getRollTimers()
|
||||
describe('test that roll loads selection in right order', () => {
|
||||
const eventlist = [
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: '1',
|
||||
timeStart: 5,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: '3',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
id: '4',
|
||||
timeStart: 30,
|
||||
timeEnd: 40,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
id: '5',
|
||||
timeStart: 40,
|
||||
timeEnd: 50,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
id: '6',
|
||||
timeStart: 50,
|
||||
timeEnd: 60,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
id: '7',
|
||||
timeStart: 60,
|
||||
timeEnd: 70,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: '8',
|
||||
timeStart: 70,
|
||||
timeEnd: 80,
|
||||
isPublic: false,
|
||||
@@ -109,7 +106,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -128,7 +125,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -147,7 +144,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -166,7 +163,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -185,7 +182,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: eventlist[4],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -204,7 +201,7 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -223,11 +220,11 @@ describe('test that roll loads selection in right order', () => {
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
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 expected = {
|
||||
nowIndex: null,
|
||||
@@ -235,21 +232,21 @@ describe('test that roll loads selection in right order', () => {
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: DAY_TO_MS - now + eventlist[0].timeStart,
|
||||
timeToNext: dayInMs - now + eventlist[0].timeStart,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList = [
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 39600000, // 11:00
|
||||
isPublic: true,
|
||||
@@ -262,34 +259,111 @@ describe('test that roll loads selection in right order', () => {
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 0,
|
||||
timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart,
|
||||
timeToNext: dayInMs - now + singleEventList[0].timeStart,
|
||||
nextEvent: singleEventList[0],
|
||||
nextPublicEvent: singleEventList[0],
|
||||
currentEvent: 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);
|
||||
});
|
||||
});
|
||||
|
||||
// test getRollTimers()
|
||||
describe('test that roll behaviour with overlapping times', () => {
|
||||
const eventlist = [
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: '1',
|
||||
timeStart: 10,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: '3',
|
||||
timeStart: 10,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
@@ -311,7 +385,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -330,7 +404,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -349,7 +423,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -368,7 +442,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -387,7 +461,7 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
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', () => {
|
||||
it('if the start time is the day after end time, and start time is earlier than now', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist = [
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: '1',
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
@@ -406,7 +480,7 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: 1,
|
||||
nowId: '1',
|
||||
publicIndex: null,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
@@ -417,34 +491,39 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist, now);
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist = [
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: '1',
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: null,
|
||||
timeToNext: eventlist[0].timeStart - now,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: null,
|
||||
currentEvent: {
|
||||
id: '1',
|
||||
isPublic: false,
|
||||
timeEnd: 66900000,
|
||||
timeStart: 67200000,
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -460,10 +539,10 @@ test('test typical scenarios', () => {
|
||||
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
|
||||
|
||||
const t2 = {
|
||||
start: 10 + DAY_TO_MS,
|
||||
start: 10 + dayInMs,
|
||||
end: 20,
|
||||
};
|
||||
const t2_expected = 20 + DAY_TO_MS;
|
||||
const t2_expected = 20 + dayInMs;
|
||||
|
||||
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
|
||||
|
||||
@@ -480,7 +559,7 @@ test('test typical scenarios', () => {
|
||||
describe('typical scenarios', () => {
|
||||
it('it updates running events correctly', () => {
|
||||
const timers = {
|
||||
selectedEventId: 1,
|
||||
selectedEventId: '1',
|
||||
current: 10,
|
||||
_finishAt: 15,
|
||||
clock: 11,
|
||||
@@ -527,7 +606,7 @@ describe('typical scenarios', () => {
|
||||
|
||||
it('flags an event end', () => {
|
||||
const timers = {
|
||||
selectedEventId: 1,
|
||||
selectedEventId: '1',
|
||||
current: 10,
|
||||
_finishAt: 11,
|
||||
clock: 12,
|
||||
@@ -584,4 +663,44 @@ describe('typical scenarios', () => {
|
||||
|
||||
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()', () => {
|
||||
it('is null if we havent started', () => {
|
||||
@@ -64,6 +66,15 @@ describe('getExpectedFinish()', () => {
|
||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
||||
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()', () => {
|
||||
@@ -94,23 +105,36 @@ describe('getCurrent()', () => {
|
||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
||||
expect(current).toBe(19);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('counts over midnight', () => {
|
||||
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;
|
||||
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', () => {
|
||||
const startedAt = 0;
|
||||
const duration = 10;
|
||||
@@ -119,8 +143,8 @@ describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', (
|
||||
const addedTime = 0;
|
||||
const clock = 0;
|
||||
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
||||
const elapsed = getElapsed(startedAt, clock);
|
||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
||||
const elapsed = duration - current;
|
||||
expect(expectedFinish).toBe(10);
|
||||
expect(elapsed).toBe(0);
|
||||
expect(current).toBe(10);
|
||||
@@ -134,10 +158,10 @@ describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', (
|
||||
const addedTime = 2;
|
||||
const clock = 5;
|
||||
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
||||
const elapsed = getElapsed(startedAt, clock);
|
||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
||||
const elapsed = duration - current;
|
||||
expect(expectedFinish).toBe(13);
|
||||
expect(elapsed).toBe(5);
|
||||
expect(elapsed).toBe(2);
|
||||
expect(current).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Utility variable: 24 hour in milliseconds .
|
||||
*/
|
||||
export const DAY_TO_MS = 86400000;
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -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
|
||||
* @param {OntimeEvent[]} rundown - List of playable events
|
||||
@@ -61,7 +50,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
|
||||
const firstEvent = orderedEvents[0];
|
||||
nextIndex = 0;
|
||||
nextEvent = firstEvent;
|
||||
timeToNext = firstEvent.timeStart + DAY_TO_MS - timeNow;
|
||||
timeToNext = firstEvent.timeStart + dayInMs - timeNow;
|
||||
|
||||
if (firstEvent.isPublic) {
|
||||
nextPublicEvent = firstEvent;
|
||||
@@ -89,6 +78,10 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
|
||||
// When does the event end (handle midnight)
|
||||
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) {
|
||||
// event ran already
|
||||
|
||||
@@ -98,7 +91,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
|
||||
currentPublicEvent = event;
|
||||
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) {
|
||||
} else if (hasNotEnded && hasStarted && !nowFound) {
|
||||
// event is running
|
||||
currentEvent = event;
|
||||
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
|
||||
|
||||
updatedTimer = _finishAt - clock;
|
||||
if (updatedTimer > dayInMs) {
|
||||
updatedTimer -= dayInMs;
|
||||
}
|
||||
|
||||
if (updatedTimer < 0) {
|
||||
isPrimaryFinished = true;
|
||||
// we need a new event
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
Playback,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
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 { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from '../TimerService.js';
|
||||
@@ -17,14 +18,14 @@ import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import {
|
||||
cachedAdd,
|
||||
cachedClear,
|
||||
cachedDelete,
|
||||
cachedEdit,
|
||||
cachedReorder,
|
||||
calculateRuntimeDelaysFrom,
|
||||
delayedRundownCacheKey,
|
||||
getDelayedRundown,
|
||||
} from './delayedRundown.utils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { clock } from '../Clock.js';
|
||||
|
||||
/**
|
||||
* Forces rundown to be recalculated
|
||||
@@ -97,8 +98,9 @@ const isNewNext = () => {
|
||||
*/
|
||||
export function updateTimer(affectedIds?: string[]) {
|
||||
const runningEventId = eventLoader.loaded.selectedEventId;
|
||||
const nextEventId = eventLoader.loaded.nextEventId;
|
||||
|
||||
if (runningEventId === null) {
|
||||
if (runningEventId === null && nextEventId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,11 +121,22 @@ export function updateTimer(affectedIds?: string[]) {
|
||||
|
||||
if (eventInMemory) {
|
||||
eventLoader.reset();
|
||||
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
|
||||
if (!loadedEvent) {
|
||||
eventTimer.stop();
|
||||
|
||||
if (eventTimer.playback === Playback.Roll) {
|
||||
const rollTimers = eventLoader.findRoll(clock.timeNow());
|
||||
if (rollTimers === null) {
|
||||
eventTimer.stop();
|
||||
} else {
|
||||
const { currentEvent, nextEvent } = rollTimers;
|
||||
eventTimer.roll(currentEvent, nextEvent);
|
||||
}
|
||||
} else {
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
const { loadedEvent } = eventLoader.loadById(runningEventId) || {};
|
||||
if (loadedEvent) {
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
} else {
|
||||
eventTimer.stop();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -215,9 +228,6 @@ export async function deleteEvent(eventId) {
|
||||
// notify event loader that rundown size has changed
|
||||
updateChangeNumEvents();
|
||||
|
||||
// invalidate cache
|
||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
}
|
||||
@@ -227,7 +237,9 @@ export async function deleteEvent(eventId) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteAllEvents() {
|
||||
await DataProvider.clearRundown();
|
||||
await cachedClear();
|
||||
|
||||
// notify timer service of changed events
|
||||
updateTimer();
|
||||
forceReset();
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export async function cachedDelete(eventId: string) {
|
||||
if (delayedRundown.findIndex((event) => event.id === eventId) >= 0) {
|
||||
invalidateFromError();
|
||||
}
|
||||
return;
|
||||
throw new Error(`Event with id ${eventId} not found`);
|
||||
}
|
||||
|
||||
let updatedRundown = DataProvider.getRundown();
|
||||
@@ -171,6 +171,12 @@ export async function cachedReorder(eventId: string, from: number, to: number) {
|
||||
return reorderedEvent;
|
||||
}
|
||||
|
||||
export async function cachedClear() {
|
||||
await DataProvider.clearRundown();
|
||||
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
|
||||
console.log(DataProvider.getRundown(), getDelayedRundown());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all delays in a given rundown
|
||||
* @param rundown
|
||||
|
||||
@@ -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
|
||||
@@ -18,7 +19,14 @@ export function getExpectedFinish(
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
if (startedAt > clock) {
|
||||
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { vi } from 'vitest';
|
||||
import { dbModel } from '../../models/dataModel.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 { EndAction, TimerType } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
describe('test json parser with valid def', () => {
|
||||
const testData = {
|
||||
@@ -898,34 +899,3 @@ describe('test views import', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-3
@@ -5,9 +5,6 @@ describe('isEmptyObject()', () => {
|
||||
const isEmpty = isEmptyObject({});
|
||||
expect(isEmpty).toBe(true);
|
||||
});
|
||||
test('throws on other types', () => {
|
||||
expect(() => isEmptyObject(12)).toThrow();
|
||||
});
|
||||
test('resolves an object with methods', () => {
|
||||
const isEmpty = isEmptyObject({ test: 'yes' });
|
||||
expect(isEmpty).toBe(false);
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import fs from 'fs';
|
||||
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 { event as eventDef } from '../models/eventsDefinition.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { deleteFile, makeString, validateDuration } from './parserUtils.js';
|
||||
import { deleteFile, makeString } from './parserUtils.js';
|
||||
import {
|
||||
parseAliases,
|
||||
parseEventData,
|
||||
@@ -322,7 +322,7 @@ export const validateEvent = (eventArgs) => {
|
||||
timeEnd: end,
|
||||
endAction: makeString(e.endAction, d.endAction),
|
||||
timerType: makeString(e.timerType, d.timerType),
|
||||
duration: validateDuration(start, end),
|
||||
duration: calculateDuration(start, end),
|
||||
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
||||
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
||||
note: makeString(e.note, d.note),
|
||||
|
||||
@@ -96,6 +96,7 @@ export const parseEventData = (data, enforce): EventData => {
|
||||
newEventData = {
|
||||
...dbModel.eventData,
|
||||
title: e.title || dbModel.eventData.title,
|
||||
description: e.description || dbModel.eventData.description,
|
||||
publicUrl: e.publicUrl || dbModel.eventData.publicUrl,
|
||||
publicInfo: e.publicInfo || dbModel.eventData.publicInfo,
|
||||
backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* @description Ensures variable is string, it skips object types
|
||||
@@ -6,23 +7,12 @@ import fs from 'fs';
|
||||
* @param {string} [fallback=''] - fallback value
|
||||
* @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;
|
||||
else if (val == null || val.constructor === Object) return fallback;
|
||||
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
|
||||
* @param {string} file - reference to file
|
||||
@@ -54,7 +44,7 @@ export const validateFile = (file) => {
|
||||
* @description Verifies if object is empty
|
||||
* @param {object} obj
|
||||
*/
|
||||
export const isEmptyObject = (obj) => {
|
||||
export const isEmptyObject = (obj: object) => {
|
||||
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
|
||||
return Object.keys(obj).length === 0;
|
||||
}
|
||||
@@ -78,7 +68,7 @@ export const mergeObject = (a, b) => {
|
||||
* @description Removes undefined
|
||||
* @param {object} obj
|
||||
*/
|
||||
export const removeUndefined = (obj) => {
|
||||
export const removeUndefined = (obj: object) => {
|
||||
const patched = {};
|
||||
Object.keys({ ...obj })
|
||||
.filter((key) => typeof obj[key] !== 'undefined')
|
||||
@@ -6,7 +6,6 @@ const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
export const DAY_TO_MS = 86400000;
|
||||
|
||||
/**
|
||||
* @description Converts an excel date to milliseconds
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
],
|
||||
"eventData": {
|
||||
"title": "All about Carlos demo event",
|
||||
"description": "Demo event for Ontime",
|
||||
"publicUrl": "www.getontime.no",
|
||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
||||
"backstageUrl": "www.getontime.no",
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
],
|
||||
"eventData": {
|
||||
"title": "All about Carlos demo event",
|
||||
"description": "Demo event for Ontime",
|
||||
"publicUrl": "www.getontime.no",
|
||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
||||
"backstageUrl": "www.getontime.no",
|
||||
|
||||
+375
-13
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"rundown": [
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"subtitle": "",
|
||||
"presenter": "",
|
||||
"note": "Ontime is an app for managing event rundowns",
|
||||
"title": "Albania",
|
||||
"subtitle": "Sekret",
|
||||
"presenter": "Ronela Hajati",
|
||||
"note": "SF1.01",
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"timeStart": 0,
|
||||
"timeEnd": 600000,
|
||||
"duration": 600000,
|
||||
"timeStart": 36000000,
|
||||
"timeEnd": 37200000,
|
||||
"duration": 1200000,
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"colour": "",
|
||||
@@ -24,16 +24,378 @@
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"type": "event",
|
||||
"revision": 1,
|
||||
"id": "5a6e1"
|
||||
"revision": 0,
|
||||
"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": {
|
||||
"title": "Ontime demo event",
|
||||
"title": "Eurovision Song Contest",
|
||||
"description": "Turin 2022",
|
||||
"publicUrl": "www.getontime.no",
|
||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
||||
"backstageUrl": "www.getontime.no",
|
||||
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
|
||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
||||
},
|
||||
"settings": {
|
||||
"app": "ontime",
|
||||
|
||||
Vendored
+1
@@ -95,6 +95,7 @@
|
||||
],
|
||||
"eventData": {
|
||||
"title": "All about Carlos demo event",
|
||||
"description": "Demo event for Ontime",
|
||||
"publicUrl": "www.getontime.no",
|
||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
||||
"backstageUrl": "www.getontime.no",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "2.0.9",
|
||||
"version": "2.3.8",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type EventData = {
|
||||
title: string;
|
||||
description: string;
|
||||
publicUrl: string;
|
||||
publicInfo: string;
|
||||
backstageUrl: string;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TimerType } from './definitions/TimerType.type.js';
|
||||
import { TitleBlock } from './definitions/runtime/TitleBlock.type.js';
|
||||
import { UserFields } from './definitions/core/UserFields.type.js';
|
||||
import { ViewSettings } from './definitions/core/Views.type.js';
|
||||
import { MaybeNumber } from './utils/utils.type.js';
|
||||
|
||||
// DATA MODEL
|
||||
export type { DatabaseModel };
|
||||
@@ -69,3 +70,6 @@ export type { TimerState };
|
||||
export type { TitleBlock };
|
||||
|
||||
// CLIENT
|
||||
|
||||
// UTILITIES
|
||||
export type { MaybeNumber };
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type MaybeNumber = number | null;
|
||||
+11
-2
@@ -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 { formatFromMillis } from './src/date-utils/formatFromMillis.js';
|
||||
export { isTimeString } from './src/date-utils/isTimeString.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';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-utils",
|
||||
"type": "module",
|
||||
"exports": "./index.ts",
|
||||
"version": "2.0.9",
|
||||
"version": "2.3.8",
|
||||
"private": true,
|
||||
"description": "shared logic for ontime",
|
||||
"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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user