diff --git a/src/globalKeys.ts b/src/globalKeys.ts index 720245df5..872f835f0 100644 --- a/src/globalKeys.ts +++ b/src/globalKeys.ts @@ -135,5 +135,9 @@ export const KEYMAP: IKeyMap = { name: "subtitleList.deleteSegment", key: "Shift+Alt+d", }, + addCue: { + name: "subtitleList.addCue", + key: "Shift+Alt+e", + }, }, }; diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index accb088a3..6c900c476 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -288,7 +288,8 @@ "addSegmentBelow": "Add segment below", "jumpToSegmentAbove": "Jump to segment above", "jumpToSegmentBelow": "Jump to segment below", - "deleteSegment": "Delete segment" + "deleteSegment": "Delete segment", + "addCue": "Add segment at current time" }, "subtitleVideoArea": { diff --git a/src/main/SubtitleTimeline.tsx b/src/main/SubtitleTimeline.tsx index a03806948..f52fdcda1 100644 --- a/src/main/SubtitleTimeline.tsx +++ b/src/main/SubtitleTimeline.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { css } from "@emotion/react"; import { SegmentsList as CuttingSegmentsList, Waveforms } from "./Timeline"; import { + addCueAtIndex, selectCurrentlyAt, selectSelectedSubtitleById, selectSelectedSubtitleId, @@ -25,6 +26,7 @@ import { ThemedTooltip } from "./Tooltip"; import { useTranslation } from "react-i18next"; import { useHotkeys } from "react-hotkeys-hook"; import { KEYMAP } from "../globalKeys"; +import { shallowEqual } from "react-redux"; /** * Copy-paste of the timeline in Video.tsx, so that we can make some small adjustments, @@ -39,6 +41,7 @@ const SubtitleTimeline: React.FC = () => { const dispatch = useAppDispatch(); const duration = useAppSelector(selectDuration); const currentlyAt = useAppSelector(selectCurrentlyAt); + const subtitleId = useAppSelector(selectSelectedSubtitleId, shallowEqual); const { ref, width = 1 } = useResizeObserver(); const refTop = useRef(null); @@ -71,6 +74,17 @@ const SubtitleTimeline: React.FC = () => { const [keyboardJumpDelta, setKeyboardJumpDelta] = useState(1000); // In milliseconds. For keyboard navigation + // Callback for adding subtitle segment by hotkey + const addCue = (time: number) => { + dispatch(addCueAtIndex({ + identifier: subtitleId, + cueIndex: -1, + text: "", + startTime: time, + endTime: time + 5000, + })); + }; + // 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) @@ -94,6 +108,11 @@ const SubtitleTimeline: React.FC = () => { () => setKeyboardJumpDelta(keyboardJumpDelta => Math.max(keyboardJumpDelta / 10, 1)), {}, [keyboardJumpDelta] ); + useHotkeys( + KEYMAP.subtitleList.addCue.key, + () => addCue(currentlyAt), + {}, [currentlyAt] + ); // Callback for the scroll container const onEndScroll = (e: ScrollEvent) => { @@ -102,6 +121,14 @@ const SubtitleTimeline: React.FC = () => { const offsetX = refTop.current.scrollLeft; const scrollLeftMax = (refTop.current.scrollWidth - refTop.current.clientWidth); dispatch(setCurrentlyAt((offsetX / scrollLeftMax) * (duration))); + + // Blur active element after scrolling, to ensure hotkeys are working + // This is a little hack to work around focus getting stuck in textarea elements from the subtitle list + try { + (document.activeElement as HTMLElement).blur(); + } catch (_e) { + console.error("Tried to blur active element, but active element cannot be blurred."); + } } }; diff --git a/src/redux/subtitleSlice.ts b/src/redux/subtitleSlice.ts index f25269282..3feb4a196 100644 --- a/src/redux/subtitleSlice.ts +++ b/src/redux/subtitleSlice.ts @@ -129,7 +129,7 @@ export const subtitleSlice = createSlice({ state.subtitles[action.payload.identifier].cues.splice(0, 0, cue); } - if (action.payload.cueIndex >= 0 || + if (action.payload.cueIndex >= 0 && action.payload.cueIndex < state.subtitles[action.payload.identifier].cues.length) { state.subtitles[action.payload.identifier].cues.splice(action.payload.cueIndex, 0, cue); }