Compare commits

..

3 Commits

Author SHA1 Message Date
Carlos Valente 770d12888d V2 beta3 (#338)
* chore: upgrade local build documentation

* style: keep scrolling event in screen

* feat: delay is time entry

* refactor: remove unused

* refactor: remove unused

* refactor: batch store updates

* refactor: virtually remove cap on events

* refactor: style and behaviour tweaks to event block

* style: tweaks on schedules

* chore: remove sentry from server

* style: reorder menu

* chore: update docs
2023-04-14 10:13:46 +02:00
Fabian Posenau 94a1369d64 fix docker on different port (#337)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-04-14 10:08:42 +02:00
Fabian Posenau 5ba27dca1d add onClick handler (#335)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-04-10 20:00:09 +02:00
50 changed files with 535 additions and 379 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ You can generate a distribution for your OS by running the following steps.
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Build the UI and server__ by running `turbo build`
- __Build the UI and server__ by running `turbo build:local`
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
The build distribution assets will be at `.apps/electron/dist`
+4 -9
View File
@@ -65,6 +65,7 @@ More documentation is available [in our docs](https://cpvalente.gitbook.io/ontim
- Backstage Info
- Public Info
- Studio Clock
- Countdown
- [Make your own?](#make-your-own-viewer)
- [x] Configurable Lower Thirds
- [x] Cuesheets with user definable fields
@@ -109,11 +110,11 @@ Taking advantage of the integrations, we currently use Ontime with:
### Make your own viewer
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside of the application.
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
<br />
See [this repository](https://github.com/cpvalente/ontime-viewer-template) with a small template on
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
how to get you started and read the docs about
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
@@ -132,13 +133,7 @@ in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime
docker pull getontime/ontime
```
```bash
# Port 4001 - ontime server port
# Port 8888 - OSC input, bound to localhost IP Address
docker run -p 4001:4001 -p 127.0.0.1:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
```
or if running from the docker compose
and use the included docker compose to get started
```bash
docker-compose up
-1
View File
@@ -20,7 +20,6 @@
"csv-stringify": "^6.2.3",
"deepmerge": "^4.3.0",
"framer-motion": "^10.10.0",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0",
+2 -2
View File
@@ -19,10 +19,10 @@ export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases
* @description finds server path given the current location, it
* @return {*}
*/
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin);
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${window.location.port}` : window.location.origin);
export const serverURL = calculateServer();
export const websocketUrl = `ws://${window.location.hostname}:${STATIC_PORT}/ws`;
export const websocketUrl = `ws://${window.location.hostname}:${window.location.port}/ws`;
export const eventURL = `${serverURL}/eventdata`;
export const rundownURL = `${serverURL}/events`;
@@ -17,7 +17,7 @@ export default function Swatch(props: SwatchProps) {
if (!color) {
return (
<div className={`${classes} ${style.center}`}>
<div className={`${classes} ${style.center}`} onClick={() => onClick('')}>
<IoBan />
</div>
);
@@ -1,13 +1,24 @@
@use '../../../../theme/v2Styles' as *;
$input-font-size: 15px;
.delayInput {
display: flex;
gap: $element-spacing;
align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
max-width: 7em;
padding-left: 16px;
color: $ontime-delay-text
}
}
.inputField {
text-align: center;
}
.delayOptions {
display: flex;
flex-direction: column;
}
@@ -1,88 +1,134 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
import { millisToString } from 'ontime-utils';
import { clamp } from '../../../utils/math';
import { useEventAction } from '../../../hooks/useEventAction';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import style from './DelayInput.module.scss';
const inputStyleProps = {
width: 20,
placeholder: '-',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
fontSize: '15px',
letterSpacing: '0.3px',
};
interface DelayInputProps {
submitHandler: (value: number) => void;
value?: number;
eventId: string;
duration: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const { eventId, duration } = props;
const { updateEvent } = useEventAction();
const [value, setValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement | null>(null);
// avoid wrong submit on cancel
let ignoreChange = false;
useEffect(() => {
if (!value) {
if (typeof duration === undefined) {
return;
}
setValue(value);
}, [value]);
setValue(millisToString(duration));
}, [duration]);
/**
* @description Prepare delay value for update
* @param {string} value string to be parsed
* @param {string} newValue string to be parsed
*/
const validate = useCallback(
(newValue?: string) => {
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
if (delayValue === value) return;
setValue(delayValue);
const validateAndSubmit = (newValue: string) => {
if (ignoreChange) {
ignoreChange = false;
return;
}
submitHandler(delayValue);
},
[submitHandler, value],
);
const isNegative = newValue.startsWith('-');
let newMillis = forgivingStringToMillis(newValue);
if (isNegative) {
newMillis = newMillis * -1;
}
if (newMillis === duration) {
return;
}
submitChange(newMillis);
setValue(millisToString(newMillis));
};
const submitChange = (value: number) => {
updateEvent({
id: eventId,
duration: value,
});
};
/**
* @description Selects input text on focus
*/
const handleFocus = () => inputRef.current?.select();
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback(
(key: string) => {
if (key === 'Enter') {
inputRef.current?.blur();
validate(inputRef.current?.value);
} else if (key === 'Escape') {
inputRef.current?.blur();
setValue(value);
}
},
[validate, value],
);
const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
inputRef.current?.blur();
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Tab') {
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Escape') {
ignoreChange = true;
setValue(millisToString(duration));
inputRef.current?.blur();
}
};
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
/**
* @description handles direction change to delay
* @param newDirection
*/
const handleSlipChange = (newDirection: 'add' | 'subtract') => {
if (newDirection === 'add') {
// add time
if (duration < 0) {
submitChange(duration * -1);
}
} else if (newDirection === 'subtract') {
// subtract time
if (duration > 0) {
submitChange(duration * -1);
}
}
};
const checkedOption = value.startsWith('-') ? 'subtract' : 'add';
return (
<label className={style.delayInput}>
<div className={style.delayInput}>
<Input
{...inputStyleProps}
size='sm'
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
value={_value}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
type='text'
placeholder='-'
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={(event) => validateAndSubmit(event.target.value)}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={9}
/>
{labelText}
</label>
<RadioGroup
className={style.delayOptions}
onChange={handleSlipChange}
value={checkedOption}
variant='ontime-block'
size='sm'
>
<Radio value='add'>Add time</Radio>
<Radio value='subtract'>Subtract time</Radio>
</RadioGroup>
</div>
);
}
@@ -25,18 +25,22 @@ export default function TimeInput(props: TimeInputProps) {
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState('');
const [value, setValue] = useState<string>('');
// avoid wrong submit on cancel
let ignoreChange = false;
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
try {
setValue(millisToString(time + delay));
// eslint-disable-next-line -- we use ignore change to stop submit on cancel
ignoreChange = true;
setValue(millisToString(time));
} catch (error) {
emitError(`Unable to parse date: ${error}`);
}
}, [delay, emitError, time]);
}, [emitError, time]);
/**
* @description Selects input text on focus
@@ -73,11 +77,8 @@ export default function TimeInput(props: TimeInputProps) {
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
if (newValMillis === time) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
@@ -87,7 +88,7 @@ export default function TimeInput(props: TimeInputProps) {
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler],
[name, previousEnd, submitHandler, time, validationHandler],
);
/**
@@ -96,10 +97,16 @@ export default function TimeInput(props: TimeInputProps) {
*/
const validateAndSubmit = useCallback(
(newValue: string) => {
if (ignoreChange) {
// eslint-disable-next-line -- we use this to prevent a wrong submit
ignoreChange = false;
return;
}
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(millisToString(ms + delay));
const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay);
setValue(millisToString(delayed));
} else {
resetValue();
}
@@ -139,8 +146,6 @@ export default function TimeInput(props: TimeInputProps) {
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
@@ -155,6 +160,7 @@ export default function TimeInput(props: TimeInputProps) {
return '';
};
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
@@ -32,7 +32,7 @@
}
&:not(:last-child) {
padding-bottom: clamp(16px, 1.5vw, 24px);
padding-bottom: 8px;
}
&--past {
@@ -6,7 +6,7 @@ import { useInterval } from '../../hooks/useInterval';
interface ScheduleContextState {
events: OntimeEvent[];
paginatedEvents: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
numPages: number;
visiblePage: number;
isBackstage: boolean;
@@ -16,22 +16,20 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps {
events: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
isBackstage?: boolean;
eventsPerPage?: number;
time?: number;
}
export const ScheduleProvider = (
{
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 4,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
export const ScheduleProvider = ({
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 8,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
const numPages = Math.ceil(events.length / eventsPerPage);
@@ -90,6 +90,7 @@ export const useCuesheet = () => {
export const setEventPlayback = {
loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
startEvent: (eventId: string) => socketSendJson('startid', eventId),
start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'),
};
@@ -2,9 +2,9 @@ import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToDelayString,
millisToMinutes,
millisToSeconds,
timeStringToMillis,
} from '../dateConfig';
describe('test string from formatDisplay function', () => {
@@ -170,88 +170,6 @@ describe('test millisToMinutes function', () => {
});
});
describe('test timeStringToMillis function', () => {
it('test with null', () => {
const t = { val: null, result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:00', () => {
const t = { val: '00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:00', () => {
const t = { val: '-00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:01', () => {
const t = { val: '00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:01', () => {
const t = { val: '-00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 01:00:01', () => {
const t = { val: '01:00:01', result: 3601000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 24:00:01', () => {
const t = { val: '24:00:01', result: 86401000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:5', () => {
const t = { val: '00:00:5', result: 5000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:1:00', () => {
const t = { val: '00:1:00', result: 60000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1:00:00', () => {
const t = { val: '1:00:00', result: 3600000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1', () => {
const t = { val: '1', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 120', () => {
const t = { val: '120', result: 120000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 56', () => {
const t = { val: '56', result: 56000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:3', () => {
const t = { val: '2:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 02:3', () => {
const t = { val: '02:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:03', () => {
const t = { val: '2:03', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
});
describe('test isTimeString() function', () => {
it('it validates time strings', () => {
const ts = ['2', '2:10', '2:10:22'];
@@ -450,3 +368,59 @@ describe('test forgivingStringToMillis()', () => {
});
});
});
describe('millisToDelayString()', () => {
it('returns null for null values', () => {
expect(millisToDelayString(null)).toBeNull();
});
it('returns null 0', () => {
expect(millisToDelayString(0)).toBeNull();
});
describe('converts values in seconds', () => {
it(`shows a simple string with value in seconds`, () => {
expect(millisToDelayString(10000)).toBe('+10sec');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-10000)).toBe('-10sec');
});
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
underAMinute.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
});
});
expect(millisToDelayString(null)).toBeNull();
});
describe('converts values in minutes', () => {
it(`shows a simple string with value in minutes`, () => {
expect(millisToDelayString(720000)).toBe('+12min');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-720000)).toBe('-12min');
});
it(`shows a simple string with value in minutes and seconds`, () => {
expect(millisToDelayString(630000)).toBe('+00:10:30');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-630000)).toBe('-00:10:30');
});
const underAnHour = [60000, 360000, 720000];
underAnHour.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
});
});
});
describe('converts values with full time string', () => {
it(`positive added time`, () => {
expect(millisToDelayString(45015000)).toBe('+12:30:15');
});
it(`negative added time`, () => {
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
});
});
});
+19 -14
View File
@@ -1,3 +1,5 @@
import { formatFromMillis } from 'ontime-utils';
import { mth, mtm, mts } from './timeConstants';
export const timeFormat = 'HH:mm';
@@ -47,20 +49,6 @@ export const millisToMinutes = (millis: number): number => {
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
};
/**
* @description Converts timestring to milliseconds
* @param {string} string - time string "23:00:12"
* @returns {number} Amount in milliseconds
*/
export const timeStringToMillis = (string: string): number => {
if (typeof string !== 'string') return 0;
const time = string.split(':');
if (time.length === 1) return Math.abs(time[0] * mts);
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
return 0;
};
/**
* @description Validates a time string
* @param {string} string - time string "23:00:12"
@@ -150,3 +138,20 @@ export const forgivingStringToMillis = (value: string, fillLeft = true): number
return millis;
};
export function millisToDelayString(millis: number | null): undefined | string | null {
if (millis == null || millis === 0) {
return null;
}
const isNegative = millis < 0;
const absMillis = Math.abs(millis);
if (absMillis < mtm) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')}sec`;
} else if (absMillis < mth && absMillis % mtm === 0) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')}min`;
} else {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
}
}
+2 -6
View File
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { Settings } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import { formatFromMillis, millisToString } from 'ontime-utils';
import { APP_SETTINGS } from '../api/apiConstants';
import { ontimeQueryClient } from '../queryClient';
@@ -39,7 +38,6 @@ type FormatOptions = {
};
/**
/**
* @description utility function to format a date in 12 or 24 hour format
* @param {number | null} milliseconds
* @param {object} [options]
@@ -54,7 +52,5 @@ export const formatTime = (milliseconds: number | null, options: FormatOptions,
}
const timeFormat = resolver();
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
return timeFormat === '12'
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
: millisToString(milliseconds, showSeconds);
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
};
@@ -144,8 +144,8 @@ $playback-width: 450px;
.eventEditor {
border-radius: 8px 8px 0 0;
background-color: $bg-container-l2;
box-shadow: rgba(0, 0, 0, 0.6) 0 3px 6px 6px;
border-top: 1px solid $white-10;
box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
border-top: 1px solid $white-20;
position: absolute;
bottom: 0;
width: 100vw;
@@ -79,7 +79,7 @@
display: block;
@include input-label;
.delayLabel {
&.delayLabel {
color: $ontime-delay-text;
}
@@ -5,7 +5,8 @@ import { millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss';
@@ -71,18 +72,15 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
updateEvent(newEventData);
};
const delayed = delay !== 0;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
const newStart = delayed ? `New start ${millisToString(timeStart + delay)}` : null;
const newEnd = delayed ? `New end ${millisToString(timeEnd + delay)}` : null;
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
const startLabel = delayTime ? `New start ${millisToString(timeStart + delay)}` : 'Start time';
const endLabel = delayTime ? `New end ${millisToString(timeEnd + delay)}` : 'End time';
const inputTimeLabels = cx([style.inputLabel, delayTime ? style.delayLabel : null]);
return (
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={style.inputLabel}>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<label className={inputTimeLabels}>{startLabel}</label>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
@@ -92,10 +90,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
placeholder='Start'
warning={warning.start}
/>
<label className={style.inputLabel}>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<label className={inputTimeLabels}>{endLabel}</label>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
+26 -26
View File
@@ -8,8 +8,8 @@ import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle'
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoScan } from '@react-icons/all-files/io5/IoScan';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi';
import { downloadRundown } from '../../common/api/ontimeApi';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent';
@@ -130,31 +130,6 @@ export default function MenuBar(props: MenuBarProps) {
isDisabled={!isElectron}
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label='Help'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
className={isIntegrationOpen ? style.open : ''}
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
@@ -170,6 +145,31 @@ export default function MenuBar(props: MenuBarProps) {
tooltip='Export showfile'
aria-label='Export showfile'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
className={isIntegrationOpen ? style.open : ''}
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label='Help'
/>
</VStack>
);
}
+2 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { OntimeRundown, SupportedEvent } from 'ontime-types';
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
import { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket';
@@ -242,6 +242,7 @@ export default function Rundown(props: RundownProps) {
previousEnd={previousEnd}
previousEventId={previousEventId}
playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
/>
{((showQuickEntry && index === cursor) || isLast) && (
<QuickAddBlock
@@ -26,10 +26,23 @@ interface RundownEntryProps {
previousEnd: number;
previousEventId?: string;
playback?: Playback; // we only care about this if this event is playing
isRolling: boolean; // we need to know even if not related to this event
}
export default function RundownEntry(props: RundownEntryProps) {
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
const {
index,
eventIndex,
data,
selected,
hasCursor,
next,
delay,
previousEnd,
previousEventId,
playback,
isRolling,
} = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent } = useEventAction();
@@ -149,6 +162,7 @@ export default function RundownEntry(props: RundownEntryProps) {
selected={selected}
hasCursor={hasCursor}
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
/>
);
@@ -23,7 +23,7 @@ $block-cursor-color: $blue-400;
}
@mixin block-spacing() {
padding: 4px 8px 4px 2px;
padding-right: 8px;
gap: 2px;
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { Button, HStack } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
@@ -9,7 +9,6 @@ import { OntimeDelay, OntimeEvent } from 'ontime-types';
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
@@ -32,7 +31,7 @@ interface DelayBlockProps {
export default function DelayBlock(props: DelayBlockProps) {
const { data, hasCursor, actionHandler } = props;
const { applyDelay, updateEvent, deleteEvent } = useEventAction();
const { applyDelay, deleteEvent } = useEventAction();
const handleRef = useRef<null | HTMLSpanElement>(null);
const {
@@ -65,28 +64,14 @@ export default function DelayBlock(props: DelayBlockProps) {
deleteEvent(data.id);
};
const delaySubmitHandler = useCallback(
(value: number) => {
const newEvent = {
id: data.id,
duration: value * 60000,
};
updateEvent(newEvent);
},
[data.id, updateEvent],
);
const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return (
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo />
</span>
<DelayInput value={delayValue} submitHandler={delaySubmitHandler} />
<DelayInput eventId={data.id} duration={data.duration} />
<HStack spacing='8px' className={style.actionOverlay}>
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
Apply
@@ -22,10 +22,30 @@ $skip-opacity: 0.1;
padding-right: $block-clearance;
gap: 2px;
@mixin declare-overrides(){
--status-color-override: #{$gray-200};
--status-color-active-override: #{$green-400};
}
&.selected {
background-color: $gray-1350;
}
&.play {
background-color: $green-700;
@include declare-overrides;
}
&.roll {
background-color: $blue-700;
@include declare-overrides;
}
&.pause {
background-color: $orange-700;
@include declare-overrides;
}
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
@@ -146,6 +166,7 @@ $skip-opacity: 0.1;
justify-content: flex-end;
align-items: center;
gap: 8px;
color: var(--status-color-override, $gray-500);
.tag {
padding-top: 1px;
@@ -156,12 +177,12 @@ $skip-opacity: 0.1;
.statusIcon {
width: 16px;
height: 16px;
color: $gray-500;
}
.statusIcon.active {
color: $active-indicator;
color: var(--status-color-active-override, $active-indicator);
}
.statusIcon.disabled {
color: $gray-1000;
}
@@ -33,6 +33,7 @@ interface EventBlockProps {
selected: boolean;
hasCursor: boolean;
playback?: Playback;
isRolling: boolean;
actionHandler: (
action: EventItemActions,
payload?:
@@ -65,6 +66,7 @@ export default function EventBlock(props: EventBlockProps) {
selected,
hasCursor,
playback,
isRolling,
actionHandler,
} = props;
@@ -128,6 +130,7 @@ export default function EventBlock(props: EventBlockProps) {
style.eventBlock,
skip ? style.skip : null,
selected ? style.selected : null,
playback ? style[playback] : null,
hasCursor ? style.hasCursor : null,
]);
@@ -157,6 +160,7 @@ export default function EventBlock(props: EventBlockProps) {
skip={skip}
selected={selected}
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
/>
)}
@@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
@@ -49,6 +49,7 @@ interface EventBlockInnerProps {
skip: boolean;
selected: boolean;
playback?: Playback;
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
}
@@ -70,6 +71,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
skip = false,
selected,
playback,
isRolling,
actionHandler,
} = props;
@@ -89,17 +91,18 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
}
}, [eventId, isOpen, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play;
const eventIsPlaying = playback === Playback.Play;
const eventIsPaused = playback === Playback.Pause;
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' };
playBtnStyles._hover = { bg: '#c05621' }; // $ontime-paused
} else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = {};
}
return !renderInner ? null : (
<>
<EventBlockPlayback eventId={eventId} skip={skip} isPlaying={eventIsPlaying} selected={selected} />
<EventBlockTimers
eventId={eventId}
timeStart={timeStart}
@@ -109,6 +112,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
previousEnd={previousEnd}
/>
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
<EventBlockPlayback
eventId={eventId}
skip={skip}
isPlaying={eventIsPlaying}
isPaused={eventIsPaused}
selected={selected}
disablePlayback={skip || isRolling}
/>
<div className={style.statusElements}>
<span className={style.eventNote}>{note}</span>
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
@@ -175,10 +186,10 @@ function EndActionIcon(props: { action: EndAction; className: string }) {
function TimerIcon(props: { type: TimerType; className: string }) {
const { type, className } = props;
if (type === TimerType.CountUp) {
return <IoCaretUp className={className} />;
return <IoArrowUp className={className} />;
}
if (type === TimerType.Clock) {
return <IoTime className={className} />;
}
return <IoCaretDown className={className} />;
return <IoArrowDown className={className} />;
}
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
@@ -16,6 +16,13 @@ const blockBtnStyle = {
size: 'sm',
};
type StyleVariant = {
'aria-label': string;
tooltip: string;
backgroundColor: string;
_hover: { backgroundColor?: string };
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
@@ -24,17 +31,55 @@ interface EventBlockPlaybackProps {
eventId: string;
skip: boolean;
isPlaying: boolean;
isPaused: boolean;
selected: boolean;
disablePlayback: boolean;
}
const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
const { eventId, skip, isPlaying, selected } = props;
const { eventId, skip, isPlaying, isPaused, selected, disablePlayback } = props;
const { updateEvent } = useEventAction();
const toggleSkip = () => {
updateEvent({ id: eventId, skip: !skip });
};
const actionHandler = () => {
// is playing -> pause
// is paused -> continue
// otherwise -> start
if (isPlaying) {
setEventPlayback.pause();
} else if (isPaused) {
setEventPlayback.start();
} else {
setEventPlayback.startEvent(eventId);
}
};
const buttonVariant: Partial<StyleVariant> = {};
if (isPaused) {
// continue
buttonVariant['aria-label'] = 'Continue event';
buttonVariant.tooltip = 'Continue event';
buttonVariant.backgroundColor = '#339E4E';
buttonVariant._hover = { backgroundColor: '#339E4Eee' };
} else if (isPlaying) {
// pause
buttonVariant['aria-label'] = 'Pause event';
buttonVariant.tooltip = 'Pause event';
buttonVariant.backgroundColor = '#c05621';
buttonVariant._hover = { backgroundColor: '#c05621ee' };
} else {
// start
buttonVariant['aria-label'] = 'Start event';
buttonVariant.tooltip = 'Start event';
if (!disablePlayback) {
buttonVariant._hover = { backgroundColor: '#339E4E' };
}
}
return (
<div className={style.playbackActions}>
<TooltipActionBtn
@@ -55,7 +100,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
aria-label='Load event'
tooltip='Load event'
icon={<IoReload className={style.flip} />}
isDisabled={skip}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)}
@@ -65,13 +110,12 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
icon={isPlaying ? <IoPlay /> : <IoPlayOutline />}
isDisabled={skip}
icon={!isPlaying ? <IoPlay /> : <IoPause />}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={isPlaying ? '#58A151' : undefined}
_hover={{ backgroundColor: isPlaying ? '#58A151' : undefined }}
{...buttonVariant}
clickHandler={actionHandler}
tabIndex={-1}
/>
</div>
@@ -1,12 +1,10 @@
@use '../../../../theme/v2Styles' as *;
.progressBar {
// layout
height: 100%;
width: 0;
border-radius: 1px 0 0 1px;
// animations
transition: 1s linear;
transition-property: width;
@@ -14,6 +12,10 @@
background-color: $playback-start;
}
&.overtime {
background-color: $playback-negative;
}
&.pause {
background-color: $ontime-paused;
}
@@ -21,8 +23,4 @@
&.roll {
background-color: $ontime-roll;
}
&.overtime {
background-color: $playback-negative;
}
}
@@ -4,7 +4,7 @@ import { millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../../common/utils/dateConfig';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss';
@@ -64,8 +64,9 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
[timeEnd, timeStart],
);
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
const newTime = millisToString(timeStart + delay);
const delayedStart = Math.max(0, timeStart + delay);
const newTime = millisToString(delayedStart);
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
return (
<div className={style.eventTimers}>
@@ -94,13 +95,14 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={duration}
delay={0}
placeholder='Duration'
previousEnd={previousEnd}
warning={warning.duration}
/>
{delay !== 0 && delay !== null && (
{delayTime && (
<div className={style.delayNote}>
{`${delayTime} minutes`}
{delayTime}
<br />
{`New start: ${newTime}`}
</div>
@@ -10,11 +10,7 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import PropTypes from 'prop-types';
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
@@ -148,7 +144,7 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
if (el) {
el.scrollIntoView({
behavior: 'smooth',
block: 'start',
block: 'center',
inline: 'nearest',
});
}
@@ -119,7 +119,7 @@
grid-area: schedule;
overflow: hidden;
height: 100%;
margin-left: 16px;
margin-left: clamp(16px, 5vw, 64px);;
}
.schedule-nav-container {
@@ -131,6 +131,8 @@
grid-area: info;
display: flex;
gap: max(1vw, 16px);
align-self: flex-end;
overflow: hidden;
&__message {
font-size: clamp(16px, 1.5vw, 24px);
@@ -141,7 +143,7 @@
}
.qr {
margin-left: auto;
margin-left: clamp(16px, 5vw, 64px);;
padding: 4px;
background-color: white;
}
@@ -99,7 +99,8 @@
grid-area: info;
display: flex;
gap: max(1vw, 16px);
min-height: max(calc(100vh / 15), 128px);
align-self: flex-end;
overflow: hidden;
&__message {
font-size: clamp(16px, 1.5vw, 24px);
@@ -110,7 +111,7 @@
}
.qr {
margin-left: auto;
margin-left: clamp(16px, 5vw, 64px);;
padding: 4px;
background-color: white;
}
@@ -6,6 +6,7 @@ $white-7: rgba(255, 255, 255, 0.07);
$white-9: rgba(255, 255, 255, 0.09);
$white-10: rgba(255, 255, 255, 0.10);
$white-13: rgba(255, 255, 255, 0.13);
$white-20: rgba(255, 255, 255, 0.20);
$black-10: rgba(0, 0, 0, 0.10);
+12
View File
@@ -21,6 +21,9 @@ export const ontimeButtonOutlined = {
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
_hover: {
backgroundColor: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#2d2d2d', // $gray-1100
},
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
@@ -34,6 +37,9 @@ export const ontimeButtonSubtle = {
border: '1px solid transparent',
_hover: {
background: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#303030', // $gray-1050
},
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
@@ -47,6 +53,9 @@ export const ontimeButtonSubtleOnLight = {
border: '1px solid transparent',
_hover: {
backgroundColor: '#cfcfcf', // $gray-200
_disabled: {
backgroundColor: '#ececec', // $gray-100
},
},
_active: {
backgroundColor: '#ececec', // $gray-200
@@ -60,6 +69,9 @@ export const ontimeGhostOnLight = {
_hover: {
color: '#595959', // $gray-800
backgroundColor: '#ececec', // $gray-200
_disabled: {
backgroundColor: 'transparent',
},
},
_active: {
backgroundColor: 'transparent',
+5 -5
View File
@@ -1,21 +1,21 @@
export const ontimeCheckboxOnDark = {
control: {
border: '1px',
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
_checked: {
borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue
},
_focus: {
boxShadow: '0 0 0 1px #578AF4'
}
boxShadow: '0 0 0 1px #578AF4', // $blue-500
},
},
label: {
fontWeight: '200',
color: '#9d9d9d', // $gray-500
_checked: {
color: '#cfcfcf', // $gray-300
}
},
},
};
+27
View File
@@ -0,0 +1,27 @@
export const ontimeBlockRadio = {
control: {
borderColor: '#262626', // $gray-1250
backgroundColor: '#262626', // $gray-1250
_checked: {
borderColor: '#262626', // $gray-1250
color: '#3182ce', // $action-blue
backgroundColor: '#3182ce', // $action-blue
},
_hover: {
color: '#3182ce', // $action-blue
backgroundColor: '#3182ce', // $action-blue
outline: 'none',
},
},
label: {
fontSize: '0.7em',
letterSpacing: '0.3px',
color: '#9d9d9d', // $gray-500
_checked: {
color: '#cfcfcf', // $gray-300
},
_hover: {
color: '#e2e2e2', // $gray-200
},
},
};
+6
View File
@@ -12,6 +12,7 @@ import { ontimeCheckboxOnDark } from './ontimeCheckbox';
import { ontimeEditable } from './ontimeEditable';
import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeModal } from './ontimeModal';
import { ontimeBlockRadio } from './ontimeRadio';
import { ontimeSelect } from './ontimeSelect';
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
import { ontimeTab } from './ontimeTab';
@@ -65,6 +66,11 @@ const theme = extendTheme({
ontime: { ...ontimeModal },
},
},
Radio: {
variants: {
'ontime-block': { ...ontimeBlockRadio },
},
},
Tabs: {
variants: {
ontime: { ...ontimeTab },
+1 -2
View File
@@ -5,8 +5,6 @@
"version": "2.0.0-beta2",
"exports": "./src/index.js",
"dependencies": {
"@sentry/node": "^7.47.0",
"@sentry/tracing": "^7.47.0",
"body-parser": "^1.20.0",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
@@ -48,6 +46,7 @@
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
"prebuild": "pnpm setdb",
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
"lint": "eslint .",
+2 -7
View File
@@ -6,7 +6,6 @@ import cors from 'cors';
// import utils
import { join, resolve } from 'path';
import { initSentry, reportSentryException } from './modules/sentry.js';
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { OSCSettings } from 'ontime-types';
@@ -39,8 +38,6 @@ if (!isProduction) {
console.log(`Ontime directory at ${currentDirectory} `);
}
initSentry(isProduction);
// Create express APP
const app = express();
app.disable('x-powered-by');
@@ -209,14 +206,12 @@ export const shutdown = async (exitCode = 0) => {
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
process.on('unhandledRejection', async (error) => {
reportSentryException(error);
process.on('unhandledRejection', async () => {
logger.error('SERVER', 'Error: unhandled rejection');
await shutdown(1);
});
process.on('uncaughtException', async (error) => {
reportSentryException(error);
process.on('uncaughtException', async () => {
logger.error('SERVER', 'Error: uncaught exception');
await shutdown(1);
});
@@ -260,9 +260,11 @@ export class EventLoader {
* Handle side effects from event loading
*/
private _loadEvent() {
eventStore.set('loaded', this.loaded);
eventStore.set('titles', this.titles);
eventStore.set('titlesPublic', this.titlesPublic);
eventStore.batchSet({
loaded: this.loaded,
titles: this.titles,
titlesPublic: this.titlesPublic,
});
}
/**
+2 -3
View File
@@ -7,7 +7,6 @@ import { ensureDirectory } from '../utils/fileManagement.js';
import { validateFile } from '../utils/parserUtils.js';
import { dbModel } from '../models/dataModel.js';
import { parseJson } from '../utils/parser.js';
import { reportSentryException } from './sentry.js';
import { pathToStartDb, resolveDbDirectory, resolveDbPath } from '../setup.js';
/**
@@ -22,8 +21,8 @@ const populateDb = () => {
if (!existsSync(dbInDisk)) {
try {
copyFileSync(pathToStartDb, dbInDisk);
} catch (error) {
reportSentryException(error);
} catch (_) {
/* we do not handle this */
}
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { copyFileSync, existsSync } from 'fs';
import { pathToStartStyles, resolveStylesDirectory, resolveStylesPath } from '../setup.js';
import { ensureDirectory } from '../utils/fileManagement.js';
import { reportSentryException } from './sentry.js';
/**
* @description ensures directories exist and populates stylesheet
@@ -15,8 +14,8 @@ export const populateStyles = () => {
if (!existsSync(stylesInDisk)) {
try {
copyFileSync(pathToStartStyles, stylesInDisk);
} catch (error) {
reportSentryException(error);
} catch (_) {
/* we do not handle this */
}
}
-19
View File
@@ -1,19 +0,0 @@
import * as Sentry from '@sentry/node';
let shouldReport;
export function initSentry(doReport) {
shouldReport = doReport;
Sentry.init({
dsn: 'https://ceb6abdce7374857bb50b65636cbaed1@o4504288369836032.ingest.sentry.io/4504288555565056',
tracesSampleRate: 1.0,
});
}
export function reportSentryException(e) {
if (shouldReport) {
Sentry.captureException(e);
} else {
console.error(e);
}
}
+21 -14
View File
@@ -1,7 +1,17 @@
import { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from 'ontime-types';
import {
OntimeBaseEvent,
OntimeBlock,
OntimeDelay,
OntimeEvent,
SupportedEvent,
} from 'ontime-types';
import { generateId } from 'ontime-utils';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { block as blockDef, delay as delayDef, event as eventDef } from '../models/eventsDefinition.js';
import {
block as blockDef,
delay as delayDef,
event as eventDef,
} from '../models/eventsDefinition.js';
import { MAX_EVENTS } from '../settings.js';
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer } from './TimerService.js';
@@ -216,30 +226,27 @@ export async function reorderEvent(eventId, from, to) {
* @param eventId
* @returns {Promise<void>}
*/
export async function applyDelay(eventId) {
export async function applyDelay(eventId: string) {
const rundown = DataProvider.getRundown();
let delayIndex = null;
let delayValue = 0;
for (const [index, e] of rundown.entries()) {
for (const [index, event] of rundown.entries()) {
// look for delay
if (delayIndex === null) {
if (e.id === eventId && e.type === SupportedEvent.Delay) {
delayValue = e.duration;
if (event.id === eventId && event.type === SupportedEvent.Delay) {
delayValue = event.duration;
delayIndex = index;
}
}
// apply delay value to all items until block or end
else {
if (e.type === SupportedEvent.Event) {
// update times
e.timeStart += delayValue;
e.timeEnd += delayValue;
// increment revision
e.revision += 1;
} else if (e.type === SupportedEvent.Block) {
if (event.type === SupportedEvent.Event) {
event.timeStart = Math.max(0, event.timeStart + delayValue);
event.timeEnd = Math.max(event.duration, event.timeStart + delayValue);
event.revision += 1;
} else if (event.type === SupportedEvent.Block) {
break;
}
}
+16 -8
View File
@@ -141,8 +141,10 @@ export class TimerService {
* @private
*/
_onLoad() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer);
eventStore.batchSet({
playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onLoad);
}
@@ -182,8 +184,10 @@ export class TimerService {
* @private
*/
_onStart() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer);
eventStore.batchSet({
playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onStart);
}
@@ -199,8 +203,10 @@ export class TimerService {
}
_onPause() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer);
eventStore.batchSet({
playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onPause);
}
@@ -214,8 +220,10 @@ export class TimerService {
}
_onStop() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer);
eventStore.batchSet({
playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onStop);
}
-1
View File
@@ -1 +0,0 @@
export const MAX_EVENTS = 255;
+1
View File
@@ -0,0 +1 @@
export const MAX_EVENTS = 32768;
+10
View File
@@ -8,6 +8,10 @@ let store: Partial<RuntimeStore> = {};
/**
* A runtime store that broadcasts its payload
* - init: allows for adding an initial payload to the store
* - batchSet: allows setting several keys with a single broadcast
* - poll: utility to return state
* - broadcast: send its payload as json object
*/
export const eventStore = {
init(payload: RuntimeStore) {
@@ -25,6 +29,12 @@ export const eventStore = {
// });
this.broadcast();
},
batchSet<K extends keyof RuntimeStore>(values: Record<K, RuntimeStore[K]>) {
Object.entries(values).forEach(([key, value]) => {
store[key] = value;
});
this.broadcast();
},
poll() {
return store;
},
+1
View File
@@ -1,2 +1,3 @@
export { formatFromMillis } from './src/date-utils/formatFromMillis.js';
export { millisToString } from './src/date-utils/millisToString.js';
export { generateId } from './src/generate-id/generateId.js';
@@ -0,0 +1,11 @@
import { DateTime } from 'luxon';
/**
* @description utility function to format a date in milliseconds using luxon
* @param {number} millis
* @param {string} format
* @return {string}
*/
export function formatFromMillis(millis: number, format: string) {
return DateTime.fromMillis(millis).toUTC().toFormat(format);
}
+6 -13
View File
@@ -70,7 +70,6 @@ importers:
eslint-plugin-testing-library: ^5.9.1
framer-motion: ^10.10.0
jsdom: ^21.1.0
luxon: ^3.3.0
ontime-types: workspace:*
ontime-utils: workspace:*
prettier: ^2.8.3
@@ -112,7 +111,6 @@ importers:
csv-stringify: 6.2.3
deepmerge: 4.3.0
framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y
luxon: 3.3.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0
@@ -177,8 +175,6 @@ importers:
apps/server:
specifiers:
'@sentry/node': ^7.47.0
'@sentry/tracing': ^7.47.0
'@types/express': ^4.17.17
'@types/node': ^16.11.7
'@types/node-osc': ^6.0.0
@@ -210,8 +206,6 @@ importers:
vitest: ^0.29.8
ws: ^8.13.0
dependencies:
'@sentry/node': 7.47.0
'@sentry/tracing': 7.47.0
body-parser: 1.20.1
cors: 2.8.5
dotenv: 16.0.3
@@ -2497,6 +2491,7 @@ packages:
tslib: 1.14.1
transitivePeerDependencies:
- supports-color
dev: true
/@sentry/react/7.47.0_react@18.2.0:
resolution: {integrity: sha512-Qy6OnlE8FivKOLo0YE7tkr+G5fLmEOkpPxj179wbY/N8kp/ALkqbVdcOrZW7AL6HCc0lphhj+0SB+tpwoPEsiQ==}
@@ -2527,13 +2522,6 @@ packages:
dependencies:
'@sentry-internal/tracing': 7.46.0
/@sentry/tracing/7.47.0:
resolution: {integrity: sha512-hJCpKdekwaFNbCVXxfCz5IxfSEJIKnkPmRSVHITOm5VhKwq2e5kmy4Rn6bzSETwJFSDE8LGbR/3eSfGTqw37XA==}
engines: {node: '>=8'}
dependencies:
'@sentry-internal/tracing': 7.47.0
dev: false
/@sentry/types/7.46.0:
resolution: {integrity: sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g==}
engines: {node: '>=8'}
@@ -3484,6 +3472,7 @@ packages:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/ajv-keywords/3.5.2_ajv@6.12.6:
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
@@ -4352,6 +4341,7 @@ packages:
optional: true
dependencies:
ms: 2.1.2
dev: true
/decamelize-keys/1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
@@ -5772,6 +5762,7 @@ packages:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/iconv-corefoundation/1.1.7:
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
@@ -6392,6 +6383,7 @@ packages:
/lru_map/0.3.3:
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
dev: true
/luxon/3.3.0:
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
@@ -6598,6 +6590,7 @@ packages:
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}