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