mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 770d12888d | |||
| 94a1369d64 | |||
| 5ba27dca1d |
+1
-1
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+4
-6
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 .",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export const MAX_EVENTS = 255;
|
||||
@@ -0,0 +1 @@
|
||||
export const MAX_EVENTS = 32768;
|
||||
@@ -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,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);
|
||||
}
|
||||
Generated
+6
-13
@@ -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==}
|
||||
|
||||
Reference in New Issue
Block a user