diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index ede9bc2..f3b1e7d 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -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 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..df13c43 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, 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 abc4a91..60ad7b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { diff --git a/src/mpris.rs b/src/mpris.rs index 533a1be..a7079c1 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,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 { 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 { 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; } }