* 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
This commit is contained in:
Carlos Valente
2023-04-14 10:13:46 +02:00
committed by GitHub
parent 94a1369d64
commit 770d12888d
48 changed files with 531 additions and 376 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 From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i` - __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` - __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` 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 - Backstage Info
- Public Info - Public Info
- Studio Clock - Studio Clock
- Countdown
- [Make your own?](#make-your-own-viewer) - [Make your own?](#make-your-own-viewer)
- [x] Configurable Lower Thirds - [x] Configurable Lower Thirds
- [x] Cuesheets with user definable fields - [x] Cuesheets with user definable fields
@@ -109,11 +110,11 @@ Taking advantage of the integrations, we currently use Ontime with:
### Make your own viewer ### 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). 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 /> <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 how to get you started and read the docs about
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api) 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 docker pull getontime/ontime
``` ```
```bash and use the included docker compose to get started
# 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
```bash ```bash
docker-compose up docker-compose up
-1
View File
@@ -20,7 +20,6 @@
"csv-stringify": "^6.2.3", "csv-stringify": "^6.2.3",
"deepmerge": "^4.3.0", "deepmerge": "^4.3.0",
"framer-motion": "^10.10.0", "framer-motion": "^10.10.0",
"luxon": "^3.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
@@ -1,13 +1,24 @@
@use '../../../../theme/v2Styles' as *; @use '../../../../theme/v2Styles' as *;
$input-font-size: 15px;
.delayInput { .delayInput {
display: flex; display: flex;
gap: $element-spacing; gap: $element-spacing;
align-items: center; align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size; 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 { .delayOptions {
text-align: center; display: flex;
} flex-direction: column;
}
@@ -1,88 +1,134 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/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'; 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 { interface DelayInputProps {
submitHandler: (value: number) => void; eventId: string;
value?: number; duration: number;
} }
export default function DelayInput(props: DelayInputProps) { export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props; const { eventId, duration } = props;
const [_value, setValue] = useState(value); const { updateEvent } = useEventAction();
const [value, setValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
// avoid wrong submit on cancel
let ignoreChange = false;
useEffect(() => { useEffect(() => {
if (!value) { if (typeof duration === undefined) {
return; return;
} }
setValue(value); setValue(millisToString(duration));
}, [value]); }, [duration]);
/** /**
* @description Prepare delay value for update * @description Prepare delay value for update
* @param {string} value string to be parsed * @param {string} newValue string to be parsed
*/ */
const validate = useCallback( const validateAndSubmit = (newValue: string) => {
(newValue?: string) => { if (ignoreChange) {
if (newValue === '') setValue(0); ignoreChange = false;
const delayValue = clamp(Number(newValue), -60, 60); return;
if (delayValue === value) return; }
setValue(delayValue);
submitHandler(delayValue); const isNegative = newValue.startsWith('-');
}, let newMillis = forgivingStringToMillis(newValue);
[submitHandler, value],
); 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 * @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
const onKeyDownHandler = useCallback( const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
(key: string) => { if (event.key === 'Enter') {
if (key === 'Enter') { inputRef.current?.blur();
inputRef.current?.blur(); validateAndSubmit((event.target as HTMLInputElement).value);
validate(inputRef.current?.value); } else if (event.key === 'Tab') {
} else if (key === 'Escape') { validateAndSubmit((event.target as HTMLInputElement).value);
inputRef.current?.blur(); } else if (event.key === 'Escape') {
setValue(value); ignoreChange = true;
} setValue(millisToString(duration));
}, inputRef.current?.blur();
[validate, value], }
); };
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 ( return (
<label className={style.delayInput}> <div className={style.delayInput}>
<Input <Input
{...inputStyleProps} size='sm'
ref={inputRef} ref={inputRef}
data-testid='delay-input' data-testid='delay-input'
className={style.inputField} className={style.inputField}
value={_value} type='text'
onChange={(event) => setValue(Number(event.target.value))} placeholder='-'
onBlur={(event) => validate(event.target.value)} variant='ontime-filled'
onKeyDown={(event) => onKeyDownHandler(event.key)} onFocus={handleFocus}
type='number' onChange={(event) => setValue(event.target.value)}
onBlur={(event) => validateAndSubmit(event.target.value)}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={9}
/> />
{labelText} <RadioGroup
</label> 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 { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { emitError } = useEmitLog(); const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null); 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 * @description Resets input value to given
*/ */
const resetValue = useCallback(() => { const resetValue = useCallback(() => {
try { 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) { } catch (error) {
emitError(`Unable to parse date: ${error}`); emitError(`Unable to parse date: ${error}`);
} }
}, [delay, emitError, time]); }, [emitError, time]);
/** /**
* @description Selects input text on focus * @description Selects input text on focus
@@ -73,11 +77,8 @@ export default function TimeInput(props: TimeInputProps) {
newValMillis = forgivingStringToMillis(newValue); newValMillis = forgivingStringToMillis(newValue);
} }
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before // check if time is different from before
if (newValMillis === originalMillis) return false; if (newValMillis === time) return false;
// validate with parent // validate with parent
if (!validationHandler(name, newValMillis)) return false; if (!validationHandler(name, newValMillis)) return false;
@@ -87,7 +88,7 @@ export default function TimeInput(props: TimeInputProps) {
return true; 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( const validateAndSubmit = useCallback(
(newValue: string) => { (newValue: string) => {
if (ignoreChange) {
// eslint-disable-next-line -- we use this to prevent a wrong submit
ignoreChange = false;
return;
}
const success = handleSubmit(newValue); const success = handleSubmit(newValue);
if (success) { if (success) {
const ms = forgivingStringToMillis(newValue); 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 { } else {
resetValue(); resetValue();
} }
@@ -139,8 +146,6 @@ export default function TimeInput(props: TimeInputProps) {
resetValue(); resetValue();
}, [emitError, resetValue, time]); }, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => { const ButtonInitial = () => {
if (name === 'timeStart') return 'S'; if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E'; if (name === 'timeEnd') return 'E';
@@ -155,6 +160,7 @@ export default function TimeInput(props: TimeInputProps) {
return ''; return '';
}; };
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]); const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]); const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
@@ -32,7 +32,7 @@
} }
&:not(:last-child) { &:not(:last-child) {
padding-bottom: clamp(16px, 1.5vw, 24px); padding-bottom: 8px;
} }
&--past { &--past {
@@ -6,7 +6,7 @@ import { useInterval } from '../../hooks/useInterval';
interface ScheduleContextState { interface ScheduleContextState {
events: OntimeEvent[]; events: OntimeEvent[];
paginatedEvents: OntimeEvent[]; paginatedEvents: OntimeEvent[];
selectedEventId: string; selectedEventId: string | null;
numPages: number; numPages: number;
visiblePage: number; visiblePage: number;
isBackstage: boolean; isBackstage: boolean;
@@ -16,22 +16,20 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps { interface ScheduleProviderProps {
events: OntimeEvent[]; events: OntimeEvent[];
selectedEventId: string; selectedEventId: string | null;
isBackstage?: boolean; isBackstage?: boolean;
eventsPerPage?: number; eventsPerPage?: number;
time?: number; time?: number;
} }
export const ScheduleProvider = ( export const ScheduleProvider = ({
{ children,
children, events,
events, selectedEventId,
selectedEventId, isBackstage = false,
isBackstage = false, eventsPerPage = 8,
eventsPerPage = 4, time = 10,
time = 10, }: PropsWithChildren<ScheduleProviderProps>) => {
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0); const [visiblePage, setVisiblePage] = useState(0);
const numPages = Math.ceil(events.length / eventsPerPage); const numPages = Math.ceil(events.length / eventsPerPage);
@@ -90,6 +90,7 @@ export const useCuesheet = () => {
export const setEventPlayback = { export const setEventPlayback = {
loadEvent: (eventId: string) => socketSendJson('loadid', eventId), loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
startEvent: (eventId: string) => socketSendJson('startid', eventId), startEvent: (eventId: string) => socketSendJson('startid', eventId),
start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'), pause: () => socketSendJson('pause'),
}; };
@@ -2,9 +2,9 @@ import {
forgivingStringToMillis, forgivingStringToMillis,
formatDisplay, formatDisplay,
isTimeString, isTimeString,
millisToDelayString,
millisToMinutes, millisToMinutes,
millisToSeconds, millisToSeconds,
timeStringToMillis,
} from '../dateConfig'; } from '../dateConfig';
describe('test string from formatDisplay function', () => { 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', () => { describe('test isTimeString() function', () => {
it('it validates time strings', () => { it('it validates time strings', () => {
const ts = ['2', '2:10', '2:10:22']; 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'; import { mth, mtm, mts } from './timeConstants';
export const timeFormat = 'HH:mm'; 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); 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 * @description Validates a time string
* @param {string} string - time string "23:00:12" * @param {string} string - time string "23:00:12"
@@ -150,3 +138,20 @@ export const forgivingStringToMillis = (value: string, fillLeft = true): number
return millis; 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 { Settings } from 'ontime-types';
import { millisToString } from 'ontime-utils'; import { formatFromMillis, millisToString } from 'ontime-utils';
import { APP_SETTINGS } from '../api/apiConstants'; import { APP_SETTINGS } from '../api/apiConstants';
import { ontimeQueryClient } from '../queryClient'; import { ontimeQueryClient } from '../queryClient';
@@ -39,7 +38,6 @@ type FormatOptions = {
}; };
/** /**
/**
* @description utility function to format a date in 12 or 24 hour format * @description utility function to format a date in 12 or 24 hour format
* @param {number | null} milliseconds * @param {number | null} milliseconds
* @param {object} [options] * @param {object} [options]
@@ -54,7 +52,5 @@ export const formatTime = (milliseconds: number | null, options: FormatOptions,
} }
const timeFormat = resolver(); const timeFormat = resolver();
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {}; const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
return timeFormat === '12' return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
: millisToString(milliseconds, showSeconds);
}; };
@@ -144,8 +144,8 @@ $playback-width: 450px;
.eventEditor { .eventEditor {
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
background-color: $bg-container-l2; background-color: $bg-container-l2;
box-shadow: rgba(0, 0, 0, 0.6) 0 3px 6px 6px; box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
border-top: 1px solid $white-10; border-top: 1px solid $white-20;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 100vw; width: 100vw;
@@ -79,7 +79,7 @@
display: block; display: block;
@include input-label; @include input-label;
.delayLabel { &.delayLabel {
color: $ontime-delay-text; color: $ontime-delay-text;
} }
@@ -5,7 +5,8 @@ import { millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput'; import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
import { 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 { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss'; import style from '../EventEditor.module.scss';
@@ -71,18 +72,15 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
updateEvent(newEventData); updateEvent(newEventData);
}; };
const delayed = delay !== 0; const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null; const startLabel = delayTime ? `New start ${millisToString(timeStart + delay)}` : 'Start time';
const newStart = delayed ? `New start ${millisToString(timeStart + delay)}` : null; const endLabel = delayTime ? `New end ${millisToString(timeEnd + delay)}` : 'End time';
const newEnd = delayed ? `New end ${millisToString(timeEnd + delay)}` : null; const inputTimeLabels = cx([style.inputLabel, delayTime ? style.delayLabel : null]);
return ( return (
<div className={style.timeOptions}> <div className={style.timeOptions}>
<div className={style.timers}> <div className={style.timers}>
<label className={style.inputLabel}> <label className={inputTimeLabels}>{startLabel}</label>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<TimeInput <TimeInput
name='timeStart' name='timeStart'
submitHandler={handleSubmit} submitHandler={handleSubmit}
@@ -92,10 +90,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
placeholder='Start' placeholder='Start'
warning={warning.start} warning={warning.start}
/> />
<label className={style.inputLabel}> <label className={inputTimeLabels}>{endLabel}</label>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<TimeInput <TimeInput
name='timeEnd' name='timeEnd'
submitHandler={handleSubmit} 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 { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoScan } from '@react-icons/all-files/io5/IoScan'; import { IoScan } from '@react-icons/all-files/io5/IoScan';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline'; 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 QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent'; import useElectronEvent from '../../common/hooks/useElectronEvent';
@@ -130,31 +130,6 @@ export default function MenuBar(props: MenuBarProps) {
isDisabled={!isElectron} isDisabled={!isElectron}
/> />
<div className={style.gap} /> <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 <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiUpload />} icon={<FiUpload />}
@@ -170,6 +145,31 @@ export default function MenuBar(props: MenuBarProps) {
tooltip='Export showfile' tooltip='Export showfile'
aria-label='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> </VStack>
); );
} }
+2 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; 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 { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket'; import { useRundownEditor } from '../../common/hooks/useSocket';
@@ -242,6 +242,7 @@ export default function Rundown(props: RundownProps) {
previousEnd={previousEnd} previousEnd={previousEnd}
previousEventId={previousEventId} previousEventId={previousEventId}
playback={isSelected ? featureData.playback : undefined} playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
/> />
{((showQuickEntry && index === cursor) || isLast) && ( {((showQuickEntry && index === cursor) || isLast) && (
<QuickAddBlock <QuickAddBlock
@@ -26,10 +26,23 @@ interface RundownEntryProps {
previousEnd: number; previousEnd: number;
previousEventId?: string; previousEventId?: string;
playback?: Playback; // we only care about this if this event is playing 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) { 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 { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent } = useEventAction(); const { addEvent, updateEvent, deleteEvent } = useEventAction();
@@ -149,6 +162,7 @@ export default function RundownEntry(props: RundownEntryProps) {
selected={selected} selected={selected}
hasCursor={hasCursor} hasCursor={hasCursor}
playback={playback} playback={playback}
isRolling={isRolling}
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
); );
@@ -23,7 +23,7 @@ $block-cursor-color: $blue-400;
} }
@mixin block-spacing() { @mixin block-spacing() {
padding: 4px 8px 4px 2px; padding-right: 8px;
gap: 2px; 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 { Button, HStack } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; 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 DelayInput from '../../../common/components/input/delay-input/DelayInput';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils'; import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu'; import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry'; import { EventItemActions } from '../RundownEntry';
@@ -32,7 +31,7 @@ interface DelayBlockProps {
export default function DelayBlock(props: DelayBlockProps) { export default function DelayBlock(props: DelayBlockProps) {
const { data, hasCursor, actionHandler } = props; const { data, hasCursor, actionHandler } = props;
const { applyDelay, updateEvent, deleteEvent } = useEventAction(); const { applyDelay, deleteEvent } = useEventAction();
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const { const {
@@ -65,28 +64,14 @@ export default function DelayBlock(props: DelayBlockProps) {
deleteEvent(data.id); 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 blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return ( return (
<div className={blockClasses} ref={setNodeRef} style={dragStyle}> <div className={blockClasses} ref={setNodeRef} style={dragStyle}>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}> <span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
<DelayInput value={delayValue} submitHandler={delaySubmitHandler} /> <DelayInput eventId={data.id} duration={data.duration} />
<HStack spacing='8px' className={style.actionOverlay}> <HStack spacing='8px' className={style.actionOverlay}>
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'> <Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
Apply Apply
@@ -22,10 +22,30 @@ $skip-opacity: 0.1;
padding-right: $block-clearance; padding-right: $block-clearance;
gap: 2px; gap: 2px;
@mixin declare-overrides(){
--status-color-override: #{$gray-200};
--status-color-active-override: #{$green-400};
}
&.selected { &.selected {
background-color: $gray-1350; 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 { &.hasCursor {
outline: 1px solid $block-cursor-color; outline: 1px solid $block-cursor-color;
} }
@@ -146,6 +166,7 @@ $skip-opacity: 0.1;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
color: var(--status-color-override, $gray-500);
.tag { .tag {
padding-top: 1px; padding-top: 1px;
@@ -156,12 +177,12 @@ $skip-opacity: 0.1;
.statusIcon { .statusIcon {
width: 16px; width: 16px;
height: 16px; height: 16px;
color: $gray-500;
} }
.statusIcon.active { .statusIcon.active {
color: $active-indicator; color: var(--status-color-active-override, $active-indicator);
} }
.statusIcon.disabled { .statusIcon.disabled {
color: $gray-1000; color: $gray-1000;
} }
@@ -33,6 +33,7 @@ interface EventBlockProps {
selected: boolean; selected: boolean;
hasCursor: boolean; hasCursor: boolean;
playback?: Playback; playback?: Playback;
isRolling: boolean;
actionHandler: ( actionHandler: (
action: EventItemActions, action: EventItemActions,
payload?: payload?:
@@ -65,6 +66,7 @@ export default function EventBlock(props: EventBlockProps) {
selected, selected,
hasCursor, hasCursor,
playback, playback,
isRolling,
actionHandler, actionHandler,
} = props; } = props;
@@ -128,6 +130,7 @@ export default function EventBlock(props: EventBlockProps) {
style.eventBlock, style.eventBlock,
skip ? style.skip : null, skip ? style.skip : null,
selected ? style.selected : null, selected ? style.selected : null,
playback ? style[playback] : null,
hasCursor ? style.hasCursor : null, hasCursor ? style.hasCursor : null,
]); ]);
@@ -157,6 +160,7 @@ export default function EventBlock(props: EventBlockProps) {
skip={skip} skip={skip}
selected={selected} selected={selected}
playback={playback} playback={playback}
isRolling={isRolling}
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
)} )}
@@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useState } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown'; import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople'; import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
@@ -49,6 +49,7 @@ interface EventBlockInnerProps {
skip: boolean; skip: boolean;
selected: boolean; selected: boolean;
playback?: Playback; playback?: Playback;
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void; actionHandler: (action: EventItemActions, payload?: any) => void;
} }
@@ -70,6 +71,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
skip = false, skip = false,
selected, selected,
playback, playback,
isRolling,
actionHandler, actionHandler,
} = props; } = props;
@@ -89,17 +91,18 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
} }
}, [eventId, isOpen, removeOpenEvent, setOpenEvent]); }, [eventId, isOpen, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play; const eventIsPlaying = playback === Playback.Play;
const eventIsPaused = playback === Playback.Pause;
const playBtnStyles = { _hover: {} }; const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) { if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' }; playBtnStyles._hover = { bg: '#c05621' }; // $ontime-paused
} else if (!skip && !eventIsPlaying) { } else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = {}; playBtnStyles._hover = {};
} }
return !renderInner ? null : ( return !renderInner ? null : (
<> <>
<EventBlockPlayback eventId={eventId} skip={skip} isPlaying={eventIsPlaying} selected={selected} />
<EventBlockTimers <EventBlockTimers
eventId={eventId} eventId={eventId}
timeStart={timeStart} timeStart={timeStart}
@@ -109,6 +112,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
previousEnd={previousEnd} previousEnd={previousEnd}
/> />
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} /> <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}> <div className={style.statusElements}>
<span className={style.eventNote}>{note}</span> <span className={style.eventNote}>{note}</span>
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}> <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 }) { function TimerIcon(props: { type: TimerType; className: string }) {
const { type, className } = props; const { type, className } = props;
if (type === TimerType.CountUp) { if (type === TimerType.CountUp) {
return <IoCaretUp className={className} />; return <IoArrowUp className={className} />;
} }
if (type === TimerType.Clock) { if (type === TimerType.Clock) {
return <IoTime className={className} />; return <IoTime className={className} />;
} }
return <IoCaretDown className={className} />; return <IoArrowDown className={className} />;
} }
@@ -1,6 +1,6 @@
import { memo } from 'react'; import { memo } from 'react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; 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 { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle'; import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline'; import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
@@ -16,6 +16,13 @@ const blockBtnStyle = {
size: 'sm', size: 'sm',
}; };
type StyleVariant = {
'aria-label': string;
tooltip: string;
backgroundColor: string;
_hover: { backgroundColor?: string };
};
const tooltipProps = { const tooltipProps = {
openDelay: tooltipDelayMid, openDelay: tooltipDelayMid,
}; };
@@ -24,17 +31,55 @@ interface EventBlockPlaybackProps {
eventId: string; eventId: string;
skip: boolean; skip: boolean;
isPlaying: boolean; isPlaying: boolean;
isPaused: boolean;
selected: boolean; selected: boolean;
disablePlayback: boolean;
} }
const EventBlockPlayback = (props: EventBlockPlaybackProps) => { const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
const { eventId, skip, isPlaying, selected } = props; const { eventId, skip, isPlaying, isPaused, selected, disablePlayback } = props;
const { updateEvent } = useEventAction(); const { updateEvent } = useEventAction();
const toggleSkip = () => { const toggleSkip = () => {
updateEvent({ id: eventId, skip: !skip }); 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 ( return (
<div className={style.playbackActions}> <div className={style.playbackActions}>
<TooltipActionBtn <TooltipActionBtn
@@ -55,7 +100,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
aria-label='Load event' aria-label='Load event'
tooltip='Load event' tooltip='Load event'
icon={<IoReload className={style.flip} />} icon={<IoReload className={style.flip} />}
isDisabled={skip} isDisabled={disablePlayback}
{...tooltipProps} {...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)} clickHandler={() => setEventPlayback.loadEvent(eventId)}
@@ -65,13 +110,12 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
variant='ontime-subtle-white' variant='ontime-subtle-white'
aria-label='Start event' aria-label='Start event'
tooltip='Start event' tooltip='Start event'
icon={isPlaying ? <IoPlay /> : <IoPlayOutline />} icon={!isPlaying ? <IoPlay /> : <IoPause />}
isDisabled={skip} isDisabled={disablePlayback}
{...tooltipProps} {...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)} {...buttonVariant}
backgroundColor={isPlaying ? '#58A151' : undefined} clickHandler={actionHandler}
_hover={{ backgroundColor: isPlaying ? '#58A151' : undefined }}
tabIndex={-1} tabIndex={-1}
/> />
</div> </div>
@@ -1,12 +1,10 @@
@use '../../../../theme/v2Styles' as *; @use '../../../../theme/v2Styles' as *;
.progressBar { .progressBar {
// layout
height: 100%; height: 100%;
width: 0; width: 0;
border-radius: 1px 0 0 1px; border-radius: 1px 0 0 1px;
// animations
transition: 1s linear; transition: 1s linear;
transition-property: width; transition-property: width;
@@ -14,6 +12,10 @@
background-color: $playback-start; background-color: $playback-start;
} }
&.overtime {
background-color: $playback-negative;
}
&.pause { &.pause {
background-color: $ontime-paused; background-color: $ontime-paused;
} }
@@ -21,8 +23,4 @@
&.roll { &.roll {
background-color: $ontime-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 TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction'; 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 { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss'; import style from '../EventBlock.module.scss';
@@ -64,8 +64,9 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
[timeEnd, timeStart], [timeEnd, timeStart],
); );
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`; const delayedStart = Math.max(0, timeStart + delay);
const newTime = millisToString(timeStart + delay); const newTime = millisToString(delayedStart);
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
return ( return (
<div className={style.eventTimers}> <div className={style.eventTimers}>
@@ -94,13 +95,14 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
submitHandler={handleSubmit} submitHandler={handleSubmit}
validationHandler={handleValidation} validationHandler={handleValidation}
time={duration} time={duration}
delay={0}
placeholder='Duration' placeholder='Duration'
previousEnd={previousEnd} previousEnd={previousEnd}
warning={warning.duration} warning={warning.duration}
/> />
{delay !== 0 && delay !== null && ( {delayTime && (
<div className={style.delayNote}> <div className={style.delayNote}>
{`${delayTime} minutes`} {delayTime}
<br /> <br />
{`New start: ${newTime}`} {`New start: ${newTime}`}
</div> </div>
@@ -10,11 +10,7 @@ import {
useSensor, useSensor,
useSensors, useSensors,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TableSettingsContext } from '../../common/context/TableSettingsContext'; import { TableSettingsContext } from '../../common/context/TableSettingsContext';
@@ -148,7 +144,7 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
if (el) { if (el) {
el.scrollIntoView({ el.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start', block: 'center',
inline: 'nearest', inline: 'nearest',
}); });
} }
@@ -119,7 +119,7 @@
grid-area: schedule; grid-area: schedule;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
margin-left: 16px; margin-left: clamp(16px, 5vw, 64px);;
} }
.schedule-nav-container { .schedule-nav-container {
@@ -131,6 +131,8 @@
grid-area: info; grid-area: info;
display: flex; display: flex;
gap: max(1vw, 16px); gap: max(1vw, 16px);
align-self: flex-end;
overflow: hidden;
&__message { &__message {
font-size: clamp(16px, 1.5vw, 24px); font-size: clamp(16px, 1.5vw, 24px);
@@ -141,7 +143,7 @@
} }
.qr { .qr {
margin-left: auto; margin-left: clamp(16px, 5vw, 64px);;
padding: 4px; padding: 4px;
background-color: white; background-color: white;
} }
@@ -99,7 +99,8 @@
grid-area: info; grid-area: info;
display: flex; display: flex;
gap: max(1vw, 16px); gap: max(1vw, 16px);
min-height: max(calc(100vh / 15), 128px); align-self: flex-end;
overflow: hidden;
&__message { &__message {
font-size: clamp(16px, 1.5vw, 24px); font-size: clamp(16px, 1.5vw, 24px);
@@ -110,7 +111,7 @@
} }
.qr { .qr {
margin-left: auto; margin-left: clamp(16px, 5vw, 64px);;
padding: 4px; padding: 4px;
background-color: white; background-color: white;
} }
@@ -6,6 +6,7 @@ $white-7: rgba(255, 255, 255, 0.07);
$white-9: rgba(255, 255, 255, 0.09); $white-9: rgba(255, 255, 255, 0.09);
$white-10: rgba(255, 255, 255, 0.10); $white-10: rgba(255, 255, 255, 0.10);
$white-13: rgba(255, 255, 255, 0.13); $white-13: rgba(255, 255, 255, 0.13);
$white-20: rgba(255, 255, 255, 0.20);
$black-10: rgba(0, 0, 0, 0.10); $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 border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
_hover: { _hover: {
backgroundColor: '#404040', // $gray-1000 backgroundColor: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#2d2d2d', // $gray-1100
},
}, },
_active: { _active: {
backgroundColor: '#2d2d2d', // $gray-1100 backgroundColor: '#2d2d2d', // $gray-1100
@@ -34,6 +37,9 @@ export const ontimeButtonSubtle = {
border: '1px solid transparent', border: '1px solid transparent',
_hover: { _hover: {
background: '#404040', // $gray-1000 background: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#303030', // $gray-1050
},
}, },
_active: { _active: {
backgroundColor: '#2d2d2d', // $gray-1100 backgroundColor: '#2d2d2d', // $gray-1100
@@ -47,6 +53,9 @@ export const ontimeButtonSubtleOnLight = {
border: '1px solid transparent', border: '1px solid transparent',
_hover: { _hover: {
backgroundColor: '#cfcfcf', // $gray-200 backgroundColor: '#cfcfcf', // $gray-200
_disabled: {
backgroundColor: '#ececec', // $gray-100
},
}, },
_active: { _active: {
backgroundColor: '#ececec', // $gray-200 backgroundColor: '#ececec', // $gray-200
@@ -60,6 +69,9 @@ export const ontimeGhostOnLight = {
_hover: { _hover: {
color: '#595959', // $gray-800 color: '#595959', // $gray-800
backgroundColor: '#ececec', // $gray-200 backgroundColor: '#ececec', // $gray-200
_disabled: {
backgroundColor: 'transparent',
},
}, },
_active: { _active: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
+5 -5
View File
@@ -1,21 +1,21 @@
export const ontimeCheckboxOnDark = { export const ontimeCheckboxOnDark = {
control: { control: {
border: '1px', border: '1px',
borderColor: '#2d2d2d', // $gray-1100 borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100 backgroundColor: '#2d2d2d', // $gray-1100
_checked: { _checked: {
borderColor: '#3182ce', // $action-blue borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue backgroundColor: '#3182ce', //$action-blue
}, },
_focus: { _focus: {
boxShadow: '0 0 0 1px #578AF4' boxShadow: '0 0 0 1px #578AF4', // $blue-500
} },
}, },
label: { label: {
fontWeight: '200', fontWeight: '200',
color: '#9d9d9d', // $gray-500 color: '#9d9d9d', // $gray-500
_checked: { _checked: {
color: '#cfcfcf', // $gray-300 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 { ontimeEditable } from './ontimeEditable';
import { ontimeMenuOnDark } from './ontimeMenu'; import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeModal } from './ontimeModal'; import { ontimeModal } from './ontimeModal';
import { ontimeBlockRadio } from './ontimeRadio';
import { ontimeSelect } from './ontimeSelect'; import { ontimeSelect } from './ontimeSelect';
import { lightSwitch, ontimeSwitch } from './ontimeSwitch'; import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
import { ontimeTab } from './ontimeTab'; import { ontimeTab } from './ontimeTab';
@@ -65,6 +66,11 @@ const theme = extendTheme({
ontime: { ...ontimeModal }, ontime: { ...ontimeModal },
}, },
}, },
Radio: {
variants: {
'ontime-block': { ...ontimeBlockRadio },
},
},
Tabs: { Tabs: {
variants: { variants: {
ontime: { ...ontimeTab }, ontime: { ...ontimeTab },
-2
View File
@@ -5,8 +5,6 @@
"version": "2.0.0-beta2", "version": "2.0.0-beta2",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@sentry/node": "^7.47.0",
"@sentry/tracing": "^7.47.0",
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
+2 -7
View File
@@ -6,7 +6,6 @@ import cors from 'cors';
// import utils // import utils
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { initSentry, reportSentryException } from './modules/sentry.js';
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js'; import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { OSCSettings } from 'ontime-types'; import { OSCSettings } from 'ontime-types';
@@ -39,8 +38,6 @@ if (!isProduction) {
console.log(`Ontime directory at ${currentDirectory} `); console.log(`Ontime directory at ${currentDirectory} `);
} }
initSentry(isProduction);
// Create express APP // Create express APP
const app = express(); const app = express();
app.disable('x-powered-by'); 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('exit', (code) => console.log(`Ontime exited with code: ${code}`));
process.on('unhandledRejection', async (error) => { process.on('unhandledRejection', async () => {
reportSentryException(error);
logger.error('SERVER', 'Error: unhandled rejection'); logger.error('SERVER', 'Error: unhandled rejection');
await shutdown(1); await shutdown(1);
}); });
process.on('uncaughtException', async (error) => { process.on('uncaughtException', async () => {
reportSentryException(error);
logger.error('SERVER', 'Error: uncaught exception'); logger.error('SERVER', 'Error: uncaught exception');
await shutdown(1); await shutdown(1);
}); });
@@ -260,9 +260,11 @@ export class EventLoader {
* Handle side effects from event loading * Handle side effects from event loading
*/ */
private _loadEvent() { private _loadEvent() {
eventStore.set('loaded', this.loaded); eventStore.batchSet({
eventStore.set('titles', this.titles); loaded: this.loaded,
eventStore.set('titlesPublic', this.titlesPublic); 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 { validateFile } from '../utils/parserUtils.js';
import { dbModel } from '../models/dataModel.js'; import { dbModel } from '../models/dataModel.js';
import { parseJson } from '../utils/parser.js'; import { parseJson } from '../utils/parser.js';
import { reportSentryException } from './sentry.js';
import { pathToStartDb, resolveDbDirectory, resolveDbPath } from '../setup.js'; import { pathToStartDb, resolveDbDirectory, resolveDbPath } from '../setup.js';
/** /**
@@ -22,8 +21,8 @@ const populateDb = () => {
if (!existsSync(dbInDisk)) { if (!existsSync(dbInDisk)) {
try { try {
copyFileSync(pathToStartDb, dbInDisk); copyFileSync(pathToStartDb, dbInDisk);
} catch (error) { } catch (_) {
reportSentryException(error); /* we do not handle this */
} }
} }
+2 -3
View File
@@ -1,7 +1,6 @@
import { copyFileSync, existsSync } from 'fs'; import { copyFileSync, existsSync } from 'fs';
import { pathToStartStyles, resolveStylesDirectory, resolveStylesPath } from '../setup.js'; import { pathToStartStyles, resolveStylesDirectory, resolveStylesPath } from '../setup.js';
import { ensureDirectory } from '../utils/fileManagement.js'; import { ensureDirectory } from '../utils/fileManagement.js';
import { reportSentryException } from './sentry.js';
/** /**
* @description ensures directories exist and populates stylesheet * @description ensures directories exist and populates stylesheet
@@ -15,8 +14,8 @@ export const populateStyles = () => {
if (!existsSync(stylesInDisk)) { if (!existsSync(stylesInDisk)) {
try { try {
copyFileSync(pathToStartStyles, stylesInDisk); copyFileSync(pathToStartStyles, stylesInDisk);
} catch (error) { } catch (_) {
reportSentryException(error); /* 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 { generateId } from 'ontime-utils';
import { DataProvider } from '../classes/data-provider/DataProvider.js'; import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { block as blockDef, delay as delayDef, event as eventDef } from '../models/eventsDefinition.js'; import {
block as blockDef,
delay as delayDef,
event as eventDef,
} from '../models/eventsDefinition.js';
import { MAX_EVENTS } from '../settings.js'; import { MAX_EVENTS } from '../settings.js';
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js'; import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer } from './TimerService.js'; import { eventTimer } from './TimerService.js';
@@ -216,30 +226,27 @@ export async function reorderEvent(eventId, from, to) {
* @param eventId * @param eventId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function applyDelay(eventId) { export async function applyDelay(eventId: string) {
const rundown = DataProvider.getRundown(); const rundown = DataProvider.getRundown();
let delayIndex = null; let delayIndex = null;
let delayValue = 0; let delayValue = 0;
for (const [index, e] of rundown.entries()) { for (const [index, event] of rundown.entries()) {
// look for delay // look for delay
if (delayIndex === null) { if (delayIndex === null) {
if (e.id === eventId && e.type === SupportedEvent.Delay) { if (event.id === eventId && event.type === SupportedEvent.Delay) {
delayValue = e.duration; delayValue = event.duration;
delayIndex = index; delayIndex = index;
} }
} }
// apply delay value to all items until block or end // apply delay value to all items until block or end
else { else {
if (e.type === SupportedEvent.Event) { if (event.type === SupportedEvent.Event) {
// update times event.timeStart = Math.max(0, event.timeStart + delayValue);
e.timeStart += delayValue; event.timeEnd = Math.max(event.duration, event.timeStart + delayValue);
e.timeEnd += delayValue; event.revision += 1;
} else if (event.type === SupportedEvent.Block) {
// increment revision
e.revision += 1;
} else if (e.type === SupportedEvent.Block) {
break; break;
} }
} }
+16 -8
View File
@@ -141,8 +141,10 @@ export class TimerService {
* @private * @private
*/ */
_onLoad() { _onLoad() {
eventStore.set('playback', this.playback); eventStore.batchSet({
eventStore.set('timer', this.timer); playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onLoad); integrationService.dispatch(TimerLifeCycle.onLoad);
} }
@@ -182,8 +184,10 @@ export class TimerService {
* @private * @private
*/ */
_onStart() { _onStart() {
eventStore.set('playback', this.playback); eventStore.batchSet({
eventStore.set('timer', this.timer); playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onStart); integrationService.dispatch(TimerLifeCycle.onStart);
} }
@@ -199,8 +203,10 @@ export class TimerService {
} }
_onPause() { _onPause() {
eventStore.set('playback', this.playback); eventStore.batchSet({
eventStore.set('timer', this.timer); playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onPause); integrationService.dispatch(TimerLifeCycle.onPause);
} }
@@ -214,8 +220,10 @@ export class TimerService {
} }
_onStop() { _onStop() {
eventStore.set('playback', this.playback); eventStore.batchSet({
eventStore.set('timer', this.timer); playback: this.playback,
timer: this.timer,
});
integrationService.dispatch(TimerLifeCycle.onStop); 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 * 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 = { export const eventStore = {
init(payload: RuntimeStore) { init(payload: RuntimeStore) {
@@ -25,6 +29,12 @@ export const eventStore = {
// }); // });
this.broadcast(); this.broadcast();
}, },
batchSet<K extends keyof RuntimeStore>(values: Record<K, RuntimeStore[K]>) {
Object.entries(values).forEach(([key, value]) => {
store[key] = value;
});
this.broadcast();
},
poll() { poll() {
return store; 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 { millisToString } from './src/date-utils/millisToString.js';
export { generateId } from './src/generate-id/generateId.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 eslint-plugin-testing-library: ^5.9.1
framer-motion: ^10.10.0 framer-motion: ^10.10.0
jsdom: ^21.1.0 jsdom: ^21.1.0
luxon: ^3.3.0
ontime-types: workspace:* ontime-types: workspace:*
ontime-utils: workspace:* ontime-utils: workspace:*
prettier: ^2.8.3 prettier: ^2.8.3
@@ -112,7 +111,6 @@ importers:
csv-stringify: 6.2.3 csv-stringify: 6.2.3
deepmerge: 4.3.0 deepmerge: 4.3.0
framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y
luxon: 3.3.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0 react-fast-compare: 3.2.0
@@ -177,8 +175,6 @@ importers:
apps/server: apps/server:
specifiers: specifiers:
'@sentry/node': ^7.47.0
'@sentry/tracing': ^7.47.0
'@types/express': ^4.17.17 '@types/express': ^4.17.17
'@types/node': ^16.11.7 '@types/node': ^16.11.7
'@types/node-osc': ^6.0.0 '@types/node-osc': ^6.0.0
@@ -210,8 +206,6 @@ importers:
vitest: ^0.29.8 vitest: ^0.29.8
ws: ^8.13.0 ws: ^8.13.0
dependencies: dependencies:
'@sentry/node': 7.47.0
'@sentry/tracing': 7.47.0
body-parser: 1.20.1 body-parser: 1.20.1
cors: 2.8.5 cors: 2.8.5
dotenv: 16.0.3 dotenv: 16.0.3
@@ -2497,6 +2491,7 @@ packages:
tslib: 1.14.1 tslib: 1.14.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/@sentry/react/7.47.0_react@18.2.0: /@sentry/react/7.47.0_react@18.2.0:
resolution: {integrity: sha512-Qy6OnlE8FivKOLo0YE7tkr+G5fLmEOkpPxj179wbY/N8kp/ALkqbVdcOrZW7AL6HCc0lphhj+0SB+tpwoPEsiQ==} resolution: {integrity: sha512-Qy6OnlE8FivKOLo0YE7tkr+G5fLmEOkpPxj179wbY/N8kp/ALkqbVdcOrZW7AL6HCc0lphhj+0SB+tpwoPEsiQ==}
@@ -2527,13 +2522,6 @@ packages:
dependencies: dependencies:
'@sentry-internal/tracing': 7.46.0 '@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: /@sentry/types/7.46.0:
resolution: {integrity: sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g==} resolution: {integrity: sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -3484,6 +3472,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/ajv-keywords/3.5.2_ajv@6.12.6: /ajv-keywords/3.5.2_ajv@6.12.6:
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
@@ -4352,6 +4341,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
dev: true
/decamelize-keys/1.1.1: /decamelize-keys/1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
@@ -5772,6 +5762,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/iconv-corefoundation/1.1.7: /iconv-corefoundation/1.1.7:
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
@@ -6392,6 +6383,7 @@ packages:
/lru_map/0.3.3: /lru_map/0.3.3:
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
dev: true
/luxon/3.3.0: /luxon/3.3.0:
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
@@ -6598,6 +6590,7 @@ packages:
/ms/2.1.2: /ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms/2.1.3: /ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}