mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-23 16:09:11 +00:00
shortcuts using useHotkeys from mantine/hooks (#907)
--------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
0c3c08ac98
commit
9e04a1dcfa
@@ -1,13 +1,15 @@
|
||||
import { Fragment, lazy, 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 { useHotkeys } from '@mantine/hooks';
|
||||
import { isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
|
||||
import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
||||
import { getFirstNormal, getLastNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
||||
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
|
||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||
@@ -26,7 +28,9 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||
|
||||
const featureData = useRundownEditor();
|
||||
const { addEvent, reorderEvent } = useEventAction();
|
||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||
|
||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
||||
|
||||
// cursor
|
||||
const { cursor, mode: appMode, setCursor } = useAppMode();
|
||||
@@ -37,9 +41,21 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// DND KIT
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const deleteAtCursor = useCallback(
|
||||
(cursor: string | null) => {
|
||||
if (!cursor) return;
|
||||
const previous = getPreviousNormal(rundown, order, cursor).entry?.id ?? null;
|
||||
deleteEvent(cursor);
|
||||
setCursor(previous);
|
||||
},
|
||||
[deleteEvent, order, rundown, setCursor],
|
||||
);
|
||||
|
||||
const insertAtCursor = useCallback(
|
||||
(type: SupportedEvent | 'clone', cursor: string | null) => {
|
||||
if (cursor === null) {
|
||||
(type: SupportedEvent | 'clone', cursor: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, cursor ?? '').entry?.id ?? null : cursor;
|
||||
|
||||
if (adjustedCursor === null) {
|
||||
// we cant clone without selection
|
||||
if (type === 'clone') {
|
||||
return;
|
||||
@@ -50,7 +66,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
}
|
||||
|
||||
if (type === 'clone') {
|
||||
const cursorEvent = rundown[cursor];
|
||||
const cursorEvent = rundown[adjustedCursor];
|
||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||
addEvent(newEvent);
|
||||
@@ -60,111 +76,90 @@ export default function Rundown({ data }: RundownProps) {
|
||||
type: SupportedEvent.Event,
|
||||
};
|
||||
const options = {
|
||||
after: cursor,
|
||||
lastEventId: cursor,
|
||||
after: adjustedCursor,
|
||||
lastEventId: adjustedCursor,
|
||||
};
|
||||
addEvent(newEvent, options);
|
||||
} else {
|
||||
addEvent({ type }, { after: cursor });
|
||||
addEvent({ type }, { after: adjustedCursor });
|
||||
}
|
||||
},
|
||||
[addEvent, rundown],
|
||||
[rundown, order, addEvent],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// handle held key
|
||||
if (event.repeat) return;
|
||||
const selectEntry = useCallback(
|
||||
(cursor: string | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
}
|
||||
let newCursor: string | undefined;
|
||||
if (cursor === null) {
|
||||
// there is no cursor, we select the first or last depending on direction if it exists
|
||||
newCursor = direction === 'up' ? getLastNormal(rundown, order)?.id : getFirstNormal(rundown, order)?.id;
|
||||
} else {
|
||||
// otherwise we select the next or previous
|
||||
newCursor =
|
||||
direction === 'up'
|
||||
? getPreviousNormal(rundown, order, cursor).entry?.id
|
||||
: getNextNormal(rundown, order, cursor).entry?.id;
|
||||
}
|
||||
|
||||
const modKeysAlt = event.altKey && !event.ctrlKey && !event.shiftKey;
|
||||
const modKeysCtrlAlt = event.altKey && event.ctrlKey && !event.shiftKey;
|
||||
|
||||
if (modKeysAlt) {
|
||||
switch (event.code) {
|
||||
case 'ArrowDown': {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
}
|
||||
const nextEvent =
|
||||
cursor === null ? getFirstNormal(rundown, order) : getNextNormal(rundown, order, cursor)?.nextEvent;
|
||||
if (nextEvent) {
|
||||
setCursor(nextEvent.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
||||
const previousEvent =
|
||||
cursor === null
|
||||
? getFirstNormal(rundown, order)
|
||||
: getPreviousNormal(rundown, order, cursor).previousEvent;
|
||||
if (previousEvent) {
|
||||
setCursor(previousEvent.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'KeyE': {
|
||||
event.preventDefault();
|
||||
insertAtCursor(SupportedEvent.Event, cursor);
|
||||
break;
|
||||
}
|
||||
case 'KeyD': {
|
||||
event.preventDefault();
|
||||
insertAtCursor(SupportedEvent.Delay, cursor);
|
||||
break;
|
||||
}
|
||||
case 'KeyB': {
|
||||
event.preventDefault();
|
||||
insertAtCursor(SupportedEvent.Block, cursor);
|
||||
break;
|
||||
}
|
||||
case 'KeyC': {
|
||||
event.preventDefault();
|
||||
insertAtCursor('clone', cursor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (modKeysCtrlAlt) {
|
||||
if (order.length < 2 || cursor == null) {
|
||||
return;
|
||||
}
|
||||
// Alt + Ctrl + Arrow Down
|
||||
if (event.code == 'ArrowDown') {
|
||||
const { nextEvent, nextIndex } = getNextNormal(rundown, order, cursor);
|
||||
if (nextEvent && nextIndex !== null) {
|
||||
reorderEvent(cursor, nextIndex - 1, nextIndex);
|
||||
}
|
||||
// Alt + Ctrl + Arrow Up
|
||||
} else if (event.code == 'ArrowUp') {
|
||||
const { previousEvent, previousIndex } = getPreviousNormal(rundown, order, cursor);
|
||||
if (previousEvent && previousIndex !== null) {
|
||||
reorderEvent(cursor, previousIndex + 1, previousIndex);
|
||||
}
|
||||
}
|
||||
if (newCursor) {
|
||||
setCursor(newCursor);
|
||||
}
|
||||
},
|
||||
[order, cursor, rundown, setCursor, insertAtCursor, reorderEvent],
|
||||
[order, rundown, setCursor],
|
||||
);
|
||||
|
||||
const moveEntry = useCallback(
|
||||
(cursor: string | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 2 || cursor == null) {
|
||||
return;
|
||||
}
|
||||
const { index } =
|
||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||
|
||||
if (index !== null) {
|
||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
||||
reorderEvent(cursor, offsetIndex, index);
|
||||
}
|
||||
},
|
||||
[order, reorderEvent, rundown],
|
||||
);
|
||||
|
||||
// shortcuts
|
||||
useHotkeys([
|
||||
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true }],
|
||||
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true }],
|
||||
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true }],
|
||||
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true }],
|
||||
|
||||
['Escape', () => setCursor(null), { preventDefault: true }],
|
||||
|
||||
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
||||
|
||||
['alt + E', () => insertAtCursor(SupportedEvent.Event, cursor), { preventDefault: true }],
|
||||
['alt + shift + E', () => insertAtCursor(SupportedEvent.Event, cursor, true), { preventDefault: true }],
|
||||
|
||||
['alt + B', () => insertAtCursor(SupportedEvent.Block, cursor), { preventDefault: true }],
|
||||
['alt + shift + B', () => insertAtCursor(SupportedEvent.Block, cursor, true), { preventDefault: true }],
|
||||
|
||||
['alt + D', () => insertAtCursor(SupportedEvent.Delay, cursor), { preventDefault: true }],
|
||||
['alt + shift + D', () => insertAtCursor(SupportedEvent.Delay, cursor, true), { preventDefault: true }],
|
||||
|
||||
['mod + C', () => setEntryCopyId(cursor), { preventDefault: true }],
|
||||
['mod + V', () => insertAtCursor('clone', entryCopyId), { preventDefault: true }],
|
||||
['mod + shift + V', () => insertAtCursor('clone', entryCopyId, true), { preventDefault: true }],
|
||||
|
||||
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
||||
]);
|
||||
|
||||
// we copy the state from the store here
|
||||
// to workaround async updates on the drag mutations
|
||||
useEffect(() => {
|
||||
setStatefulEntries(order);
|
||||
}, [order]);
|
||||
|
||||
// listen to keys
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
// in run mode, we follow selection
|
||||
if (appMode !== AppMode.Run || !featureData?.selectedEventId) {
|
||||
|
||||
Reference in New Issue
Block a user