From 0c43779f32c011511d013bc24b420bcc5e20b5bb Mon Sep 17 00:00:00 2001 From: Arnei Date: Mon, 2 Feb 2026 11:18:14 +0100 Subject: [PATCH] Allow typing time in current time field This patch lets users change the current time by typing. They can do so by clicking on the current time which will then turn into an input field (focusing and pressing enter also works). They can then change the time by typing. The changed time is only applied on blur or pressing "Enter". Any inputs that do not fit the syntax are ignored. --- src/main/Cutting.tsx | 1 + src/main/SubtitleVideoArea.tsx | 2 + src/main/ThumbnailGeneration.tsx | 1 + src/main/VideoControls.tsx | 128 +++++++++++++++++++++++++++---- 4 files changed, 119 insertions(+), 13 deletions(-) diff --git a/src/main/Cutting.tsx b/src/main/Cutting.tsx index 8df320b60..c298802f8 100644 --- a/src/main/Cutting.tsx +++ b/src/main/Cutting.tsx @@ -135,6 +135,7 @@ const Cutting: React.FC = () => { selectIsMuted={selectIsMuted} selectVolume={selectVolume} selectIsPlayPreview={selectIsPlayPreview} + setCurrentlyAt={setCurrentlyAt} setIsPlaying={setIsPlaying} setIsMuted={setIsMuted} setVolume={setVolume} diff --git a/src/main/SubtitleVideoArea.tsx b/src/main/SubtitleVideoArea.tsx index cbf03a789..86d05af59 100644 --- a/src/main/SubtitleVideoArea.tsx +++ b/src/main/SubtitleVideoArea.tsx @@ -20,6 +20,7 @@ import VideoControls from "./VideoControls"; import Select from "react-select"; import { selectFieldStyle } from "../cssStyles"; import { ActionCreatorWithPayload, AsyncThunk } from "@reduxjs/toolkit"; +import { setCurrentlyAt } from "../redux/subtitleSlice"; /** * A part of the subtitle editor that displays a video and related controls @@ -163,6 +164,7 @@ const SubtitleVideoArea: React.FC<{ selectIsMuted={selectIsMuted} selectVolume={selectVolume} selectIsPlayPreview={selectIsPlayPreview} + setCurrentlyAt={setCurrentlyAt} setIsPlaying={setIsPlaying} setIsMuted={setIsMuted} setVolume={setVolume} diff --git a/src/main/ThumbnailGeneration.tsx b/src/main/ThumbnailGeneration.tsx index 526d914a9..3a31768c8 100644 --- a/src/main/ThumbnailGeneration.tsx +++ b/src/main/ThumbnailGeneration.tsx @@ -178,6 +178,7 @@ const ThumbnailGeneration: React.FC = () => { selectIsMuted={selectIsMuted} selectVolume={selectVolume} selectIsPlayPreview={selectIsPlayPreview} + setCurrentlyAt={setCurrentlyAt} setIsPlaying={setIsPlaying} setIsMuted={setIsMuted} setVolume={setVolume} diff --git a/src/main/VideoControls.tsx b/src/main/VideoControls.tsx index b0646805e..56414b415 100644 --- a/src/main/VideoControls.tsx +++ b/src/main/VideoControls.tsx @@ -11,7 +11,7 @@ import { } from "../redux/videoSlice"; import { convertMsToReadableString } from "../util/utilityFunctions"; -import { BREAKPOINTS, basicButtonStyle, undisplayContainer } from "../cssStyles"; +import { BREAKPOINTS, basicButtonStyle, undisplay, undisplayContainer } from "../cssStyles"; import { KEYMAP, rewriteKeys } from "../globalKeys"; import { useTranslation } from "react-i18next"; @@ -35,6 +35,7 @@ const VideoControls: React.FC<{ selectIsMuted: (state: RootState) => boolean, selectVolume: (state: RootState) => number, selectIsPlayPreview: (state: RootState) => boolean, + setCurrentlyAt: ActionCreatorWithPayload, setIsPlaying: ActionCreatorWithPayload, setIsMuted: ActionCreatorWithPayload, setVolume: ActionCreatorWithPayload, @@ -47,6 +48,7 @@ const VideoControls: React.FC<{ selectIsMuted, selectVolume, selectIsPlayPreview, + setCurrentlyAt, setIsPlaying, setIsMuted, setVolume, @@ -76,6 +78,8 @@ const VideoControls: React.FC<{
{jumpToPreviousSegment && ( number, + setCurrentlyAt: ActionCreatorWithPayload, + setIsPlaying: ActionCreatorWithPayload, }> = ({ selectCurrentlyAt, + setCurrentlyAt, + setIsPlaying, }) => { const { t } = useTranslation(); + const theme = useTheme(); // Init redux variables - const currentlyAt = useAppSelector(selectCurrentlyAt); const duration = useAppSelector(selectDuration); - const theme = useTheme(); const timeDisplayStyle = css({ display: "flex", flexDirection: "row", gap: "5px", + alignItems: "center", }); const timeTextStyle = (theme: Theme) => css({ @@ -355,23 +363,117 @@ const TimeDisplay: React.FC<{ return (
- - - -
{" / "}
+ +
{" / "}
-
- {new Date((duration ? duration : 0)).toISOString().substr(11, 10)} +
+ {formatMs(duration ? duration : 0)}
); }; +const CurrentTime: React.FC<{ + selectCurrentlyAt: (state: RootState) => number; + setCurrentlyAt: ActionCreatorWithPayload, + setIsPlaying: ActionCreatorWithPayload, +}> = ({ + selectCurrentlyAt, + setCurrentlyAt, + setIsPlaying, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const currentlyAt = useAppSelector(selectCurrentlyAt); + + const [editing, setEditing] = React.useState(false); + const [value, setValue] = React.useState(formatMs(currentlyAt)); + + const parseTime = (value: string) => { + const parts = value.split(":").map(Number); + if (parts.some(isNaN)) { + return null; + } + + const [hh = 0, mm = 0, ss = 0] = parts; + return ((hh * 60 + mm) * 60 + ss) * 1000; + }; + + React.useEffect(() => { + if (!editing) { + setValue(formatMs(currentlyAt)); + } + }, [currentlyAt, editing]); + + const commit = () => { + const parsedTime = parseTime(value); + if (parsedTime) { + dispatch(setCurrentlyAt(parsedTime)); + } + setEditing(false); + }; + + const cancel = () => { + setValue(formatMs(currentlyAt)); + setEditing(false); + }; + + const inputStyle = css({ + maxWidth: "77px", + }); + + return ( + + {editing ? ( + setValue(e.target.value)} + onBlur={commit} + onKeyDown={e => { + if (e.key === "Enter") { commit(); } + if (e.key === "Escape") { cancel(); } + }} + aria-label={t("video.time-aria")} + css={inputStyle} + /> + ) : ( + + )} + + ); +}; + +const formatMs = (ms: number) => { + return new Date(ms).toISOString().substr(11, 10); +}; + const VolumeSlider: React.FC<{ selectIsMuted: (state: RootState) => boolean, setIsMuted: ActionCreatorWithPayload,