77 proxy ,
88 signal ,
99 useEffect ,
10+ useListener ,
1011} from "@odoo/owl" ;
1112import { useHotkey } from "@web/core/hotkeys/hotkey_hook" ;
1213import { CallDebriefTimeline } from "@mail/views/fields/call_debrief/call_debrief_timeline" ;
@@ -38,6 +39,7 @@ export class CallDebrief extends Component {
3839 this . isSwitchingSegment = false ;
3940
4041 this . mediaPlayer = signal ( null ) ;
42+ this . rootElement = signal ( null ) ;
4143
4244 this . orm = useService ( "orm" ) ;
4345 this . state = proxy ( {
@@ -46,10 +48,11 @@ export class CallDebrief extends Component {
4648 currentSegment : undefined ,
4749 error : "" ,
4850 isPlaying : false ,
51+ isFullscreen : false ,
4952 playbackRate : 1 ,
5053 volume : 1 ,
5154 isMuted : false ,
52- feedback : { text : "" , id : Date . now ( ) } ,
55+ feedback : { icon : "" , text : "" , id : Date . now ( ) } ,
5356 } ) ;
5457
5558 this . onMediaLoadedCallback = null ;
@@ -71,13 +74,21 @@ export class CallDebrief extends Component {
7174
7275 useHotkey ( "k" , ( ) => this . togglePlay ( ) , { global : true } ) ;
7376 useHotkey ( "space" , ( ) => this . togglePlay ( ) , { global : true } ) ;
74- useHotkey ( "j" , ( ) => this . seekRelative ( - 5 ) , { global : true , allowRepeat : true } ) ;
75- useHotkey ( "l" , ( ) => this . seekRelative ( 5 ) , { global : true , allowRepeat : true } ) ;
76- useHotkey ( "arrowleft" , ( ) => this . seekRelative ( - 5 ) , { global : true , allowRepeat : true } ) ;
77- useHotkey ( "arrowright" , ( ) => this . seekRelative ( 5 ) , { global : true , allowRepeat : true } ) ;
78- useHotkey ( "m" , ( ) => this . toggleMute ( ) , { global : true } ) ;
79- useHotkey ( "shift+>" , ( ) => this . adjustPlaybackRate ( 1 ) , { global : true } ) ;
80- useHotkey ( "shift+<" , ( ) => this . adjustPlaybackRate ( - 1 ) , { global : true } ) ;
77+ useHotkey ( "j" , ( ) => { this . seekRelative ( - 5 ) , this . showMediaControlsFeedback ( "skip-backward" ) ; } , { global : true , allowRepeat : true } ) ;
78+ useHotkey ( "l" , ( ) => { this . seekRelative ( 5 ) , this . showMediaControlsFeedback ( "skip-forward" ) ; } , { global : true , allowRepeat : true } ) ;
79+ useHotkey ( "arrowleft" , ( ) => { this . seekRelative ( - 5 ) , this . showMediaControlsFeedback ( "skip-backward" ) ; } , { global : true , allowRepeat : true } ) ;
80+ useHotkey ( "arrowright" , ( ) => { this . seekRelative ( 5 ) , this . showMediaControlsFeedback ( "skip-forward" ) ; } , { global : true , allowRepeat : true } ) ;
81+ useHotkey ( "m" , ( ) => { this . toggleMute ( ) ; this . showMediaControlsFeedback ( "mute" ) ; } , { global : true } ) ;
82+ // Supports AZERTY keyboard layouts
83+ useHotkey ( "shift+." , ( ) => { this . adjustPlaybackRate ( 1 ) ; this . showMediaControlsFeedback ( "playback-speed" ) ; } , { global : true } ) ;
84+ useHotkey ( "shift+?" , ( ) => { this . adjustPlaybackRate ( - 1 ) ; this . showMediaControlsFeedback ( "playback-speed" ) ; } , { global : true } ) ;
85+ // Supports QWERTY keyboard layouts
86+ useHotkey ( "shift+>" , ( ) => { this . adjustPlaybackRate ( 1 ) ; this . showMediaControlsFeedback ( "playback-speed" ) ; } , { global : true } ) ;
87+ useHotkey ( "shift+<" , ( ) => { this . adjustPlaybackRate ( - 1 ) ; this . showMediaControlsFeedback ( "playback-speed" ) ; } , { global : true } ) ;
88+ useHotkey ( "f" , ( ) => { this . toggleFullscreen ( ) ; this . showMediaControlsFeedback ( "fullscreen" ) ; } , { global : true } ) ;
89+ useListener ( document , "fullscreenchange" , ( ) => {
90+ this . state . isFullscreen = ! ! document . fullscreenElement ;
91+ } ) ;
8192
8293 onWillUnmount ( ( ) => {
8394 clearTimeout ( this . feedbackTimeout ) ;
@@ -107,7 +118,7 @@ export class CallDebrief extends Component {
107118 }
108119
109120 onMediaError ( ) {
110- this . showFeedback ( _t ( "Media Error" ) ) ;
121+ this . showVideoFeedback ( _t ( "Media Error" ) ) ;
111122 console . warn ( "Media playback error. The format might not be supported by your browser." ) ;
112123 }
113124
@@ -338,16 +349,16 @@ export class CallDebrief extends Component {
338349
339350 /**
340351 * Pauses the media element and optionally displays a feedback message.
341- * @param {string|false } feedback - The text to display. Pass false to suppress feedback.
352+ * @param {string|boolean } feedback - Optional text to display alongside the pause icon . Pass false to suppress feedback.
342353 */
343- _pause ( feedback = _t ( "Pause" ) ) {
354+ _pause ( feedback = true ) {
344355 const mediaPlayer = this . mediaPlayer ( ) ;
345356 if ( mediaPlayer ) {
346357 mediaPlayer . pause ( ) ;
347358 }
348359 this . state . isPlaying = false ;
349- if ( feedback ) {
350- this . showFeedback ( feedback ) ;
360+ if ( feedback !== false ) {
361+ this . showVideoFeedback ( typeof feedback === "string" ? feedback : undefined , "fa-pause" ) ;
351362 }
352363 }
353364
@@ -432,37 +443,48 @@ export class CallDebrief extends Component {
432443
433444 const newRate = this . playbackRates [ newIndex ] ;
434445 this . state . playbackRate = newRate ;
435- this . showFeedback ( `${ newRate } x` ) ;
446+ this . showVideoFeedback ( `${ newRate } x` ) ;
436447 }
437448
438- showFeedback ( text ) {
439- this . state . feedback = { text, id : Date . now ( ) } ;
449+ showVideoFeedback ( text , icon ) {
450+ this . state . feedback = { text, icon , id : Date . now ( ) } ;
440451 if ( this . feedbackTimeout ) {
441452 clearTimeout ( this . feedbackTimeout ) ;
442453 }
443454 this . feedbackTimeout = setTimeout ( ( ) => {
444- this . state . feedback . text = "" ;
455+ this . state . feedback = null ;
445456 } , 750 ) ;
446457 }
447458
459+ showMediaControlsFeedback ( action ) {
460+ const el = this . rootElement ( ) ?. querySelector ( `[data-control-feedback="${ action } "]` ) ;
461+ if ( ! el ) {
462+ return ;
463+ }
464+ el . classList . remove ( "o-CallDebrief-hotkeyFeedback" ) ;
465+ void el . offsetWidth ; // Force reflow to restart animation
466+ el . classList . add ( "o-CallDebrief-hotkeyFeedback" ) ;
467+ el . addEventListener ( "animationend" , ( ) => el . classList . remove ( "o-CallDebrief-hotkeyFeedback" ) , { once : true } ) ;
468+ }
469+
448470 togglePlay ( ) {
449471 const media = this . mediaPlayer ( ) ;
450472 if ( ! media ) {
451473 return ;
452474 }
453475 if ( this . state . currentTime >= this . callDurationSeconds - 0.5 ) {
454- this . showFeedback ( _t ( "End of Media" ) ) ;
476+ this . showVideoFeedback ( _t ( "End of Media" ) ) ;
455477 return ;
456478 }
457479 if ( this . state . isPlaying ) {
458480 this . _pause ( ) ;
459481 } else {
460482 media . play ( ) . catch ( ( e ) => {
461483 this . state . isPlaying = false ;
462- this . showFeedback ( _t ( "Playback Error" ) ) ;
484+ this . showVideoFeedback ( _t ( "Playback Error" ) ) ;
463485 } ) ;
464486 this . state . isPlaying = true ;
465- this . showFeedback ( _t ( "Play" ) ) ;
487+ this . showVideoFeedback ( undefined , "fa-play" ) ;
466488 }
467489 }
468490
@@ -472,8 +494,8 @@ export class CallDebrief extends Component {
472494 Math . min ( this . callDurationSeconds , this . state . currentTime + delta )
473495 ) ;
474496 this . setPlaybackTime ( { timestamp : newTime } ) ;
475- const direction = delta > 0 ? "+ " : "- " ;
476- this . showFeedback ( ` ${ direction } ${ Math . abs ( delta ) } s` ) ;
497+ const direction = delta > 0 ? "fa-forward " : "fa-backward " ;
498+ this . showVideoFeedback ( undefined , direction ) ;
477499 }
478500
479501 setPlaybackRate ( ev ) {
@@ -491,12 +513,33 @@ export class CallDebrief extends Component {
491513 this . state . isMuted = this . state . volume === 0 ;
492514 }
493515
516+ onVolumeChange ( ev ) {
517+ this . state . volume = ev . target . volume ;
518+ this . state . isMuted = ev . target . muted ;
519+ }
520+
521+ onRateChange ( ev ) {
522+ this . state . playbackRate = ev . target . playbackRate ;
523+ }
524+
494525 toggleMute ( ) {
495526 this . state . isMuted = ! this . state . isMuted ;
496527 if ( ! this . state . isMuted && this . state . volume === 0 ) {
497528 this . state . volume = 0.5 ;
498529 }
499- this . showFeedback ( this . state . isMuted ? _t ( "Muted" ) : _t ( "Unmuted" ) ) ;
530+ this . showVideoFeedback ( undefined , this . state . isMuted ? "fa-volume-off" : "fa-volume-up" ) ;
531+ }
532+
533+ toggleFullscreen ( ) {
534+ const rootEl = this . rootElement ( ) ;
535+ if ( ! rootEl || ! this . hasVideo ) {
536+ return ;
537+ }
538+ if ( ! document . fullscreenElement ) {
539+ rootEl . requestFullscreen ( ) ;
540+ } else {
541+ document . exitFullscreen ( ) ;
542+ }
500543 }
501544}
502545
0 commit comments