Skip to content

Commit 84b1dce

Browse files
committed
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.
1 parent ca19996 commit 84b1dce

File tree

4 files changed

+119
-13
lines changed

4 files changed

+119
-13
lines changed

src/main/Cutting.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ const Cutting: React.FC = () => {
134134
selectIsMuted={selectIsMuted}
135135
selectVolume={selectVolume}
136136
selectIsPlayPreview={selectIsPlayPreview}
137+
setCurrentlyAt={setCurrentlyAt}
137138
setIsPlaying={setIsPlaying}
138139
setIsMuted={setIsMuted}
139140
setVolume={setVolume}

src/main/SubtitleVideoArea.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import VideoControls from "./VideoControls";
2020
import Select from "react-select";
2121
import { selectFieldStyle } from "../cssStyles";
2222
import { ActionCreatorWithPayload, AsyncThunk } from "@reduxjs/toolkit";
23+
import { setCurrentlyAt } from "../redux/subtitleSlice";
2324

2425
/**
2526
* A part of the subtitle editor that displays a video and related controls
@@ -163,6 +164,7 @@ const SubtitleVideoArea: React.FC<{
163164
selectIsMuted={selectIsMuted}
164165
selectVolume={selectVolume}
165166
selectIsPlayPreview={selectIsPlayPreview}
167+
setCurrentlyAt={setCurrentlyAt}
166168
setIsPlaying={setIsPlaying}
167169
setIsMuted={setIsMuted}
168170
setVolume={setVolume}

src/main/Thumbnail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ const Thumbnail: React.FC = () => {
148148
selectIsMuted={selectIsMuted}
149149
selectVolume={selectVolume}
150150
selectIsPlayPreview={selectIsPlayPreview}
151+
setCurrentlyAt={setCurrentlyAt}
151152
setIsPlaying={setIsPlaying}
152153
setIsMuted={setIsMuted}
153154
setVolume={setVolume}

src/main/VideoControls.tsx

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "../redux/videoSlice";
1212

1313
import { convertMsToReadableString } from "../util/utilityFunctions";
14-
import { BREAKPOINTS, basicButtonStyle, undisplayContainer } from "../cssStyles";
14+
import { BREAKPOINTS, basicButtonStyle, undisplay, undisplayContainer } from "../cssStyles";
1515

1616
import { KEYMAP, rewriteKeys } from "../globalKeys";
1717
import { useTranslation } from "react-i18next";
@@ -35,6 +35,7 @@ const VideoControls: React.FC<{
3535
selectIsMuted: (state: RootState) => boolean,
3636
selectVolume: (state: RootState) => number,
3737
selectIsPlayPreview: (state: RootState) => boolean,
38+
setCurrentlyAt: ActionCreatorWithPayload<number, string>,
3839
setIsPlaying: ActionCreatorWithPayload<boolean, string>,
3940
setIsMuted: ActionCreatorWithPayload<boolean, string>,
4041
setVolume: ActionCreatorWithPayload<number, string>,
@@ -47,6 +48,7 @@ const VideoControls: React.FC<{
4748
selectIsMuted,
4849
selectVolume,
4950
selectIsPlayPreview,
51+
setCurrentlyAt,
5052
setIsPlaying,
5153
setIsMuted,
5254
setVolume,
@@ -76,6 +78,8 @@ const VideoControls: React.FC<{
7678
<div css={videoControlsRowStyle}>
7779
<TimeDisplay
7880
selectCurrentlyAt={selectCurrentlyAt}
81+
setCurrentlyAt={setCurrentlyAt}
82+
setIsPlaying={setIsPlaying}
7983
/>
8084
{jumpToPreviousSegment && (
8185
<PreviousButton
@@ -332,21 +336,25 @@ const NextButton: React.FC<{
332336
*/
333337
const TimeDisplay: React.FC<{
334338
selectCurrentlyAt: (state: RootState) => number,
339+
setCurrentlyAt: ActionCreatorWithPayload<number, string>,
340+
setIsPlaying: ActionCreatorWithPayload<boolean, string>,
335341
}> = ({
336342
selectCurrentlyAt,
343+
setCurrentlyAt,
344+
setIsPlaying,
337345
}) => {
338346

339347
const { t } = useTranslation();
348+
const theme = useTheme();
340349

341350
// Init redux variables
342-
const currentlyAt = useAppSelector(selectCurrentlyAt);
343351
const duration = useAppSelector(selectDuration);
344-
const theme = useTheme();
345352

346353
const timeDisplayStyle = css({
347354
display: "flex",
348355
flexDirection: "row",
349356
gap: "5px",
357+
alignItems: "center",
350358
});
351359

352360
const timeTextStyle = (theme: Theme) => css({
@@ -356,23 +364,117 @@ const TimeDisplay: React.FC<{
356364

357365
return (
358366
<div css={timeDisplayStyle}>
359-
<ThemedTooltip title={t("video.current-time-tooltip")}>
360-
<time css={timeTextStyle(theme)}
361-
tabIndex={0} role="timer" aria-label={t("video.time-aria") + ": " + convertMsToReadableString(currentlyAt)}>
362-
{new Date((currentlyAt ? currentlyAt : 0)).toISOString().substr(11, 10)}
363-
</time>
364-
</ThemedTooltip>
365-
<div css={undisplayContainer(BREAKPOINTS.medium)}>{" / "}</div>
367+
<CurrentTime
368+
selectCurrentlyAt={selectCurrentlyAt}
369+
setCurrentlyAt={setCurrentlyAt}
370+
setIsPlaying={setIsPlaying}
371+
/>
372+
<div css={undisplay(BREAKPOINTS.medium)}>{" / "}</div>
366373
<ThemedTooltip title={t("video.time-duration-tooltip")}>
367-
<div css={[timeTextStyle(theme), undisplayContainer(BREAKPOINTS.medium)]}
368-
tabIndex={0} aria-label={t("video.duration-aria") + ": " + convertMsToReadableString(duration)}>
369-
{new Date((duration ? duration : 0)).toISOString().substr(11, 10)}
374+
<div css={[timeTextStyle(theme), undisplay(BREAKPOINTS.medium)]}
375+
tabIndex={0}
376+
aria-label={t("video.duration-aria") + ": " + convertMsToReadableString(duration)}
377+
>
378+
{formatMs(duration ? duration : 0)}
370379
</div>
371380
</ThemedTooltip>
372381
</div>
373382
);
374383
};
375384

385+
const CurrentTime: React.FC<{
386+
selectCurrentlyAt: (state: RootState) => number;
387+
setCurrentlyAt: ActionCreatorWithPayload<number, string>,
388+
setIsPlaying: ActionCreatorWithPayload<boolean, string>,
389+
}> = ({
390+
selectCurrentlyAt,
391+
setCurrentlyAt,
392+
setIsPlaying,
393+
}) => {
394+
const { t } = useTranslation();
395+
const dispatch = useAppDispatch();
396+
397+
const currentlyAt = useAppSelector(selectCurrentlyAt);
398+
399+
const [editing, setEditing] = React.useState(false);
400+
const [value, setValue] = React.useState(formatMs(currentlyAt));
401+
402+
const parseTime = (value: string) => {
403+
const parts = value.split(":").map(Number);
404+
if (parts.some(isNaN)) {
405+
return null;
406+
}
407+
408+
const [hh = 0, mm = 0, ss = 0] = parts;
409+
return ((hh * 60 + mm) * 60 + ss) * 1000;
410+
};
411+
412+
React.useEffect(() => {
413+
if (!editing) {
414+
setValue(formatMs(currentlyAt));
415+
}
416+
}, [currentlyAt, editing]);
417+
418+
const commit = () => {
419+
const parsedTime = parseTime(value);
420+
if (parsedTime) {
421+
dispatch(setCurrentlyAt(parsedTime));
422+
}
423+
setEditing(false);
424+
};
425+
426+
const cancel = () => {
427+
setValue(formatMs(currentlyAt));
428+
setEditing(false);
429+
};
430+
431+
const inputStyle = css({
432+
maxWidth: "77px",
433+
});
434+
435+
return (
436+
<ThemedTooltip title={t("video.current-time-tooltip")}>
437+
{editing ? (
438+
<input
439+
autoFocus
440+
value={value}
441+
onChange={e => setValue(e.target.value)}
442+
onBlur={commit}
443+
onKeyDown={e => {
444+
if (e.key === "Enter") { commit(); }
445+
if (e.key === "Escape") { cancel(); }
446+
}}
447+
aria-label={t("video.time-aria")}
448+
css={inputStyle}
449+
/>
450+
) : (
451+
<time
452+
tabIndex={0}
453+
role="timer"
454+
onClick={() => {
455+
setEditing(true);
456+
dispatch(setIsPlaying(false));
457+
}}
458+
onKeyDown={e => {
459+
if (e.key === "Enter" || e.key === " ") {
460+
e.preventDefault();
461+
setEditing(true);
462+
dispatch(setIsPlaying(false));
463+
}
464+
}}
465+
aria-label={t("video.time-aria") + ": " + convertMsToReadableString(currentlyAt)}
466+
>
467+
{formatMs(currentlyAt)}
468+
</time>
469+
)}
470+
</ThemedTooltip>
471+
);
472+
};
473+
474+
const formatMs = (ms: number) => {
475+
return new Date(ms).toISOString().substr(11, 10);
476+
};
477+
376478
const VolumeSlider: React.FC<{
377479
selectIsMuted: (state: RootState) => boolean,
378480
setIsMuted: ActionCreatorWithPayload<boolean, string>,

0 commit comments

Comments
 (0)