mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 20:09:11 +00:00
V2 beta1 (#326)
* style: labels on added time * style: remove mentions of PiP * refactor: unify usage of ms for timers * refactor: create events with 0 duration * style: several small tweaks * refactor: keep block when applying delays * feat: blocks have titles * style: improvements in time entry warnings * style: override progress bar styles * style: prevent overflow * feat: show character count in editor * refactor: provide initial payload * refactor: lower test boundary * refactor: get colour from swatches * style: rename title block * chore: version bump
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.0.0-beta1",
|
"version": "2.0.0-beta2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.5.1",
|
"@chakra-ui/react": "^2.5.1",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const minimalLocation = 'minimal';
|
|||||||
const speakerLocation = 'speaker';
|
const speakerLocation = 'speaker';
|
||||||
const smLocation = 'sm';
|
const smLocation = 'sm';
|
||||||
const publicLocation = 'public';
|
const publicLocation = 'public';
|
||||||
const pipLocation = 'pip';
|
|
||||||
const studioLocation = 'studio';
|
const studioLocation = 'studio';
|
||||||
const cuesheetLocation = 'cuesheet';
|
const cuesheetLocation = 'cuesheet';
|
||||||
const countdownLocation = 'countdown';
|
const countdownLocation = 'countdown';
|
||||||
@@ -17,7 +16,6 @@ export const viewerLocations = [
|
|||||||
{ link: smLocation, label: 'Backstage screen' },
|
{ link: smLocation, label: 'Backstage screen' },
|
||||||
{ link: publicLocation, label: 'Public screen' },
|
{ link: publicLocation, label: 'Public screen' },
|
||||||
{ link: lowerLocation, label: 'Lower thirds' },
|
{ link: lowerLocation, label: 'Lower thirds' },
|
||||||
{ link: pipLocation, label: 'Picture in Picture' },
|
|
||||||
{ link: studioLocation, label: 'Studio clock' },
|
{ link: studioLocation, label: 'Studio clock' },
|
||||||
{ link: countdownLocation, label: 'Countdown' },
|
{ link: countdownLocation, label: 'Countdown' },
|
||||||
{ link: cuesheetLocation, label: 'Cuesheet' },
|
{ link: cuesheetLocation, label: 'Cuesheet' },
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
input[type="color"] {
|
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Input } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
|
||||||
|
|
||||||
import style from './ColourInput.module.scss';
|
|
||||||
|
|
||||||
interface ColourInputProps {
|
|
||||||
value: string;
|
|
||||||
name: EventEditorSubmitActions;
|
|
||||||
handleChange: (newValue: EventEditorSubmitActions, name: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ColourInput(props: ColourInputProps) {
|
|
||||||
const { value, name, handleChange } = props;
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
className={style.colourInput}
|
|
||||||
type='color'
|
|
||||||
value={value}
|
|
||||||
onChange={(event) => handleChange(name, event.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
|
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './SwatchSelect.module.scss';
|
||||||
|
|
||||||
|
interface SwatchProps {
|
||||||
|
color: string;
|
||||||
|
onClick: (color: string) => void;
|
||||||
|
isSelected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Swatch(props: SwatchProps) {
|
||||||
|
const { color, isSelected, onClick } = props;
|
||||||
|
|
||||||
|
const classes = cx([style.swatch, isSelected ? style.selected : null]);
|
||||||
|
|
||||||
|
if (!color) {
|
||||||
|
return (
|
||||||
|
<div className={`${classes} ${style.center}`}>
|
||||||
|
<IoBan />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={() => onClick(color)} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
cursor: pointer;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 4px solid #262626;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border: 2px solid #578AF4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
color: #578AF4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
||||||
|
|
||||||
|
import Swatch from './Swatch';
|
||||||
|
|
||||||
|
import style from './SwatchSelect.module.scss';
|
||||||
|
|
||||||
|
interface ColourInputProps {
|
||||||
|
value: string;
|
||||||
|
name: EventEditorSubmitActions;
|
||||||
|
handleChange: (newValue: EventEditorSubmitActions, name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colours = [
|
||||||
|
'',
|
||||||
|
'#FFCC78', // $orange-400
|
||||||
|
'#FFAB33', // $orange-600
|
||||||
|
'#77C785', // $green-400
|
||||||
|
'#339E4E', // $green-600
|
||||||
|
'#779BE7', // $blue-400
|
||||||
|
'#3E75E8', // $blue-600
|
||||||
|
'#FF7878', // $red-400
|
||||||
|
'#ED3333', // $red-600
|
||||||
|
'#A790F5', // $violet-400
|
||||||
|
'#8064E1', // $violet-600
|
||||||
|
'#9d9d9d', // $gray-500
|
||||||
|
'#ececec', // $gray-100
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SwatchSelect(props: ColourInputProps) {
|
||||||
|
const { value, name, handleChange } = props;
|
||||||
|
|
||||||
|
const setColour = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
if (newValue !== value) {
|
||||||
|
handleChange(name, newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleChange, name, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.list}>
|
||||||
|
{colours.map((colour) => (
|
||||||
|
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ const inputStyleProps = {
|
|||||||
size: 'sm',
|
size: 'sm',
|
||||||
color: '#E69056',
|
color: '#E69056',
|
||||||
variant: 'ontime-filled',
|
variant: 'ontime-filled',
|
||||||
|
fontSize: '15px',
|
||||||
|
letterSpacing: '0.3px',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DelayInputProps {
|
interface DelayInputProps {
|
||||||
@@ -24,7 +26,9 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value == null) return;
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
@@ -36,7 +40,6 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
(newValue?: string) => {
|
(newValue?: string) => {
|
||||||
if (newValue === '') setValue(0);
|
if (newValue === '') setValue(0);
|
||||||
const delayValue = clamp(Number(newValue), -60, 60);
|
const delayValue = clamp(Number(newValue), -60, 60);
|
||||||
|
|
||||||
if (delayValue === value) return;
|
if (delayValue === value) return;
|
||||||
setValue(delayValue);
|
setValue(delayValue);
|
||||||
|
|
||||||
@@ -49,15 +52,18 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
* @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((key: string) => {
|
const onKeyDownHandler = useCallback(
|
||||||
if (key === 'Enter') {
|
(key: string) => {
|
||||||
inputRef.current?.blur();
|
if (key === 'Enter') {
|
||||||
validate(inputRef.current?.value);
|
inputRef.current?.blur();
|
||||||
} else if (key === 'Escape') {
|
validate(inputRef.current?.value);
|
||||||
inputRef.current?.blur();
|
} else if (key === 'Escape') {
|
||||||
setValue(value);
|
inputRef.current?.blur();
|
||||||
}
|
setValue(value);
|
||||||
}, [validate, value]);
|
}
|
||||||
|
},
|
||||||
|
[validate, value],
|
||||||
|
);
|
||||||
|
|
||||||
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
|
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
|
||||||
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
|
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ interface BaseProps {
|
|||||||
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
|
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextAreaProps {
|
interface TextInputProps extends BaseProps {
|
||||||
|
isTextArea?: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextAreaProps extends BaseProps {
|
||||||
isTextArea: true;
|
isTextArea: true;
|
||||||
resize?: 'horizontal' | 'vertical' | 'none';
|
resize?: 'horizontal' | 'vertical' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextInputProps = BaseProps & TextAreaProps;
|
type InputProps = TextInputProps | TextAreaProps;
|
||||||
|
|
||||||
export default function TextInput(props: TextInputProps) {
|
export default function TextInput(props: InputProps) {
|
||||||
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler, resize = 'none' } = props;
|
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
@@ -31,6 +35,11 @@ export default function TextInput(props: TextInputProps) {
|
|||||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||||
|
|
||||||
|
let resize = 'none';
|
||||||
|
if (isTextArea) {
|
||||||
|
resize = (props as TextAreaProps)?.resize ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
return isTextArea ? (
|
return isTextArea ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -14,8 +14,7 @@ export default function useReactiveTextInput(
|
|||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof initialText === 'undefined') {
|
if (typeof initialText === 'undefined') {
|
||||||
@@ -58,7 +57,6 @@ export default function useReactiveTextInput(
|
|||||||
[initialText, submitCallback],
|
[initialText, submitCallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Handles common keys for submit and cancel
|
* @description Handles common keys for submit and cancel
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
@@ -81,8 +79,8 @@ export default function useReactiveTextInput(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
onChange: (event) => handleChange((event.target as HTMLInputElement).value),
|
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
||||||
onBlur: (event) => handleSubmit((event.target as HTMLInputElement).value),
|
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
||||||
onKeyDown: (event) => keyHandler(event.key),
|
onKeyDown: (event: KeyboardEvent) => keyHandler(event.key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
@use "../../../../theme/v2Styles" as *;
|
||||||
|
|
||||||
$input-font-size: 15px;
|
$input-font-size: 15px;
|
||||||
$input-delayed-border-color: #E69056;
|
$input-delayed-border-color: #E69056;
|
||||||
|
|
||||||
.timeInput {
|
.timeInput {
|
||||||
width: fit-content !important;
|
width: fit-content !important;
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
max-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft,
|
||||||
.inputButton {
|
.inputButton {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
@@ -15,6 +22,13 @@ $input-delayed-border-color: #E69056;
|
|||||||
padding: 0 0 0 2.6em;
|
padding: 0 0 0 2.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
&::after {
|
||||||
|
content: "*";
|
||||||
|
color: $warning-orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.delayed {
|
&.delayed {
|
||||||
.inputField {
|
.inputField {
|
||||||
border: 1px solid $input-delayed-border-color;
|
border: 1px solid $input-delayed-border-color;
|
||||||
|
|||||||
@@ -2,26 +2,27 @@ import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } f
|
|||||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
|
||||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||||
import { useEmitLog } from '../../../stores/logger';
|
import { useEmitLog } from '../../../stores/logger';
|
||||||
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
import { TimeEntryField } from '../../../utils/timesManager';
|
import { TimeEntryField } from '../../../utils/timesManager';
|
||||||
|
|
||||||
import style from './TimeInput.module.scss';
|
import style from './TimeInput.module.scss';
|
||||||
|
|
||||||
interface TimeInputProps {
|
interface TimeInputProps {
|
||||||
name: TimeEntryField;
|
name: TimeEntryField;
|
||||||
submitHandler: (field: EventEditorSubmitActions, value: number) => void;
|
submitHandler: (field: TimeEntryField, value: number) => void;
|
||||||
time?: number;
|
time?: number;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
validationHandler: (entry: TimeEntryField, val: number) => boolean;
|
validationHandler: (entry: TimeEntryField, val: number) => boolean;
|
||||||
previousEnd?: number;
|
previousEnd?: number;
|
||||||
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimeInput(props: TimeInputProps) {
|
export default function TimeInput(props: TimeInputProps) {
|
||||||
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0 } = 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('');
|
||||||
@@ -30,7 +31,6 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
* @description Resets input value to given
|
* @description Resets input value to given
|
||||||
*/
|
*/
|
||||||
const resetValue = useCallback(() => {
|
const resetValue = useCallback(() => {
|
||||||
// Todo: check if change is necessary
|
|
||||||
try {
|
try {
|
||||||
setValue(millisToString(time + delay));
|
setValue(millisToString(time + delay));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -149,20 +149,23 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ButtonTooltip = () => {
|
const ButtonTooltip = () => {
|
||||||
if (name === 'timeStart') return 'Start';
|
if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`;
|
||||||
if (name === 'timeEnd') return 'End';
|
if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`;
|
||||||
if (name === 'durationOverride') return 'Duration';
|
if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`;
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
|
||||||
|
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
|
<InputGroup size='sm' className={inputClasses}>
|
||||||
<InputLeftElement width='fit-content'>
|
<InputLeftElement className={style.inputLeft}>
|
||||||
<Tooltip label={ButtonTooltip()} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
<Tooltip label={ButtonTooltip()} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
className={`${style.inputButton} ${isDelayed ? style.delayed : ''}`}
|
className={buttonClasses}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
||||||
borderRight='1px solid transparent'
|
borderRight='1px solid transparent'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ $progress-bar-br: 6px;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: $progress-bar-size;
|
height: $progress-bar-size;
|
||||||
border-radius: $progress-bar-br;
|
border-radius: $progress-bar-br;
|
||||||
background-color: var(--card-background-color-override, $viewer-card-bg-color);
|
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -17,7 +17,7 @@ $progress-bar-br: 6px;
|
|||||||
.progress-bar__indicator {
|
.progress-bar__indicator {
|
||||||
height: $progress-bar-size;
|
height: $progress-bar-size;
|
||||||
border-radius: $progress-bar-br;
|
border-radius: $progress-bar-br;
|
||||||
background-color: var(--accent-color-override, $accent-color);
|
background-color: var(--timer-progress-override, $accent-color);
|
||||||
transition: 1s linear;
|
transition: 1s linear;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ export default function ProgressBar(props: ProgressBarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`progress-bar__bg ${hidden ? 'progress-bar__bg--hidden' : ''} ${className}`}>
|
<div className={`progress-bar__bg ${hidden ? 'progress-bar__bg--hidden' : ''} ${className}`}>
|
||||||
<div
|
<div className='progress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||||
className='progress-bar__indicator'
|
|
||||||
style={{ width: `${percentComplete}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { formatDisplay, millisToSeconds } from '../../utils/dateConfig';
|
import { formatDisplay } from '../../utils/dateConfig';
|
||||||
|
|
||||||
import './TimerDisplay.scss';
|
import './TimerDisplay.scss';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const TimerDisplay = (props: TimerDisplayProps) => {
|
|||||||
if (time === null || typeof time === 'undefined' || isNaN(time)) {
|
if (time === null || typeof time === 'undefined' || isNaN(time)) {
|
||||||
display = '-- : -- : --';
|
display = '-- : -- : --';
|
||||||
} else {
|
} else {
|
||||||
display = formatDisplay(millisToSeconds(time));
|
display = formatDisplay(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNegative = (time ?? 0) < 0;
|
const isNegative = (time ?? 0) < 0;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const useEventAction = () => {
|
|||||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
||||||
if (typeof previousEvent !== 'undefined' && previousEvent.type === 'event') {
|
if (typeof previousEvent !== 'undefined' && previousEvent.type === 'event') {
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
|
newEvent.timeEnd = previousEvent.timeEnd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ describe('test string from formatDisplay function', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test with valid millis', () => {
|
it('test with valid millis', () => {
|
||||||
const t = { val: 3600, result: '01:00:00' };
|
const t = { val: 3600000, result: '01:00:00' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with negative millis', () => {
|
it('test with negative millis', () => {
|
||||||
const t = { val: -3600, result: '01:00:00' };
|
const t = { val: -3600000, result: '01:00:00' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,17 +39,17 @@ describe('test string from formatDisplay function', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test with 86400 (24 hours)', () => {
|
it('test with 86400 (24 hours)', () => {
|
||||||
const t = { val: 86400, result: '00:00:00' };
|
const t = { val: 86400000, result: '00:00:00' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with 86401 (24 hours and 1 second)', () => {
|
it('test with 86401 (24 hours and 1 second)', () => {
|
||||||
const t = { val: 86401, result: '00:00:01' };
|
const t = { val: 86401000, result: '00:00:01' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||||
const t = { val: -86401, result: '00:00:01' };
|
const t = { val: -86401000, result: '00:00:01' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,12 +61,12 @@ describe('test string from formatDisplay function with hidezero', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test with valid millis', () => {
|
it('test with valid millis', () => {
|
||||||
const t = { val: 3600, result: '01:00:00' };
|
const t = { val: 3600000, result: '01:00:00' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with negative millis', () => {
|
it('test with negative millis', () => {
|
||||||
const t = { val: -3600, result: '01:00:00' };
|
const t = { val: -3600000, result: '01:00:00' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,17 +81,17 @@ describe('test string from formatDisplay function with hidezero', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test with 86400 (24 hours)', () => {
|
it('test with 86400 (24 hours)', () => {
|
||||||
const t = { val: 86400, result: '00:00' };
|
const t = { val: 86400000, result: '00:00' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with 86401 (24 hours and 1 second)', () => {
|
it('test with 86401 (24 hours and 1 second)', () => {
|
||||||
const t = { val: 86401, result: '00:01' };
|
const t = { val: 86401000, result: '00:01' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||||
const t = { val: -86401, result: '00:01' };
|
const t = { val: -86401000, result: '00:01' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ export const timeFormatSeconds = 'HH:mm:ss';
|
|||||||
/**
|
/**
|
||||||
* another go at simpler string formatting (counters)
|
* another go at simpler string formatting (counters)
|
||||||
* @description Converts seconds to string representing time
|
* @description Converts seconds to string representing time
|
||||||
* @param {number | null} seconds - time in seconds
|
* @param {number | null} milliseconds - time in seconds
|
||||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||||
* @returns {string} String representing absolute time 00:12:02
|
* @returns {string} String representing absolute time 00:12:02
|
||||||
*/
|
*/
|
||||||
export function formatDisplay(seconds: number | null, hideZero = false): string {
|
export function formatDisplay(milliseconds: number | null, hideZero = false): string {
|
||||||
if (typeof seconds !== 'number') {
|
if (typeof milliseconds !== 'number') {
|
||||||
return hideZero ? '00:00' : '00:00:00';
|
return hideZero ? '00:00' : '00:00:00';
|
||||||
}
|
}
|
||||||
|
|
||||||
// add an extra 0 if necessary
|
// add an extra 0 if necessary
|
||||||
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
|
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
|
||||||
|
|
||||||
const s = Math.abs(seconds);
|
const s = Math.abs(millisToSeconds(milliseconds));
|
||||||
const hours = Math.floor((s / 3600) % 24);
|
const hours = Math.floor((s / 3600) % 24);
|
||||||
const minutes = Math.floor((s % 3600) / 60);
|
const minutes = Math.floor((s % 3600) / 60);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type TimeEntryField = 'timeStart' |'timeEnd' | 'durationOverride';
|
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Milliseconds in a day
|
* @description Milliseconds in a day
|
||||||
@@ -14,7 +14,12 @@ export const calculateDuration = (start: number, end: number): number =>
|
|||||||
/**
|
/**
|
||||||
* @description Checks which field the value relates to
|
* @description Checks which field the value relates to
|
||||||
*/
|
*/
|
||||||
export const handleTimeEntry = (field: TimeEntryField, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => {
|
export const handleTimeEntry = (
|
||||||
|
field: TimeEntryField,
|
||||||
|
val: number,
|
||||||
|
timeStart: number,
|
||||||
|
timeEnd: number,
|
||||||
|
): { start: number; end: number; durationOverride: boolean } => {
|
||||||
let start = timeStart;
|
let start = timeStart;
|
||||||
let end = timeEnd;
|
let end = timeEnd;
|
||||||
let durationOverride = false;
|
let durationOverride = false;
|
||||||
@@ -32,13 +37,18 @@ export const handleTimeEntry = (field: TimeEntryField, val: number, timeStart: n
|
|||||||
/**
|
/**
|
||||||
* @description Validates time entry
|
* @description Validates time entry
|
||||||
*/
|
*/
|
||||||
export const validateEntry = (field: TimeEntryField, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => {
|
export const validateEntry = (
|
||||||
const validate = { value: true, catch: '' };
|
field: TimeEntryField,
|
||||||
|
value: number,
|
||||||
|
timeStart: number,
|
||||||
|
timeEnd: number,
|
||||||
|
): { value: boolean; warnings: { start?: string; end?: string; duration?: string } } => {
|
||||||
|
const validate = { value: true, warnings: { start: '', end: '', duration: '' } };
|
||||||
|
|
||||||
const { start, end } = handleTimeEntry(field, value, timeStart, timeEnd);
|
const { start, end } = handleTimeEntry(field, value, timeStart, timeEnd);
|
||||||
|
|
||||||
if (end < start) {
|
if (end < start) {
|
||||||
validate.catch = 'Start time later than end time';
|
validate.warnings.start = 'Start time later than end time';
|
||||||
}
|
}
|
||||||
|
|
||||||
return validate;
|
return validate;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { millisToString } from 'ontime-utils';
|
|||||||
|
|
||||||
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
||||||
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
||||||
import { millisToMinutes } from '../../../common/utils/dateConfig';
|
import { millisToMinutes, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
import TapButton from './TapButton';
|
import TapButton from './TapButton';
|
||||||
@@ -30,7 +30,30 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
|||||||
const hasAddedTime = Boolean(timer.addedTime);
|
const hasAddedTime = Boolean(timer.addedTime);
|
||||||
|
|
||||||
const rollLabel = isRolling ? 'Roll mode active' : '';
|
const rollLabel = isRolling ? 'Roll mode active' : '';
|
||||||
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timer.addedTime)} minutes` : '';
|
|
||||||
|
const resolveAddedTimeLabel = () => {
|
||||||
|
function resolveClosestUnit(ms: number) {
|
||||||
|
if (ms < 6000) {
|
||||||
|
return `${millisToSeconds(ms)} seconds`;
|
||||||
|
} else if (ms < 12000) {
|
||||||
|
return `1 minute`;
|
||||||
|
} else {
|
||||||
|
return `${millisToMinutes(ms)} minutes`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer.addedTime > 0) {
|
||||||
|
return `Added ${resolveClosestUnit(timer.addedTime)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer.addedTime < 0) {
|
||||||
|
return `Removed ${resolveClosestUnit(timer.addedTime)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const addedTimeLabel = resolveAddedTimeLabel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.timeContainer}>
|
<div className={style.timeContainer}>
|
||||||
|
|||||||
@@ -64,10 +64,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputLabel {
|
@mixin input-label() {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
display: block;
|
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countedInput {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
@include input-label;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLabel {
|
||||||
|
display: block;
|
||||||
|
@include input-label;
|
||||||
|
|
||||||
.delayLabel {
|
.delayLabel {
|
||||||
color: $ontime-delay-text;
|
color: $ontime-delay-text;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
import { Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
|
||||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||||
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
import SwatchSelect from '../../common/components/input/colour-input/SwatchSelect';
|
||||||
import TextInput from '../../common/components/input/text-input/TextInput';
|
|
||||||
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 useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
@@ -16,6 +14,9 @@ import { millisToMinutes } from '../../common/utils/dateConfig';
|
|||||||
import getDelayTo from '../../common/utils/getDelayTo';
|
import getDelayTo from '../../common/utils/getDelayTo';
|
||||||
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
|
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
|
||||||
|
|
||||||
|
import CountedTextArea from './composite/CountedTextArea';
|
||||||
|
import CountedTextInput from './composite/CountedTextInput';
|
||||||
|
|
||||||
import style from './EventEditor.module.scss';
|
import style from './EventEditor.module.scss';
|
||||||
|
|
||||||
export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
||||||
@@ -24,10 +25,11 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
|||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const { openId } = useEventEditorStore();
|
const { openId } = useEventEditorStore();
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
const { emitWarning, emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
const [delay, setDelay] = useState(0);
|
const [delay, setDelay] = useState(0);
|
||||||
|
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || !openId) {
|
if (!data || !openId) {
|
||||||
@@ -85,16 +87,14 @@ export default function EventEditor() {
|
|||||||
|
|
||||||
const timerValidationHandler = useCallback(
|
const timerValidationHandler = useCallback(
|
||||||
(entry: TimeEntryField, val: number) => {
|
(entry: TimeEntryField, val: number) => {
|
||||||
if (!event) {
|
if (!event?.timeStart) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
|
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
|
||||||
if (!valid.value) {
|
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
|
||||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
|
||||||
}
|
|
||||||
return valid.value;
|
return valid.value;
|
||||||
},
|
},
|
||||||
[event, emitWarning],
|
[event?.timeStart, event?.timeEnd],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
@@ -145,6 +145,7 @@ export default function EventEditor() {
|
|||||||
time={event.timeStart}
|
time={event.timeStart}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
placeholder='Start'
|
placeholder='Start'
|
||||||
|
warning={warning.start}
|
||||||
/>
|
/>
|
||||||
<label className={style.inputLabel}>
|
<label className={style.inputLabel}>
|
||||||
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
|
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
|
||||||
@@ -157,6 +158,7 @@ export default function EventEditor() {
|
|||||||
time={event.timeEnd}
|
time={event.timeEnd}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
placeholder='End'
|
placeholder='End'
|
||||||
|
warning={warning.end}
|
||||||
/>
|
/>
|
||||||
<label className={style.inputLabel}>Duration</label>
|
<label className={style.inputLabel}>Duration</label>
|
||||||
<TimeInput
|
<TimeInput
|
||||||
@@ -165,6 +167,7 @@ export default function EventEditor() {
|
|||||||
validationHandler={timerValidationHandler}
|
validationHandler={timerValidationHandler}
|
||||||
time={event.duration}
|
time={event.duration}
|
||||||
placeholder='Duration'
|
placeholder='Duration'
|
||||||
|
warning={warning.duration}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timeSettings}>
|
<div className={style.timeSettings}>
|
||||||
@@ -202,40 +205,28 @@ export default function EventEditor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.titles}>
|
<div className={style.titles}>
|
||||||
<div className={style.left}>
|
<div className={style.left}>
|
||||||
<div className={style.column}>
|
<CountedTextInput field='title' label='Title' initialValue={event.title} submitHandler={handleSubmit} />
|
||||||
<label className={style.inputLabel}>Title</label>
|
<CountedTextInput
|
||||||
<TextInput field='title' initialText={event.title} submitHandler={handleSubmit} />
|
field='presenter'
|
||||||
</div>
|
label='Presenter'
|
||||||
<div className={style.column}>
|
initialValue={event.presenter}
|
||||||
<label className={style.inputLabel}>Presenter</label>
|
submitHandler={handleSubmit}
|
||||||
<TextInput field='presenter' initialText={event.presenter} submitHandler={handleSubmit} />
|
/>
|
||||||
</div>
|
<CountedTextInput
|
||||||
<div className={style.column}>
|
field='subtitle'
|
||||||
<label className={style.inputLabel}>Subtitle</label>
|
label='Subtitle'
|
||||||
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
|
initialValue={event.subtitle}
|
||||||
</div>
|
submitHandler={handleSubmit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.right}>
|
<div className={style.right}>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<label className={style.inputLabel}>Colour</label>
|
<label className={style.inputLabel}>Colour</label>
|
||||||
<div className={style.inline}>
|
<div className={style.inline}>
|
||||||
<ColourInput name='colour' value={event?.colour} handleChange={handleSubmit} />
|
<SwatchSelect name='colour' value={event.colour} handleChange={handleSubmit} />
|
||||||
<Button leftIcon={<IoBan />} onClick={() => handleSubmit('colour', '')} variant='ontime-subtle' size='sm'>
|
|
||||||
Clear colour
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${style.column} ${style.fullHeight}`}>
|
<CountedTextArea field='note' label='Note' initialValue={event.note} submitHandler={handleSubmit} />
|
||||||
<label className={style.inputLabel}>Note</label>
|
|
||||||
<TextInput
|
|
||||||
field='note'
|
|
||||||
initialText={event.note}
|
|
||||||
submitHandler={handleSubmit}
|
|
||||||
isTextArea
|
|
||||||
isFullHeight
|
|
||||||
resize='none'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Textarea } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
import { EventEditorSubmitActions } from '../EventEditor';
|
||||||
|
|
||||||
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
|
interface CountedTextAreaProps {
|
||||||
|
field: EventEditorSubmitActions;
|
||||||
|
label: string;
|
||||||
|
initialValue: string;
|
||||||
|
submitHandler: (field: EventEditorSubmitActions, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CountedTextArea(props: CountedTextAreaProps) {
|
||||||
|
const { field, label, initialValue, submitHandler } = props;
|
||||||
|
|
||||||
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.column} ${style.fullHeight}`}>
|
||||||
|
<div className={style.countedInput}>
|
||||||
|
<label className={style.inputLabel}>{label}</label>
|
||||||
|
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
size='sm'
|
||||||
|
resize='none'
|
||||||
|
variant='ontime-filled'
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
data-testid='input-textarea'
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
import { EventEditorSubmitActions } from '../EventEditor';
|
||||||
|
|
||||||
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
|
interface CountedTextInputProps {
|
||||||
|
field: EventEditorSubmitActions;
|
||||||
|
label: string;
|
||||||
|
initialValue: string;
|
||||||
|
submitHandler: (field: EventEditorSubmitActions, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CountedTextInput(props: CountedTextInputProps) {
|
||||||
|
const { field, label, initialValue, submitHandler } = props;
|
||||||
|
|
||||||
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, {
|
||||||
|
submitOnEnter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.column}>
|
||||||
|
<div className={style.countedInput}>
|
||||||
|
<label className={style.inputLabel}>{label}</label>
|
||||||
|
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
data-testid='input-textfield'
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,14 +42,14 @@ export default function ViewsSettingsModal() {
|
|||||||
try {
|
try {
|
||||||
await postView(formData);
|
await postView(formData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
emitError(`Error view settings: ${error}`)
|
emitError(`Error view settings: ${error}`);
|
||||||
} finally{
|
} finally {
|
||||||
await refetch();
|
await refetch();
|
||||||
setChanged(false);
|
setChanged(false);
|
||||||
}
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
[emitError, formData, refetch]
|
[emitError, formData, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +72,7 @@ export default function ViewsSettingsModal() {
|
|||||||
setFormData(temp);
|
setFormData(temp);
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
},
|
},
|
||||||
[formData]
|
[formData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,18 +89,30 @@ export default function ViewsSettingsModal() {
|
|||||||
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
|
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
|
||||||
CSS Style Overrides
|
CSS Style Overrides
|
||||||
</span>
|
</span>
|
||||||
This feature allows user defined CSS to override the application stylesheets as a way to
|
This feature allows user defined CSS to override the application stylesheets as a way to customise viewers
|
||||||
customise viewers appearance.
|
appearance.
|
||||||
|
<br />
|
||||||
|
Currently the feature affects the following views
|
||||||
<br />
|
<br />
|
||||||
Currently the feature affects the following views<br />
|
|
||||||
<ul className={style.featureList}>
|
<ul className={style.featureList}>
|
||||||
<li><IoCheckmarkSharp /> Stage timer</li>
|
<li>
|
||||||
<li><IoCheckmarkSharp /> Clock</li>
|
<IoCheckmarkSharp /> Stage timer
|
||||||
<li><IoCheckmarkSharp /> Minimal timer</li>
|
</li>
|
||||||
<li><IoCheckmarkSharp /> Backstage screen</li>
|
<li>
|
||||||
<li><IoCheckmarkSharp /> Public screen</li>
|
<IoCheckmarkSharp /> Clock
|
||||||
<li><IoCheckmarkSharp /> Picture in Picture</li>
|
</li>
|
||||||
<li><IoCheckmarkSharp /> Countdown</li>
|
<li>
|
||||||
|
<IoCheckmarkSharp /> Minimal timer
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IoCheckmarkSharp /> Backstage screen
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IoCheckmarkSharp /> Public screen
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IoCheckmarkSharp /> Countdown
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
Read more about it in the documentation{' '}
|
Read more about it in the documentation{' '}
|
||||||
<a
|
<a
|
||||||
@@ -123,19 +135,12 @@ export default function ViewsSettingsModal() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<EnableBtn
|
<EnableBtn
|
||||||
active={formData.overrideStyles}
|
active={formData.overrideStyles}
|
||||||
text={
|
text={formData.overrideStyles ? 'Style Override Enabled' : 'Style Override Disabled'}
|
||||||
formData.overrideStyles ? 'Style Override Enabled' : 'Style Override Disabled'
|
|
||||||
}
|
|
||||||
actionHandler={() => handleChange('overrideStyles', !formData.overrideStyles)}
|
actionHandler={() => handleChange('overrideStyles', !formData.overrideStyles)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<SubmitContainer
|
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
|
||||||
revert={revert}
|
|
||||||
submitting={submitting}
|
|
||||||
changed={changed}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 1fr auto;
|
grid-template-columns: 32px 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $secondary-block-height;
|
height: $secondary-block-height;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
|||||||
import { OntimeBlock, OntimeEvent } from 'ontime-types';
|
import { OntimeBlock, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||||
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
|
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
|
||||||
import { EventItemActions } from '../RundownEntry';
|
import { EventItemActions } from '../RundownEntry';
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export default function BlockBlock(props: BlockBlockProps) {
|
|||||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
|
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||||
<BlockActionMenu className={style.actionOverlay} showAdd showDelay enableDelete actionHandler={actionHandler} />
|
<BlockActionMenu className={style.actionOverlay} showAdd showDelay enableDelete actionHandler={actionHandler} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './TitleEditor.module.scss';
|
||||||
|
|
||||||
|
interface TitleEditorProps {
|
||||||
|
title: string;
|
||||||
|
eventId: string;
|
||||||
|
placeholder: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||||
|
const { title, eventId, placeholder, className } = props;
|
||||||
|
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||||
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBlockTitle(title);
|
||||||
|
}, [title]);
|
||||||
|
|
||||||
|
const handleTitle = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
if (text === title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanVal = text.trim();
|
||||||
|
setBlockTitle(cleanVal);
|
||||||
|
|
||||||
|
updateEvent({ id: eventId, title: cleanVal });
|
||||||
|
},
|
||||||
|
[title, updateEvent, eventId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const classes = cx([className, style.eventTitle, !blockTitle ? style.noTitle : null]);
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
variant='ontime'
|
||||||
|
value={blockTitle}
|
||||||
|
className={classes}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value) => setBlockTitle(value)}
|
||||||
|
onSubmit={(value) => handleTitle(value)}
|
||||||
|
>
|
||||||
|
<EditablePreview className={style.preview} />
|
||||||
|
<EditableInput />
|
||||||
|
</Editable>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@use '../../../theme/v2Styles' as *;
|
||||||
|
|
||||||
|
.titleEditor {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noTitle {
|
||||||
|
.preview {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,18 +87,6 @@
|
|||||||
|
|
||||||
.eventTitle {
|
.eventTitle {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&.noTitle {
|
|
||||||
.preview {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventActions {
|
.eventActions {
|
||||||
@@ -158,6 +146,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
padding-top: 1px;
|
||||||
font-size: 0.55em;
|
font-size: 0.55em;
|
||||||
color: $active-indicator;
|
color: $active-indicator;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { IoCaretDownCircle } from '@react-icons/all-files/io5/IoCaretDownCircle';
|
import { IoCaretDownCircle } from '@react-icons/all-files/io5/IoCaretDownCircle';
|
||||||
import { IoCaretUpCircle } from '@react-icons/all-files/io5/IoCaretUpCircle';
|
import { IoCaretUpCircle } from '@react-icons/all-files/io5/IoCaretUpCircle';
|
||||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||||
@@ -17,10 +17,10 @@ import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
|||||||
import { EndAction, Playback, TimerType } from 'ontime-types';
|
import { EndAction, Playback, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
|
||||||
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
||||||
import { useEventEditorStore } from '../../../common/stores/eventEditor';
|
import { useEventEditorStore } from '../../../common/stores/eventEditor';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||||
import { EventItemActions } from '../RundownEntry';
|
import { EventItemActions } from '../RundownEntry';
|
||||||
|
|
||||||
import BlockActionMenu from './composite/BlockActionMenu';
|
import BlockActionMenu from './composite/BlockActionMenu';
|
||||||
@@ -78,38 +78,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
actionHandler,
|
actionHandler,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { updateEvent } = useEventAction();
|
|
||||||
|
|
||||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
|
||||||
const [renderInner, setRenderInner] = useState(false);
|
const [renderInner, setRenderInner] = useState(false);
|
||||||
const setOpenEvent = useEventEditorStore((state) => state.setOpenEvent);
|
const setOpenEvent = useEventEditorStore((state) => state.setOpenEvent);
|
||||||
const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent);
|
const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent);
|
||||||
|
|
||||||
// Todo: could I re-render the item without causing a state change here?
|
|
||||||
// ?? use refs instead?
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRenderInner(true);
|
setRenderInner(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBlockTitle(title);
|
|
||||||
}, [title]);
|
|
||||||
|
|
||||||
const handleTitle = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
if (text === title) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanVal = text.trim();
|
|
||||||
setBlockTitle(cleanVal);
|
|
||||||
|
|
||||||
updateEvent({ id: eventId, title: cleanVal });
|
|
||||||
},
|
|
||||||
[title, updateEvent, eventId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleOpenEvent = useCallback(() => {
|
const toggleOpenEvent = useCallback(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
removeOpenEvent();
|
removeOpenEvent();
|
||||||
@@ -173,17 +149,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
actionHandler={actionHandler}
|
actionHandler={actionHandler}
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
/>
|
/>
|
||||||
<Editable
|
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||||
variant='ontime'
|
|
||||||
value={blockTitle}
|
|
||||||
className={`${style.eventTitle} ${!title ? style.noTitle : ''}`}
|
|
||||||
placeholder='Event title'
|
|
||||||
onChange={(value) => setBlockTitle(value)}
|
|
||||||
onSubmit={(value) => handleTitle(value)}
|
|
||||||
>
|
|
||||||
<EditablePreview className={style.preview} />
|
|
||||||
<EditableInput />
|
|
||||||
</Editable>
|
|
||||||
<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}`}>
|
||||||
|
|||||||
+22
-23
@@ -1,18 +1,25 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { useEmitLog } from '@/common/stores/logger';
|
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { millisToMinutes } from '../../../../common/utils/dateConfig';
|
import { millisToMinutes } from '../../../../common/utils/dateConfig';
|
||||||
import { validateEntry } from '../../../../common/utils/timesManager';
|
import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
|
||||||
|
import { EventItemActions } from '../../RundownEntry';
|
||||||
|
|
||||||
import style from '../EventBlock.module.scss';
|
import style from '../EventBlock.module.scss';
|
||||||
|
|
||||||
export default function EventBlockTimers(props) {
|
interface EventBlockTimerProps {
|
||||||
|
timeStart: number;
|
||||||
|
timeEnd: number;
|
||||||
|
duration: number;
|
||||||
|
delay: number;
|
||||||
|
actionHandler: (action: EventItemActions, payload?: any) => void;
|
||||||
|
previousEnd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventBlockTimers(props: EventBlockTimerProps) {
|
||||||
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
|
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
|
||||||
const { emitWarning } = useEmitLog();
|
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
|
||||||
|
|
||||||
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
||||||
const newTime = millisToString(timeStart + delay);
|
const newTime = millisToString(timeStart + delay);
|
||||||
@@ -24,21 +31,19 @@ export default function EventBlockTimers(props) {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
const handleValidation = useCallback(
|
const handleValidation = useCallback(
|
||||||
(field, value) => {
|
(field: TimeEntryField, value: number) => {
|
||||||
const valid = validateEntry(field, value, timeStart, timeEnd);
|
const valid = validateEntry(field, value, timeStart, timeEnd);
|
||||||
if (valid.catch) {
|
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
|
||||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
|
||||||
}
|
|
||||||
return valid.value;
|
return valid.value;
|
||||||
},
|
},
|
||||||
[emitWarning, timeEnd, timeStart]
|
[timeEnd, timeStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(field, value) => {
|
(field: TimeEntryField, value: number) => {
|
||||||
actionHandler('update', { field, value });
|
actionHandler('update', { field, value });
|
||||||
},
|
},
|
||||||
[actionHandler]
|
[actionHandler],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +56,7 @@ export default function EventBlockTimers(props) {
|
|||||||
delay={delay}
|
delay={delay}
|
||||||
placeholder='Start'
|
placeholder='Start'
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
|
warning={warning.start}
|
||||||
/>
|
/>
|
||||||
<TimeInput
|
<TimeInput
|
||||||
name='timeEnd'
|
name='timeEnd'
|
||||||
@@ -60,6 +66,7 @@ export default function EventBlockTimers(props) {
|
|||||||
delay={delay}
|
delay={delay}
|
||||||
placeholder='End'
|
placeholder='End'
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
|
warning={warning.end}
|
||||||
/>
|
/>
|
||||||
<TimeInput
|
<TimeInput
|
||||||
name='durationOverride'
|
name='durationOverride'
|
||||||
@@ -68,6 +75,7 @@ export default function EventBlockTimers(props) {
|
|||||||
time={duration}
|
time={duration}
|
||||||
placeholder='Duration'
|
placeholder='Duration'
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
|
warning={warning.duration}
|
||||||
/>
|
/>
|
||||||
{delay !== 0 && delay !== null && (
|
{delay !== 0 && delay !== null && (
|
||||||
<div className={style.delayNote}>
|
<div className={style.delayNote}>
|
||||||
@@ -79,12 +87,3 @@ export default function EventBlockTimers(props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBlockTimers.propTypes = {
|
|
||||||
timeStart: PropTypes.number,
|
|
||||||
timeEnd: PropTypes.number,
|
|
||||||
duration: PropTypes.number,
|
|
||||||
delay: PropTypes.number,
|
|
||||||
actionHandler: PropTypes.func,
|
|
||||||
previousEnd: PropTypes.number,
|
|
||||||
};
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
gap: 10%;
|
gap: 10%;
|
||||||
|
|
||||||
.quickBtn {
|
.quickBtn {
|
||||||
|
font-weight: 400;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { TableSettingsContext } from '../../common/context/TableSettingsContext'
|
|||||||
import useFullscreen from '../../common/hooks/useFullscreen';
|
import useFullscreen from '../../common/hooks/useFullscreen';
|
||||||
import { useTimer } from '../../common/hooks/useSocket';
|
import { useTimer } from '../../common/hooks/useSocket';
|
||||||
import useEventData from '../../common/hooks-query/useEventData';
|
import useEventData from '../../common/hooks-query/useEventData';
|
||||||
import { formatDisplay, millisToSeconds } from '../../common/utils/dateConfig';
|
import { formatDisplay } from '../../common/utils/dateConfig';
|
||||||
import { formatTime } from '../../common/utils/time';
|
import { formatTime } from '../../common/utils/time';
|
||||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export default function TableHeader({ handleCSVExport, featureData }) {
|
|||||||
|
|
||||||
// prepare presentation variables
|
// prepare presentation variables
|
||||||
const isOvertime = timer.current < 0;
|
const isOvertime = timer.current < 0;
|
||||||
const timerNow = `${isOvertime ? '-' : ''}${formatDisplay(millisToSeconds(timer.current))}`;
|
const timerNow = `${isOvertime ? '-' : ''}${formatDisplay(timer.current)}`;
|
||||||
const timeNow = formatTime(timer.clock, {
|
const timeNow = formatTime(timer.clock, {
|
||||||
showSeconds: true,
|
showSeconds: true,
|
||||||
format: 'hh:mm:ss a',
|
format: 'hh:mm:ss a',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
|||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
@@ -66,7 +66,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
if (time.current === null) {
|
if (time.current === null) {
|
||||||
stageTimer = '- - : - -';
|
stageTimer = '- - : - -';
|
||||||
} else {
|
} else {
|
||||||
stageTimer = formatDisplay(Math.abs(millisToSeconds(time.current)), true);
|
stageTimer = formatDisplay(Math.abs(time.current), true);
|
||||||
if (isNegative) {
|
if (isNegative) {
|
||||||
stageTimer = `-${stageTimer}`;
|
stageTimer = `-${stageTimer}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TimerType } from 'ontime-types';
|
import { TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
const formatOptions = {
|
const formatOptions = {
|
||||||
@@ -36,7 +36,7 @@ export function formatTimerDisplay(timer?: string | number | null): string {
|
|||||||
} else if (timer === null || typeof timer === 'undefined' || isNaN(timer)) {
|
} else if (timer === null || typeof timer === 'undefined' || isNaN(timer)) {
|
||||||
display = '-- : -- : --';
|
display = '-- : -- : --';
|
||||||
} else {
|
} else {
|
||||||
display = formatDisplay(millisToSeconds(timer), true);
|
display = formatDisplay(timer, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return display;
|
return display;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { overrideStylesURL } from '../../../common/api/apiConstants';
|
|||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
@@ -102,8 +102,8 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
runningMessage === TimerMessage.ended
|
runningMessage === TimerMessage.ended
|
||||||
? formatTime(runningTimer, formatOptionsFinished)
|
? formatTime(runningTimer, formatOptionsFinished)
|
||||||
: formatDisplay(
|
: formatDisplay(
|
||||||
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
|
isSelected ? runningTimer : runningTimer + delay,
|
||||||
isSelected || time.waiting,
|
isSelected || runningMessage === TimerMessage.waiting,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const sanitiseTitle = (title: string | null) => (title ? title : '{no tit
|
|||||||
export const fetchTimerData = (
|
export const fetchTimerData = (
|
||||||
time: TimeManagerType,
|
time: TimeManagerType,
|
||||||
follow: OntimeEvent,
|
follow: OntimeEvent,
|
||||||
selectedId: string,
|
selectedId: string | null,
|
||||||
): { message: TimerMessage; timer: number } => {
|
): { message: TimerMessage; timer: number } => {
|
||||||
let message;
|
let message;
|
||||||
let timer;
|
let timer;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function StudioClock(props) {
|
|||||||
{title.titleNext}
|
{title.titleNext}
|
||||||
</div>
|
</div>
|
||||||
<div className={isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
<div className={isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||||
{selectedId != null && formatDisplay(time.current)}
|
{selectedId !== null && formatDisplay(time.current)}
|
||||||
</div>
|
</div>
|
||||||
<div className='clock-indicators'>
|
<div className='clock-indicators'>
|
||||||
{activeIndicators.map((i) => (
|
{activeIndicators.map((i) => (
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
const isNegative =
|
const isNegative =
|
||||||
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||||
|
|
||||||
const showEndMessage = time.current < 0 && general.endMessage;
|
const showEndMessage = (time.current ?? 1) < 0 && general.endMessage;
|
||||||
const showProgress = time.playback !== Playback.Stop;
|
const showProgress = time.playback !== Playback.Stop;
|
||||||
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||||
const showClock = time.timerType !== TimerType.Clock;
|
const showClock = time.timerType !== TimerType.Clock;
|
||||||
@@ -110,8 +110,8 @@ export default function Timer(props: TimerProps) {
|
|||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
||||||
now={time.current}
|
now={time.current || 0}
|
||||||
complete={time.duration}
|
complete={time.duration || 0}
|
||||||
hidden={!showProgress}
|
hidden={!showProgress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0-beta1",
|
"version": "2.0.0-beta2",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "2.0.0-beta1",
|
"version": "2.0.0-beta2",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "^7.24.1",
|
"@sentry/node": "^7.24.1",
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class SocketServer implements IAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// message is any serializable value
|
// message is any serializable value
|
||||||
send(message: unknown) {
|
sendAsJson(message: unknown) {
|
||||||
this.wss?.clients.forEach((client) => {
|
this.wss?.clients.forEach((client) => {
|
||||||
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
||||||
client.send(JSON.stringify(message));
|
client.send(JSON.stringify(message));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { socket } from './WebsocketAdapter.js';
|
|||||||
* @param payload -- possible patch payload
|
* @param payload -- possible patch payload
|
||||||
*/
|
*/
|
||||||
export function sendRefetch(payload: any | null = null) {
|
export function sendRefetch(payload: any | null = null) {
|
||||||
socket.send({
|
socket.sendAsJson({
|
||||||
type: 'ontime-refetch',
|
type: 'ontime-refetch',
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { integrationService } from './services/integration-service/IntegrationSe
|
|||||||
import { logger } from './classes/Logger.js';
|
import { logger } from './classes/Logger.js';
|
||||||
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
||||||
import { populateStyles } from './modules/loadStyles.js';
|
import { populateStyles } from './modules/loadStyles.js';
|
||||||
|
import { eventStore, getInitialPayload } from './stores/EventStore.js';
|
||||||
|
|
||||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
|
|
||||||
@@ -132,7 +133,10 @@ export const startServer = async () => {
|
|||||||
expressServer = http.createServer(app);
|
expressServer = http.createServer(app);
|
||||||
|
|
||||||
socket.init(expressServer);
|
socket.init(expressServer);
|
||||||
|
|
||||||
|
// provide initial payload to event store
|
||||||
eventLoader.init();
|
eventLoader.init();
|
||||||
|
eventStore.init(getInitialPayload());
|
||||||
|
|
||||||
expressServer.listen(serverPort, '0.0.0.0');
|
expressServer.listen(serverPort, '0.0.0.0');
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket.send({
|
socket.sendAsJson({
|
||||||
type: 'ontime-log',
|
type: 'ontime-log',
|
||||||
payload: log,
|
payload: log,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Class Event Provider is a mediator for handling the local db
|
* Class Event Provider is a mediator for handling the local db
|
||||||
* and adds logic specific to ontime data
|
* and adds logic specific to ontime data
|
||||||
*/
|
*/
|
||||||
import { EventData, ViewSettings } from 'ontime-types';
|
import { EventData, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { data, db } from '../../modules/loadDb.js';
|
import { data, db } from '../../modules/loadDb.js';
|
||||||
import { safeMerge } from './DataProvider.utils.js';
|
import { safeMerge } from './DataProvider.utils.js';
|
||||||
@@ -35,7 +35,9 @@ export class DataProvider {
|
|||||||
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
|
const eventIndex = data.rundown.findIndex((e) => e.id === eventId);
|
||||||
const persistedEvent = data.rundown[eventIndex];
|
const persistedEvent = data.rundown[eventIndex];
|
||||||
const newEvent = { ...persistedEvent, ...newData };
|
const newEvent = { ...persistedEvent, ...newData };
|
||||||
newEvent.revision++;
|
if (newEvent.type === SupportedEvent.Event) {
|
||||||
|
newEvent.revision++;
|
||||||
|
}
|
||||||
data.rundown[eventIndex] = newEvent;
|
data.rundown[eventIndex] = newEvent;
|
||||||
await this.persist();
|
await this.persist();
|
||||||
return data.rundown[eventIndex];
|
return data.rundown[eventIndex];
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export class EventLoader {
|
|||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to delay init until the store is ready
|
||||||
init() {
|
init() {
|
||||||
this.reset();
|
this.reset(false);
|
||||||
this.loadedEvent = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
-2
@@ -2,12 +2,14 @@
|
|||||||
--background-color-override: #ececec;
|
--background-color-override: #ececec;
|
||||||
--color-override: #101010;
|
--color-override: #101010;
|
||||||
--secondary-color-override: #404040;
|
--secondary-color-override: #404040;
|
||||||
--accent-color-override: #FA5656;
|
--accent-color-override: #fa5656;
|
||||||
--label-color-override: #6c6c6c;
|
--label-color-override: #6c6c6c;
|
||||||
--timer-color-override: #202020;
|
--timer-color-override: #202020;
|
||||||
--card-background-color-override: #FFF;
|
--card-background-color-override: #fff;
|
||||||
--font-family-override: "Open Sans";
|
--font-family-override: "Open Sans";
|
||||||
--font-family-bold-override: "Arial Black";
|
--font-family-bold-override: "Arial Black";
|
||||||
|
--timer-progress-bg-override: #fff;
|
||||||
|
--timer-progress-override: #202020;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
|
|||||||
@@ -34,5 +34,6 @@ export const delay: Omit<OntimeDelay, 'id'> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const block: Omit<OntimeBlock, 'id'> = {
|
export const block: Omit<OntimeBlock, 'id'> = {
|
||||||
|
title: '',
|
||||||
type: SupportedEvent.Block,
|
type: SupportedEvent.Block,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -219,7 +219,9 @@ export class PlaybackService {
|
|||||||
if (eventTimer.loadedTimerId) {
|
if (eventTimer.loadedTimerId) {
|
||||||
const delayInMs = delayTime * 1000 * 60;
|
const delayInMs = delayTime * 1000 * 60;
|
||||||
eventTimer.delay(delayInMs);
|
eventTimer.delay(delayInMs);
|
||||||
logger.info('PLAYBACK', `Added ${delayTime} min delay`);
|
delayInMs > 0
|
||||||
|
? logger.info('PLAYBACK', `Added ${delayTime} min delay`)
|
||||||
|
: logger.info('PLAYBACK', `Removed ${delayTime} min delay`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent } 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';
|
||||||
@@ -218,15 +218,13 @@ export async function reorderEvent(eventId, from, to) {
|
|||||||
*/
|
*/
|
||||||
export async function applyDelay(eventId) {
|
export async function applyDelay(eventId) {
|
||||||
const rundown = DataProvider.getRundown();
|
const rundown = DataProvider.getRundown();
|
||||||
// AUX
|
|
||||||
let delayIndex = null;
|
let delayIndex = null;
|
||||||
let blockIndex = null;
|
|
||||||
let delayValue = 0;
|
let delayValue = 0;
|
||||||
|
|
||||||
for (const [index, e] of rundown.entries()) {
|
for (const [index, e] of rundown.entries()) {
|
||||||
// look for delay
|
// look for delay
|
||||||
if (delayIndex === null) {
|
if (delayIndex === null) {
|
||||||
if (e.id === eventId && e.type === 'delay') {
|
if (e.id === eventId && e.type === SupportedEvent.Delay) {
|
||||||
delayValue = e.duration;
|
delayValue = e.duration;
|
||||||
delayIndex = index;
|
delayIndex = index;
|
||||||
}
|
}
|
||||||
@@ -234,16 +232,14 @@ export async function applyDelay(eventId) {
|
|||||||
|
|
||||||
// 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 === 'event') {
|
if (e.type === SupportedEvent.Event) {
|
||||||
// update times
|
// update times
|
||||||
e.timeStart += delayValue;
|
e.timeStart += delayValue;
|
||||||
e.timeEnd += delayValue;
|
e.timeEnd += delayValue;
|
||||||
|
|
||||||
// increment revision
|
// increment revision
|
||||||
e.revision += 1;
|
e.revision += 1;
|
||||||
} else if (e.type === 'block') {
|
} else if (e.type === SupportedEvent.Block) {
|
||||||
// save id and stop
|
|
||||||
blockIndex = index;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,10 +252,6 @@ export async function applyDelay(eventId) {
|
|||||||
// delete delay
|
// delete delay
|
||||||
rundown.splice(delayIndex, 1);
|
rundown.splice(delayIndex, 1);
|
||||||
|
|
||||||
// delete block
|
|
||||||
// index would have moved down since we deleted delay
|
|
||||||
if (blockIndex) rundown.splice(blockIndex - 1, 1);
|
|
||||||
|
|
||||||
// update rundown
|
// update rundown
|
||||||
await DataProvider.setRundown(rundown);
|
await DataProvider.setRundown(rundown);
|
||||||
updateTimer();
|
updateTimer();
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { RuntimeStore } from 'ontime-types';
|
import { RuntimeStore } from 'ontime-types';
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
|
import { eventTimer } from '../services/TimerService.js';
|
||||||
|
import { messageService } from '../services/message-service/MessageService.js';
|
||||||
|
import { eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||||
|
|
||||||
const store: Partial<RuntimeStore> = {};
|
let store: Partial<RuntimeStore> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A runtime store that broadcasts its payload
|
* A runtime store that broadcasts its payload
|
||||||
*/
|
*/
|
||||||
export const eventStore = {
|
export const eventStore = {
|
||||||
|
init(payload: RuntimeStore) {
|
||||||
|
store = payload;
|
||||||
|
},
|
||||||
get<T extends keyof RuntimeStore>(key: T) {
|
get<T extends keyof RuntimeStore>(key: T) {
|
||||||
return store[key];
|
return store[key];
|
||||||
},
|
},
|
||||||
@@ -23,9 +29,35 @@ export const eventStore = {
|
|||||||
return store;
|
return store;
|
||||||
},
|
},
|
||||||
broadcast() {
|
broadcast() {
|
||||||
socket.send({
|
socket.sendAsJson({
|
||||||
type: 'ontime',
|
type: 'ontime',
|
||||||
payload: store,
|
payload: store,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module initialises the services and provides initial payload for the store
|
||||||
|
* Currently registered objects in store
|
||||||
|
* - Timer Service timer
|
||||||
|
* - Timer Service playback
|
||||||
|
* - Message Service timerMessage
|
||||||
|
* - Message Service publicMessage
|
||||||
|
* - Message Service lowerMessage
|
||||||
|
* - Message Service onAir
|
||||||
|
* - Event Loader loaded
|
||||||
|
* - Event Loader titles
|
||||||
|
* - Event Loader titlesPublic
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getInitialPayload = () => ({
|
||||||
|
timer: eventTimer.timer,
|
||||||
|
playback: eventTimer.playback,
|
||||||
|
timerMessage: messageService.timerMessage,
|
||||||
|
publicMessage: messageService.publicMessage,
|
||||||
|
lowerMessage: messageService.lowerMessage,
|
||||||
|
onAir: messageService.onAir,
|
||||||
|
loaded: eventLoader.loaded,
|
||||||
|
titles: eventLoader.titles,
|
||||||
|
titlesPublic: eventLoader.titlesPublic,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import getRandomName from '../getRandomName.js';
|
import getRandomName from '../getRandomName.js';
|
||||||
|
|
||||||
test('generates 100 unique names', () => {
|
test('generates unique names', () => {
|
||||||
const names = new Set();
|
const names = new Set();
|
||||||
let attempts = 1;
|
let attempts = 1;
|
||||||
while (names.size < 100) {
|
while (names.size < 10) {
|
||||||
names.add(getRandomName());
|
names.add(getRandomName());
|
||||||
attempts++;
|
attempts++;
|
||||||
}
|
}
|
||||||
expect(attempts).toBeLessThan(105);
|
expect(attempts).toBeLessThan(50);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
id: e.id || generateId(),
|
id: e.id || generateId(),
|
||||||
});
|
});
|
||||||
} else if (e.type === 'block') {
|
} else if (e.type === 'block') {
|
||||||
rundown.push({ ...blockDef, id: e.id || generateId() });
|
rundown.push({ ...blockDef, title: e.title, id: e.id || generateId() });
|
||||||
} else {
|
} else {
|
||||||
console.log('ERROR: undefined event type, skipping');
|
console.log('ERROR: undefined event type, skipping');
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0-beta1",
|
"version": "2.0.0-beta2",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type OntimeDelay = OntimeBaseEvent & {
|
|||||||
|
|
||||||
export type OntimeBlock = OntimeBaseEvent & {
|
export type OntimeBlock = OntimeBaseEvent & {
|
||||||
type: SupportedEvent.Block;
|
type: SupportedEvent.Block;
|
||||||
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OntimeEvent = OntimeBaseEvent & {
|
export type OntimeEvent = OntimeBaseEvent & {
|
||||||
|
|||||||
Reference in New Issue
Block a user