@@ -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,7 @@ const VideoControls: React.FC<{
7577 < div css = { videoControlsRowStyle } >
7678 < TimeDisplay
7779 selectCurrentlyAt = { selectCurrentlyAt }
80+ setCurrentlyAt = { setCurrentlyAt }
7881 />
7982 { jumpToPreviousSegment && (
8083 < PreviousButton
@@ -331,41 +334,136 @@ const NextButton: React.FC<{
331334 */
332335const TimeDisplay : React . FC < {
333336 selectCurrentlyAt : ( state : RootState ) => number ,
337+ setCurrentlyAt : ActionCreatorWithPayload < number , string > ,
334338} > = ( {
335339 selectCurrentlyAt,
340+ setCurrentlyAt,
336341} ) => {
337342
338343 const { t } = useTranslation ( ) ;
344+ const theme = useTheme ( ) ;
339345
340346 // Init redux variables
341- const currentlyAt = useAppSelector ( selectCurrentlyAt ) ;
342347 const duration = useAppSelector ( selectDuration ) ;
343- const theme = useTheme ( ) ;
348+
349+ const timeDisplayStyle = css ( {
350+ display : "flex" ,
351+ flexDirection : "row" ,
352+ gap : "5px" ,
353+ alignItems : "center" ,
354+ } ) ;
344355
345356 const timeTextStyle = ( theme : Theme ) => css ( {
346357 display : "inline-block" ,
347358 color : `${ theme . text } ` ,
348359 } ) ;
349360
350361 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 >
362+ < div css = { timeDisplayStyle } >
363+ < CurrentTime
364+ selectCurrentlyAt = { selectCurrentlyAt }
365+ setCurrentlyAt = { setCurrentlyAt }
366+ />
358367 < div css = { undisplay ( BREAKPOINTS . medium ) } > { " / " } </ div >
359368 < ThemedTooltip title = { t ( "video.time-duration-tooltip" ) } >
360369 < 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 ) }
370+ tabIndex = { 0 }
371+ aria-label = { t ( "video.duration-aria" ) + ": " + convertMsToReadableString ( duration ) }
372+ >
373+ { formatMs ( duration ? duration : 0 ) }
363374 </ div >
364375 </ ThemedTooltip >
365376 </ div >
366377 ) ;
367378} ;
368379
380+ const CurrentTime : React . FC < {
381+ selectCurrentlyAt : ( state : RootState ) => number ;
382+ setCurrentlyAt : ActionCreatorWithPayload < number , string > ,
383+ } > = ( {
384+ selectCurrentlyAt,
385+ setCurrentlyAt,
386+ } ) => {
387+ const { t } = useTranslation ( ) ;
388+ const dispatch = useAppDispatch ( ) ;
389+
390+ const currentlyAt = useAppSelector ( selectCurrentlyAt ) ;
391+
392+ const [ editing , setEditing ] = React . useState ( false ) ;
393+ const [ value , setValue ] = React . useState ( formatMs ( currentlyAt ) ) ;
394+
395+ const parseTime = ( value : string ) => {
396+ const parts = value . split ( ":" ) . map ( Number ) ;
397+ if ( parts . some ( isNaN ) ) {
398+ return null ;
399+ }
400+
401+ const [ hh = 0 , mm = 0 , ss = 0 ] = parts ;
402+ return ( ( hh * 60 + mm ) * 60 + ss ) * 1000 ;
403+ } ;
404+
405+ React . useEffect ( ( ) => {
406+ if ( ! editing ) {
407+ setValue ( formatMs ( currentlyAt ) ) ;
408+ }
409+ } , [ currentlyAt , editing ] ) ;
410+
411+ const commit = ( ) => {
412+ const parsedTime = parseTime ( value ) ;
413+ if ( parsedTime ) {
414+ dispatch ( setCurrentlyAt ( parsedTime ) ) ;
415+ }
416+ setEditing ( false ) ;
417+ } ;
418+
419+ const cancel = ( ) => {
420+ setValue ( formatMs ( currentlyAt ) ) ;
421+ setEditing ( false ) ;
422+ } ;
423+
424+ const inputStyle = css ( {
425+ maxWidth : "77px" ,
426+ } ) ;
427+
428+ return (
429+ < ThemedTooltip title = { t ( "video.current-time-tooltip" ) } >
430+ { editing ? (
431+ < input
432+ autoFocus
433+ value = { value }
434+ onChange = { e => setValue ( e . target . value ) }
435+ onBlur = { commit }
436+ onKeyDown = { e => {
437+ if ( e . key === "Enter" ) { commit ( ) ; }
438+ if ( e . key === "Escape" ) { cancel ( ) ; }
439+ } }
440+ aria-label = { t ( "video.time-aria" ) }
441+ css = { inputStyle }
442+ />
443+ ) : (
444+ < time
445+ tabIndex = { 0 }
446+ role = "timer"
447+ onClick = { ( ) => setEditing ( true ) }
448+ onKeyDown = { e => {
449+ if ( e . key === "Enter" || e . key === " " ) {
450+ e . preventDefault ( ) ;
451+ setEditing ( true ) ;
452+ }
453+ } }
454+ aria-label = { t ( "video.time-aria" ) + ": " + convertMsToReadableString ( currentlyAt ) }
455+ >
456+ { formatMs ( currentlyAt ) }
457+ </ time >
458+ ) }
459+ </ ThemedTooltip >
460+ ) ;
461+ } ;
462+
463+ const formatMs = ( ms : number ) => {
464+ return new Date ( ms ) . toISOString ( ) . substr ( 11 , 10 ) ;
465+ } ;
466+
369467const VolumeSlider : React . FC < {
370468 selectIsMuted : ( state : RootState ) => boolean ,
371469 setIsMuted : ActionCreatorWithPayload < boolean , string > ,
0 commit comments