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 (
-
-
{/* Mini Timeline. Makes it easier to understand position in scrollable timeline */}
-
-
+
+ setCurrentlyAtToClick(e)}
+ css={{
+ position: 'relative',
+ width: '100%',
+ height: '15px',
+ background: `linear-gradient(to right, grey ${(currentlyAt / duration) * 100}%, lightgrey ${(currentlyAt / duration) * 100}%)`,
+ borderRadius: '3px',
+ }}
+ ref={refMini}
+ >
setCurrentlyAtToClick(e)}
- css={{
- position: 'relative',
- width: '100%',
- height: '15px',
- background: `linear-gradient(to right, grey ${(currentlyAt / duration) * 100}%, lightgrey ${(currentlyAt / duration) * 100}%)`,
- borderRadius: '3px',
- }}
- ref={refMini}
- >
-
-
-
-
+ css={{position: 'absolute', width: '2px', height: '100%', left: (currentlyAt / duration) * (widthMiniTimeline), top: 0, background: 'black'}}
+ />
+
+
diff --git a/src/main/Timeline.tsx b/src/main/Timeline.tsx
index 4697d8e7a..6ca99eedf 100644
--- a/src/main/Timeline.tsx
+++ b/src/main/Timeline.tsx
@@ -16,14 +16,14 @@ import useResizeObserver from "use-resize-observer";
import { Waveform } from '../util/waveform'
import { convertMsToReadableString } from '../util/utilityFunctions';
-import { GlobalHotKeys } from 'react-hotkeys';
-import { scrubberKeyMap } from '../globalKeys';
+import { KEYMAP, rewriteKeys } from '../globalKeys';
import { useTranslation } from 'react-i18next';
import { ActionCreatorWithPayload } from '@reduxjs/toolkit';
import { RootState } from '../redux/store';
import { useTheme } from "../themes";
import { ThemedTooltip } from './Tooltip';
+import { useHotkeys } from 'react-hotkeys-hook';
import { spinningStyle } from '../cssStyles';
/**
@@ -183,12 +183,10 @@ export const Scrubber: React.FC<{
// Callbacks for keyboard controls
// TODO: Better increases and decreases than ten intervals
// TODO: Additional helpful controls (e.g. jump to start/end of segment/next segment)
- const handlers = {
- left: () => dispatch(setCurrentlyAt(Math.max(currentlyAt - keyboardJumpDelta, 0))),
- right: () => dispatch(setCurrentlyAt(Math.min(currentlyAt + keyboardJumpDelta, duration))),
- increase: () => setKeyboardJumpDelta(keyboardJumpDelta => Math.min(keyboardJumpDelta * 10, 1000000)),
- decrease: () => setKeyboardJumpDelta(keyboardJumpDelta => Math.max(keyboardJumpDelta / 10, 1))
- }
+ useHotkeys(KEYMAP.timeline.left.key, () => dispatch(setCurrentlyAt(Math.max(currentlyAt - keyboardJumpDelta, 0))), {}, [currentlyAt, keyboardJumpDelta]);
+ useHotkeys(KEYMAP.timeline.right.key, () => dispatch(setCurrentlyAt(Math.min(currentlyAt + keyboardJumpDelta, duration))), {}, [currentlyAt, keyboardJumpDelta, duration]);
+ useHotkeys(KEYMAP.timeline.increase.key, () => setKeyboardJumpDelta(keyboardJumpDelta => Math.min(keyboardJumpDelta * 10, 1000000)), {}, [keyboardJumpDelta]);
+ useHotkeys(KEYMAP.timeline.decrease.key, () => setKeyboardJumpDelta(keyboardJumpDelta => Math.max(keyboardJumpDelta / 10, 1)), {}, [keyboardJumpDelta]);
const scrubberStyle = css({
backgroundColor: `${theme.scrubber}`,
@@ -239,31 +237,29 @@ export const Scrubber: React.FC<{
// }
return (
-
-
-
-
-
-
+
+
+
);
};
diff --git a/src/main/VideoControls.tsx b/src/main/VideoControls.tsx
index 9c7fc6150..3ebcca8ae 100644
--- a/src/main/VideoControls.tsx
+++ b/src/main/VideoControls.tsx
@@ -13,9 +13,7 @@ import {
import { convertMsToReadableString } from '../util/utilityFunctions'
import { basicButtonStyle, flexGapReplacementStyle } from "../cssStyles";
-import { GlobalHotKeys, KeyMapOptions } from 'react-hotkeys';
-import { videoPlayerKeyMap } from "../globalKeys";
-import { SyntheticEvent } from "react";
+import { KEYMAP, rewriteKeys } from "../globalKeys";
import { useTranslation } from 'react-i18next';
import { RootState } from "../redux/store";
@@ -23,6 +21,7 @@ import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
import { ThemedTooltip } from "./Tooltip";
import { Theme, useTheme } from "../themes";
+import { useHotkeys } from "react-hotkeys-hook";
/**
* Contains controls for manipulating multiple video players at once
@@ -111,9 +110,7 @@ const PreviewMode: React.FC<{
const theme = useTheme();
// Change preview mode from "on" to "off" and vice versa
- const switchPlayPreview = (event: KeyboardEvent | SyntheticEvent, 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 switchPlayPreview = (ref: React.RefObject | undefined) => {
dispatch(setIsPlayPreview(!isPlayPreview))
// Lose focus if clicked by mouse
@@ -123,10 +120,7 @@ const PreviewMode: React.FC<{
}
// Maps functions to hotkeys
- const handlers = {
- // preview: switchPlayPreview,
- preview: (keyEvent?: KeyboardEvent) => { if (keyEvent) { switchPlayPreview(keyEvent, undefined) } }
- }
+ useHotkeys(KEYMAP.videoPlayer.preview.key, () => switchPlayPreview(undefined), {preventDefault: true}, [isPlayPreview]);
const previewModeStyle = css({
cursor: "pointer",
@@ -150,17 +144,16 @@ const PreviewMode: React.FC<{
return (
switchPlayPreview(event, ref)}
+ aria-label={t("video.previewButton-aria", { hotkeyName: rewriteKeys(KEYMAP.videoPlayer.preview.key) })}
+ onClick={() => switchPlayPreview(ref)}
onKeyDown={(event: React.KeyboardEvent
) => { if (event.key === " ") {
- switchPlayPreview(event, undefined)
+ switchPlayPreview(undefined)
} }}>
-
{t("video.previewButton")}
@@ -190,15 +183,12 @@ const PlayButton: React.FC<{
const theme = useTheme();
// Change play mode from "on" to "off" and vice versa
- const switchIsPlaying = (event: KeyboardEvent | SyntheticEvent) => {
- event.preventDefault() // Prevent page scrolling due to Space bar press
+ const switchIsPlaying = () => {
dispatch(setIsPlaying(!isPlaying))
}
// Maps functions to hotkeys
- const handlers = {
- play: (keyEvent?: KeyboardEvent) => { if (keyEvent) { switchIsPlaying(keyEvent) } }
- }
+ useHotkeys(KEYMAP.videoPlayer.play.key, () => switchIsPlaying(), {preventDefault: true}, [isPlaying]);
const playButtonStyle = css({
justifySelf: 'center',
@@ -220,13 +210,12 @@ const PlayButton: React.FC<{
return (
-
{ switchIsPlaying(event) }}
+ onClick={() => { switchIsPlaying() }}
onKeyDown={(event: React.KeyboardEvent) => { if (event.key === "Enter") { // "Space" is handled by global key
- switchIsPlaying(event)
+ switchIsPlaying()
} }}>
{isPlaying ? : }
diff --git a/src/main/VideoPlayers.tsx b/src/main/VideoPlayers.tsx
index d0c89de45..0a61b724f 100644
--- a/src/main/VideoPlayers.tsx
+++ b/src/main/VideoPlayers.tsx
@@ -197,7 +197,7 @@ export const VideoPlayer = React.forwardRef(
}
useEffect(() => {
- // Seek if the position in the video got changed externally
+ // Seek if the position in the video got changed externally
if (!isPlaying && ref.current && ready) {
ref.current.seekTo(currentlyAt, "seconds")
}
@@ -209,11 +209,14 @@ export const VideoPlayer = React.forwardRef(
ref.current.seekTo(currentlyAt, "seconds")
dispatch(setClickTriggered(false))
}
- if (!isAspectRatioUpdated && ready) { // if (!isAspectRatioUpdated && ref.current && ready) {
+ })
+
+ useEffect(() => {
+ if (!isAspectRatioUpdated && ready) {
// Update the store with video dimensions for rendering purposes
updateAspectRatio();
}
- })
+ }, [isAspectRatioUpdated, ready])
// Callback specifically for the subtitle editor view
// When changing urls while the player is playing, don't reset to 0
diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts
index 7512131d7..86cf7f524 100644
--- a/src/redux/videoSlice.ts
+++ b/src/redux/videoSlice.ts
@@ -329,8 +329,14 @@ const skipDeletedSegments = (state: video) => {
* TODO: Improve calculation to handle multiple rows of videos
*/
export const calculateTotalAspectRatio = (aspectRatios: video["aspectRatios"]) => {
- const minHeight = Math.min(...aspectRatios.map(o => o.height))
+ let minHeight = Math.min(...aspectRatios.map(o => o.height))
let minWidth = Math.min(...aspectRatios.map(o => o.width))
+ // Getting the aspect ratios of every video can take several seconds
+ // So we assume a default resolution until then
+ if (!minHeight || !minWidth) {
+ minHeight = 720
+ minWidth = 1280
+ }
minWidth *= aspectRatios.length
return Math.min((minHeight / minWidth) * 100, (9 / 32) * 100)
}