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
repeat-disabled = Repeat disabled
repeat-always = Repeat always
repeat-once = Repeat once
repeat-playlist = Repeat playlist
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 {
#[default]
Disabled,
Once,
Always,
Track,
Playlist,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]

View file

@ -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<i32>,
#[cfg(feature = "xdg-portal")]
inhibit: tokio::sync::watch::Sender<bool>,
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,
));

View file

@ -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(())