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:
Alex Christoffer Rasmussen
2024-04-27 17:00:41 +01:00
committed by GitHub
parent 0c3c08ac98
commit 9e04a1dcfa
19 changed files with 424 additions and 253 deletions
+87 -92
View File
@@ -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) {