diff --git a/psst-gui/src/cmd.rs b/psst-gui/src/cmd.rs index 9e897e79..e6888090 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::{AlbumLink, Nav, PlaybackPayload, PlaylistLink, QueueBehavior, QueueEntry}, ui::find::Find, }; @@ -46,6 +46,8 @@ pub const PLAYBACK_BLOCKED: Selector = Selector::new("app.playback-blocked"); pub const PLAYBACK_STOPPED: Selector = Selector::new("app.playback-stopped"); // Playback control +pub const PLAY_ALBUM: Selector = Selector::new("app.play-album"); +pub const PLAY_PLAYLIST: Selector = Selector::new("app.play-playlist"); pub const PLAY: Selector = Selector::new("app.play-index"); pub const PLAY_TRACKS: Selector = Selector::new("app.play-tracks"); pub const PLAY_PREVIOUS: Selector = Selector::new("app.play-previous"); diff --git a/psst-gui/src/delegate.rs b/psst-gui/src/delegate.rs index a209b1ad..4363ca90 100644 --- a/psst-gui/src/delegate.rs +++ b/psst-gui/src/delegate.rs @@ -3,9 +3,12 @@ use druid::{ commands, AppDelegate, Application, Command, DelegateCtx, Env, Event, Handled, Target, WindowDesc, WindowId, }; +use rand::seq::IndexedRandom; use std::fs; +use std::sync::Arc; use threadpool::ThreadPool; +use crate::data::Track; use crate::ui::playlist::{ RENAME_PLAYLIST, RENAME_PLAYLIST_CONFIRM, UNFOLLOW_PLAYLIST, UNFOLLOW_PLAYLIST_CONFIRM, }; @@ -139,6 +142,30 @@ impl AppDelegate for Delegate { data: &mut AppState, _env: &Env, ) -> Handled { + if let Some(playlist_link) = cmd.get(cmd::PLAY_PLAYLIST) { + if let Some(tracks) = data.playlist_detail.tracks.resolved() { + play_items_with_mode( + ctx, + &tracks.tracks.iter().collect::>(), + crate::data::PlaybackOrigin::Playlist(playlist_link.clone()), + data.playback.queue_behavior, + |t: &&Arc| crate::data::Playable::Track((*t).clone()), + ); + } + return Handled::Yes; + } + if let Some(album_link) = cmd.get(cmd::PLAY_ALBUM) { + if let Some(album) = data.album_detail.album.resolved() { + play_items_with_mode( + ctx, + &album.data.tracks.iter().collect::>(), + crate::data::PlaybackOrigin::Album(album_link.clone()), + data.playback.queue_behavior, + |t: &&Arc| crate::data::Playable::Track((*t).clone()), + ); + } + return Handled::Yes; + } if cmd.is(cmd::SHOW_CREDITS_WINDOW) { let _window_id = self.show_credits(ctx); if let Some(track) = cmd.get(cmd::SHOW_CREDITS_WINDOW) { @@ -325,3 +352,35 @@ impl Delegate { } } } + +fn play_items_with_mode( + ctx: &mut DelegateCtx, + items: &[T], + origin: crate::data::PlaybackOrigin, + queue_behavior: crate::data::QueueBehavior, + to_playable: F, +) where + F: Fn(&T) -> crate::data::Playable, +{ + if items.is_empty() { + return; + } + + let playables: Vec<_> = items.iter().map(&to_playable).collect(); + let position = if queue_behavior == crate::data::QueueBehavior::Random { + let mut rng = rand::rng(); + (0..playables.len()) + .collect::>() + .choose(&mut rng) + .copied() + .unwrap_or(0) + } else { + 0 + }; + let payload = crate::data::PlaybackPayload { + items: playables.into(), + origin, + position, + }; + ctx.submit_command(crate::cmd::PLAY_TRACKS.with(payload)); +} diff --git a/psst-gui/src/ui/mod.rs b/psst-gui/src/ui/mod.rs index 6e13e016..4a113e9e 100644 --- a/psst-gui/src/ui/mod.rs +++ b/psst-gui/src/ui/mod.rs @@ -244,7 +244,9 @@ fn root_widget() -> impl Widget { .must_fill_main_axis(true) .with_child(topbar_back_button_widget()) .with_child(topbar_title_widget()) + .with_flex_spacer(1.0) .with_child(topbar_sort_widget()) + .with_child(topbar_play_button_widget()) .background(Border::Bottom.with_color(theme::BACKGROUND_DARK)); let main = Flex::column() @@ -467,6 +469,32 @@ fn volume_slider() -> impl Widget { ) } +fn topbar_play_button_widget() -> impl Widget { + let play_button = icons::PLAY + .scale((theme::grid(2.0), theme::grid(2.0))) + .padding(theme::grid(1.0)) + .link() + .rounded(theme::BUTTON_BORDER_RADIUS) + .on_left_click(|ctx, _, data: &mut AppState, _| match &data.nav { + Nav::AlbumDetail(link, _) => { + ctx.submit_command(cmd::PLAY_ALBUM.with(link.clone())); + } + Nav::PlaylistDetail(link) => { + ctx.submit_command(cmd::PLAY_PLAYLIST.with(link.clone())); + } + _ => {} + }); + + Either::new( + |data: &AppState, _| { + matches!(data.nav, Nav::AlbumDetail(..)) || matches!(data.nav, Nav::PlaylistDetail(..)) + }, + play_button, + Empty, + ) + .padding((0.0, 0.0, theme::grid(1.0), 0.0)) +} + fn topbar_sort_widget() -> impl Widget { let up_icon = icons::UP.scale((10.0, theme::grid(2.0))); let down_icon = icons::DOWN.scale((10.0, theme::grid(2.0)));