mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 00:29:41 +00:00
Compare commits
7 Commits
v3.6.0
..
play-sound
| Author | SHA1 | Date | |
|---|---|---|---|
| f554605007 | |||
| 86e6f8b58c | |||
| 1de062a38e | |||
| eb7a30fcf3 | |||
| e2cbe43a2b | |||
| 7877af571f | |||
| 086bd5b644 |
@@ -14,8 +14,10 @@
|
|||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
||||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||||
|
|
||||||
## Using Ontime?
|
## Need help?
|
||||||
|
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
|
||||||
|
|
||||||
|
## Using Ontime?
|
||||||
Let us know!
|
Let us know!
|
||||||
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
||||||
|
|
||||||
@@ -35,6 +37,16 @@ Ontime is made by entertainment and broadcast engineers and used by
|
|||||||
- Theatres and opera houses
|
- Theatres and opera houses
|
||||||
- Houses of worship
|
- Houses of worship
|
||||||
|
|
||||||
|
## Main features
|
||||||
|
- [x] **Multiplatform**: Available for Windows / MacOS, Linux. You can also self host with the docker image
|
||||||
|
- [x] **In any device**: Ontime is available in the local network to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
|
||||||
|
- [x] **Made for teams**: Ontime caters to different roles in your production team: directors, operators, backstage and front of house signage...
|
||||||
|
- [x] **Delay workflows**: Manage and communicate runtime delays in real-time to your team
|
||||||
|
- [x] **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
|
||||||
|
- [x] **Focus on integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
|
||||||
|
|
||||||
|
... and a lot more ...
|
||||||
|
|
||||||
### For live environments
|
### For live environments
|
||||||
|
|
||||||
Ontime is designed for use in live environments. \
|
Ontime is designed for use in live environments. \
|
||||||
@@ -83,6 +95,7 @@ IP.ADDRESS:4001/clock > Simple clock view
|
|||||||
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
|
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
|
||||||
IP.ADDRESS:4001/countdown > Countdown to anything
|
IP.ADDRESS:4001/countdown > Countdown to anything
|
||||||
IP.ADDRESS:4001/studio > Studio Clock
|
IP.ADDRESS:4001/studio > Studio Clock
|
||||||
|
IP.ADDRESS:4001/timeline > Timeline
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -100,26 +113,13 @@ IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
|||||||
IP.ADDRESS:4001/operator > automated views for operators
|
IP.ADDRESS:4001/operator > automated views for operators
|
||||||
```
|
```
|
||||||
|
|
||||||
More documentation is available [in our docs](https://docs.getontime.no)
|
More information is available [in our docs](https://docs.getontime.no)
|
||||||
|
|
||||||
## Main features
|
|
||||||
|
|
||||||
- [x] Distribute data over network and render it in the browser
|
|
||||||
- [x] Collaborative
|
|
||||||
- [x] Extendable
|
|
||||||
- [x] Send messages to different screen types
|
|
||||||
- [x] Differentiate between backstage and public data
|
|
||||||
- [x] Workflow for managing delays
|
|
||||||
- [x] Rich protocol integrations for Control and Feedback
|
|
||||||
- [x] For servers: use docker to run Ontime in in a server, configure from a browser anywhere
|
|
||||||
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
|
||||||
- [x] Companion integration [follow link](https://bitfocus.io/connections/getontime-ontime)
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Continued development
|
### Continued development
|
||||||
|
|
||||||
Ontime is under active development. We continue adding and tweaking features in collaboration with users.
|
Ontime is under active development. We continue adding and improving features in collaboration with users.
|
||||||
|
|
||||||
Have an idea? Reach out via [email](mail@getontime.no)
|
Have an idea? Reach out via [email](mail@getontime.no)
|
||||||
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
@@ -161,8 +161,9 @@ If you are a developer and would like to contribute with code, please open an is
|
|||||||
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
- [Ontime website](https://getontime.no)
|
||||||
See the [Ontime website](https://getontime.no) here and the link to the [documentation](https://docs.getontime.no)
|
- [Documentation](https://docs.getontime.no)
|
||||||
|
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Binary file not shown.
@@ -176,6 +176,14 @@ export const useTimer = () => {
|
|||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useTimerPhase = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
phase: state.timer.phase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
export const useClock = () => {
|
export const useClock = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Panel.Title>
|
</Panel.Title>
|
||||||
{error && <Panel.Error>{error}</Panel.Error>}
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
<div className={style.innerColumn}>
|
<Panel.Section className={style.innerColumn}>
|
||||||
<label>
|
<label>
|
||||||
Project title
|
Project title
|
||||||
<Input
|
<Input
|
||||||
@@ -145,7 +145,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { PROJECT_DATA } from '../../../../common/api/constants';
|
import { PROJECT_DATA } from '../../../../common/api/constants';
|
||||||
import { getDb, patchData } from '../../../../common/api/db';
|
import { getDb, patchData } from '../../../../common/api/db';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import { makeProjectPatch } from './project.utils';
|
import { makeProjectPatch } from './project.utils';
|
||||||
@@ -92,7 +93,7 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Panel.Title>
|
</Panel.Title>
|
||||||
{error && <Panel.Error>{error}</Panel.Error>}
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
<div className={style.innerColumn}>
|
<Panel.Section className={cx([style.innerColumn, style.inlineLabels])}>
|
||||||
<Panel.Description>
|
<Panel.Description>
|
||||||
Select partial data from {`"${fileName}"`} to merge into the current project.
|
Select partial data from {`"${fileName}"`} to merge into the current project.
|
||||||
<br /> This process is irreversible.
|
<br /> This process is irreversible.
|
||||||
@@ -121,7 +122,7 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
<Switch variant='ontime' {...register('http')} />
|
<Switch variant='ontime' {...register('http')} />
|
||||||
HTTP Integration
|
HTTP Integration
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineLabels {
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1em;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ $table-header-font-size: calc(1rem - 3px);
|
|||||||
|
|
||||||
font-size: $table-header-font-size;
|
font-size: $table-header-font-size;
|
||||||
color: $gray-700;
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: $gray-1300;
|
background-color: $gray-1300;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.resizer {
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,30 +132,19 @@ $table-header-font-size: calc(1rem - 3px);
|
|||||||
|
|
||||||
.resizer {
|
.resizer {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
opacity: 0;
|
opacity: $opacity-disabled;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 3px;
|
width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
transform: translateX(50%);
|
|
||||||
background-color: $action-blue;
|
background-color: $action-blue;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|
||||||
transition-duration: $transition-time-action;
|
|
||||||
transition-property: width, background-color;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: $opacity-disabled;
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.isResizing {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: 6px;
|
|
||||||
background-color: $action-blue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { CSS } from '@dnd-kit/utilities';
|
|||||||
import { Header } from '@tanstack/react-table';
|
import { Header } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
|
||||||
|
|
||||||
import styles from '../Cuesheet.module.scss';
|
import styles from '../Cuesheet.module.scss';
|
||||||
|
|
||||||
interface SortableCellProps {
|
interface SortableCellProps {
|
||||||
@@ -29,8 +27,6 @@ export function SortableCell({ header, style, children }: SortableCellProps) {
|
|||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizerClasses = cx([styles.resizer, header.column.getIsResizing() ? styles.isResizing : null]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan}>
|
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan}>
|
||||||
<div {...attributes} {...listeners}>
|
<div {...attributes} {...listeners}>
|
||||||
@@ -41,7 +37,7 @@ export function SortableCell({ header, style, children }: SortableCellProps) {
|
|||||||
onMouseDown: header.getResizeHandler(),
|
onMouseDown: header.getResizeHandler(),
|
||||||
onTouchStart: header.getResizeHandler(),
|
onTouchStart: header.getResizeHandler(),
|
||||||
}}
|
}}
|
||||||
className={resizerClasses}
|
className={styles.resizer}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { usePrevious } from '@mantine/hooks';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
CustomFields,
|
CustomFields,
|
||||||
@@ -12,12 +14,14 @@ import {
|
|||||||
ViewSettings,
|
ViewSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
|
import sound from '../../../assets/sounds/buzzer.mp3';
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import { FitText } from '../../../common/components/fit-text/FitText';
|
import { FitText } from '../../../common/components/fit-text/FitText';
|
||||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import { useTimerPhase } from '../../../common/hooks/useSocket';
|
||||||
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
||||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||||
@@ -59,12 +63,28 @@ interface TimerProps {
|
|||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usePhaseEvent = () => {
|
||||||
|
const { phase } = useTimerPhase();
|
||||||
|
const previousValue = usePrevious(phase);
|
||||||
|
|
||||||
|
const audio = useMemo(() => new Audio(sound), []);
|
||||||
|
|
||||||
|
if (previousValue !== TimerPhase.None && previousValue !== phase && phase === TimerPhase.Overtime) {
|
||||||
|
try {
|
||||||
|
audio.play();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Audio playback prevented', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function Timer(props: TimerProps) {
|
export default function Timer(props: TimerProps) {
|
||||||
const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props;
|
const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props;
|
||||||
|
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
usePhaseEvent();
|
||||||
|
|
||||||
useWindowTitle('Timer');
|
useWindowTitle('Timer');
|
||||||
|
|
||||||
|
|||||||
@@ -174,13 +174,6 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
// register global shortcuts
|
|
||||||
// (available regardless of whether app is in focus)
|
|
||||||
// bring focus to window
|
|
||||||
globalShortcut.register('Alt+1', () => {
|
|
||||||
bringToFront();
|
|
||||||
});
|
|
||||||
|
|
||||||
startBackend()
|
startBackend()
|
||||||
.then((port) => {
|
.then((port) => {
|
||||||
// Load page served by node or use React dev run
|
// Load page served by node or use React dev run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
@@ -1,249 +1,243 @@
|
|||||||
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
import { apply } from '../delayUtils.js';
|
import { apply } from '../delayUtils.js';
|
||||||
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
describe('apply() ', () => {
|
/**
|
||||||
describe('in a rundown without the delay field, persisted rundown', () => {
|
* Small utility to fill in the necessary data for the test
|
||||||
it('applies delays', () => {
|
*/
|
||||||
const delayId = '1';
|
function makeOntimeEvent(event: Partial<OntimeEvent>): OntimeEvent {
|
||||||
const testRundown: OntimeRundown = [
|
return { ...event, type: SupportedEvent.Event, revision: 1 } as OntimeEvent;
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: 10 } as OntimeDelay,
|
}
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
|
||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
/**
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 10, timeEnd: 20, duration: 10, revision: 2 } as OntimeEvent,
|
* Small utility to make a delay event
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 10, timeEnd: 20, duration: 10, revision: 2 } as OntimeEvent,
|
*/
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
function makeOntimeDelay(duration: number): OntimeDelay {
|
||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
return { id: 'delay', type: SupportedEvent.Delay, duration } as OntimeDelay;
|
||||||
];
|
}
|
||||||
|
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
describe('apply()', () => {
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
it('applies a positive delay to the rundown', () => {
|
||||||
});
|
const testRundown = [
|
||||||
it('applies negative delays', () => {
|
makeOntimeDelay(10),
|
||||||
const delayId = '1';
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||||
const testRundown: OntimeRundown = [
|
makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: -10 } as OntimeDelay,
|
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 20, timeEnd: 40, duration: 20, revision: 1 } as OntimeEvent,
|
makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
];
|
||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
const updatedRundown = apply('delay', testRundown);
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 2 } as OntimeEvent,
|
expect(updatedRundown).not.toBe(testRundown);
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 10, timeEnd: 30, duration: 20, revision: 2 } as OntimeEvent,
|
expect(updatedRundown).toMatchObject([
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
{ id: '1', timeStart: 10, timeEnd: 20, duration: 10, revision: 2 },
|
||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
{ id: '2', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '1' },
|
||||||
];
|
{ id: '3' },
|
||||||
|
{ id: '4', timeStart: 30, timeEnd: 40, duration: 10, revision: 2, linkStart: null },
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
{ id: '5', timeStart: 40, timeEnd: 50, duration: 10, revision: 2, linkStart: '4' },
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
]);
|
||||||
});
|
|
||||||
it('maintains constant duration', () => {
|
|
||||||
const delayId = '1';
|
|
||||||
const testRundown: OntimeRundown = [
|
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: -30 } as OntimeDelay,
|
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 20, timeEnd: 40, duration: 20, revision: 1 } as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 2 } as OntimeEvent,
|
|
||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 20, duration: 20, revision: 2 } as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('in a rundown with the delay field, cached rundown', () => {
|
|
||||||
it('applies delays', () => {
|
|
||||||
const delayId = '1';
|
|
||||||
const testRundown: OntimeRundown = [
|
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: 10 } as OntimeDelay,
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 10,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 10,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
it('applies negative delays', () => {
|
||||||
{
|
const testRundown = [
|
||||||
id: '2',
|
makeOntimeDelay(-10),
|
||||||
type: SupportedEvent.Event,
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||||
timeStart: 10,
|
makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||||
timeEnd: 20,
|
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
duration: 10,
|
makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||||
revision: 2,
|
makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||||
delay: 0,
|
];
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 20,
|
|
||||||
duration: 10,
|
|
||||||
revision: 2,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
const updatedRundown = apply('delay', testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toMatchObject([
|
||||||
});
|
{ id: '1', timeStart: 0, timeEnd: 10, duration: 10, revision: 2 },
|
||||||
it('applies negative delays', () => {
|
{ id: '2', timeStart: 0, timeEnd: 10, duration: 10, revision: 2, linkStart: null },
|
||||||
const delayId = '1';
|
{ id: '3' },
|
||||||
const testRundown: OntimeRundown = [
|
{ id: '4', timeStart: 10, timeEnd: 20, duration: 10, revision: 2, linkStart: null },
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: -10 } as OntimeDelay,
|
{ id: '5', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '4' },
|
||||||
{
|
]);
|
||||||
id: '2',
|
});
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: -10,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 20,
|
|
||||||
timeEnd: 40,
|
|
||||||
duration: 20,
|
|
||||||
revision: 1,
|
|
||||||
delay: -10,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
it('should account for minimum duration and start when applying negative delays', () => {
|
||||||
{
|
const testRundown: OntimeRundown = [
|
||||||
id: '2',
|
makeOntimeDelay(-50),
|
||||||
type: SupportedEvent.Event,
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||||
timeStart: 0,
|
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, linkStart: '1' }),
|
||||||
timeEnd: 10,
|
];
|
||||||
duration: 10,
|
|
||||||
revision: 2,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 30,
|
|
||||||
duration: 20,
|
|
||||||
revision: 2,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
const expected = [
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 2 } as OntimeEvent,
|
||||||
});
|
{
|
||||||
it('maintains constant duration', () => {
|
id: '2',
|
||||||
const delayId = '1';
|
type: SupportedEvent.Event,
|
||||||
const testRundown: OntimeRundown = [
|
timeStart: 50,
|
||||||
{ id: delayId, type: SupportedEvent.Delay, duration: -30 } as OntimeDelay,
|
timeEnd: 100,
|
||||||
{
|
duration: 50,
|
||||||
id: '2',
|
linkStart: null,
|
||||||
type: SupportedEvent.Event,
|
revision: 2,
|
||||||
timeStart: 0,
|
} as OntimeEvent,
|
||||||
timeEnd: 10,
|
];
|
||||||
duration: 10,
|
|
||||||
revision: 1,
|
|
||||||
delay: -30,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 20,
|
|
||||||
timeEnd: 40,
|
|
||||||
duration: 20,
|
|
||||||
revision: 1,
|
|
||||||
delay: -30,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
const updatedRundown = apply('delay', testRundown);
|
||||||
{
|
expect(updatedRundown).toMatchObject(expected);
|
||||||
id: '2',
|
});
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 10,
|
|
||||||
duration: 10,
|
|
||||||
revision: 2,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 20,
|
|
||||||
duration: 20,
|
|
||||||
revision: 2,
|
|
||||||
delay: 0,
|
|
||||||
} as OntimeEvent,
|
|
||||||
];
|
|
||||||
|
|
||||||
const updatedRundown = apply(delayId, testRundown);
|
it('unlinks events to maintain gaps when applying positive delays', () => {
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
const testRundown = [
|
||||||
});
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||||
|
makeOntimeDelay(50),
|
||||||
|
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(apply('delay', testRundown)).toMatchObject([
|
||||||
|
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 150,
|
||||||
|
timeEnd: 200,
|
||||||
|
duration: 50,
|
||||||
|
linkStart: null,
|
||||||
|
revision: 2,
|
||||||
|
} as OntimeEvent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains links if there is no gap', () => {
|
||||||
|
const testRundown = [
|
||||||
|
makeOntimeDelay(50),
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||||
|
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(apply('delay', testRundown)).toMatchObject([
|
||||||
|
{ id: '1', type: SupportedEvent.Event, timeStart: 50, timeEnd: 150, duration: 100, revision: 2 } as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 150,
|
||||||
|
timeEnd: 200,
|
||||||
|
duration: 50,
|
||||||
|
linkStart: '1',
|
||||||
|
revision: 2,
|
||||||
|
} as OntimeEvent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlinks events to maintain gaps when applying negative delays', () => {
|
||||||
|
const testRundown = [
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||||
|
makeOntimeDelay(-50),
|
||||||
|
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(apply('delay', testRundown)).toMatchObject([
|
||||||
|
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 50,
|
||||||
|
timeEnd: 100,
|
||||||
|
duration: 50,
|
||||||
|
linkStart: null,
|
||||||
|
revision: 2,
|
||||||
|
} as OntimeEvent,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gaps reduce positive delay', () => {
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
makeOntimeDelay(100),
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||||
|
// gap 50
|
||||||
|
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50 }),
|
||||||
|
// gap 50
|
||||||
|
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50 }),
|
||||||
|
// gap 50
|
||||||
|
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50 }),
|
||||||
|
// linked
|
||||||
|
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = apply('delay', testRundown);
|
||||||
|
expect(updatedRundown).toMatchObject([
|
||||||
|
{ id: '1', timeStart: 0 + 100, timeEnd: 100 + 100, duration: 100, revision: 2 },
|
||||||
|
// gap 50 (100 - 50)
|
||||||
|
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
||||||
|
// gap 50 (50 - 50)
|
||||||
|
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2 },
|
||||||
|
// gap (delay is 0)
|
||||||
|
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
||||||
|
// linked
|
||||||
|
{ id: '5', timeStart: 350, timeEnd: 400, duration: 50, revision: 1, linkStart: '4' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gaps reduce positive delay (2)', () => {
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
makeOntimeDelay(2 * MILLIS_PER_HOUR),
|
||||||
|
makeOntimeEvent({
|
||||||
|
id: '1',
|
||||||
|
timeStart: 46800000, // 13:00:00
|
||||||
|
timeEnd: 50400000, // 14:00:00
|
||||||
|
duration: MILLIS_PER_HOUR,
|
||||||
|
}),
|
||||||
|
// gap 1h
|
||||||
|
makeOntimeEvent({
|
||||||
|
id: '2',
|
||||||
|
timeStart: 54000000, // 15:00:00
|
||||||
|
timeEnd: 57600000, // 16:00:00
|
||||||
|
duration: MILLIS_PER_HOUR,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = apply('delay', testRundown);
|
||||||
|
expect(updatedRundown).toMatchObject([
|
||||||
|
{ id: '1', timeStart: 54000000 /* 16 */, revision: 2 },
|
||||||
|
// gap 1h (2h - 1h)
|
||||||
|
{ id: '2', timeStart: 57600000 /* 16 */, revision: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes empty delays without applying changes', () => {
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
makeOntimeDelay(0),
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = apply('delay', testRundown);
|
||||||
|
expect(updatedRundown).toMatchObject([{ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes delays in last position without applying changes', () => {
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||||
|
makeOntimeDelay(100),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = apply('delay', testRundown);
|
||||||
|
expect(updatedRundown).toMatchObject([{ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlinks events to across blocks is it is the first event after the delay', () => {
|
||||||
|
const testRundown = [
|
||||||
|
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||||
|
makeOntimeDelay(50),
|
||||||
|
{ id: 'block', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||||
|
];
|
||||||
|
expect(apply('delay', testRundown)).toMatchObject([
|
||||||
|
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: 'block', type: SupportedEvent.Block },
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 150,
|
||||||
|
timeEnd: 200,
|
||||||
|
duration: 50,
|
||||||
|
linkStart: null,
|
||||||
|
revision: 2,
|
||||||
|
} as OntimeEvent,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent } from 'ontime-types';
|
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
import { getTimeFromPrevious, deleteAtIndex } from 'ontime-utils';
|
||||||
import { deleteAtIndex } from '../../../../../packages/utils/src/array-utils/arrayUtils.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates all delays in a given rundown
|
* Calculates all delays in a given rundown
|
||||||
@@ -92,10 +91,7 @@ export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies delay from given event ID, deletes the delay event after
|
* Applies delay from given event ID, deletes the delay event after
|
||||||
* @param eventId
|
|
||||||
* @param rundown
|
|
||||||
* @throws {Error} if event ID not found or is not a delay
|
* @throws {Error} if event ID not found or is not a delay
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||||
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||||
@@ -109,27 +105,71 @@ export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
|||||||
throw new Error('Given event ID is not a delay');
|
throw new Error('Given event ID is not a delay');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedRundown = [...rundown];
|
// if the delay is empty, or the last element, we can just delete it
|
||||||
const delayValue = delayEvent.duration;
|
if (delayEvent.duration === 0 || delayIndex === rundown.length - 1) {
|
||||||
|
return deleteAtIndex(delayIndex, rundown);
|
||||||
if (delayValue === 0 || delayIndex === rundown.length - 1) {
|
|
||||||
// nothing to apply
|
|
||||||
return updatedRundown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = delayIndex + 1; i < rundown.length; i++) {
|
/**
|
||||||
const currentEvent = updatedRundown[i];
|
* We apply the delay to the rundown
|
||||||
|
* This logic is mostly in sync with rundownCache.generate
|
||||||
|
*/
|
||||||
|
const updatedRundown = structuredClone(rundown);
|
||||||
|
let delayValue = delayEvent.duration;
|
||||||
|
let lastEntry: OntimeEvent | null = null;
|
||||||
|
let isFirstEvent = true;
|
||||||
|
|
||||||
if (isOntimeBlock(currentEvent)) {
|
for (let i = delayIndex + 1; i < updatedRundown.length; i++) {
|
||||||
break;
|
const currentEntry = updatedRundown[i];
|
||||||
} else if (isOntimeEvent(currentEvent)) {
|
|
||||||
currentEvent.timeStart = Math.max(0, currentEvent.timeStart + delayValue);
|
// we don't do operation on other event types
|
||||||
currentEvent.timeEnd = Math.max(currentEvent.duration, currentEvent.timeEnd + delayValue);
|
if (!isOntimeEvent(currentEntry)) {
|
||||||
if (currentEvent.delay) {
|
continue;
|
||||||
currentEvent.delay = currentEvent.delay - delayValue;
|
|
||||||
}
|
|
||||||
currentEvent.revision += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to remove the link in the first event to maintain the gap
|
||||||
|
let shouldUnlink = isFirstEvent;
|
||||||
|
isFirstEvent = false;
|
||||||
|
|
||||||
|
// if the event is not linked, we try and maintain gaps
|
||||||
|
if (lastEntry !== null) {
|
||||||
|
const timeFromPrevious: number = getTimeFromPrevious(
|
||||||
|
currentEntry.timeStart,
|
||||||
|
lastEntry.timeStart,
|
||||||
|
lastEntry.timeEnd,
|
||||||
|
lastEntry.duration,
|
||||||
|
);
|
||||||
|
|
||||||
|
// when applying negative delays, we need to unlink the event
|
||||||
|
// if the previous event was fully consumed by the delay
|
||||||
|
if (currentEntry.linkStart && delayValue < 0 && lastEntry.timeStart + delayValue < 0) {
|
||||||
|
shouldUnlink = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeFromPrevious > 0) {
|
||||||
|
delayValue = Math.max(delayValue - timeFromPrevious, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayValue === 0) {
|
||||||
|
// we can bail from continuing if there are no further delays to apply
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the current entry before making mutations on its values
|
||||||
|
lastEntry = { ...currentEntry };
|
||||||
|
|
||||||
|
if (shouldUnlink) {
|
||||||
|
currentEntry.linkStart = null;
|
||||||
|
shouldUnlink = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// event times move up by the delay value
|
||||||
|
// we dont update the delay value since we would need to iterate through the entire dataset
|
||||||
|
// this is handled by the rundownCache.generate function
|
||||||
|
currentEntry.timeStart = Math.max(0, currentEntry.timeStart + delayValue);
|
||||||
|
currentEntry.timeEnd = Math.max(currentEntry.duration, currentEntry.timeEnd + delayValue);
|
||||||
|
currentEntry.revision += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return deleteAtIndex(delayIndex, updatedRundown);
|
return deleteAtIndex(delayIndex, updatedRundown);
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
Reference in New Issue
Block a user