From de70009ed2169b48d4ee1b38cb3d75fa5d73095f Mon Sep 17 00:00:00 2001 From: norepro Date: Wed, 26 Nov 2025 18:26:06 -0800 Subject: [PATCH] Add three-state repeat button Add a new button to the controls that defines whether the media repeats at the end of the stream. The states are: - Disabled: Media does not repeat. This is existing behavior and the default state. - Always: Media always repeats. - Once: Media repeats once. Flag is reset when loading new media. Tested all three states with both audio and video. Added a `rustfmt.toml` to avoid reordering imports. They keep moving around in previous pull requests, best to just define what we want in the repository. Fixes: #39 --- rustfmt.toml | 1 + src/config.rs | 16 ++++++++++++++++ src/main.rs | 46 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..fa09141 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +reorder_imports = false diff --git a/src/config.rs b/src/config.rs index c8acef2..fd10712 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,10 +40,23 @@ impl Default for Config { } } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum RepeatState { + Disabled, + Once, + Always, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct PlayerState { + pub repeat: RepeatState, +} + #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ConfigState { pub recent_files: VecDeque, pub recent_projects: VecDeque, + pub player_state: PlayerState, } impl Default for ConfigState { @@ -51,6 +64,9 @@ impl Default for ConfigState { Self { recent_files: VecDeque::new(), recent_projects: VecDeque::new(), + player_state: PlayerState { + repeat: RepeatState::Disabled, + }, } } } diff --git a/src/main.rs b/src/main.rs index 3b1a303..b0e8eb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ use std::{ use tokio::sync::mpsc; use crate::{ - config::{CONFIG_VERSION, Config, ConfigState}, + config::{CONFIG_VERSION, Config, ConfigState, RepeatState}, key_bind::{KeyBind, key_binds}, project::ProjectNode, }; @@ -280,6 +280,7 @@ pub enum Message { Pause, Play, PlayPause, + RepeatToggled(RepeatState), Scrolled(ScrollDelta), Seek(f64), SeekRelative(f64), @@ -320,6 +321,7 @@ pub struct App { current_text: Option, #[cfg(feature = "xdg-portal")] inhibit: tokio::sync::watch::Sender, + has_media_repeated: bool, } impl App { @@ -344,6 +346,7 @@ impl App { self.current_audio = -1; self.text_codes.clear(); self.current_text = None; + self.has_media_repeated = false; self.update_mpris_meta(); self.update_nav_bar_active(); self.allow_idle(); @@ -375,6 +378,7 @@ impl App { }; self.duration = video.duration().as_secs_f64(); + self.has_media_repeated = false; let pipeline = video.pipeline(); self.video_opt = Some(video); @@ -875,6 +879,7 @@ impl Application for App { current_text: None, #[cfg(feature = "xdg-portal")] inhibit, + has_media_repeated: false, }; // Do not show nav bar by default. Will be opened by open_project if needed @@ -1222,6 +1227,11 @@ impl Application for App { } } } + Message::RepeatToggled(state) => { + self.flags.config_state.player_state.repeat = state; + self.update_controls(true); + self.save_config_state(); + } Message::Scrolled(delta) => { let nav_bar_toggled = self.core.nav_bar_active(); if let Some(video) = &mut self.video_opt { @@ -1304,7 +1314,20 @@ impl Application for App { } } Message::EndOfStream => { - println!("end of stream"); + println!( + "end of stream, repeat={:?}, has_media_repeated={:?}", + self.flags.config_state.player_state.repeat, self.has_media_repeated + ); + + match self.flags.config_state.player_state.repeat { + RepeatState::Always | RepeatState::Once if !self.has_media_repeated => { + if let Some(video) = &mut self.video_opt { + self.has_media_repeated = true; + video.restart_stream().expect("restart_stream"); + } + } + _ => {} + } } Message::MissingPlugin(element) => { if let Some(video) = &mut self.video_opt { @@ -1620,7 +1643,7 @@ impl Application for App { ); } if self.controls { - let mut row = widget::row::with_capacity(7) + let mut row = widget::row::with_capacity(8) .align_items(Alignment::Center) .spacing(space_xxs) .push( @@ -1633,6 +1656,23 @@ impl Application for App { ) .on_press(Message::PlayPause), ); + row = row.push( + widget::button::icon( + widget::icon::from_name(match self.flags.config_state.player_state.repeat { + RepeatState::Disabled => "media-playlist-no-repeat-symbolic", + RepeatState::Always => "media-playlist-repeat-symbolic", + RepeatState::Once => "media-playlist-repeat-song-symbolic", + }) + .size(16), + ) + .on_press(Message::RepeatToggled( + match self.flags.config_state.player_state.repeat { + RepeatState::Disabled => RepeatState::Always, + RepeatState::Always => RepeatState::Once, + RepeatState::Once => RepeatState::Disabled, + }, + )), + ); if self.core.is_condensed() { row = row.push(widget::horizontal_space(Length::Fill)); } else {