@@ -31,6 +31,8 @@ use crate::tui::{
3131 HistoryPanelState , StartupProgressState , StartupScreenState , N_BANDS ,
3232} ;
3333
34+ const SEEK_STEP : Duration = Duration :: from_secs ( 5 ) ;
35+
3436pub struct PlaybackContext < ' a > {
3537 pub cmd_rx : & ' a Receiver < KeyCommand > ,
3638 pub media : Option < MediaSessionHandle > ,
@@ -158,6 +160,8 @@ fn browse_history_session(
158160 | KeyCommand :: TogglePause
159161 | KeyCommand :: NextTrack
160162 | KeyCommand :: PreviousTrack
163+ | KeyCommand :: SeekForward
164+ | KeyCommand :: SeekBackward
161165 | KeyCommand :: ToggleFullscreen
162166 | KeyCommand :: ToggleFavorite
163167 | KeyCommand :: ToggleHistory => { }
@@ -348,7 +352,8 @@ fn run_loop(
348352 ctx : & PlaybackContext ,
349353) -> Result < LoopAction > {
350354 // Frame rate control: only render when needed
351- const RENDER_FPS : u64 = 30 ; // Target 30 FPS while playing
355+ // Target 30 FPS while playing.
356+ const RENDER_FPS : u64 = 30 ;
352357 // The paused screensaver's slowest sinusoid cycles every ~13s, so a few
353358 // frames per second is indistinguishable to the eye and keeps idle CPU low.
354359 const RENDER_FPS_PAUSED : u64 = 4 ;
@@ -486,6 +491,16 @@ fn dispatch_command(
486491 Ok ( None )
487492 }
488493 }
494+ KeyCommand :: SeekForward => {
495+ seek_track ( state, player, SeekDirection :: Forward , media) ?;
496+ * needs_render = true ;
497+ Ok ( None )
498+ }
499+ KeyCommand :: SeekBackward => {
500+ seek_track ( state, player, SeekDirection :: Backward , media) ?;
501+ * needs_render = true ;
502+ Ok ( None )
503+ }
489504 KeyCommand :: ToggleFullscreen => {
490505 state. fullscreen = !state. fullscreen ;
491506 * needs_render = true ;
@@ -576,13 +591,63 @@ fn dispatch_command(
576591 }
577592}
578593
594+ #[ derive( Clone , Copy ) ]
595+ enum SeekDirection {
596+ Backward ,
597+ Forward ,
598+ }
599+
600+ fn seek_track (
601+ state : & mut AppState ,
602+ player : & AudioPlayer ,
603+ direction : SeekDirection ,
604+ media : Option < & MediaSessionHandle > ,
605+ ) -> Result < ( ) > {
606+ let target = seek_target ( state. elapsed ( ) , direction, state. duration ) ;
607+ if !player. seek_to ( target) ? {
608+ return Ok ( ( ) ) ;
609+ }
610+
611+ set_elapsed ( state, target) ;
612+ if let Some ( media) = media {
613+ media. set_playback ( state. paused , state. elapsed ( ) ) ;
614+ }
615+ Ok ( ( ) )
616+ }
617+
618+ fn seek_target (
619+ current : Duration ,
620+ direction : SeekDirection ,
621+ duration : Option < Duration > ,
622+ ) -> Duration {
623+ let target = match direction {
624+ SeekDirection :: Backward => current. saturating_sub ( SEEK_STEP ) ,
625+ SeekDirection :: Forward => current + SEEK_STEP ,
626+ } ;
627+
628+ match duration {
629+ Some ( duration) => target. min ( duration) ,
630+ None => target,
631+ }
632+ }
633+
634+ fn set_elapsed ( state : & mut AppState , elapsed : Duration ) {
635+ if state. paused {
636+ state. pause_elapsed = elapsed;
637+ } else {
638+ state. loop_start = Instant :: now ( ) - elapsed;
639+ }
640+ }
641+
579642#[ derive( Debug , Eq , PartialEq ) ]
580643pub ( crate ) enum KeyCommand {
581644 None ,
582645 Quit ,
583646 TogglePause ,
584647 NextTrack ,
585648 PreviousTrack ,
649+ SeekForward ,
650+ SeekBackward ,
586651 ToggleFullscreen ,
587652 ToggleFavorite ,
588653 ToggleHistory ,
@@ -619,6 +684,8 @@ fn handle_key_event(key: KeyEvent, state: &AppState) -> KeyCommand {
619684 ( KeyCode :: Char ( ' ' ) , _) => KeyCommand :: TogglePause ,
620685 ( KeyCode :: Char ( 'n' ) , _) => KeyCommand :: NextTrack ,
621686 ( KeyCode :: Char ( 'b' ) , _) => KeyCommand :: PreviousTrack ,
687+ ( KeyCode :: Right , _) => KeyCommand :: SeekForward ,
688+ ( KeyCode :: Left , _) => KeyCommand :: SeekBackward ,
622689 ( KeyCode :: Char ( 'f' ) , _) => KeyCommand :: ToggleFullscreen ,
623690 ( KeyCode :: Char ( 's' ) , _) => KeyCommand :: ToggleFavorite ,
624691 ( KeyCode :: Char ( 'p' ) , modifiers) if modifiers. contains ( KeyModifiers :: SUPER ) => {
@@ -1429,6 +1496,43 @@ mod tests {
14291496 ) ;
14301497 }
14311498
1499+ #[ test]
1500+ fn arrow_keys_seek_during_playback ( ) {
1501+ let state = base_state ( ) ;
1502+ assert_eq ! (
1503+ handle_key_event( KeyEvent :: new( KeyCode :: Right , KeyModifiers :: NONE ) , & state) ,
1504+ KeyCommand :: SeekForward
1505+ ) ;
1506+ assert_eq ! (
1507+ handle_key_event( KeyEvent :: new( KeyCode :: Left , KeyModifiers :: NONE ) , & state) ,
1508+ KeyCommand :: SeekBackward
1509+ ) ;
1510+ }
1511+
1512+ #[ test]
1513+ fn seek_target_clamps_to_track_bounds ( ) {
1514+ assert_eq ! (
1515+ seek_target(
1516+ Duration :: from_secs( 2 ) ,
1517+ SeekDirection :: Backward ,
1518+ Some ( Duration :: from_secs( 30 ) )
1519+ ) ,
1520+ Duration :: from_secs( 0 )
1521+ ) ;
1522+ assert_eq ! (
1523+ seek_target(
1524+ Duration :: from_secs( 28 ) ,
1525+ SeekDirection :: Forward ,
1526+ Some ( Duration :: from_secs( 30 ) )
1527+ ) ,
1528+ Duration :: from_secs( 30 )
1529+ ) ;
1530+ assert_eq ! (
1531+ seek_target( Duration :: from_secs( 28 ) , SeekDirection :: Forward , None ) ,
1532+ Duration :: from_secs( 33 )
1533+ ) ;
1534+ }
1535+
14321536 #[ test]
14331537 fn history_uses_vim_keys ( ) {
14341538 let mut state = base_state ( ) ;
@@ -1478,6 +1582,14 @@ mod tests {
14781582 ) ,
14791583 KeyCommand :: HistorySortNext
14801584 ) ;
1585+ assert_eq ! (
1586+ handle_key_event( KeyEvent :: new( KeyCode :: Right , KeyModifiers :: NONE ) , & state) ,
1587+ KeyCommand :: None
1588+ ) ;
1589+ assert_eq ! (
1590+ handle_key_event( KeyEvent :: new( KeyCode :: Left , KeyModifiers :: NONE ) , & state) ,
1591+ KeyCommand :: None
1592+ ) ;
14811593 }
14821594
14831595 #[ test]
0 commit comments