Rename repeat options to Disabled, Track, and Playlist

Change track and playlist to repeat current track indefinitely.
This commit is contained in:
norepro 2025-11-27 20:43:40 -08:00
parent ff97fa2f62
commit b559de8fc5
4 changed files with 33 additions and 52 deletions

View file

@ -36,5 +36,5 @@ quit = Quit
# Controls # Controls
repeat-disabled = Repeat disabled repeat-disabled = Repeat disabled
repeat-always = Repeat always repeat-playlist = Repeat playlist
repeat-once = Repeat once repeat-track = Repeat track

View file

@ -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 { pub enum RepeatState {
#[default]
Disabled, Disabled,
Once, Track,
Always, Playlist,
} }
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]

View file

@ -232,7 +232,7 @@ pub struct MprisState {
position_micros: i64, position_micros: i64,
paused: bool, paused: bool,
volume: f64, volume: f64,
will_repeat: bool, repeat_state: RepeatState,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -322,7 +322,6 @@ pub struct App {
current_text: Option<i32>, current_text: Option<i32>,
#[cfg(feature = "xdg-portal")] #[cfg(feature = "xdg-portal")]
inhibit: tokio::sync::watch::Sender<bool>, inhibit: tokio::sync::watch::Sender<bool>,
has_media_repeated: bool,
} }
impl App { impl App {
@ -347,7 +346,6 @@ impl App {
self.current_audio = -1; self.current_audio = -1;
self.text_codes.clear(); self.text_codes.clear();
self.current_text = None; self.current_text = None;
self.has_media_repeated = false;
self.update_mpris_meta(); self.update_mpris_meta();
self.update_nav_bar_active(); self.update_nav_bar_active();
self.allow_idle(); self.allow_idle();
@ -379,7 +377,6 @@ impl App {
}; };
self.duration = video.duration().as_secs_f64(); self.duration = video.duration().as_secs_f64();
self.has_media_repeated = false;
let pipeline = video.pipeline(); let pipeline = video.pipeline();
self.video_opt = Some(video); self.video_opt = Some(video);
@ -743,15 +740,12 @@ impl App {
position_micros: (self.position * 1_000_000.0) as i64, position_micros: (self.position * 1_000_000.0) as i64,
paused: true, paused: true,
volume: 0.0, volume: 0.0,
will_repeat: false, repeat_state: RepeatState::Disabled,
}; };
if let Some(video) = &self.video_opt { if let Some(video) = &self.video_opt {
new.paused = video.paused(); new.paused = video.paused();
new.volume = video.volume(); new.volume = video.volume();
new.repeat_state = self.flags.config_state.player_state.repeat;
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 { if new != *old {
*old = new.clone(); *old = new.clone();
@ -885,7 +879,6 @@ impl Application for App {
current_text: None, current_text: None,
#[cfg(feature = "xdg-portal")] #[cfg(feature = "xdg-portal")]
inhibit, inhibit,
has_media_repeated: false,
}; };
// Do not show nav bar by default. Will be opened by open_project if needed // 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 => { Message::EndOfStream => {
let repeat_state = &self.flags.config_state.player_state.repeat; let repeat_state = &self.flags.config_state.player_state.repeat;
println!( println!("end of stream, repeat={:?}", repeat_state);
"end of stream, repeat={:?}, has_media_repeated={:?}",
repeat_state, self.has_media_repeated
);
match repeat_state { if matches!(repeat_state, RepeatState::Playlist | RepeatState::Track) {
RepeatState::Always | RepeatState::Once if let Some(video) = &mut self.video_opt {
if (*repeat_state == RepeatState::Always || !self.has_media_repeated) => // Workaround: Explicitly seeking to the start before `restart_stream`.
{ // This prevents its internal `pause(false)` from triggering a second EndOfStream message
if let Some(video) = &mut self.video_opt { // that breaks RepeatState::Once. This results in a double seek but avoids single repeat
self.has_media_repeated = true; // not working at all. `restart_stream` is still required to set internal `is_eos` value.
video.seek(0, false).expect("seek");
// Workaround: Explicitly seeking to the start before `restart_stream`. video.restart_stream().expect("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) => { Message::MissingPlugin(element) => {
@ -1675,22 +1658,22 @@ impl Application for App {
widget::button::icon( widget::button::icon(
widget::icon::from_name(match self.flags.config_state.player_state.repeat { widget::icon::from_name(match self.flags.config_state.player_state.repeat {
RepeatState::Disabled => "media-playlist-no-repeat-symbolic", RepeatState::Disabled => "media-playlist-no-repeat-symbolic",
RepeatState::Always => "media-playlist-repeat-symbolic", RepeatState::Playlist => "media-playlist-repeat-symbolic",
RepeatState::Once => "media-playlist-repeat-song-symbolic", RepeatState::Track => "media-playlist-repeat-song-symbolic",
}) })
.size(16), .size(16),
) )
.on_press(Message::RepeatToggled( .on_press(Message::RepeatToggled(
match self.flags.config_state.player_state.repeat { match self.flags.config_state.player_state.repeat {
RepeatState::Disabled => RepeatState::Always, RepeatState::Disabled => RepeatState::Playlist,
RepeatState::Always => RepeatState::Once, RepeatState::Playlist => RepeatState::Track,
RepeatState::Once => RepeatState::Disabled, RepeatState::Track => RepeatState::Disabled,
}, },
)), )),
match self.flags.config_state.player_state.repeat { match self.flags.config_state.player_state.repeat {
RepeatState::Disabled => fl!("repeat-disabled"), RepeatState::Disabled => fl!("repeat-disabled"),
RepeatState::Always => fl!("repeat-always"), RepeatState::Playlist => fl!("repeat-playlist"),
RepeatState::Once => fl!("repeat-once"), RepeatState::Track => fl!("repeat-track"),
}, },
widget::tooltip::Position::Top, widget::tooltip::Position::Top,
)); ));

View file

@ -60,12 +60,10 @@ impl MprisState {
} }
fn loop_status(&self) -> LoopStatus { fn loop_status(&self) -> LoopStatus {
if self.will_repeat { match self.repeat_state {
// TODO: Our choice is between Track and Playlist. Track is the best match for current repeat behavior, RepeatState::Disabled => LoopStatus::None,
// but this may change when we implement mpris playlists. RepeatState::Playlist => LoopStatus::Playlist,
LoopStatus::Track RepeatState::Track => LoopStatus::Track,
} else {
LoopStatus::None
} }
} }
} }
@ -210,11 +208,10 @@ impl PlayerInterface for Player {
async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> { async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> {
log::info!("SetLoopStatus({})", loop_status); log::info!("SetLoopStatus({})", loop_status);
let repeat_state = if loop_status == LoopStatus::None { let repeat_state = match loop_status {
RepeatState::Disabled LoopStatus::None => RepeatState::Disabled,
} else { LoopStatus::Playlist => RepeatState::Playlist,
// TODO: This may change when we implement mpris playlists. LoopStatus::Track => RepeatState::Track,
RepeatState::Always
}; };
self.message(Message::RepeatToggled(repeat_state)).await?; self.message(Message::RepeatToggled(repeat_state)).await?;
Ok(()) Ok(())