diff --git a/package-lock.json b/package-lock.json index d164031a8..808bfc59e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "react-dom": "^18.2.0", "react-draggable": "^4.4.5", "react-final-form": "^6.5.9", - "react-hotkeys": "^2.0.0", + "react-hotkeys-hook": "^4.4.1", "react-i18next": "^13.2.1", "react-icons": "^4.9.0", "react-indiana-drag-scroll": "^2.2.0", @@ -15855,15 +15855,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-hotkeys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", - "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", - "dependencies": { - "prop-types": "^15.6.1" - }, + "node_modules/react-hotkeys-hook": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", + "integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==", "peerDependencies": { - "react": ">= 0.14.0" + "react": ">=16.8.1", + "react-dom": ">=16.8.1" } }, "node_modules/react-i18next": { diff --git a/package.json b/package.json index d0dd728fa..48a283e27 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "react-dom": "^18.2.0", "react-draggable": "^4.4.5", "react-final-form": "^6.5.9", - "react-hotkeys": "^2.0.0", + "react-hotkeys-hook": "^4.4.1", "react-i18next": "^13.2.1", "react-icons": "^4.9.0", "react-indiana-drag-scroll": "^2.2.0", diff --git a/src/config.ts b/src/config.ts index e9d4d3818..cb977caab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,6 @@ */ import parseToml from '@iarna/toml/parse-string'; import deepmerge from 'deepmerge'; -import { configure } from 'react-hotkeys'; import { Flavor } from './types'; /** @@ -173,25 +172,6 @@ export const init = async () => { settings.callbackUrl = settings.allowedCallbackPrefixes.some( p => settings.callbackUrl?.startsWith(p) ) ? settings.callbackUrl : undefined; - - // Configure hotkeys - configure({ - ignoreTags: [], // Do not ignore hotkeys when focused on a textarea, input, select - ignoreEventsCondition: (e: any) => { - // Ignore hotkeys when focused on a textarea, input, select IF that hotkey is expected to perform - // a certain function in that element that is more important than any hotkey function - // (e.g. you need "Space" in a textarea to create whitespaces, not play/pause videos) - if (e.target && e.target.tagName) { - const tagname = e.target.tagName.toLowerCase() - if ((tagname === "textarea" || tagname === "input" || tagname === "select") - && (!e.altKey && !e.ctrlKey) - && (e.code === "Space" || e.code === "ArrowLeft" || e.code === "ArrowRight" || e.code === "ArrowUp" || e.code === "ArrowDown")) { - return true - } - } - return false - }, - }) }; /** diff --git a/src/globalKeys.ts b/src/globalKeys.ts index c0dd3e564..0476d48ff 100644 --- a/src/globalKeys.ts +++ b/src/globalKeys.ts @@ -1,4 +1,3 @@ -import { ApplicationKeyMap, ExtendedKeyMapOptions, KeyMapOptions, MouseTrapKeySequence } from 'react-hotkeys'; /** * Contains mappings for special keyboard controls, beyond what is usually expected of a webpage * Learn more about keymaps at https://github.com/greena13/react-hotkeys#defining-key-maps (12.03.2021) @@ -8,7 +7,8 @@ import { ApplicationKeyMap, ExtendedKeyMapOptions, KeyMapOptions, MouseTrapKeySe * * If you add a new keyMap, be sure to add it to the getAllHotkeys function */ -import { KeyMap } from "react-hotkeys"; +import { match } from '@opencast/appkit'; +import { ParseKeys } from 'i18next'; import { isMacOs } from 'react-device-detect'; // Groups for displaying hotkeys in the overview page @@ -20,7 +20,7 @@ const groupSubtitleList = "keyboardControls.groupSubtitleList" /** * Helper function that rewrites keys based on the OS */ -const rewriteKeys = (key: string) => { +export const rewriteKeys = (key: string) => { let newKey = key if (isMacOs) { newKey = newKey.replace("Alt", "Option") @@ -29,155 +29,95 @@ const rewriteKeys = (key: string) => { return newKey } -/** - * (Semi-) global map for video player controls - */ -export const videoPlayerKeyMap: KeyMap = { - preview: { - name: "video.previewButton", - sequence: rewriteKeys("Control+Alt+p"), - action: "keydown", - group: groupVideoPlayer, - }, - play: { - name: "keyboardControls.videoPlayButton", - sequence: rewriteKeys("Space"), - sequences: [rewriteKeys("Control+Alt+Space"), "Space"], - action: "keydown", - group: groupVideoPlayer, - }, +export const getGroupName = (groupName: string) : ParseKeys => { + return match(groupName, { + videoPlayer: () => groupVideoPlayer, + cutting: () => groupCuttingView, + timeline: () => groupCuttingViewScrubber, + subtitleList: () => groupSubtitleList, + }) } -/** - * (Semi-) global map for the buttons in the cutting view - */ -export const cuttingKeyMap: KeyMap = { - cut: { - name: "cuttingActions.cut-button", - sequence: rewriteKeys("Control+Alt+c"), - action: "keydown", - group: groupCuttingView, - }, - delete: { - name: "cuttingActions.delete-button", - sequence: rewriteKeys("Control+Alt+d"), - action: "keydown", - group: groupCuttingView, - }, - mergeLeft: { - name: "cuttingActions.mergeLeft-button", - sequence: rewriteKeys("Control+Alt+n"), - action: "keydown", - group: groupCuttingView, - }, - mergeRight: { - name: "cuttingActions.mergeRight-button", - sequence: rewriteKeys("Control+Alt+m"), - action: "keydown", - group: groupCuttingView, - }, +export interface IKeyMap { + [property: string]: IKeyGroup } -/** - * (Semi-) global map for moving the scrubber - */ -export const scrubberKeyMap: KeyMap = { - left: { - name: "keyboardControls.scrubberLeft", - // Typescript requires 'sequence' even though there is 'sequences, but it doesn't do anything? - sequence: rewriteKeys("Control+Alt+j"), - sequences: [rewriteKeys("Control+Alt+j"), "Left"], - action: "keydown", - group: groupCuttingViewScrubber, - }, - right: { - name: "keyboardControls.scrubberRight", - // Typescript requires 'sequence' even though there is 'sequences, but it doesn't do anything? - sequence: rewriteKeys("Control+Alt+l"), - sequences: [rewriteKeys("Control+Alt+l"), "Right"], - action: "keydown", - group: groupCuttingViewScrubber, - }, - increase: { - name: "keyboardControls.scrubberIncrease", - // Typescript requires 'sequence' even though there is 'sequences, but it doesn't do anything? - sequence: rewriteKeys("Control+Alt+i"), - sequences: [rewriteKeys("Control+Alt+i"), "Up"], - action: "keydown", - group: groupCuttingViewScrubber, - }, - decrease: { - name: "keyboardControls.scrubberDecrease", - // Typescript requires 'sequence' even though there is 'sequences, but it doesn't do anything? - sequence: rewriteKeys("Control+Alt+k"), - sequences: [rewriteKeys("Control+Alt+k"), "Down"], - action: "keydown", - group: groupCuttingViewScrubber, - }, +export interface IKeyGroup { + [property: string]: IKey } -export const subtitleListKeyMap: KeyMap = { - addAbove: { - name: "subtitleList.addSegmentAbove", - sequence: rewriteKeys("Control+Alt+q"), - action: "keydown", - group: groupSubtitleList, - }, - addBelow: { - name: "subtitleList.addSegmentBelow", - sequence: rewriteKeys("Control+Alt+a"), - action: "keydown", - group: groupSubtitleList, +export interface IKey { + name: string + key: string +} + +export const KEYMAP: IKeyMap = { + videoPlayer: { + play: { + name: "keyboardControls.videoPlayButton", + key: "Shift+Alt+Space, Space", + }, + preview: { + name: "video.previewButton", + key: "Shift+Alt+p", + } }, - jumpAbove: { - name: "subtitleList.jumpToSegmentAbove", - sequence: rewriteKeys("Control+Alt+w"), - action: "keydown", - group: groupSubtitleList, + cutting: { + cut: { + name: "cuttingActions.cut-button", + key: "Shift+Alt+c", + }, + delete: { + name: "cuttingActions.delete-button", + key: "Shift+Alt+d", + }, + mergeLeft: { + name: "cuttingActions.mergeLeft-button", + key: "Shift+Alt+n", + }, + mergeRight: { + name: "cuttingActions.mergeRight-button", + key: "Shift+Alt+m", + }, }, - jumpBelow: { - name: "subtitleList.jumpToSegmentBelow", - sequence: rewriteKeys("Control+Alt+s"), - action: "keydown", - group: groupSubtitleList, + timeline: { + left: { + name: "keyboardControls.scrubberLeft", + key: "Shift+Alt+j , Left", + }, + right: { + name: "keyboardControls.scrubberRight", + key: "Shift+Alt+l, Right", + }, + increase: { + name: "keyboardControls.scrubberIncrease", + key: "Shift+Alt+i, Up", + }, + decrease: { + name: "keyboardControls.scrubberDecrease", + key: "Shift+Alt+k, Down", + }, }, - delete: { - name: "subtitleList.deleteSegment", - sequence: rewriteKeys("Control+Alt+d"), - action: "keydown", - group: groupSubtitleList, - } -} - -/** - * Combines all keyMaps into a single list of keys for KeyboardControls to display - * Placing this under the keyMaps is important, else the translation hooks won't happen - */ -export const getAllHotkeys = () => { - const allKeyMaps = [videoPlayerKeyMap, cuttingKeyMap, scrubberKeyMap, subtitleListKeyMap] - const allKeys : ApplicationKeyMap = {} - - for (const keyMap of allKeyMaps) { - for (const [key, value] of Object.entries(keyMap)) { - - // Parse sequences - let sequences : KeyMapOptions[] = [] - if ((value as ExtendedKeyMapOptions).sequences !== undefined) { - for (const sequence of (value as ExtendedKeyMapOptions).sequences) { - sequences.push({sequence: sequence as MouseTrapKeySequence, action: (value as ExtendedKeyMapOptions).action}) - } - } else { - sequences = [{sequence: (value as ExtendedKeyMapOptions).sequence, action: (value as ExtendedKeyMapOptions).action }] - } - - // Create new key - allKeys[key] = { - name: (value as ExtendedKeyMapOptions).name, - group: (value as ExtendedKeyMapOptions).group, - sequences: sequences, - } + subtitleList: { + addAbove: { + name: "subtitleList.addSegmentAbove", + key: "Shift+Alt+q", + }, + addBelow: { + name: "subtitleList.addSegmentBelow", + key: "Shift+Alt+a", + }, + jumpAbove: { + name: "subtitleList.jumpToSegmentAbove", + key: "Shift+Alt+w", + }, + jumpBelow: { + name: "subtitleList.jumpToSegmentBelow", + key: "Shift+Alt+s", + }, + delete: { + name: "subtitleList.deleteSegment", + key: "Shift+Alt+d", } } - - return allKeys } diff --git a/src/index.tsx b/src/index.tsx index 4baaac58d..4ce4aa329 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOMClient from 'react-dom/client'; import './index.css'; import App from './App'; import { Provider } from 'react-redux' @@ -8,8 +8,6 @@ import store from './redux/store' import { init } from './config' import { sleep } from './util/utilityFunctions' -import { GlobalHotKeys } from 'react-hotkeys'; - import "@fontsource-variable/roboto-flex"; import './i18n/config'; @@ -17,6 +15,13 @@ import './i18n/config'; import '@opencast/appkit/dist/colors.css' import { ColorSchemeProvider } from '@opencast/appkit'; +const container = document.getElementById('root') +if (!container) { + throw new Error('Failed to find the root element'); +} +const root = ReactDOMClient.createRoot(container); + + // Load config here // Load the rest of the application and try to fetch the settings file from the // server. @@ -25,30 +30,22 @@ const initialize = Promise.race([ sleep(300), ]); -const render = (body: JSX.Element) => { - ReactDOM.render(body, document.getElementById('root')); -}; - initialize.then( () => { - ReactDOM.render( + root.render( - {/* Workaround for getApplicationKeyMap based on https://github.com/greena13/react-hotkeys/issues/228 */} - - - - - + + + , - document.getElementById('root') ); }, // This error case is vey unlikely to occur. - e => render(

+ e => root.render(

{`Fatal error while loading app: ${e.message}`}
This might be caused by a incorrect configuration by the system administrator. diff --git a/src/main/Body.tsx b/src/main/Body.tsx index 2bdc6a03d..94861948a 100644 --- a/src/main/Body.tsx +++ b/src/main/Body.tsx @@ -13,7 +13,6 @@ import { selectIsEnd } from '../redux/endSlice' import { selectIsError } from "../redux/errorSlice"; import { settings } from '../config'; - const Body: React.FC = () => { const isEnd = useSelector(selectIsEnd) diff --git a/src/main/CuttingActions.tsx b/src/main/CuttingActions.tsx index 680118066..2a6f92ad8 100644 --- a/src/main/CuttingActions.tsx +++ b/src/main/CuttingActions.tsx @@ -1,4 +1,4 @@ -import React, { SyntheticEvent } from "react"; +import React from "react"; import { basicButtonStyle, customIconStyle } from '../cssStyles' @@ -12,13 +12,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { cut, markAsDeletedOrAlive, selectIsCurrentSegmentAlive, mergeLeft, mergeRight, mergeAll } from '../redux/videoSlice' -import { GlobalHotKeys, KeySequence, KeyMapOptions } from "react-hotkeys"; -import { cuttingKeyMap } from "../globalKeys"; +import { KEYMAP, rewriteKeys } from "../globalKeys"; import { ActionCreatorWithoutPayload } from "@reduxjs/toolkit"; import { useTranslation } from 'react-i18next'; import { useTheme } from "../themes"; import { ThemedTooltip } from "./Tooltip"; +import { useHotkeys } from "react-hotkeys-hook"; /** * Defines the different actions a user can perform while in cutting mode @@ -36,9 +36,7 @@ const CuttingActions: React.FC = () => { * @param action redux event to dispatch * @param ref Pass a reference if the clicked element should lose focus */ - const dispatchAction = (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => { - event.preventDefault() // Prevent page scrolling due to Space bar press - event.stopPropagation() // Prevent video playback due to Space bar press + const dispatchAction = (action: ActionCreatorWithoutPayload, ref?: React.RefObject) => { dispatch(action()) // Lose focus if clicked by mouse @@ -48,12 +46,10 @@ const CuttingActions: React.FC = () => { } // Maps functions to hotkeys - const handlers = { - cut: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if (keyEvent) { dispatchAction(keyEvent, cut, undefined) } }, - delete: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if (keyEvent) { dispatchAction(keyEvent, markAsDeletedOrAlive, undefined) } }, - mergeLeft: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if (keyEvent) { dispatchAction(keyEvent, mergeLeft, undefined) } }, - mergeRight: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if (keyEvent) { dispatchAction(keyEvent, mergeRight, undefined) } }, - } + useHotkeys(KEYMAP.cutting.cut.key, () => dispatchAction(cut), {preventDefault: true}, [cut]); + useHotkeys(KEYMAP.cutting.delete.key, () => dispatchAction(markAsDeletedOrAlive), {preventDefault: true}, [markAsDeletedOrAlive]); + useHotkeys(KEYMAP.cutting.mergeLeft.key, () => dispatchAction(mergeLeft), {preventDefault: true}, [mergeLeft]); + useHotkeys(KEYMAP.cutting.mergeRight.key, () => dispatchAction(mergeRight), {preventDefault: true}, [mergeRight]); const cuttingStyle = css({ display: 'flex', @@ -68,45 +64,43 @@ const CuttingActions: React.FC = () => { }) return ( - -

- -
- -
- -
- -
- - {/* - */} -
- +
+ +
+ +
+ +
+ +
+ + {/* + */} +
); }; @@ -122,7 +116,7 @@ const cuttingActionButtonStyle = css({ interface cuttingActionsButtonInterface { Icon: IconType, actionName: string, - actionHandler: (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => void, + actionHandler: (action: ActionCreatorWithoutPayload, ref?: React.RefObject) => void, action: ActionCreatorWithoutPayload, tooltip: string, ariaLabelText: string, @@ -141,9 +135,9 @@ const CuttingActionsButton: React.FC = ({Icon, ac
actionHandler(event, action, ref)} + onClick={() => actionHandler(action, ref)} onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { - actionHandler(event, action, undefined) + actionHandler(action) } }} > @@ -154,9 +148,9 @@ const CuttingActionsButton: React.FC = ({Icon, ac }; interface markAsDeleteButtonInterface { - actionHandler: (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => void, + actionHandler: (action: ActionCreatorWithoutPayload, ref?: React.RefObject) => void, action: ActionCreatorWithoutPayload, - hotKeyName: KeySequence, + hotKeyName: string, } /** @@ -175,9 +169,9 @@ const MarkAsDeletedButton : React.FC = ({actionHand ref={ref} role="button" tabIndex={0} aria-label={t('cuttingActions.delete-restore-tooltip-aria', { hotkeyName: hotKeyName })} - onClick={(event: SyntheticEvent) => actionHandler(event, action, ref)} + onClick={() => actionHandler(action, ref)} onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { - actionHandler(event, action, undefined) + actionHandler(action) } }} > {isCurrentSegmentAlive ? : } diff --git a/src/main/KeyboardControls.tsx b/src/main/KeyboardControls.tsx index ebb12ba9c..f861df1be 100644 --- a/src/main/KeyboardControls.tsx +++ b/src/main/KeyboardControls.tsx @@ -3,14 +3,13 @@ import { ParseKeys } from "i18next"; import React from "react"; -import { KeyMapDisplayOptions } from 'react-hotkeys'; import { useTranslation, Trans} from "react-i18next"; import { flexGapReplacementStyle } from "../cssStyles"; -import { getAllHotkeys } from "../globalKeys"; +import { getGroupName, KEYMAP, rewriteKeys } from "../globalKeys"; import { useTheme } from "../themes"; import { titleStyle, titleStyleBold } from '../cssStyles' -const Group: React.FC<{name: ParseKeys, entries: KeyMapDisplayOptions[]}> = ({name, entries}) => { +const Group: React.FC<{name: ParseKeys, entries: { [key: string]: string[][] }}> = ({name, entries}) => { const { t } = useTranslation(); const theme = useTheme(); @@ -35,14 +34,14 @@ const Group: React.FC<{name: ParseKeys, entries: KeyMapDisplayOptions[]}> = ({na return (

{t(name)}

- {entries.map((entry: KeyMapDisplayOptions, index: number) => ( - - ))} + {Object.entries(entries).map(([key, value], index) => + + )}
) } -const Entry: React.FC<{params: KeyMapDisplayOptions}> = ({params}) => { +const Entry: React.FC<{name: string, sequences: string[][] }> = ({name, sequences}) => { const { t } = useTranslation(); const theme = useTheme(); @@ -64,12 +63,6 @@ const Entry: React.FC<{params: KeyMapDisplayOptions}> = ({params}) => { color: `${theme.text}`, }) - const sequencesStyle = css({ - display: 'flex', - flexDirection: 'row', - ...(flexGapReplacementStyle(10, true)) - }) - const sequenceStyle = css({ display: 'flex', flexDirection: 'row', @@ -95,20 +88,18 @@ const Entry: React.FC<{params: KeyMapDisplayOptions}> = ({params}) => { return (
-
{params.name || t("keyboardControls.missingLabel")}
-
- {params.sequences.map((sequence, index, arr) => ( -
- {sequence.sequence.toString().split('+').map((singleKey, index, {length}) => ( - <> -
{singleKey}
- {length - 1 !== index ?
+
: ''} - - ))} -
{arr.length - 1 !== index && t("keyboardControls.sequenceSeparator")}
-
- ))} -
+
{name || t("keyboardControls.missingLabel")}
+ {sequences.map((sequence, index, arr) => ( +
+ {sequence.map((singleKey, index) => ( + <> +
{singleKey}
+ {sequence.length - 1 !== index &&
+
} + + ))} +
{arr.length - 1 !== index && t("keyboardControls.sequenceSeparator")}
+
+ ))}
) } @@ -119,8 +110,6 @@ const KeyboardControls: React.FC = () => { const { t } = useTranslation(); const theme = useTheme() - const keyMap = getAllHotkeys() - const groupsStyle = css({ display: 'flex', flexDirection: 'row' as const, @@ -130,30 +119,19 @@ const KeyboardControls: React.FC = () => { }) const render = () => { - if (keyMap && Object.keys(keyMap).length > 0) { - - const obj: Record> = {} - obj[t("keyboardControls.defaultGroupName")] = [] // For keys without a group - - // Sort by group - for (const [, value] of Object.entries(keyMap)) { - if (value.group) { - if (obj[value.group]) { - obj[value.group].push(value) - } else { - obj[value.group] = [value] - } - } else { - obj[t("keyboardControls.defaultGroupName")].push(value) - } - } + if (KEYMAP && Object.keys(KEYMAP).length > 0) { const groups: JSX.Element[] = []; - for (const key in obj) { - if (obj[key].length > 0) { - groups.push(); - } - } + Object.entries(KEYMAP).forEach(([groupName, group], index) => { + const entries : { [groupName: string]: string[][] } = {} + Object.entries(group).forEach(([, action]) => { + const sequences = action.key.split(",").map(item => item.trim()) + entries[action.name] = Object.entries(sequences).map(([, sequence]) => { + return sequence.split("+").map(item => rewriteKeys(item.trim())) + }) + }) + groups.push() + }) return (
diff --git a/src/main/SubtitleListEditor.tsx b/src/main/SubtitleListEditor.tsx index e0c58d45a..3f91ffabc 100644 --- a/src/main/SubtitleListEditor.tsx +++ b/src/main/SubtitleListEditor.tsx @@ -4,11 +4,10 @@ import { LuPlus, LuTrash} from "react-icons/lu"; import { memoize } from "lodash" import React, { useRef } from "react" import { useEffect, useState } from "react" -import { HotKeys } from "react-hotkeys" import { useTranslation } from "react-i18next" import { shallowEqual, useDispatch, useSelector } from "react-redux" import { basicButtonStyle, flexGapReplacementStyle } from "../cssStyles" -import { subtitleListKeyMap } from "../globalKeys" +import { KEYMAP } from "../globalKeys" import { addCueAtIndex, removeCue, selectFocusSegmentId, @@ -30,6 +29,7 @@ import { CSSProperties } from "react" import AutoSizer from "react-virtualized-auto-sizer" import { useTheme } from "../themes"; import { ThemedTooltip } from "./Tooltip" +import { useHotkeys } from "react-hotkeys-hook" import { useColorScheme } from "@opencast/appkit"; /** @@ -276,23 +276,35 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => { } // Maps functions to hotkeys - const handlers = { - addAbove: () => addCueAbove(), - addBelow: () => addCueBelow(), - jumpAbove: () => { - dispatch(setFocusSegmentTriggered(true)) - dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal})) - }, - jumpBelow: () => { - dispatch(setFocusSegmentTriggered(true)) - dispatch(setFocusToSegmentBelowId({identifier: identifier, segmentId: cue.idInternal})) - }, - delete: () => { - dispatch(setFocusSegmentTriggered(true)) - dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal})) - deleteCue() - }, - } + const hotkeyRef = useHotkeys([ + KEYMAP.subtitleList.addAbove.key, + KEYMAP.subtitleList.addBelow.key, + KEYMAP.subtitleList.jumpAbove.key, + KEYMAP.subtitleList.jumpBelow.key, + KEYMAP.subtitleList.delete.key + ], (_, handler) => { + switch (handler.keys?.join('')) { + case KEYMAP.subtitleList.addAbove.key.split('+').pop(): + addCueAbove() + break; + case KEYMAP.subtitleList.addBelow.key.split('+').pop(): + addCueBelow() + break; + case KEYMAP.subtitleList.jumpAbove.key.split('+').pop(): + dispatch(setFocusSegmentTriggered(true)) + dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal})) + break; + case KEYMAP.subtitleList.jumpBelow.key.split('+').pop(): + dispatch(setFocusSegmentTriggered(true)) + dispatch(setFocusToSegmentBelowId({identifier: identifier, segmentId: cue.idInternal})) + break; + case KEYMAP.subtitleList.delete.key.split('+').pop(): + dispatch(setFocusSegmentTriggered(true)) + dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal})) + deleteCue() + break; + } + }, { enableOnFormTags: ['input', 'select', 'textarea'] }, [identifier, cue, props.index]) const setTimeToSegmentStart = () => { dispatch(setCurrentlyAt(cue.startTime)) @@ -367,84 +379,82 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => { }) return ( - -
- -