diff --git a/psst-core/src/player/mod.rs b/psst-core/src/player/mod.rs index 735ba654..1ef5be6b 100644 --- a/psst-core/src/player/mod.rs +++ b/psst-core/src/player/mod.rs @@ -119,7 +119,10 @@ impl Player { PlayerCommand::Seek { position } => self.seek(position), PlayerCommand::Configure { config } => self.configure(config), PlayerCommand::SetQueueBehavior { behavior } => self.queue.set_behaviour(behavior), + PlayerCommand::SkipToPlaceInQueue { item } => self.queue.skip_to_place_in_queue(item), + PlayerCommand::ClearQueue => self.queue.clear_user_items(), PlayerCommand::AddToQueue { item } => self.queue.add(item), + PlayerCommand::RemoveFromQueue { item } => self.queue.remove(item), PlayerCommand::SetVolume { volume } => self.set_volume(volume), } } @@ -423,9 +426,16 @@ pub enum PlayerCommand { SetQueueBehavior { behavior: QueueBehavior, }, + SkipToPlaceInQueue { + item: usize, + }, AddToQueue { item: PlaybackItem, }, + RemoveFromQueue { + item: usize, + }, + ClearQueue, /// Change playback volume to a value in 0.0..=1.0 range. SetVolume { volume: f64, diff --git a/psst-core/src/player/queue.rs b/psst-core/src/player/queue.rs index acbea36e..cd4c61d8 100644 --- a/psst-core/src/player/queue.rs +++ b/psst-core/src/player/queue.rs @@ -18,51 +18,106 @@ impl Default for QueueBehavior { pub struct Queue { items: Vec, + added_items_in_main_queue: Vec<(usize, usize)>, user_items: Vec, position: usize, user_items_position: usize, positions: Vec, behavior: QueueBehavior, + playing_from_user_items: bool, } impl Queue { pub fn new() -> Self { Self { items: Vec::new(), + added_items_in_main_queue: Vec::new(), user_items: Vec::new(), position: 0, user_items_position: 0, positions: Vec::new(), behavior: QueueBehavior::default(), + playing_from_user_items: false, } } - + pub fn clear(&mut self) { self.items.clear(); self.positions.clear(); self.position = 0; } + pub fn clear_user_items(&mut self) { + self.user_items.clear(); + self.user_items_position = 0; + } + pub fn fill(&mut self, items: Vec, position: usize) { self.positions.clear(); + self.added_items_in_main_queue.clear(); self.items = items; self.position = position; self.compute_positions(); } + + pub fn skip_to_place_in_queue(&mut self, index: usize) { + if self.playing_from_user_items { + self.user_items = self.user_items.split_off(index + 1); + } + else { + self.user_items = self.user_items.split_off(index); + } + self.user_items_position = 0; + } pub fn add(&mut self, item: PlaybackItem) { self.user_items.push(item); } + pub fn get_playing_from_user_items_bool(&mut self) -> bool{ + self.playing_from_user_items + } + + pub fn remove(&mut self, index: usize) { + if self.playing_from_user_items { + self.user_items.remove(index+1); + } + else { + self.user_items.remove(index); + } + if self.user_items_position < index && self.user_items_position > 0 { + self.user_items_position -= 1; + } + } + fn handle_added_queue(&mut self) { + if !self.added_items_in_main_queue.is_empty() { + let item_index = self.added_items_in_main_queue[0].0; + let position_index = self.added_items_in_main_queue[0].1; + self.items.remove(item_index - 1); + self.positions.remove(position_index); + + self.added_items_in_main_queue.remove(0); + if self.position > 0 { + self.position -= 1; + } + } + if self.user_items.len() > self.user_items_position { self.items.insert( self.positions.len(), self.user_items[self.user_items_position], ); + self.positions .insert(self.position + 1, self.positions.len()); self.user_items_position += 1; + + self.added_items_in_main_queue.push((self.positions.len(), self.position + 1)); + self.playing_from_user_items = true; + } + else { + self.playing_from_user_items = false; } } diff --git a/psst-gui/build.rs b/psst-gui/build.rs index 1870fcd1..1499033f 100644 --- a/psst-gui/build.rs +++ b/psst-gui/build.rs @@ -21,7 +21,7 @@ fn add_windows_icon() { res.compile().expect("Could not attach exe icon"); fn load_images() -> Vec> { - let sizes = vec![32, 64, 128, 256]; + let sizes = [32, 64, 128, 256]; sizes .iter() .map(|s| { diff --git a/psst-gui/src/cmd.rs b/psst-gui/src/cmd.rs index 65957e13..84ccba2e 100644 --- a/psst-gui/src/cmd.rs +++ b/psst-gui/src/cmd.rs @@ -54,6 +54,11 @@ pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector::new("ap pub const PLAY_QUEUE_BEHAVIOR: Selector = Selector::new("app.play-queue-behavior"); pub const PLAY_SEEK: Selector = Selector::new("app.play-seek"); +// Queue control +pub const REMOVE_FROM_QUEUE: Selector = Selector::new("app.remove-from-queue"); +pub const CLEAR_QUEUE: Selector = Selector::new("app.clear-queue"); +pub const SKIP_TO_PLACE_IN_QUEUE: Selector = Selector::new("app.skip-to-place-in-queue"); + // Sorting control pub const SORT_BY_DATE_ADDED: Selector = Selector::new("app.sort-by-date-added"); pub const SORT_BY_TITLE: Selector = Selector::new("app.sort-by-title"); diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 79b8b811..e4b74d5b 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -1,6 +1,5 @@ use std::{ - thread::{self, JoinHandle}, - time::Duration, + cmp::Ordering, thread::{self, JoinHandle}, time::Duration }; use crossbeam_channel::Sender; @@ -299,6 +298,19 @@ impl PlaybackController { item: *item, })); } + fn skip_to_place_in_queue(&mut self, item: &usize) { + self.send(PlayerEvent::Command(PlayerCommand::SkipToPlaceInQueue { + item: *item, + })); + } + fn remove_from_queue(&mut self, item: &usize) { + self.send(PlayerEvent::Command(PlayerCommand::RemoveFromQueue { + item: *item, + })); + } + fn clear_queue(&mut self) { + self.send(PlayerEvent::Command(PlayerCommand::ClearQueue)); + } fn set_queue_behavior(&mut self, behavior: QueueBehavior) { self.send(PlayerEvent::Command(PlayerCommand::SetQueueBehavior { @@ -344,6 +356,13 @@ where Event::Command(cmd) if cmd.is(cmd::PLAYBACK_PLAYING) => { let (item, progress) = cmd.get_unchecked(cmd::PLAYBACK_PLAYING); + // TODO: this falsely removes the song if you click on a song from the playlist that is also in the queue, not sure how to solve this? + if !data.added_queue.displayed_queue.is_empty() && data.playback.now_playing.as_mut().is_some_and(|np| { + np.origin.to_string() == PlaybackOrigin::Queue.to_string() + && np.item.id() == data.added_queue.displayed_queue[0].item.id() + }) { + data.added_queue.displayed_queue.remove(0); + } if let Some(queued) = data.queued_entry(*item) { data.start_playback(queued.item, queued.origin, progress.to_owned()); self.update_media_control_playback(&data.playback); @@ -418,6 +437,7 @@ where self.add_to_queue(item); data.add_queued_entry(entry.clone()); + data.info_alert("Track added to queue."); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::PLAY_QUEUE_BEHAVIOR) => { @@ -436,6 +456,52 @@ where } ctx.set_handled(); } + Event::Command(cmd) if cmd.is(cmd::SKIP_TO_PLACE_IN_QUEUE) => { + let track_pos = *cmd.get_unchecked(cmd::SKIP_TO_PLACE_IN_QUEUE); + match track_pos.cmp(&0) { + Ordering::Greater => { + if data.playback.queue.is_empty() || (data.playback.queue.len() <= 1 && data.playback.queue[0].origin.to_string() == PlaybackOrigin::Queue.to_string()) { + data.playback.queue.clear(); + data.playback.queue.push_back(data.added_queue.displayed_queue[track_pos].clone()); + data.added_queue.displayed_queue = data.added_queue.displayed_queue.split_off(track_pos); + self.skip_to_place_in_queue(&(track_pos+1)); + self.play(&data.playback.queue, track_pos);} + else if data.playback.now_playing.is_some() { + data.added_queue.displayed_queue = data.added_queue.displayed_queue.split_off(track_pos); + self.skip_to_place_in_queue(&track_pos); + self.next(); + } + }, + Ordering::Equal => { + if data.playback.queue.is_empty() || (data.playback.queue.len() <= 1 && data.playback.queue[0].origin.to_string() == PlaybackOrigin::Queue.to_string()) { + data.playback.queue.clear(); + data.playback.queue.push_back(data.added_queue.displayed_queue[track_pos].clone()); + self.remove_from_queue(&track_pos); + data.added_queue.displayed_queue.remove(track_pos); + self.play(&data.playback.queue, track_pos); + } + else if data.playback.now_playing.is_some() { + self.next(); + } + } + _ => {}} + + ctx.set_handled(); + } + Event::Command(cmd) if cmd.is(cmd::REMOVE_FROM_QUEUE) => { + let item = cmd.get_unchecked(cmd::REMOVE_FROM_QUEUE); + data.added_queue.displayed_queue.remove(*item); + self.remove_from_queue(item); + data.info_alert("Track removed from queue."); + + ctx.set_handled(); + } + Event::Command(cmd) if cmd.is(cmd::CLEAR_QUEUE) => { + data.added_queue.displayed_queue.clear(); + self.clear_queue(); + data.info_alert("Tracks cleared from queue."); + ctx.set_handled(); + } // Keyboard shortcuts. Event::KeyDown(key) if key.code == Code::Space => { self.pause_or_resume(); diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index cbb897f4..cb12e5c2 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -94,6 +94,7 @@ pub struct Config { pub last_route: Option