From f3d6469beda96f425efa63b1edf13af1cc69da17 Mon Sep 17 00:00:00 2001 From: Rodrigo Rivero Date: Thu, 7 May 2026 18:53:08 -0400 Subject: [PATCH] feat: add skip forward and backward playback controls --- .../16x16/apps/jump-backward-10-symbolic.svg | 5 + .../16x16/apps/jump-forward-10-symbolic.svg | 5 + src/main.rs | 114 ++++++++++++++++-- src/mpris.rs | 8 +- 4 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg create mode 100644 res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg diff --git a/res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg b/res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg new file mode 100644 index 0000000..51c26ca --- /dev/null +++ b/res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg b/res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg new file mode 100644 index 0000000..bc4095f --- /dev/null +++ b/res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main.rs b/src/main.rs index d689ce3..2c11f3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,12 @@ const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0; const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1; const GST_PLAY_FLAG_TEXT: i32 = 1 << 2; +const JUMP_BACKWARD_ICON: &[u8] = + include_bytes!("../res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg"); + +const JUMP_FORWARD_ICON: &[u8] = + include_bytes!("../res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg"); + use std::error::Error; fn language_name(code: &str) -> Option { @@ -170,6 +176,8 @@ pub enum Action { FolderOpenRecent(usize), Fullscreen, PlayPause, + PlayPrev, + PlayNext, SeekBackward, SeekForward, NextFrame, @@ -193,6 +201,8 @@ impl MenuAction for Action { Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index), Self::Fullscreen => Message::Fullscreen, Self::PlayPause => Message::PlayPause, + Self::PlayPrev => Message::PlayPrev, + Self::PlayNext => Message::PlayNext, Self::SeekBackward => Message::SeekRelative(-10.0), Self::SeekForward => Message::SeekRelative(10.0), Self::NextFrame => Message::NextFrame, @@ -291,6 +301,7 @@ pub enum Message { Seek(f64), SeekRelative(f64), SeekRelease, + PlayPrev, PlayNext, NextFrame, PreviousFrame, @@ -1338,6 +1349,55 @@ impl Application for App { } } + Message::PlayPrev => { + if self.flags.config_state.player_state.repeat == RepeatState::Track { + return Task::none(); + } + + //first we get info about current media id & position in nav_bar + let curr_id = self.nav_model.active(); + let curr_position = match self.nav_model.position(curr_id) { + Some(pos) => pos, + None => { + log::warn!("Failed to get position of current media: {:?}", curr_id); + return self.update(Message::EndOfStream); + } + }; + + if let Some(video) = &mut self.video_opt { + self.position = video.position().as_secs_f64(); + + if self.position > 3.0 { + video.seek(0, false).expect("seek"); + } else { + if self.nav_model.activate_position(curr_position - 1) { + let curr_id = self.nav_model.active(); + match self.nav_model.data::(curr_id) { + //The prev one is a media file, we play it. + Some(ProjectNode::File { .. }) => { + return self.on_nav_select(curr_id); + } + + //The prev one is a folder. We expand it and recall PlayPrev. + Some(ProjectNode::Folder { .. }) => { + let _ = self.on_nav_select(curr_id); + return self.update(Message::PlayPrev); + } + + //Unknown type. We do nothing. + _ => log::warn!( + "unknown type: {:?}", + self.nav_model.data::(curr_id) + ), + } + } else { + // first file + video.seek(0, false).expect("seek"); + } + } + } + } + Message::PlayNext => { // TODO: known limitations: // 1) if the user collapses the folder entry while a song is playing, @@ -1779,19 +1839,55 @@ impl Application for App { ); } if self.controls { - let mut row = widget::row::with_capacity(7) + let mut row = widget::row::with_capacity(9) .align_y(Alignment::Center) .spacing(space_xxs) .push( - widget::button::icon( - if self.video_opt.as_ref().is_none_or(|video| video.paused()) { - widget::icon::from_name("media-playback-start-symbolic").size(16) - } else { - widget::icon::from_name("media-playback-pause-symbolic").size(16) - }, - ) - .on_press(Message::PlayPause), + if self + .video_opt + .as_ref() + .map_or(true, |video| video.has_video()) + { + widget::button::icon( + widget::icon::from_svg_bytes(JUMP_BACKWARD_ICON).symbolic(true), + ) + .on_press(Message::SeekRelative(-10.0)) + } else { + widget::button::icon( + widget::icon::from_name("media-skip-backward-symbolic").size(16), + ) + .on_press(Message::PlayPrev) + }, ); + row = row.push( + widget::button::icon( + if self.video_opt.as_ref().map_or(true, |video| video.paused()) { + widget::icon::from_name("media-playback-start-symbolic").size(16) + } else { + widget::icon::from_name("media-playback-pause-symbolic").size(16) + }, + ) + .on_press(Message::PlayPause), + ); + + row = row.push( + if self + .video_opt + .as_ref() + .map_or(true, |video| video.has_video()) + { + widget::button::icon( + widget::icon::from_svg_bytes(JUMP_FORWARD_ICON).symbolic(true), + ) + .on_press(Message::SeekRelative(10.0)) + } else { + widget::button::icon( + widget::icon::from_name("media-skip-forward-symbolic").size(16), + ) + .on_press(Message::PlayNext) + }, + ); + row = row.push(widget::tooltip( widget::button::icon( widget::icon::from_name(match self.flags.config_state.player_state.repeat { diff --git a/src/mpris.rs b/src/mpris.rs index 31a088f..8c87244 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -150,12 +150,12 @@ impl RootInterface for Player { impl PlayerInterface for Player { async fn next(&self) -> fdo::Result<()> { log::info!("Next"); - Ok(()) + self.message(Message::PlayNext).await } async fn previous(&self) -> fdo::Result<()> { log::info!("Previous"); - Ok(()) + self.message(Message::PlayPrev).await } async fn pause(&self) -> fdo::Result<()> { @@ -271,12 +271,12 @@ impl PlayerInterface for Player { async fn can_go_next(&self) -> fdo::Result { log::info!("CanGoNext"); - Ok(false) + Ok(true) } async fn can_go_previous(&self) -> fdo::Result { log::info!("CanGoPrevious"); - Ok(false) + Ok(true) } async fn can_play(&self) -> fdo::Result {