From d9046d324f3d86738abe096d1b48ccf31054a69e Mon Sep 17 00:00:00 2001 From: TheForgotten Date: Sun, 15 Jun 2025 12:11:40 +0100 Subject: [PATCH 1/4] Add new custom icon for no loop behavior --- psst-gui/src/widget/icons.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/psst-gui/src/widget/icons.rs b/psst-gui/src/widget/icons.rs index bfeba75b..5c99fc6c 100644 --- a/psst-gui/src/widget/icons.rs +++ b/psst-gui/src/widget/icons.rs @@ -93,6 +93,12 @@ pub static PLAY_LOOP_ALL: SvgIcon = SvgIcon { svg_size: Size::new(16.0, 16.0), op: PaintOp::Fill, }; +// SF Pro Regular - repeat (custom - modified with a forward slash) +pub static PLAY_LOOP_NONE: SvgIcon = SvgIcon { + svg_path: "M7.1964 8.7467V9.7913h3.7718c.8588 0 1.4163-.5324 1.4163-1.346V8.1189c0-.3516.2812-.6328.6278-.6328.3515 0 .6278.2813.6278.6328v.447c0 1.5067-1.0648 2.461-2.7673 2.461H7.1964v1.0296c0 .3013-.1909.4921-.4922.4921-.1356 0-.2662-.0502-.3666-.1356L4.3638 10.7606c-.2461-.2059-.2461-.5324 0-.7282L6.3376 8.385c.1005-.0803.231-.1356.3666-.1356.3013 0 .4922.1959.4922.4972ZM2.3599 7.3705v-.447c0-1.5117 1.0698-2.4609 2.7723-2.4609H8.8036V3.4381c0-.3013.1908-.4922.4972-.4922.1356 0 .2612.0502.3616.1356l1.9788 1.6523c.2411.2009.2461.5324 0 .7282L9.6624 7.1094c-.1005.0804-.226.1306-.3616.1306-.3064 0-.4972-.1908-.4972-.4922V5.6981H5.0318c-.8538 0-1.4163.5274-1.4163 1.346v.3264c0 .3465-.2762.6278-.6278.6278-.3465 0-.6278-.2813-.6278-.6278Zm0 5.178L13.632 4.1959l.0081-1.25L2.37 11.2985Z", + svg_size: Size::new(16.0, 16.0), + op: PaintOp::Fill, +}; // SFsymbols - music.note pub static MUSIC_NOTE: SvgIcon = SvgIcon { From 90a5fee42eaf24dae683dfb364f169662af27fcb Mon Sep 17 00:00:00 2001 From: TheForgotten Date: Sun, 15 Jun 2025 12:16:14 +0100 Subject: [PATCH 2/4] Separate QueueBehavior into ShuffleBehavior and LoopBehavior Co-authored-by: JOSEALM3IDA --- psst-core/src/player/mod.rs | 16 +++++-- psst-core/src/player/queue.rs | 59 ++++++++++++++----------- psst-gui/src/cmd.rs | 6 ++- psst-gui/src/controller/playback.rs | 46 ++++++++++++++------ psst-gui/src/data/config.rs | 8 ++-- psst-gui/src/data/mod.rs | 19 +++++--- psst-gui/src/data/playback.rs | 15 +++++-- psst-gui/src/ui/playback.rs | 67 +++++++++++++++++++++-------- 8 files changed, 160 insertions(+), 76 deletions(-) diff --git a/psst-core/src/player/mod.rs b/psst-core/src/player/mod.rs index 3de35edd..dc6fbdf8 100644 --- a/psst-core/src/player/mod.rs +++ b/psst-core/src/player/mod.rs @@ -19,7 +19,7 @@ use crate::{ use self::{ file::MediaPath, item::{LoadedPlaybackItem, PlaybackItem}, - queue::{Queue, QueueBehavior}, + queue::{LoopBehavior, Queue, ShuffleBehavior}, worker::PlaybackManager, }; @@ -118,7 +118,12 @@ impl Player { PlayerCommand::Stop => self.stop(), PlayerCommand::Seek { position } => self.seek(position), PlayerCommand::Configure { config } => self.configure(config), - PlayerCommand::SetQueueBehavior { behavior } => self.queue.set_behaviour(behavior), + PlayerCommand::SetShuffleBehavior { shuffle_behavior } => { + self.queue.set_shuffle_behaviour(shuffle_behavior) + } + PlayerCommand::SetLoopBehavior { loop_behavior } => { + self.queue.set_loop_behaviour(loop_behavior) + } PlayerCommand::AddToQueue { item } => self.queue.add(item), PlayerCommand::SetVolume { volume } => self.set_volume(volume), } @@ -420,8 +425,11 @@ pub enum PlayerCommand { Configure { config: PlaybackConfig, }, - SetQueueBehavior { - behavior: QueueBehavior, + SetShuffleBehavior { + shuffle_behavior: ShuffleBehavior, + }, + SetLoopBehavior { + loop_behavior: LoopBehavior, }, AddToQueue { item: PlaybackItem, diff --git a/psst-core/src/player/queue.rs b/psst-core/src/player/queue.rs index 622440a7..3517d414 100644 --- a/psst-core/src/player/queue.rs +++ b/psst-core/src/player/queue.rs @@ -3,26 +3,38 @@ use rand::prelude::SliceRandom; use super::PlaybackItem; #[derive(Debug)] -pub enum QueueBehavior { +pub enum ShuffleBehavior { Sequential, Random, - LoopTrack, - LoopAll, } -impl Default for QueueBehavior { +#[derive(Debug)] +pub enum LoopBehavior { + Track, + All, + None, +} + +impl Default for ShuffleBehavior { fn default() -> Self { Self::Sequential } } +impl Default for LoopBehavior { + fn default() -> Self { + Self::None + } +} + pub struct Queue { items: Vec, user_items: Vec, position: usize, user_items_position: usize, positions: Vec, - behavior: QueueBehavior, + shuffle_behavior: ShuffleBehavior, + loop_behavior: LoopBehavior, } impl Queue { @@ -33,7 +45,8 @@ impl Queue { position: 0, user_items_position: 0, positions: Vec::new(), - behavior: QueueBehavior::default(), + shuffle_behavior: ShuffleBehavior::default(), + loop_behavior: LoopBehavior::default(), } } @@ -66,8 +79,13 @@ impl Queue { } } - pub fn set_behaviour(&mut self, behavior: QueueBehavior) { - self.behavior = behavior; + pub fn set_shuffle_behaviour(&mut self, shuffle_behavior: ShuffleBehavior) { + self.shuffle_behavior = shuffle_behavior; + self.compute_positions(); + } + + pub fn set_loop_behaviour(&mut self, loop_behavior: LoopBehavior) { + self.loop_behavior = loop_behavior; self.compute_positions(); } @@ -82,7 +100,7 @@ impl Queue { // Start with an ordered 1:1 mapping. self.positions = (0..self.items.len()).collect(); - if let QueueBehavior::Random = self.behavior { + if let ShuffleBehavior::Random = self.shuffle_behavior { // Swap the current position with the first item, so we will start from the // beginning, with the full queue ahead of us. Then shuffle the rest of the // items and set the position to 0. @@ -127,28 +145,21 @@ impl Queue { } fn previous_position(&self) -> usize { - match self.behavior { - QueueBehavior::Sequential - | QueueBehavior::Random - | QueueBehavior::LoopTrack - | QueueBehavior::LoopAll => self.position.saturating_sub(1), - } + self.position.saturating_sub(1) } fn next_position(&self) -> usize { - match self.behavior { - QueueBehavior::Sequential | QueueBehavior::Random | QueueBehavior::LoopTrack => { - self.position + 1 - } - QueueBehavior::LoopAll => (self.position + 1) % self.items.len(), + match self.loop_behavior { + LoopBehavior::Track | LoopBehavior::None => self.position + 1, + LoopBehavior::All => (self.position + 1) % self.items.len(), } } fn following_position(&self) -> usize { - match self.behavior { - QueueBehavior::Sequential | QueueBehavior::Random => self.position + 1, - QueueBehavior::LoopTrack => self.position, - QueueBehavior::LoopAll => (self.position + 1) % self.items.len(), + match self.loop_behavior { + LoopBehavior::None => self.position + 1, + LoopBehavior::Track => self.position, + LoopBehavior::All => (self.position + 1) % self.items.len(), } } } diff --git a/psst-gui/src/cmd.rs b/psst-gui/src/cmd.rs index 9e897e79..a905b00c 100644 --- a/psst-gui/src/cmd.rs +++ b/psst-gui/src/cmd.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use crate::{ - data::{Nav, PlaybackPayload, QueueBehavior, QueueEntry}, + data::{LoopBehavior, Nav, PlaybackPayload, QueueEntry, ShuffleBehavior}, ui::find::Find, }; @@ -54,7 +54,9 @@ pub const PLAY_RESUME: Selector = Selector::new("app.play-resume"); pub const PLAY_NEXT: Selector = Selector::new("app.play-next"); pub const PLAY_STOP: Selector = Selector::new("app.play-stop"); pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector::new("app.add-to-queue"); -pub const PLAY_QUEUE_BEHAVIOR: Selector = Selector::new("app.play-queue-behavior"); +pub const PLAY_SHUFFLE_BEHAVIOR: Selector = + Selector::new("app.play-shuffle-behavior"); +pub const PLAY_LOOP_BEHAVIOR: Selector = Selector::new("app.play-loop-behavior"); pub const PLAY_SEEK: Selector = Selector::new("app.play-seek"); pub const SKIP_TO_POSITION: Selector = Selector::new("app.skip-to-position"); diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 9913b9a1..08fbb203 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -23,12 +23,13 @@ use souvlaki::{ }; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::data::ShuffleBehavior; use crate::{ cmd, data::Nav, data::{ - AppState, Config, NowPlaying, Playable, Playback, PlaybackOrigin, PlaybackState, - QueueBehavior, QueueEntry, + AppState, Config, LoopBehavior, NowPlaying, Playable, Playback, PlaybackOrigin, + PlaybackState, QueueEntry, }, ui::lyrics, }; @@ -392,13 +393,23 @@ impl PlaybackController { })); } - fn set_queue_behavior(&mut self, behavior: QueueBehavior) { - self.send(PlayerEvent::Command(PlayerCommand::SetQueueBehavior { - behavior: match behavior { - QueueBehavior::Sequential => psst_core::player::queue::QueueBehavior::Sequential, - QueueBehavior::Random => psst_core::player::queue::QueueBehavior::Random, - QueueBehavior::LoopTrack => psst_core::player::queue::QueueBehavior::LoopTrack, - QueueBehavior::LoopAll => psst_core::player::queue::QueueBehavior::LoopAll, + fn set_shuffle_behavior(&mut self, shuffle_behavior: ShuffleBehavior) { + self.send(PlayerEvent::Command(PlayerCommand::SetShuffleBehavior { + shuffle_behavior: match shuffle_behavior { + ShuffleBehavior::Sequential => { + psst_core::player::queue::ShuffleBehavior::Sequential + } + ShuffleBehavior::Random => psst_core::player::queue::ShuffleBehavior::Random, + }, + })); + } + + fn set_loop_behavior(&mut self, loop_behavior: LoopBehavior) { + self.send(PlayerEvent::Command(PlayerCommand::SetLoopBehavior { + loop_behavior: match loop_behavior { + LoopBehavior::Track => psst_core::player::queue::LoopBehavior::Track, + LoopBehavior::All => psst_core::player::queue::LoopBehavior::All, + LoopBehavior::None => psst_core::player::queue::LoopBehavior::None, }, })); } @@ -526,10 +537,16 @@ where data.add_queued_entry(entry.clone()); ctx.set_handled(); } - Event::Command(cmd) if cmd.is(cmd::PLAY_QUEUE_BEHAVIOR) => { - let behavior = cmd.get_unchecked(cmd::PLAY_QUEUE_BEHAVIOR); - data.set_queue_behavior(behavior.to_owned()); - self.set_queue_behavior(behavior.to_owned()); + Event::Command(cmd) if cmd.is(cmd::PLAY_SHUFFLE_BEHAVIOR) => { + let behavior = cmd.get_unchecked(cmd::PLAY_SHUFFLE_BEHAVIOR); + data.set_shuffle_behavior(behavior.to_owned()); + self.set_shuffle_behavior(behavior.to_owned()); + ctx.set_handled(); + } + Event::Command(cmd) if cmd.is(cmd::PLAY_LOOP_BEHAVIOR) => { + let behavior = cmd.get_unchecked(cmd::PLAY_LOOP_BEHAVIOR); + data.set_loop_behavior(behavior.to_owned()); + self.set_loop_behavior(behavior.to_owned()); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::PLAY_SEEK) => { @@ -600,7 +617,8 @@ where // Initialize values loaded from the config. self.set_volume(data.playback.volume); - self.set_queue_behavior(data.playback.queue_behavior); + self.set_shuffle_behavior(data.playback.shuffle_behavior); + self.set_loop_behavior(data.playback.loop_behavior); // Request focus so we can receive keyboard events. ctx.submit_command(cmd::SET_FOCUS.to(ctx.widget_id())); diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index fc96b839..594e772c 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -18,7 +18,7 @@ use psst_core::{ }; use serde::{Deserialize, Serialize}; -use super::{Nav, Promise, QueueBehavior, SliderScrollScale}; +use super::{LoopBehavior, Nav, Promise, ShuffleBehavior, SliderScrollScale}; use crate::ui::theme; #[derive(Clone, Debug, Data, Lens)] @@ -114,7 +114,8 @@ pub struct Config { pub theme: Theme, pub volume: f64, pub last_route: Option