Merge pull request #192 from norepro/feat/repeat-toggle

Add repeat control
This commit is contained in:
Jeremy Soller 2026-02-24 15:26:33 -07:00 committed by GitHub
commit 1afac98574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 84 additions and 5 deletions

View file

@ -32,3 +32,8 @@ 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-track = Repeat track

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
reorder_imports = false

View file

@ -40,10 +40,23 @@ impl Default for Config {
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum RepeatState {
#[default]
Disabled,
Track,
}
#[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<url::Url>,
pub recent_projects: VecDeque<PathBuf>,
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,
},
}
}
}

View file

@ -32,7 +32,7 @@ use std::{
use tokio::sync::mpsc;
use crate::{
config::{Config, ConfigState, CONFIG_VERSION},
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
key_bind::{key_binds, KeyBind},
project::ProjectNode,
};
@ -230,6 +230,7 @@ pub struct MprisState {
position_micros: i64,
paused: bool,
volume: f64,
repeat_state: RepeatState,
}
#[derive(Clone, Debug)]
@ -278,6 +279,7 @@ pub enum Message {
Pause,
Play,
PlayPause,
RepeatToggled(RepeatState),
Scrolled(ScrollDelta),
Seek(f64),
SeekRelative(f64),
@ -428,6 +430,7 @@ impl App {
}
self.inhibit_idle();
self.set_looping_from_repeat_state();
self.update_flags();
self.update_mpris_meta();
self.update_title()
@ -737,10 +740,12 @@ impl App {
position_micros: (self.position * 1_000_000.0) as i64,
paused: true,
volume: 0.0,
repeat_state: RepeatState::Disabled,
};
if let Some(video) = &self.video_opt {
new.paused = video.paused();
new.volume = video.volume();
new.repeat_state = self.flags.config_state.player_state.repeat;
}
if new != *old {
*old = new.clone();
@ -814,6 +819,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.
@ -1221,6 +1232,12 @@ 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();
}
Message::Scrolled(delta) => {
let nav_bar_toggled = self.core.nav_bar_active();
if let Some(video) = &mut self.video_opt {
@ -1352,7 +1369,10 @@ impl Application for App {
}
Message::EndOfStream => {
println!("end of stream");
println!(
"end of stream, repeat={:?}",
self.flags.config_state.player_state.repeat
);
}
Message::MissingPlugin(element) => {
@ -1668,7 +1688,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(
@ -1681,6 +1701,26 @@ impl Application for App {
)
.on_press(Message::PlayPause),
);
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",
RepeatState::Track => "media-playlist-repeat-song-symbolic",
})
.size(16),
)
.on_press(Message::RepeatToggled(
match self.flags.config_state.player_state.repeat {
RepeatState::Disabled => RepeatState::Track,
RepeatState::Track => RepeatState::Disabled,
},
)),
match self.flags.config_state.player_state.repeat {
RepeatState::Disabled => fl!("repeat-disabled"),
RepeatState::Track => fl!("repeat-track"),
},
widget::tooltip::Position::Top,
));
if self.core.is_condensed() {
row = row.push(widget::horizontal_space(Length::Fill));
} else {

View file

@ -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,13 @@ impl MprisState {
PlaybackStatus::Playing
}
}
fn loop_status(&self) -> LoopStatus {
match self.repeat_state {
RepeatState::Disabled => LoopStatus::None,
RepeatState::Track => LoopStatus::Track,
}
}
}
pub struct Player {
@ -194,11 +201,17 @@ impl PlayerInterface for Player {
async fn loop_status(&self) -> fdo::Result<LoopStatus> {
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 = match loop_status {
LoopStatus::None => RepeatState::Disabled,
LoopStatus::Track | LoopStatus::Playlist => RepeatState::Track,
};
self.message(Message::RepeatToggled(repeat_state)).await?;
Ok(())
}
@ -418,6 +431,10 @@ pub fn subscription() -> Subscription<Message> {
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;
}
}