* chore: upgrade local build documentation

* style: keep scrolling event in screen

* feat: delay is time entry

* refactor: remove unused

* refactor: remove unused

* refactor: batch store updates

* refactor: virtually remove cap on events

* refactor: style and behaviour tweaks to event block

* style: tweaks on schedules

* chore: remove sentry from server

* style: reorder menu

* chore: update docs
This commit is contained in:
Carlos Valente
2023-04-14 10:13:46 +02:00
committed by GitHub
parent 94a1369d64
commit 770d12888d
48 changed files with 531 additions and 376 deletions
-1
View File
@@ -20,7 +20,6 @@
"csv-stringify": "^6.2.3",
"deepmerge": "^4.3.0",
"framer-motion": "^10.10.0",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0",
@@ -1,13 +1,24 @@
@use '../../../../theme/v2Styles' as *;
$input-font-size: 15px;
.delayInput {
display: flex;
gap: $element-spacing;
align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
max-width: 7em;
padding-left: 16px;
color: $ontime-delay-text
}
}
.inputField {
text-align: center;
}
.delayOptions {
display: flex;
flex-direction: column;
}
@@ -1,88 +1,134 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
import { millisToString } from 'ontime-utils';
import { clamp } from '../../../utils/math';
import { useEventAction } from '../../../hooks/useEventAction';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import style from './DelayInput.module.scss';
const inputStyleProps = {
width: 20,
placeholder: '-',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
fontSize: '15px',
letterSpacing: '0.3px',
};
interface DelayInputProps {
submitHandler: (value: number) => void;
value?: number;
eventId: string;
duration: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const { eventId, duration } = props;
const { updateEvent } = useEventAction();
const [value, setValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement | null>(null);
// avoid wrong submit on cancel
let ignoreChange = false;
useEffect(() => {
if (!value) {
if (typeof duration === undefined) {
return;
}
setValue(value);
}, [value]);
setValue(millisToString(duration));
}, [duration]);
/**
* @description Prepare delay value for update
* @param {string} value string to be parsed
* @param {string} newValue string to be parsed
*/
const validate = useCallback(
(newValue?: string) => {
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
if (delayValue === value) return;
setValue(delayValue);
const validateAndSubmit = (newValue: string) => {
if (ignoreChange) {
ignoreChange = false;
return;
}
submitHandler(delayValue);
},
[submitHandler, value],
);
const isNegative = newValue.startsWith('-');
let newMillis = forgivingStringToMillis(newValue);
if (isNegative) {
newMillis = newMillis * -1;
}
if (newMillis === duration) {
return;
}
submitChange(newMillis);
setValue(millisToString(newMillis));
};
const submitChange = (value: number) => {
updateEvent({
id: eventId,
duration: value,
});
};
/**
* @description Selects input text on focus
*/
const handleFocus = () => inputRef.current?.select();
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback(
(key: string) => {
if (key === 'Enter') {
inputRef.current?.blur();
validate(inputRef.current?.value);
} else if (key === 'Escape') {
inputRef.current?.blur();
setValue(value);
}
},
[validate, value],
);
const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
inputRef.current?.blur();
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Tab') {
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Escape') {
ignoreChange = true;
setValue(millisToString(duration));
inputRef.current?.blur();
}
};
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
/**
* @description handles direction change to delay
* @param newDirection
*/
const handleSlipChange = (newDirection: 'add' | 'subtract') => {
if (newDirection === 'add') {
// add time
if (duration < 0) {
submitChange(duration * -1);
}
} else if (newDirection === 'subtract') {
// subtract time
if (duration > 0) {
submitChange(duration * -1);
}
}
};
const checkedOption = value.startsWith('-') ? 'subtract' : 'add';
return (
<label className={style.delayInput}>
<div className={style.delayInput}>
<Input
{...inputStyleProps}
size='sm'
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
value={_value}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
type='text'
placeholder='-'
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={(event) => validateAndSubmit(event.target.value)}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={9}
/>
{labelText}
</label>
<RadioGroup
className={style.delayOptions}
onChange={handleSlipChange}
value={checkedOption}
variant='ontime-block'
size='sm'
>
<Radio value='add'>Add time</Radio>
<Radio value='subtract'>Subtract time</Radio>
</RadioGroup>
</div>
);
}
@@ -25,18 +25,22 @@ export default function TimeInput(props: TimeInputProps) {
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState('');
const [value, setValue] = useState<string>('');
// avoid wrong submit on cancel
let ignoreChange = false;
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
try {
setValue(millisToString(time + delay));
// eslint-disable-next-line -- we use ignore change to stop submit on cancel
ignoreChange = true;
setValue(millisToString(time));
} catch (error) {
emitError(`Unable to parse date: ${error}`);
}
}, [delay, emitError, time]);
}, [emitError, time]);
/**
* @description Selects input text on focus
@@ -73,11 +77,8 @@ export default function TimeInput(props: TimeInputProps) {
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
if (newValMillis === time) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
@@ -87,7 +88,7 @@ export default function TimeInput(props: TimeInputProps) {
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler],
[name, previousEnd, submitHandler, time, validationHandler],
);
/**
@@ -96,10 +97,16 @@ export default function TimeInput(props: TimeInputProps) {
*/
const validateAndSubmit = useCallback(
(newValue: string) => {
if (ignoreChange) {
// eslint-disable-next-line -- we use this to prevent a wrong submit
ignoreChange = false;
return;
}
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(millisToString(ms + delay));
const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay);
setValue(millisToString(delayed));
} else {
resetValue();
}
@@ -139,8 +146,6 @@ export default function TimeInput(props: TimeInputProps) {
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
@@ -155,6 +160,7 @@ export default function TimeInput(props: TimeInputProps) {
return '';
};
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
@@ -32,7 +32,7 @@
}
&:not(:last-child) {
padding-bottom: clamp(16px, 1.5vw, 24px);
padding-bottom: 8px;
}
&--past {
@@ -6,7 +6,7 @@ import { useInterval } from '../../hooks/useInterval';
interface ScheduleContextState {
events: OntimeEvent[];
paginatedEvents: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
numPages: number;
visiblePage: number;
isBackstage: boolean;
@@ -16,22 +16,20 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps {
events: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
isBackstage?: boolean;
eventsPerPage?: number;
time?: number;
}
export const ScheduleProvider = (
{
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 4,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
export const ScheduleProvider = ({
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 8,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
const numPages = Math.ceil(events.length / eventsPerPage);
@@ -90,6 +90,7 @@ export const useCuesheet = () => {
export const setEventPlayback = {
loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
startEvent: (eventId: string) => socketSendJson('startid', eventId),
start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'),
};
@@ -2,9 +2,9 @@ import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToDelayString,
millisToMinutes,
millisToSeconds,
timeStringToMillis,
} from '../dateConfig';
describe('test string from formatDisplay function', () => {
@@ -170,88 +170,6 @@ describe('test millisToMinutes function', () => {
});
});
describe('test timeStringToMillis function', () => {
it('test with null', () => {
const t = { val: null, result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:00', () => {
const t = { val: '00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:00', () => {
const t = { val: '-00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:01', () => {
const t = { val: '00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:01', () => {
const t = { val: '-00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 01:00:01', () => {
const t = { val: '01:00:01', result: 3601000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 24:00:01', () => {
const t = { val: '24:00:01', result: 86401000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:5', () => {
const t = { val: '00:00:5', result: 5000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:1:00', () => {
const t = { val: '00:1:00', result: 60000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1:00:00', () => {
const t = { val: '1:00:00', result: 3600000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1', () => {
const t = { val: '1', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 120', () => {
const t = { val: '120', result: 120000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 56', () => {
const t = { val: '56', result: 56000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:3', () => {
const t = { val: '2:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 02:3', () => {
const t = { val: '02:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:03', () => {
const t = { val: '2:03', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
});
describe('test isTimeString() function', () => {
it('it validates time strings', () => {
const ts = ['2', '2:10', '2:10:22'];
@@ -450,3 +368,59 @@ describe('test forgivingStringToMillis()', () => {
});
});
});
describe('millisToDelayString()', () => {
it('returns null for null values', () => {
expect(millisToDelayString(null)).toBeNull();
});
it('returns null 0', () => {
expect(millisToDelayString(0)).toBeNull();
});
describe('converts values in seconds', () => {
it(`shows a simple string with value in seconds`, () => {
expect(millisToDelayString(10000)).toBe('+10sec');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-10000)).toBe('-10sec');
});
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
underAMinute.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
});
});
expect(millisToDelayString(null)).toBeNull();
});
describe('converts values in minutes', () => {
it(`shows a simple string with value in minutes`, () => {
expect(millisToDelayString(720000)).toBe('+12min');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-720000)).toBe('-12min');
});
it(`shows a simple string with value in minutes and seconds`, () => {
expect(millisToDelayString(630000)).toBe('+00:10:30');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-630000)).toBe('-00:10:30');
});
const underAnHour = [60000, 360000, 720000];
underAnHour.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
});
});
});
describe('converts values with full time string', () => {
it(`positive added time`, () => {
expect(millisToDelayString(45015000)).toBe('+12:30:15');
});
it(`negative added time`, () => {
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
});
});
});
+19 -14
View File
@@ -1,3 +1,5 @@
import { formatFromMillis } from 'ontime-utils';
import { mth, mtm, mts } from './timeConstants';
export const timeFormat = 'HH:mm';
@@ -47,20 +49,6 @@ export const millisToMinutes = (millis: number): number => {
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
};
/**
* @description Converts timestring to milliseconds
* @param {string} string - time string "23:00:12"
* @returns {number} Amount in milliseconds
*/
export const timeStringToMillis = (string: string): number => {
if (typeof string !== 'string') return 0;
const time = string.split(':');
if (time.length === 1) return Math.abs(time[0] * mts);
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
return 0;
};
/**
* @description Validates a time string
* @param {string} string - time string "23:00:12"
@@ -150,3 +138,20 @@ export const forgivingStringToMillis = (value: string, fillLeft = true): number
return millis;
};
export function millisToDelayString(millis: number | null): undefined | string | null {
if (millis == null || millis === 0) {
return null;
}
const isNegative = millis < 0;
const absMillis = Math.abs(millis);
if (absMillis < mtm) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')}sec`;
} else if (absMillis < mth && absMillis % mtm === 0) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')}min`;
} else {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
}
}
+2 -6
View File
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { Settings } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import { formatFromMillis, millisToString } from 'ontime-utils';
import { APP_SETTINGS } from '../api/apiConstants';
import { ontimeQueryClient } from '../queryClient';
@@ -39,7 +38,6 @@ type FormatOptions = {
};
/**
/**
* @description utility function to format a date in 12 or 24 hour format
* @param {number | null} milliseconds
* @param {object} [options]
@@ -54,7 +52,5 @@ export const formatTime = (milliseconds: number | null, options: FormatOptions,
}
const timeFormat = resolver();
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
return timeFormat === '12'
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
: millisToString(milliseconds, showSeconds);
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
};
@@ -144,8 +144,8 @@ $playback-width: 450px;
.eventEditor {
border-radius: 8px 8px 0 0;
background-color: $bg-container-l2;
box-shadow: rgba(0, 0, 0, 0.6) 0 3px 6px 6px;
border-top: 1px solid $white-10;
box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
border-top: 1px solid $white-20;
position: absolute;
bottom: 0;
width: 100vw;
@@ -79,7 +79,7 @@
display: block;
@include input-label;
.delayLabel {
&.delayLabel {
color: $ontime-delay-text;
}
@@ -5,7 +5,8 @@ import { millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss';
@@ -71,18 +72,15 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
updateEvent(newEventData);
};
const delayed = delay !== 0;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
const newStart = delayed ? `New start ${millisToString(timeStart + delay)}` : null;
const newEnd = delayed ? `New end ${millisToString(timeEnd + delay)}` : null;
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
const startLabel = delayTime ? `New start ${millisToString(timeStart + delay)}` : 'Start time';
const endLabel = delayTime ? `New end ${millisToString(timeEnd + delay)}` : 'End time';
const inputTimeLabels = cx([style.inputLabel, delayTime ? style.delayLabel : null]);
return (
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={style.inputLabel}>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<label className={inputTimeLabels}>{startLabel}</label>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
@@ -92,10 +90,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
placeholder='Start'
warning={warning.start}
/>
<label className={style.inputLabel}>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<label className={inputTimeLabels}>{endLabel}</label>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
+26 -26
View File
@@ -8,8 +8,8 @@ import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle'
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoScan } from '@react-icons/all-files/io5/IoScan';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi';
import { downloadRundown } from '../../common/api/ontimeApi';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent';
@@ -130,31 +130,6 @@ export default function MenuBar(props: MenuBarProps) {
isDisabled={!isElectron}
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label='Help'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
className={isIntegrationOpen ? style.open : ''}
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
@@ -170,6 +145,31 @@ export default function MenuBar(props: MenuBarProps) {
tooltip='Export showfile'
aria-label='Export showfile'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
className={isIntegrationOpen ? style.open : ''}
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label='Help'
/>
</VStack>
);
}
+2 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { OntimeRundown, SupportedEvent } from 'ontime-types';
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
import { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket';
@@ -242,6 +242,7 @@ export default function Rundown(props: RundownProps) {
previousEnd={previousEnd}
previousEventId={previousEventId}
playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
/>
{((showQuickEntry && index === cursor) || isLast) && (
<QuickAddBlock
@@ -26,10 +26,23 @@ interface RundownEntryProps {
previousEnd: number;
previousEventId?: string;
playback?: Playback; // we only care about this if this event is playing
isRolling: boolean; // we need to know even if not related to this event
}
export default function RundownEntry(props: RundownEntryProps) {
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
const {
index,
eventIndex,
data,
selected,
hasCursor,
next,
delay,
previousEnd,
previousEventId,
playback,
isRolling,
} = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent } = useEventAction();
@@ -149,6 +162,7 @@ export default function RundownEntry(props: RundownEntryProps) {
selected={selected}
hasCursor={hasCursor}
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
/>
);
@@ -23,7 +23,7 @@ $block-cursor-color: $blue-400;
}
@mixin block-spacing() {
padding: 4px 8px 4px 2px;
padding-right: 8px;
gap: 2px;
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { Button, HStack } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
@@ -9,7 +9,6 @@ import { OntimeDelay, OntimeEvent } from 'ontime-types';
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
@@ -32,7 +31,7 @@ interface DelayBlockProps {
export default function DelayBlock(props: DelayBlockProps) {
const { data, hasCursor, actionHandler } = props;
const { applyDelay, updateEvent, deleteEvent } = useEventAction();
const { applyDelay, deleteEvent } = useEventAction();
const handleRef = useRef<null | HTMLSpanElement>(null);
const {
@@ -65,28 +64,14 @@ export default function DelayBlock(props: DelayBlockProps) {
deleteEvent(data.id);
};
const delaySubmitHandler = useCallback(
(value: number) => {
const newEvent = {
id: data.id,
duration: value * 60000,
};
updateEvent(newEvent);
},
[data.id, updateEvent],
);
const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return (
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo />
</span>
<DelayInput value={delayValue} submitHandler={delaySubmitHandler} />
<DelayInput eventId={data.id} duration={data.duration} />
<HStack spacing='8px' className={style.actionOverlay}>
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
Apply
@@ -22,10 +22,30 @@ $skip-opacity: 0.1;
padding-right: $block-clearance;
gap: 2px;
@mixin declare-overrides(){
--status-color-override: #{$gray-200};
--status-color-active-override: #{$green-400};
}
&.selected {
background-color: $gray-1350;
}
&.play {
background-color: $green-700;
@include declare-overrides;
}
&.roll {
background-color: $blue-700;
@include declare-overrides;
}
&.pause {
background-color: $orange-700;
@include declare-overrides;
}
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
@@ -146,6 +166,7 @@ $skip-opacity: 0.1;
justify-content: flex-end;
align-items: center;
gap: 8px;
color: var(--status-color-override, $gray-500);
.tag {
padding-top: 1px;
@@ -156,12 +177,12 @@ $skip-opacity: 0.1;
.statusIcon {
width: 16px;
height: 16px;
color: $gray-500;
}
.statusIcon.active {
color: $active-indicator;
color: var(--status-color-active-override, $active-indicator);
}
.statusIcon.disabled {
color: $gray-1000;
}
@@ -33,6 +33,7 @@ interface EventBlockProps {
selected: boolean;
hasCursor: boolean;
playback?: Playback;
isRolling: boolean;
actionHandler: (
action: EventItemActions,
payload?:
@@ -65,6 +66,7 @@ export default function EventBlock(props: EventBlockProps) {
selected,
hasCursor,
playback,
isRolling,
actionHandler,
} = props;
@@ -128,6 +130,7 @@ export default function EventBlock(props: EventBlockProps) {
style.eventBlock,
skip ? style.skip : null,
selected ? style.selected : null,
playback ? style[playback] : null,
hasCursor ? style.hasCursor : null,
]);
@@ -157,6 +160,7 @@ export default function EventBlock(props: EventBlockProps) {
skip={skip}
selected={selected}
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
/>
)}
@@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
@@ -49,6 +49,7 @@ interface EventBlockInnerProps {
skip: boolean;
selected: boolean;
playback?: Playback;
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
}
@@ -70,6 +71,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
skip = false,
selected,
playback,
isRolling,
actionHandler,
} = props;
@@ -89,17 +91,18 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
}
}, [eventId, isOpen, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play;
const eventIsPlaying = playback === Playback.Play;
const eventIsPaused = playback === Playback.Pause;
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' };
playBtnStyles._hover = { bg: '#c05621' }; // $ontime-paused
} else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = {};
}
return !renderInner ? null : (
<>
<EventBlockPlayback eventId={eventId} skip={skip} isPlaying={eventIsPlaying} selected={selected} />
<EventBlockTimers
eventId={eventId}
timeStart={timeStart}
@@ -109,6 +112,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
previousEnd={previousEnd}
/>
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
<EventBlockPlayback
eventId={eventId}
skip={skip}
isPlaying={eventIsPlaying}
isPaused={eventIsPaused}
selected={selected}
disablePlayback={skip || isRolling}
/>
<div className={style.statusElements}>
<span className={style.eventNote}>{note}</span>
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
@@ -175,10 +186,10 @@ function EndActionIcon(props: { action: EndAction; className: string }) {
function TimerIcon(props: { type: TimerType; className: string }) {
const { type, className } = props;
if (type === TimerType.CountUp) {
return <IoCaretUp className={className} />;
return <IoArrowUp className={className} />;
}
if (type === TimerType.Clock) {
return <IoTime className={className} />;
}
return <IoCaretDown className={className} />;
return <IoArrowDown className={className} />;
}
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
@@ -16,6 +16,13 @@ const blockBtnStyle = {
size: 'sm',
};
type StyleVariant = {
'aria-label': string;
tooltip: string;
backgroundColor: string;
_hover: { backgroundColor?: string };
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
@@ -24,17 +31,55 @@ interface EventBlockPlaybackProps {
eventId: string;
skip: boolean;
isPlaying: boolean;
isPaused: boolean;
selected: boolean;
disablePlayback: boolean;
}
const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
const { eventId, skip, isPlaying, selected } = props;
const { eventId, skip, isPlaying, isPaused, selected, disablePlayback } = props;
const { updateEvent } = useEventAction();
const toggleSkip = () => {
updateEvent({ id: eventId, skip: !skip });
};
const actionHandler = () => {
// is playing -> pause
// is paused -> continue
// otherwise -> start
if (isPlaying) {
setEventPlayback.pause();
} else if (isPaused) {
setEventPlayback.start();
} else {
setEventPlayback.startEvent(eventId);
}
};
const buttonVariant: Partial<StyleVariant> = {};
if (isPaused) {
// continue
buttonVariant['aria-label'] = 'Continue event';
buttonVariant.tooltip = 'Continue event';
buttonVariant.backgroundColor = '#339E4E';
buttonVariant._hover = { backgroundColor: '#339E4Eee' };
} else if (isPlaying) {
// pause
buttonVariant['aria-label'] = 'Pause event';
buttonVariant.tooltip = 'Pause event';
buttonVariant.backgroundColor = '#c05621';
buttonVariant._hover = { backgroundColor: '#c05621ee' };
} else {
// start
buttonVariant['aria-label'] = 'Start event';
buttonVariant.tooltip = 'Start event';
if (!disablePlayback) {
buttonVariant._hover = { backgroundColor: '#339E4E' };
}
}
return (
<div className={style.playbackActions}>
<TooltipActionBtn
@@ -55,7 +100,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
aria-label='Load event'
tooltip='Load event'
icon={<IoReload className={style.flip} />}
isDisabled={skip}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)}
@@ -65,13 +110,12 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
icon={isPlaying ? <IoPlay /> : <IoPlayOutline />}
isDisabled={skip}
icon={!isPlaying ? <IoPlay /> : <IoPause />}
isDisabled={disablePlayback}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={isPlaying ? '#58A151' : undefined}
_hover={{ backgroundColor: isPlaying ? '#58A151' : undefined }}
{...buttonVariant}
clickHandler={actionHandler}
tabIndex={-1}
/>
</div>
@@ -1,12 +1,10 @@
@use '../../../../theme/v2Styles' as *;
.progressBar {
// layout
height: 100%;
width: 0;
border-radius: 1px 0 0 1px;
// animations
transition: 1s linear;
transition-property: width;
@@ -14,6 +12,10 @@
background-color: $playback-start;
}
&.overtime {
background-color: $playback-negative;
}
&.pause {
background-color: $ontime-paused;
}
@@ -21,8 +23,4 @@
&.roll {
background-color: $ontime-roll;
}
&.overtime {
background-color: $playback-negative;
}
}
@@ -4,7 +4,7 @@ import { millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../../common/utils/dateConfig';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss';
@@ -64,8 +64,9 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
[timeEnd, timeStart],
);
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
const newTime = millisToString(timeStart + delay);
const delayedStart = Math.max(0, timeStart + delay);
const newTime = millisToString(delayedStart);
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
return (
<div className={style.eventTimers}>
@@ -94,13 +95,14 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={duration}
delay={0}
placeholder='Duration'
previousEnd={previousEnd}
warning={warning.duration}
/>
{delay !== 0 && delay !== null && (
{delayTime && (
<div className={style.delayNote}>
{`${delayTime} minutes`}
{delayTime}
<br />
{`New start: ${newTime}`}
</div>
@@ -10,11 +10,7 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import PropTypes from 'prop-types';
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
@@ -148,7 +144,7 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
if (el) {
el.scrollIntoView({
behavior: 'smooth',
block: 'start',
block: 'center',
inline: 'nearest',
});
}
@@ -119,7 +119,7 @@
grid-area: schedule;
overflow: hidden;
height: 100%;
margin-left: 16px;
margin-left: clamp(16px, 5vw, 64px);;
}
.schedule-nav-container {
@@ -131,6 +131,8 @@
grid-area: info;
display: flex;
gap: max(1vw, 16px);
align-self: flex-end;
overflow: hidden;
&__message {
font-size: clamp(16px, 1.5vw, 24px);
@@ -141,7 +143,7 @@
}
.qr {
margin-left: auto;
margin-left: clamp(16px, 5vw, 64px);;
padding: 4px;
background-color: white;
}
@@ -99,7 +99,8 @@
grid-area: info;
display: flex;
gap: max(1vw, 16px);
min-height: max(calc(100vh / 15), 128px);
align-self: flex-end;
overflow: hidden;
&__message {
font-size: clamp(16px, 1.5vw, 24px);
@@ -110,7 +111,7 @@
}
.qr {
margin-left: auto;
margin-left: clamp(16px, 5vw, 64px);;
padding: 4px;
background-color: white;
}
@@ -6,6 +6,7 @@ $white-7: rgba(255, 255, 255, 0.07);
$white-9: rgba(255, 255, 255, 0.09);
$white-10: rgba(255, 255, 255, 0.10);
$white-13: rgba(255, 255, 255, 0.13);
$white-20: rgba(255, 255, 255, 0.20);
$black-10: rgba(0, 0, 0, 0.10);
+12
View File
@@ -21,6 +21,9 @@ export const ontimeButtonOutlined = {
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
_hover: {
backgroundColor: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#2d2d2d', // $gray-1100
},
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
@@ -34,6 +37,9 @@ export const ontimeButtonSubtle = {
border: '1px solid transparent',
_hover: {
background: '#404040', // $gray-1000
_disabled: {
backgroundColor: '#303030', // $gray-1050
},
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
@@ -47,6 +53,9 @@ export const ontimeButtonSubtleOnLight = {
border: '1px solid transparent',
_hover: {
backgroundColor: '#cfcfcf', // $gray-200
_disabled: {
backgroundColor: '#ececec', // $gray-100
},
},
_active: {
backgroundColor: '#ececec', // $gray-200
@@ -60,6 +69,9 @@ export const ontimeGhostOnLight = {
_hover: {
color: '#595959', // $gray-800
backgroundColor: '#ececec', // $gray-200
_disabled: {
backgroundColor: 'transparent',
},
},
_active: {
backgroundColor: 'transparent',
+5 -5
View File
@@ -1,21 +1,21 @@
export const ontimeCheckboxOnDark = {
control: {
border: '1px',
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
_checked: {
borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue
},
_focus: {
boxShadow: '0 0 0 1px #578AF4'
}
boxShadow: '0 0 0 1px #578AF4', // $blue-500
},
},
label: {
fontWeight: '200',
color: '#9d9d9d', // $gray-500
_checked: {
color: '#cfcfcf', // $gray-300
}
},
},
};
+27
View File
@@ -0,0 +1,27 @@
export const ontimeBlockRadio = {
control: {
borderColor: '#262626', // $gray-1250
backgroundColor: '#262626', // $gray-1250
_checked: {
borderColor: '#262626', // $gray-1250
color: '#3182ce', // $action-blue
backgroundColor: '#3182ce', // $action-blue
},
_hover: {
color: '#3182ce', // $action-blue
backgroundColor: '#3182ce', // $action-blue
outline: 'none',
},
},
label: {
fontSize: '0.7em',
letterSpacing: '0.3px',
color: '#9d9d9d', // $gray-500
_checked: {
color: '#cfcfcf', // $gray-300
},
_hover: {
color: '#e2e2e2', // $gray-200
},
},
};
+6
View File
@@ -12,6 +12,7 @@ import { ontimeCheckboxOnDark } from './ontimeCheckbox';
import { ontimeEditable } from './ontimeEditable';
import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeModal } from './ontimeModal';
import { ontimeBlockRadio } from './ontimeRadio';
import { ontimeSelect } from './ontimeSelect';
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
import { ontimeTab } from './ontimeTab';
@@ -65,6 +66,11 @@ const theme = extendTheme({
ontime: { ...ontimeModal },
},
},
Radio: {
variants: {
'ontime-block': { ...ontimeBlockRadio },
},
},
Tabs: {
variants: {
ontime: { ...ontimeTab },