From de70009ed2169b48d4ee1b38cb3d75fa5d73095f Mon Sep 17 00:00:00 2001 From: norepro Date: Wed, 26 Nov 2025 18:26:06 -0800 Subject: [PATCH 1/8] 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 { From ca0de9d07e7eb33c9d98960472350f5e5be88f8e Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 00:08:05 -0800 Subject: [PATCH 2/8] Add tooltip to repeat control --- i18n/en/cosmic_player.ftl | 6 ++++++ src/main.rs | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index ede9bc2..89c6492 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -32,3 +32,9 @@ open-media-folder = Open media folder... open-recent-media-folder = Open recent media folder close-media-folder = Close media folder quit = Quit + +# Controls + +repeat-disabled = Repeat disabled +repeat-always = Repeat always +repeat-once = Repeat once diff --git a/src/main.rs b/src/main.rs index b0e8eb3..0236de2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1656,7 +1656,7 @@ impl Application for App { ) .on_press(Message::PlayPause), ); - row = row.push( + row = row.push(widget::tooltip( widget::button::icon( widget::icon::from_name(match self.flags.config_state.player_state.repeat { RepeatState::Disabled => "media-playlist-no-repeat-symbolic", @@ -1672,7 +1672,13 @@ impl Application for App { RepeatState::Once => RepeatState::Disabled, }, )), - ); + match self.flags.config_state.player_state.repeat { + RepeatState::Disabled => fl!("repeat-disabled"), + RepeatState::Always => fl!("repeat-always"), + RepeatState::Once => fl!("repeat-once"), + }, + widget::tooltip::Position::Top, + )); if self.core.is_condensed() { row = row.push(widget::horizontal_space(Length::Fill)); } else { From 636a73a27f1feb0440f5bc580920c0f56efa7bcd Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 01:27:29 -0800 Subject: [PATCH 3/8] Fix incorrect match arm guard --- src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0236de2..2701ca0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1314,13 +1314,16 @@ impl Application for App { } } Message::EndOfStream => { + let repeat_state = &self.flags.config_state.player_state.repeat; println!( "end of stream, repeat={:?}, has_media_repeated={:?}", - self.flags.config_state.player_state.repeat, self.has_media_repeated + repeat_state, self.has_media_repeated ); - match self.flags.config_state.player_state.repeat { - RepeatState::Always | RepeatState::Once if !self.has_media_repeated => { + match repeat_state { + RepeatState::Always | RepeatState::Once + if (*repeat_state == RepeatState::Always || !self.has_media_repeated) => + { if let Some(video) = &mut self.video_opt { self.has_media_repeated = true; video.restart_stream().expect("restart_stream"); From 2476557ec366cebb177c897ab88ddd5686007d55 Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 02:02:46 -0800 Subject: [PATCH 4/8] mpris --- src/main.rs | 6 ++++++ src/mpris.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2701ca0..8dd28c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -232,6 +232,7 @@ pub struct MprisState { position_micros: i64, paused: bool, volume: f64, + will_repeat: bool, } #[derive(Clone, Debug)] @@ -742,10 +743,15 @@ impl App { position_micros: (self.position * 1_000_000.0) as i64, paused: true, volume: 0.0, + will_repeat: false, }; if let Some(video) = &self.video_opt { new.paused = video.paused(); new.volume = video.volume(); + + let repeat_state = &self.flags.config_state.player_state.repeat; + new.will_repeat = *repeat_state == RepeatState::Always + || (*repeat_state == RepeatState::Once && !self.has_media_repeated); } if new != *old { *old = new.clone(); diff --git a/src/mpris.rs b/src/mpris.rs index 533a1be..799a40e 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -10,7 +10,7 @@ use mpris_server::{ use std::{any::TypeId, future, process}; use tokio::sync::{Mutex, mpsc}; -use crate::{Message, MprisEvent, MprisMeta, MprisState}; +use crate::{Message, MprisEvent, MprisMeta, MprisState, config::RepeatState}; impl MprisMeta { fn metadata(&self) -> Metadata { @@ -58,6 +58,16 @@ impl MprisState { PlaybackStatus::Playing } } + + fn loop_status(&self) -> LoopStatus { + if self.will_repeat { + // TODO: Our choice is between Track and Playlist. Track is the best match for current repeat behavior, + // but this may change when we implement mpris playlists. + LoopStatus::Track + } else { + LoopStatus::None + } + } } pub struct Player { @@ -194,11 +204,19 @@ impl PlayerInterface for Player { async fn loop_status(&self) -> fdo::Result { log::info!("LoopStatus"); - Ok(LoopStatus::None) + let state = self.state.lock().await; + Ok(state.loop_status()) } async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> { log::info!("SetLoopStatus({})", loop_status); + let repeat_state = if loop_status == LoopStatus::None { + RepeatState::Disabled + } else { + // TODO: This may change when we implement mpris playlists. + RepeatState::Always + }; + self.message(Message::RepeatToggled(repeat_state)).await?; Ok(()) } @@ -418,6 +436,10 @@ pub fn subscription() -> Subscription { position: Time::from_micros(new.position_micros), }); } + let new_loop_status = new.loop_status(); + if new_loop_status != old.loop_status() { + props.push(Property::LoopStatus(new_loop_status)); + } *old = new; } } From ff97fa2f62ea207d76489fff17d0042898839b03 Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 11:41:45 -0800 Subject: [PATCH 5/8] Workaround double EOS with explicit seek --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 8dd28c9..5814c13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1332,6 +1332,12 @@ impl Application for App { { if let Some(video) = &mut self.video_opt { self.has_media_repeated = true; + + // Workaround: Explicitly seeking to the start before `restart_stream`. + // This prevents its internal `pause(false)` from triggering a second EndOfStream message + // that breaks RepeatState::Once. This results in a double seek but avoids single repeat + // not working at all. `restart_stream` is still required to set internal `is_eos` value. + video.seek(0, false).expect("seek"); video.restart_stream().expect("restart_stream"); } } From b559de8fc55a3cc16dccc8e6d91617ede8b5dec8 Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 20:43:40 -0800 Subject: [PATCH 6/8] Rename repeat options to Disabled, Track, and Playlist Change track and playlist to repeat current track indefinitely. --- i18n/en/cosmic_player.ftl | 4 +-- src/config.rs | 7 ++--- src/main.rs | 55 ++++++++++++++------------------------- src/mpris.rs | 19 ++++++-------- 4 files changed, 33 insertions(+), 52 deletions(-) diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index 89c6492..6ed7fb8 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -36,5 +36,5 @@ quit = Quit # Controls repeat-disabled = Repeat disabled -repeat-always = Repeat always -repeat-once = Repeat once +repeat-playlist = Repeat playlist +repeat-track = Repeat track diff --git a/src/config.rs b/src/config.rs index fd10712..599deb1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,11 +40,12 @@ impl Default for Config { } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub enum RepeatState { + #[default] Disabled, - Once, - Always, + Track, + Playlist, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/main.rs b/src/main.rs index 5814c13..499d9f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -232,7 +232,7 @@ pub struct MprisState { position_micros: i64, paused: bool, volume: f64, - will_repeat: bool, + repeat_state: RepeatState, } #[derive(Clone, Debug)] @@ -322,7 +322,6 @@ pub struct App { current_text: Option, #[cfg(feature = "xdg-portal")] inhibit: tokio::sync::watch::Sender, - has_media_repeated: bool, } impl App { @@ -347,7 +346,6 @@ 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(); @@ -379,7 +377,6 @@ impl App { }; self.duration = video.duration().as_secs_f64(); - self.has_media_repeated = false; let pipeline = video.pipeline(); self.video_opt = Some(video); @@ -743,15 +740,12 @@ impl App { position_micros: (self.position * 1_000_000.0) as i64, paused: true, volume: 0.0, - will_repeat: false, + repeat_state: RepeatState::Disabled, }; if let Some(video) = &self.video_opt { new.paused = video.paused(); new.volume = video.volume(); - - let repeat_state = &self.flags.config_state.player_state.repeat; - new.will_repeat = *repeat_state == RepeatState::Always - || (*repeat_state == RepeatState::Once && !self.has_media_repeated); + new.repeat_state = self.flags.config_state.player_state.repeat; } if new != *old { *old = new.clone(); @@ -885,7 +879,6 @@ 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 @@ -1321,27 +1314,17 @@ impl Application for App { } Message::EndOfStream => { let repeat_state = &self.flags.config_state.player_state.repeat; - println!( - "end of stream, repeat={:?}, has_media_repeated={:?}", - repeat_state, self.has_media_repeated - ); + println!("end of stream, repeat={:?}", repeat_state); - match repeat_state { - RepeatState::Always | RepeatState::Once - if (*repeat_state == RepeatState::Always || !self.has_media_repeated) => - { - if let Some(video) = &mut self.video_opt { - self.has_media_repeated = true; - - // Workaround: Explicitly seeking to the start before `restart_stream`. - // This prevents its internal `pause(false)` from triggering a second EndOfStream message - // that breaks RepeatState::Once. This results in a double seek but avoids single repeat - // not working at all. `restart_stream` is still required to set internal `is_eos` value. - video.seek(0, false).expect("seek"); - video.restart_stream().expect("restart_stream"); - } + if matches!(repeat_state, RepeatState::Playlist | RepeatState::Track) { + if let Some(video) = &mut self.video_opt { + // Workaround: Explicitly seeking to the start before `restart_stream`. + // This prevents its internal `pause(false)` from triggering a second EndOfStream message + // that breaks RepeatState::Once. This results in a double seek but avoids single repeat + // not working at all. `restart_stream` is still required to set internal `is_eos` value. + video.seek(0, false).expect("seek"); + video.restart_stream().expect("restart_stream"); } - _ => {} } } Message::MissingPlugin(element) => { @@ -1675,22 +1658,22 @@ impl Application for App { 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", + RepeatState::Playlist => "media-playlist-repeat-symbolic", + RepeatState::Track => "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, + RepeatState::Disabled => RepeatState::Playlist, + RepeatState::Playlist => RepeatState::Track, + RepeatState::Track => RepeatState::Disabled, }, )), match self.flags.config_state.player_state.repeat { RepeatState::Disabled => fl!("repeat-disabled"), - RepeatState::Always => fl!("repeat-always"), - RepeatState::Once => fl!("repeat-once"), + RepeatState::Playlist => fl!("repeat-playlist"), + RepeatState::Track => fl!("repeat-track"), }, widget::tooltip::Position::Top, )); diff --git a/src/mpris.rs b/src/mpris.rs index 799a40e..0685986 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -60,12 +60,10 @@ impl MprisState { } fn loop_status(&self) -> LoopStatus { - if self.will_repeat { - // TODO: Our choice is between Track and Playlist. Track is the best match for current repeat behavior, - // but this may change when we implement mpris playlists. - LoopStatus::Track - } else { - LoopStatus::None + match self.repeat_state { + RepeatState::Disabled => LoopStatus::None, + RepeatState::Playlist => LoopStatus::Playlist, + RepeatState::Track => LoopStatus::Track, } } } @@ -210,11 +208,10 @@ impl PlayerInterface for Player { async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> { log::info!("SetLoopStatus({})", loop_status); - let repeat_state = if loop_status == LoopStatus::None { - RepeatState::Disabled - } else { - // TODO: This may change when we implement mpris playlists. - RepeatState::Always + let repeat_state = match loop_status { + LoopStatus::None => RepeatState::Disabled, + LoopStatus::Playlist => RepeatState::Playlist, + LoopStatus::Track => RepeatState::Track, }; self.message(Message::RepeatToggled(repeat_state)).await?; Ok(()) From 42b1f6e5e80c6b5b0c595d7d8a7b73f74428e75b Mon Sep 17 00:00:00 2001 From: norepro Date: Thu, 27 Nov 2025 21:03:57 -0800 Subject: [PATCH 7/8] Remove Playlist repeat option --- i18n/en/cosmic_player.ftl | 1 - src/config.rs | 1 - src/main.rs | 7 ++----- src/mpris.rs | 4 +--- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index 6ed7fb8..f3b1e7d 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -36,5 +36,4 @@ quit = Quit # Controls repeat-disabled = Repeat disabled -repeat-playlist = Repeat playlist repeat-track = Repeat track diff --git a/src/config.rs b/src/config.rs index 599deb1..df13c43 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,7 +45,6 @@ pub enum RepeatState { #[default] Disabled, Track, - Playlist, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/main.rs b/src/main.rs index 499d9f5..bf1d98a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1316,7 +1316,7 @@ impl Application for App { let repeat_state = &self.flags.config_state.player_state.repeat; println!("end of stream, repeat={:?}", repeat_state); - if matches!(repeat_state, RepeatState::Playlist | RepeatState::Track) { + if matches!(repeat_state, RepeatState::Track) { if let Some(video) = &mut self.video_opt { // Workaround: Explicitly seeking to the start before `restart_stream`. // This prevents its internal `pause(false)` from triggering a second EndOfStream message @@ -1658,21 +1658,18 @@ impl Application for App { widget::button::icon( widget::icon::from_name(match self.flags.config_state.player_state.repeat { RepeatState::Disabled => "media-playlist-no-repeat-symbolic", - RepeatState::Playlist => "media-playlist-repeat-symbolic", RepeatState::Track => "media-playlist-repeat-song-symbolic", }) .size(16), ) .on_press(Message::RepeatToggled( match self.flags.config_state.player_state.repeat { - RepeatState::Disabled => RepeatState::Playlist, - RepeatState::Playlist => RepeatState::Track, + RepeatState::Disabled => RepeatState::Track, RepeatState::Track => RepeatState::Disabled, }, )), match self.flags.config_state.player_state.repeat { RepeatState::Disabled => fl!("repeat-disabled"), - RepeatState::Playlist => fl!("repeat-playlist"), RepeatState::Track => fl!("repeat-track"), }, widget::tooltip::Position::Top, diff --git a/src/mpris.rs b/src/mpris.rs index 0685986..a7079c1 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -62,7 +62,6 @@ impl MprisState { fn loop_status(&self) -> LoopStatus { match self.repeat_state { RepeatState::Disabled => LoopStatus::None, - RepeatState::Playlist => LoopStatus::Playlist, RepeatState::Track => LoopStatus::Track, } } @@ -210,8 +209,7 @@ impl PlayerInterface for Player { log::info!("SetLoopStatus({})", loop_status); let repeat_state = match loop_status { LoopStatus::None => RepeatState::Disabled, - LoopStatus::Playlist => RepeatState::Playlist, - LoopStatus::Track => RepeatState::Track, + LoopStatus::Track | LoopStatus::Playlist => RepeatState::Track, }; self.message(Message::RepeatToggled(repeat_state)).await?; Ok(()) From a83951f1bad3f621d2d25a45814b6261268b296a Mon Sep 17 00:00:00 2001 From: norepro Date: Sun, 30 Nov 2025 20:58:28 -0800 Subject: [PATCH 8/8] Use set_looping from iced_video instead of EOS signal --- src/main.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index bf1d98a..3170610 100644 --- a/src/main.rs +++ b/src/main.rs @@ -431,6 +431,7 @@ impl App { } self.inhibit_idle(); + self.set_looping_from_repeat_state(); self.update_flags(); self.update_mpris_meta(); self.update_title() @@ -819,6 +820,12 @@ impl App { #[cfg(feature = "xdg-portal")] let _ = self.inhibit.send(true); } + + fn set_looping_from_repeat_state(&mut self) { + if let Some(video) = &mut self.video_opt { + video.set_looping(self.flags.config_state.player_state.repeat == RepeatState::Track); + } + } } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -1228,6 +1235,7 @@ impl Application for App { } Message::RepeatToggled(state) => { self.flags.config_state.player_state.repeat = state; + self.set_looping_from_repeat_state(); self.update_controls(true); self.save_config_state(); } @@ -1313,19 +1321,10 @@ impl Application for App { } } Message::EndOfStream => { - let repeat_state = &self.flags.config_state.player_state.repeat; - println!("end of stream, repeat={:?}", repeat_state); - - if matches!(repeat_state, RepeatState::Track) { - if let Some(video) = &mut self.video_opt { - // Workaround: Explicitly seeking to the start before `restart_stream`. - // This prevents its internal `pause(false)` from triggering a second EndOfStream message - // that breaks RepeatState::Once. This results in a double seek but avoids single repeat - // not working at all. `restart_stream` is still required to set internal `is_eos` value. - video.seek(0, false).expect("seek"); - video.restart_stream().expect("restart_stream"); - } - } + println!( + "end of stream, repeat={:?}", + self.flags.config_state.player_state.repeat + ); } Message::MissingPlugin(element) => { if let Some(video) = &mut self.video_opt {