Skip to content

Commit a0084eb

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 d9938eb commit a0084eb

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
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: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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,
@@ -75,6 +77,8 @@ const VideoControls: React.FC<{
7577
<div css={videoControlsRowStyle}>
7678
<TimeDisplay
7779
selectCurrentlyAt={selectCurrentlyAt}
80+
setCurrentlyAt={setCurrentlyAt}
81+
setIsPlaying={setIsPlaying}
7882
/>
7983
{jumpToPreviousSegment && (
8084
<PreviousButton
@@ -331,41 +335,145 @@ const NextButton: React.FC<{
331335
*/
332336
const TimeDisplay: React.FC<{
333337
selectCurrentlyAt: (state: RootState) => number,
338+
setCurrentlyAt: ActionCreatorWithPayload<number, string>,
339+
setIsPlaying: ActionCreatorWithPayload<boolean, string>,
334340
}> = ({
335341
selectCurrentlyAt,
342+
setCurrentlyAt,
343+
setIsPlaying,
336344
}) => {
337345

338346
const { t } = useTranslation();
347+
const theme = useTheme();
339348

340349
// Init redux variables
341-
const currentlyAt = useAppSelector(selectCurrentlyAt);
342350
const duration = useAppSelector(selectDuration);
343-
const theme = useTheme();
351+
352+
const timeDisplayStyle = css({
353+
display: "flex",
354+
flexDirection: "row",
355+
gap: "5px",
356+
alignItems: "center",
357+
});
344358

345359
const timeTextStyle = (theme: Theme) => css({
346360
display: "inline-block",
347361
color: `${theme.text}`,
348362
});
349363

350364
return (
351-
<div css={{ display: "flex", flexDirection: "row", gap: "5px" }}>
352-
<ThemedTooltip title={t("video.current-time-tooltip")}>
353-
<time css={timeTextStyle(theme)}
354-
tabIndex={0} role="timer" aria-label={t("video.time-aria") + ": " + convertMsToReadableString(currentlyAt)}>
355-
{new Date((currentlyAt ? currentlyAt : 0)).toISOString().substr(11, 10)}
356-
</time>
357-
</ThemedTooltip>
365+
<div css={timeDisplayStyle}>
366+
<CurrentTime
367+
selectCurrentlyAt={selectCurrentlyAt}
368+
setCurrentlyAt={setCurrentlyAt}
369+
setIsPlaying={setIsPlaying}
370+
/>
358371
<div css={undisplay(BREAKPOINTS.medium)}>{" / "}</div>
359372
<ThemedTooltip title={t("video.time-duration-tooltip")}>
360373
<div css={[timeTextStyle(theme), undisplay(BREAKPOINTS.medium)]}
361-
tabIndex={0} aria-label={t("video.duration-aria") + ": " + convertMsToReadableString(duration)}>
362-
{new Date((duration ? duration : 0)).toISOString().substr(11, 10)}
374+
tabIndex={0}
375+
aria-label={t("video.duration-aria") + ": " + convertMsToReadableString(duration)}
376+
>
377+
{formatMs(duration ? duration : 0)}
363378
</div>
364379
</ThemedTooltip>
365380
</div>
366381
);
367382
};
368383

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

0 commit comments

Comments
 (0)