From baca090890a1fd4c4b142e1ff9405eb41bb4281c Mon Sep 17 00:00:00 2001 From: SO9010 Date: Wed, 28 Aug 2024 17:03:49 +0100 Subject: [PATCH 01/30] Add cover image to all tracks, if enabled --- psst-gui/src/ui/album.rs | 1 + psst-gui/src/ui/artist.rs | 1 + psst-gui/src/ui/recommend.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 515549f6..a6b8e19d 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -75,6 +75,7 @@ fn loaded_detail_widget() -> impl Widget>>> { number: true, title: true, artist: true, + cover: true, ..track::Display::empty() }, }); diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 354bed4a..73a79bf0 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -127,6 +127,7 @@ fn top_tracks_widget() -> impl Widget> { title: true, album: true, popularity: true, + cover: true, ..track::Display::empty() }, }) diff --git a/psst-gui/src/ui/recommend.rs b/psst-gui/src/ui/recommend.rs index 98e91a80..9197cd31 100644 --- a/psst-gui/src/ui/recommend.rs +++ b/psst-gui/src/ui/recommend.rs @@ -131,6 +131,7 @@ fn track_results_widget() -> impl Widget> { title: true, artist: true, album: true, + cover: true, ..track::Display::empty() }, }) From 795da0a898e7aef9ebfef30814309b8906be2ace Mon Sep 17 00:00:00 2001 From: SO9010 Date: Wed, 28 Aug 2024 21:08:05 +0100 Subject: [PATCH 02/30] fix --- .gitignore | 3 +++ psst-gui/src/ui/album.rs | 1 - psst-gui/src/ui/artist.rs | 47 +++++++++++++++++++++++++++++++---- psst-gui/src/ui/search.rs | 2 +- psst-gui/src/webapi/client.rs | 42 +++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 54354ea7..eeb9b2af 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ cache *.iml rust-toolchain *.ico +psst-gui/src/ui/artist.rs +psst-gui/src/ui/search.rs +psst-gui/src/webapi/client.rs diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index a6b8e19d..515549f6 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -75,7 +75,6 @@ fn loaded_detail_widget() -> impl Widget>>> { number: true, title: true, artist: true, - cover: true, ..track::Display::empty() }, }); diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 73a79bf0..70ee42ca 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -1,8 +1,5 @@ use druid::{ - im::Vector, - kurbo::Circle, - widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, - Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, Widget, WidgetExt, + im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List, Scroll}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, Widget, WidgetExt }; use crate::{ @@ -21,10 +18,26 @@ pub const LOAD_DETAIL: Selector = Selector::new("app.artist.load-det pub fn detail_widget() -> impl Widget { Flex::column() + .with_child(async_main_artist_widget()) .with_child(async_top_tracks_widget()) .with_child(async_albums_widget().padding((theme::grid(1.0), 0.0))) .with_child(async_related_widget().padding((theme::grid(1.0), 0.0))) } +//WebApi::global().get_musicbrainz_artist(&d.url()), +fn async_main_artist_widget() -> impl Widget { + Async::new( + utils::spinner_widget, + artist_widget, + utils::error_widget, + ) + .lens(AppState::artist_detail.then(ArtistDetail::artist)) + .on_command_async( + LOAD_DETAIL, + |d| WebApi::global().get_artist(&d.id), + |_, data, d| data.artist_detail.artist.defer(d), + |_, data, r| data.artist_detail.artist.update(r), + ) +} fn async_top_tracks_widget() -> impl Widget { Async::new( @@ -83,6 +96,22 @@ fn async_related_widget() -> impl Widget { } pub fn artist_widget() -> impl Widget { + let artist_image = artist_cover_widget(theme::grid(21.0)); + Flex::row() + .with_child(artist_image) + .with_child( + Scroll::new( + Label::new( + "Coldplay are a British rock band formed in London in 1997, consisting of vocalist and pianist Chris Martin, lead guitarist Jonny Buckland, bassist Guy Berryman, drummer and percussionist Will Champion, and manager Phil Harvey. They are best known for their live performances, having also impacted popular culture with their artistry, advocacy and achievements. \n The members of the band initially met at University College London, calling themselves Big Fat Noises and changing to Starfish, before settling on the current name. After releasing Safety (1998) independently, Coldplay signed with Parlophone in 1999 and wrote their debut album, Parachutes (2000). It featured breakthrough single \"Yellow\" and received a Brit Award for British Album of the Year and a Grammy Award for Best Alternative Music Album. The group's follow-up, A Rush of Blood to the Head (2002), won the same accolades. X&Y (2005) later saw the completion of what they considered a trilogy, being nominated for Best Rock Album as well. Its successor, Viva la Vida or Death and All His Friends (2008), prevailed in the category. Both albums were the best-selling of their years, topping the charts in over 30 countries. Viva la Vida's title track also became the first British act single to lead the Billboard Hot 100 and UK Singles Chart simultaneously in the 21st century." + ) + .with_line_break_mode(LineBreaking::WordWrap) + .fix_width(theme::grid(35.0)) + ) + .fix_size(theme::grid(35.0), theme::grid(21.0)) + ) + .context_menu(|artist| artist_menu(&artist.link())) +} +pub fn recommended_artist_widget() -> impl Widget { let artist_image = cover_widget(theme::grid(7.0)); let artist_label = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -112,6 +141,14 @@ pub fn link_widget() -> impl Widget { .context_menu(artist_menu) } +pub fn artist_cover_widget(size: f64) -> impl Widget { + RemoteImage::new(utils::placeholder_widget(), move |artist: &Artist, _| { + artist.image(size, size).map(|image| image.url.clone()) + }) + .fix_size(size, size) + .clip(Size::new(size, size).to_rounded_rect(4.0)) +} + pub fn cover_widget(size: f64) -> impl Widget { let radius = size / 2.0; RemoteImage::new(utils::placeholder_widget(), move |artist: &Artist, _| { @@ -150,7 +187,7 @@ fn related_widget() -> impl Widget>> { Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(header_widget("Related Artists")) - .with_child(List::new(artist_widget)) + .with_child(List::new(recommended_artist_widget)) .lens(Cached::data) } diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index 5d135a78..1784b8e9 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -101,7 +101,7 @@ fn artist_results_widget() -> impl Widget> { Empty, Flex::column() .with_child(header_widget("Artists")) - .with_child(List::new(artist::artist_widget)), + .with_child(List::new(artist::recommended_artist_widget)), ) .lens(Ctx::data().then(SearchResults::artists)) } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index de6bb444..09cb9e31 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -79,6 +79,20 @@ impl WebApi { Ok(request) } + fn request_mbi(&self, method: &str, path: impl Display) -> Result { + // The token should be in the format + let request = self + .agent + + .request(method, &format!("https://musicbrainz.org/{}", path)); + //.set("Authorization", &format!("Bearer {}", &token)); + Ok(request) + } + + fn get_from_mbi(&self, path: impl Display) -> Result { + self.request_mbi("GET", path) + } + fn get(&self, path: impl Display) -> Result { self.request("GET", path) } @@ -320,6 +334,34 @@ impl WebApi { let result: Cached = self.load_cached(request, "related-artists", id)?; Ok(result.map(|result| result.artists)) } + + // https://musicbrainz.org/ (MusicBrainz Section) + // https://beta.musicbrainz.org/ws/2/url/?query=url:https://open.spotify.com/artist/1WZarnZpWEv7dDtjAETt4X + pub fn get_musicbrainz_artist(&self, url: &str) -> Result { + let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; + let result: String = self.load(request)?; + Ok(result) + } + + pub fn get_artist_socials(&self, url: &str) -> Result, Error> { + let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; + let mbi: String = self.load(request)?; + + let scnd_request = self.get_from_mbi(format!("/ws/2/artist/{}?inc=url-rels&fmt=json", mbi))?; + let links: Vector = self.load(scnd_request)?; + + Ok(links) + } + + pub fn get_artist_wiki(&self, url: &str) -> Result { + let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; + let mbi: String = self.load(request)?; + + let scnd_request = self.get_from_mbi(format!("/ws/2/artist/{}?inc=url-rels&fmt=json", mbi))?; + let links: Vector = self.load(scnd_request)?; + + Ok(links) + } } /// Album endpoints. From 8f9d6beb65a474f4160a4533ca887c2979903add Mon Sep 17 00:00:00 2001 From: SO9010 Date: Thu, 5 Sep 2024 20:55:28 +0100 Subject: [PATCH 03/30] Homepage and artist init --- psst-gui/src/data/home.rs | 71 +++++++++++++++++++++++++ psst-gui/src/data/mod.rs | 14 +++-- psst-gui/src/data/playback.rs | 3 ++ psst-gui/src/ui/artist.rs | 17 ++++++ psst-gui/src/ui/home.rs | 93 ++++++++++++++++++++++++++++++--- psst-gui/src/ui/playable.rs | 22 ++++++-- psst-gui/src/ui/playback.rs | 2 + psst-gui/src/ui/playlist.rs | 36 +++++++++++++ psst-gui/src/webapi/client.rs | 98 +++++++++++++++++++++++++++++++++-- 9 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 psst-gui/src/data/home.rs diff --git a/psst-gui/src/data/home.rs b/psst-gui/src/data/home.rs new file mode 100644 index 00000000..33334e2e --- /dev/null +++ b/psst-gui/src/data/home.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use druid::{im::Vector, Data, Lens}; +use serde::{Deserialize, Serialize}; + +use crate::data::{Album, Cached, Image, Promise, Track}; + +use super::Playlist; + +#[derive(Clone, Data, Lens)] +pub struct HomeDetail { + pub made_for_you: Promise> + pub top_tracks: Promise, + pub related_artists: Promise>, ArtistLink>, +} + +#[derive(Clone, Data, Lens, Deserialize)] +pub struct Artist { + pub id: Arc, + pub name: Arc, + pub images: Vector, +} + +impl Artist { + pub fn image(&self, width: f64, height: f64) -> Option<&Image> { + Image::at_least_of_size(&self.images, width, height) + } + + pub fn link(&self) -> ArtistLink { + ArtistLink { + id: self.id.clone(), + name: self.name.clone(), + } + } +} + +#[derive(Clone, Data, Lens)] +pub struct ArtistAlbums { + pub albums: Vector>, + pub singles: Vector>, + pub compilations: Vector>, + pub appears_on: Vector>, +} + +#[derive(Clone, Data, Lens)] +pub struct ArtistTracks { + pub id: Arc, + pub name: Arc, + pub tracks: Vector>, +} + +impl ArtistTracks { + pub fn link(&self) -> ArtistLink { + ArtistLink { + id: self.id.clone(), + name: self.name.clone(), + } + } +} + +#[derive(Clone, Debug, Data, Lens, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct ArtistLink { + pub id: Arc, + pub name: Arc, +} + +impl ArtistLink { + pub fn url(&self) -> String { + format!("https://open.spotify.com/artist/{id}", id = self.id) + } +} diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 2c1c33fe..1f787ed5 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -79,7 +79,7 @@ pub struct AppState { pub show_detail: ShowDetail, pub library: Arc, pub common_ctx: Arc, - pub personalized: Personalized, + pub home_detail: HomeDetail, pub alerts: Vector, pub finder: Finder, pub added_queue: Vector, @@ -130,6 +130,11 @@ impl AppState { knobs: Default::default(), results: Promise::Empty, }, + home_detail: HomeDetail { + made_for_you: Promise::Empty, + user_top_tracks: Promise::Empty, + user_top_artists: Promise::Empty, + }, album_detail: AlbumDetail { album: Promise::Empty, }, @@ -149,9 +154,6 @@ impl AppState { }, library, common_ctx, - personalized: Personalized { - made_for_you: Promise::Empty, - }, alerts: Vector::new(), finder: Finder::new(), } @@ -511,8 +513,10 @@ impl CommonCtx { pub type WithCtx = Ctx, T>; #[derive(Clone, Data, Lens)] -pub struct Personalized { +pub struct HomeDetail { pub made_for_you: Promise>, + pub user_top_tracks: Promise>>, + pub user_top_artists: Promise>, } static ALERT_ID: AtomicUsize = AtomicUsize::new(0); diff --git a/psst-gui/src/data/playback.rs b/psst-gui/src/data/playback.rs index 7a99de1f..e64ab586 100644 --- a/psst-gui/src/data/playback.rs +++ b/psst-gui/src/data/playback.rs @@ -107,6 +107,7 @@ impl NowPlaying { #[derive(Clone, Debug, Data)] pub enum PlaybackOrigin { + Home, Library, Album(AlbumLink), Artist(ArtistLink), @@ -119,6 +120,7 @@ pub enum PlaybackOrigin { impl PlaybackOrigin { pub fn to_nav(&self) -> Nav { match &self { + PlaybackOrigin::Home => Nav::Home, PlaybackOrigin::Library => Nav::SavedTracks, PlaybackOrigin::Album(link) => Nav::AlbumDetail(link.clone()), PlaybackOrigin::Artist(link) => Nav::ArtistDetail(link.clone()), @@ -133,6 +135,7 @@ impl PlaybackOrigin { impl fmt::Display for PlaybackOrigin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { + PlaybackOrigin::Home => f.write_str("Home"), PlaybackOrigin::Library => f.write_str("Saved Tracks"), PlaybackOrigin::Album(link) => link.name.fmt(f), PlaybackOrigin::Artist(link) => link.name.fmt(f), diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 70ee42ca..b5f5f1df 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -111,6 +111,23 @@ pub fn artist_widget() -> impl Widget { ) .context_menu(|artist| artist_menu(&artist.link())) } +pub fn horizontal_recommended_artist_widget() -> impl Widget { + let artist_image = cover_widget(theme::grid(21.0)); + let artist_label = Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .lens(Artist::name); + let artist = Flex::column() + .with_child(artist_image) + .with_default_spacer() + .with_child(artist_label); + artist + .padding(theme::grid(0.5)) + .link() + .on_left_click(|ctx, _, artist, _| { + ctx.submit_command(cmd::NAVIGATE.with(Nav::ArtistDetail(artist.link()))); + }) + .context_menu(|artist| artist_menu(&artist.link())) +} pub fn recommended_artist_widget() -> impl Widget { let artist_image = cover_widget(theme::grid(7.0)); let artist_label = Label::raw() diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index cfb498bb..c589ef95 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -1,12 +1,17 @@ +use std::sync::Arc; + +use druid::im::Vector; +use druid::widget::{Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; -use crate::data::Ctx; +use crate::data::{Ctx, HomeDetail, Track, WithCtx}; use crate::{ - data::{AppState, Personalized}, + data::AppState, webapi::WebApi, widget::{Async, MyWidgetExt}, }; +use super::{artist, playable, theme, track}; use super::{ playlist, utils::{error_widget, spinner_widget}, @@ -15,22 +20,98 @@ use super::{ pub const LOAD_MADE_FOR_YOU: Selector = Selector::new("app.home.load-made-for-your"); pub fn home_widget() -> impl Widget { + Flex::column() + .with_child(Label::new("Made for you").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_default_spacer() + .with_child(made_for_you_widget()) + .with_default_spacer() + .with_child(Label::new("Your top artists").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_default_spacer() + .with_child(user_top_artists_widget()) + .with_default_spacer() + .with_child(Label::new("Your top tracks").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_default_spacer() + .with_child(user_top_tracks_widget()) +} + +fn made_for_you_widget() -> impl Widget { Async::new( spinner_widget, - || List::new(playlist::playlist_widget), + || Scroll::new( + List::new( + || playlist::horizontal_playlist_widget(false, true) + ).horizontal() + ).horizontal(), + // TODO Add a function which allows people to scroll with their scroll wheel!!! error_widget, ) .lens( Ctx::make( AppState::common_ctx, - AppState::personalized.then(Personalized::made_for_you), + AppState::home_detail.then(HomeDetail::made_for_you), ) .then(Ctx::in_promise()), ) .on_command_async( LOAD_MADE_FOR_YOU, |_| WebApi::global().get_made_for_you(), - |_, data, d| data.personalized.made_for_you.defer(d), - |_, data, r| data.personalized.made_for_you.update(r), + |_, data, d| data.home_detail.made_for_you.defer(d), + |_, data, r| data.home_detail.made_for_you.update(r), + ) +} + +fn user_top_artists_widget() -> impl Widget { + Async::new( + spinner_widget, + || Scroll::new( + List::new( + || artist::horizontal_recommended_artist_widget() + ).horizontal() + // TODO Add a function which allows people to scroll with their scroll wheel!!! + ).horizontal(), + error_widget, + ) + .lens( + AppState::home_detail.then(HomeDetail::user_top_artists) + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().get_user_top_artist(), + |_, data, d| data.home_detail.user_top_artists.defer(d), + |_, data, r| data.home_detail.user_top_artists.update(r), ) + } + +fn top_tracks_widget() -> impl Widget>>> { + playable::list_widget(playable::Display { + track: track::Display { + title: true, + album: true, + popularity: true, + cover: true, + ..track::Display::empty() + }, + }) +} + +fn user_top_tracks_widget() -> impl Widget { + Async::new( + spinner_widget, + || top_tracks_widget(), + error_widget, + ) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::user_top_tracks), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().get_user_top_tracks(), + |_, data, d| data.home_detail.user_top_tracks.defer(d), + |_, data, r| data.home_detail.user_top_tracks.update(r), + ) +} \ No newline at end of file diff --git a/psst-gui/src/ui/playable.rs b/psst-gui/src/ui/playable.rs index c4b99179..1dff8536 100644 --- a/psst-gui/src/ui/playable.rs +++ b/psst-gui/src/ui/playable.rs @@ -12,9 +12,7 @@ use druid::{ use crate::{ cmd, data::{ - Album, ArtistTracks, CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, - PlaybackPayload, PlaylistTracks, Recommendations, SavedTracks, SearchResults, ShowEpisodes, - WithCtx, + Album, ArtistTracks, CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, PlaybackPayload, PlaylistTracks, Recommendations, SavedTracks, SearchResults, ShowEpisodes, Track, WithCtx }, ui::theme, }; @@ -152,6 +150,24 @@ impl PlayableIter for Arc { } } +// This should change to a more specific name as it could be confusing for others +// As at the moment this is only used for the home page! +impl PlayableIter for Vector> { + fn origin(&self) -> PlaybackOrigin { + PlaybackOrigin::Home + } + + fn for_each(&self, mut cb: impl FnMut(Playable, usize)) { + for (position, track) in self.iter().enumerate() { + cb(Playable::Track(track.to_owned()), position); + } + } + + fn count(&self) -> usize { + self.len() + } +} + impl PlayableIter for PlaylistTracks { fn origin(&self) -> PlaybackOrigin { PlaybackOrigin::Playlist(self.link()) diff --git a/psst-gui/src/ui/playback.rs b/psst-gui/src/ui/playback.rs index 47dc7cdb..b4f5a7b5 100644 --- a/psst-gui/src/ui/playback.rs +++ b/psst-gui/src/ui/playback.rs @@ -172,6 +172,8 @@ fn cover_widget(size: f64) -> impl Widget { fn playback_origin_icon(origin: &PlaybackOrigin) -> &'static SvgIcon { match origin { + // TODO add home widget + PlaybackOrigin::Home => &icons::PODCAST, PlaybackOrigin::Library => &icons::HEART, PlaybackOrigin::Album { .. } => &icons::ALBUM, PlaybackOrigin::Artist { .. } => &icons::ARTIST, diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index 936159ab..4ca093c8 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -318,6 +318,42 @@ fn information_section(title_msg: &str, description_msg: &str) -> impl Widget impl Widget> { + let playlist_image = rounded_cover_widget(theme::grid(21.0)).lens(Ctx::data()); + + let mut playlist = Flex::column() + .with_child(playlist_image) + .with_default_spacer(); + + if show_name { + let playlist_name = Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::Clip) + .lens(Ctx::data().then(Playlist::name)); + playlist.add_child(playlist_name); + playlist.add_spacer(2.0); + } + + if show_description { + let playlist_description = Label::raw() + .with_line_break_mode(LineBreaking::WordWrap) + .with_text_color(theme::PLACEHOLDER_COLOR) + .with_text_size(theme::TEXT_SIZE_SMALL) + .lens(Ctx::data().then(Playlist::description)); + playlist.add_child(playlist_description); + } + + playlist + .padding(theme::grid(1.0)) + .link() + .rounded(theme::BUTTON_BORDER_RADIUS) + .on_left_click(|ctx, _, playlist, _| { + ctx.submit_command(cmd::NAVIGATE.with(Nav::PlaylistDetail(playlist.data.link()))); + }) + .fix_width(theme::grid(25.0)) + .context_menu(playlist_menu_ctx) +} + pub fn playlist_widget() -> impl Widget> { let playlist_image = rounded_cover_widget(theme::grid(6.0)).lens(Ctx::data()); diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 09cb9e31..37fbaccf 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -28,7 +28,7 @@ use crate::{ data::{ Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, Nav, Page, Playlist, Range, Recommendations, RecommendationsRequest, - SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile, + SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile, }, error::Error, }; @@ -201,6 +201,50 @@ impl WebApi { Ok(()) } + /// Very similar to `for_all_pages`, but only returns a certain number of results + /// TODO: test properly + fn for_some_pages( + &self, + request: Request, + lim: usize, + mut func: impl FnMut(Page) -> Result<(), Error>, + ) -> Result<(), Error> { + let mut limit = 50; + let mut offset = 0; + if lim < limit { + limit = lim; + let req = request + .clone() + .query("limit", &limit.to_string()) + .query("offset", &offset.to_string()); + + let page: Page = self.load(req)?; + + func(page)?; + } else { + loop { + let req = request + .clone() + .query("limit", &limit.to_string()) + .query("offset", &offset.to_string()); + + let page: Page = self.load(req)?; + + let page_total = limit/lim; + let page_offset = page.offset; + let page_limit = page.limit; + func(page)?; + + if page_total > offset && offset < self.paginated_limit { + limit = page_limit; + offset = page_offset + page_limit; + } else { + break; + } + } + } + Ok(()) + } /// Load a paginated result set by sending `request` with added pagination /// parameters and return the aggregated results. Use with GET requests. fn load_all_pages( @@ -217,6 +261,22 @@ impl WebApi { Ok(results) } + /// Does a similar thing as `load_all_pages`, but limiting the number of results + fn load_some_pages( + &self, + request: Request, + number: usize, + ) -> Result, Error> { + let mut results = Vector::new(); + + self.for_some_pages(request, number, |page| { + results.append(page.items); + Ok(()) + })?; + + Ok(results) + } + /// Load local track files from the official client's database. pub fn load_local_tracks(&self, username: &str) { if let Err(err) = self @@ -245,13 +305,41 @@ impl WebApi { } } -/// Other endpoints. +/// User endpoints. impl WebApi { + // https://developer.spotify.com/documentation/web-api/reference/get-users-profile pub fn get_user_profile(&self) -> Result { let request = self.get("v1/me")?; let result = self.load(request)?; Ok(result) } + + // https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks + // TODO Cache this. + pub fn get_user_top_tracks(&self) -> Result>, Error> { + let request = self.get("v1/me/top/tracks")? + .query("market", "from_token"); + + let result: Vector> = self.load_some_pages(request, 30)?; + + Ok(result) + } + + // TODO Cache this. + pub fn get_user_top_artist(&self) -> Result, Error> { + #[derive(Clone, Data, Deserialize)] + struct Artists { + artists: Artist, + } + + let request = self.get("v1/me/top/artists")?; + + Ok(self + .load_some_pages(request, 10)? + .into_iter() + .map(|item: Artist| item) + .collect()) + } } /// Artist endpoints. @@ -309,7 +397,7 @@ impl WebApi { Ok(artist_albums) } - // https://developer.spotify.com/documentation/web-api/reference/get-artists-top-tracks/ + // https://developer.spotify.com/documentation/web-api/reference/get-an-artists-top-tracks pub fn get_artist_top_tracks(&self, id: &str) -> Result>, Error> { #[derive(Deserialize)] struct Tracks { @@ -323,7 +411,7 @@ impl WebApi { Ok(result.tracks) } - // https://developer.spotify.com/documentation/web-api/reference/get-related-artists/ + // https://developer.spotify.com/documentation/web-api/reference/get-an-artists-related-artists pub fn get_related_artists(&self, id: &str) -> Result>, Error> { #[derive(Clone, Data, Deserialize)] struct Artists { @@ -353,6 +441,7 @@ impl WebApi { Ok(links) } + /* pub fn get_artist_wiki(&self, url: &str) -> Result { let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; let mbi: String = self.load(request)?; @@ -362,6 +451,7 @@ impl WebApi { Ok(links) } + */ } /// Album endpoints. From a17c6b887853fb52004d5057c9fe33f11a88b3ed Mon Sep 17 00:00:00 2001 From: SO9010 Date: Wed, 11 Sep 2024 21:06:25 +0100 Subject: [PATCH 04/30] VERY ROUGH start, currently only processing made for you in the custom way, VERY CHUNKY --- psst-gui/src/data/mod.rs | 12 +- psst-gui/src/ui/home.rs | 94 ++++++-- psst-gui/src/ui/search.rs | 11 +- psst-gui/src/webapi/client.rs | 435 ++++++++++++++++++++++++++-------- 4 files changed, 437 insertions(+), 115 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 4579ced7..76f8067c 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -56,7 +56,7 @@ pub use crate::data::{ show::{Episode, EpisodeId, EpisodeLink, Show, ShowDetail, ShowEpisodes, ShowLink}, slider_scroll_scale::SliderScrollScale, track::{AudioAnalysis, Track, TrackId}, - user::UserProfile, + user::{UserProfile, PublicUser}, utils::{Cached, Float64, Image, Page}, }; @@ -135,6 +135,7 @@ impl AppState { made_for_you: Promise::Empty, user_top_tracks: Promise::Empty, user_top_artists: Promise::Empty, + made_for_x_hub: Promise::Empty, }, album_detail: AlbumDetail { album: Promise::Empty, @@ -518,6 +519,15 @@ pub struct HomeDetail { pub made_for_you: Promise>, pub user_top_tracks: Promise>>, pub user_top_artists: Promise>, + pub made_for_x_hub: Promise, +} + +#[derive(Clone, Data, Lens)] +pub struct MixedView { + pub playlists: Vector, + pub artists: Vector, + pub albums: Vector, + pub shows: Vector, } static ALERT_ID: AtomicUsize = AtomicUsize::new(0); diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index c589ef95..864b49cc 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -1,17 +1,18 @@ use std::sync::Arc; use druid::im::Vector; -use druid::widget::{Flex, Label, Scroll}; +use druid::widget::{Either, Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; -use crate::data::{Ctx, HomeDetail, Track, WithCtx}; +use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Playlist, Show, Track, WithCtx}; +use crate::widget::Empty; use crate::{ data::AppState, webapi::WebApi, widget::{Async, MyWidgetExt}, }; -use super::{artist, playable, theme, track}; +use super::{album, artist, playable, show, theme, track}; use super::{ playlist, utils::{error_widget, spinner_widget}, @@ -23,7 +24,7 @@ pub fn home_widget() -> impl Widget { Flex::column() .with_child(Label::new("Made for you").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) .with_default_spacer() - .with_child(made_for_you_widget()) + .with_child(results_widget()) .with_default_spacer() .with_child(Label::new("Your top artists").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) .with_default_spacer() @@ -34,30 +35,93 @@ pub fn home_widget() -> impl Widget { .with_child(user_top_tracks_widget()) } -fn made_for_you_widget() -> impl Widget { +pub fn results_widget() -> impl Widget { Async::new( spinner_widget, - || Scroll::new( - List::new( - || playlist::horizontal_playlist_widget(false, true) - ).horizontal() - ).horizontal(), - // TODO Add a function which allows people to scroll with their scroll wheel!!! + loaded_results_widget, error_widget, ) .lens( Ctx::make( AppState::common_ctx, - AppState::home_detail.then(HomeDetail::made_for_you), + AppState::home_detail.then(HomeDetail::made_for_x_hub), ) .then(Ctx::in_promise()), ) .on_command_async( LOAD_MADE_FOR_YOU, - |_| WebApi::global().get_made_for_you(), - |_, data, d| data.home_detail.made_for_you.defer(d), - |_, data, r| data.home_detail.made_for_you.update(r), + |q| WebApi::global().get_made_for_you(), + |_, data, q| data.home_detail.made_for_x_hub.defer(q), + |_, data, r| data.home_detail.made_for_x_hub.update(r), + ) +} + +fn loaded_results_widget() -> impl Widget> { + Either::new( + |results: &WithCtx, _| { + results.data.artists.is_empty() + && results.data.albums.is_empty() + && results.data.playlists.is_empty() + && results.data.shows.is_empty() + }, + Label::new("No results") + .with_text_size(theme::TEXT_SIZE_LARGE) + .with_text_color(theme::PLACEHOLDER_COLOR) + .padding(theme::grid(6.0)) + .center(), + Flex::column() + .with_child(artist_results_widget()) + .with_child(album_results_widget()) + .with_child(playlist_results_widget()) + .with_child(show_results_widget()), + ) +} +fn artist_results_widget() -> impl Widget> { + Either::new( + |artists: &Vector, _| artists.is_empty(), + Empty, + Flex::column() + .with_child(List::new(artist::recommended_artist_widget)), + ) + .lens(Ctx::data().then(MixedView::artists)) +} + +fn album_results_widget() -> impl Widget> { + Either::new( + |albums: &Vector, _| albums.is_empty(), + Empty, + Flex::column() + .with_child(Label::new("not implemented")), + ) + .lens(Ctx::data().then(MixedView::albums)) +} + +fn playlist_results_widget() -> impl Widget> { + Either::new( + |playlists: &WithCtx, _| playlists.data.playlists.is_empty(), + Empty, + Flex::column() + .with_child( + // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), + // May be nicer + Scroll::new( + List::new( + || playlist::horizontal_playlist_widget(false, true) + ).horizontal() + ).horizontal() + .lens(Ctx::map(MixedView::playlists)), + ), + ) +} + +fn show_results_widget() -> impl Widget> { + Either::new( + |shows: &Vector, _| shows.is_empty(), + Empty, + Flex::column() + .with_child(Label::new("not implemented")), ) + .lens(Ctx::data().then(MixedView::shows)) } fn user_top_artists_widget() -> impl Widget { diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index 1784b8e9..f5784758 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use druid::{ im::Vector, - widget::{CrossAxisAlignment, Either, Flex, Label, LabelText, List, TextBox}, + widget::{CrossAxisAlignment, Either, Flex, Label, LabelText, List, Scroll, TextBox}, Data, LensExt, Selector, Widget, WidgetExt, }; @@ -142,7 +142,14 @@ fn playlist_results_widget() -> impl Widget> { Flex::column() .with_child(header_widget("Playlists")) .with_child( - List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), + // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), + // May be nicer + Scroll::new( + List::new( + || playlist::horizontal_playlist_widget(false, true) + ).horizontal() + ).horizontal() + .lens(Ctx::map(SearchResults::playlists)), ), ) } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 37fbaccf..98d21169 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -8,9 +8,7 @@ use std::{ }; use druid::{ - im::Vector, - image::{self, ImageFormat}, - Data, ImageBuf, + im::Vector, image::{self, ImageFormat}, kurbo::MinDistance, Data, ImageBuf }; use itertools::Itertools; use once_cell::sync::OnceCell; @@ -26,9 +24,7 @@ use psst_core::{ use crate::{ data::{ - Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, - EpisodeLink, Nav, Page, Playlist, Range, Recommendations, RecommendationsRequest, - SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile, + self, library_derived_lenses::playlists, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile }, error::Error, }; @@ -70,43 +66,36 @@ impl WebApi { Ok(token.token) } - fn request(&self, method: &str, path: impl Display) -> Result { + fn build_request(&self, + method: &str, + base_url: &str, + path: impl Display, + ) -> Result { let token = self.access_token()?; let request = self .agent - .request(method, &format!("https://api.spotify.com/{}", path)) + .request(method, &format!("https://{}/{}", base_url, path)) .set("Authorization", &format!("Bearer {}", &token)); Ok(request) } - fn request_mbi(&self, method: &str, path: impl Display) -> Result { - // The token should be in the format - let request = self - .agent - - .request(method, &format!("https://musicbrainz.org/{}", path)); - //.set("Authorization", &format!("Bearer {}", &token)); - Ok(request) - } - - fn get_from_mbi(&self, path: impl Display) -> Result { - self.request_mbi("GET", path) + fn request(&self, method: &str, base_url: &str, path: impl Display) -> Result { + self.build_request(method, base_url, path) } - fn get(&self, path: impl Display) -> Result { - self.request("GET", path) - } + fn get(&self, path: impl Display, base_url: Option<&str>) -> Result { + self.request("GET", base_url.unwrap_or("api.spotify.com"), path) } - fn put(&self, path: impl Display) -> Result { - self.request("PUT", path) + fn put(&self, path: impl Display, base_url: Option<&str>) -> Result { + self.request("GET", base_url.unwrap_or("api.spotify.com"), path) } - fn post(&self, path: impl Display) -> Result { - self.request("POST", path) + fn post(&self, path: impl Display, base_url: Option<&str>) -> Result { + self.request("GET", base_url.unwrap_or("api.spotify.com"), path) } - fn delete(&self, path: impl Display) -> Result { - self.request("DELETE", path) + fn delete(&self, path: impl Display, base_url: Option<&str>) -> Result { + self.request("GET", base_url.unwrap_or("api.spotify.com"), path) } fn with_retry(f: impl Fn() -> Result) -> Result { @@ -287,6 +276,13 @@ impl WebApi { log::error!("failed to read local tracks: {}", err); } } + pub fn transform_section_item_json_map(item: &serde_json::Value) -> Option { + item.get("data") + .and_then(|data| data.get("homeSections")) + .and_then(|home_sections| home_sections.get("sections")) + .and_then(|sections| sections.get(0)) + .cloned() + } } static GLOBAL_WEBAPI: OnceCell> = OnceCell::new(); @@ -309,7 +305,7 @@ impl WebApi { impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-users-profile pub fn get_user_profile(&self) -> Result { - let request = self.get("v1/me")?; + let request = self.get("v1/me", None)?; let result = self.load(request)?; Ok(result) } @@ -317,7 +313,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks // TODO Cache this. pub fn get_user_top_tracks(&self) -> Result>, Error> { - let request = self.get("v1/me/top/tracks")? + let request = self.get("v1/me/top/tracks", None)? .query("market", "from_token"); let result: Vector> = self.load_some_pages(request, 30)?; @@ -332,7 +328,7 @@ impl WebApi { artists: Artist, } - let request = self.get("v1/me/top/artists")?; + let request = self.get("v1/me/top/artists", None)?; Ok(self .load_some_pages(request, 10)? @@ -346,7 +342,7 @@ impl WebApi { impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-artist/ pub fn get_artist(&self, id: &str) -> Result { - let request = self.get(format!("v1/artists/{}", id))?; + let request = self.get(format!("v1/artists/{}", id), None)?; let result = self.load_cached(request, "artist", id)?; Ok(result.data) } @@ -354,7 +350,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-an-artists-albums/ pub fn get_artist_albums(&self, id: &str) -> Result { let request = self - .get(format!("v1/artists/{}/albums", id))? + .get(format!("v1/artists/{}/albums", id), None)? .query("market", "from_token"); let result: Vector> = self.load_all_pages(request)?; @@ -405,7 +401,7 @@ impl WebApi { } let request = self - .get(format!("v1/artists/{}/top-tracks", id))? + .get(format!("v1/artists/{}/top-tracks", id), None)? .query("market", "from_token"); let result: Tracks = self.load(request)?; Ok(result.tracks) @@ -418,40 +414,10 @@ impl WebApi { artists: Vector, } - let request = self.get(format!("v1/artists/{}/related-artists", id))?; + let request = self.get(format!("v1/artists/{}/related-artists", id), None)?; let result: Cached = self.load_cached(request, "related-artists", id)?; Ok(result.map(|result| result.artists)) } - - // https://musicbrainz.org/ (MusicBrainz Section) - // https://beta.musicbrainz.org/ws/2/url/?query=url:https://open.spotify.com/artist/1WZarnZpWEv7dDtjAETt4X - pub fn get_musicbrainz_artist(&self, url: &str) -> Result { - let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; - let result: String = self.load(request)?; - Ok(result) - } - - pub fn get_artist_socials(&self, url: &str) -> Result, Error> { - let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; - let mbi: String = self.load(request)?; - - let scnd_request = self.get_from_mbi(format!("/ws/2/artist/{}?inc=url-rels&fmt=json", mbi))?; - let links: Vector = self.load(scnd_request)?; - - Ok(links) - } - - /* - pub fn get_artist_wiki(&self, url: &str) -> Result { - let request = self.get_from_mbi(format!("ws/2/url/?query=url:{}", url))?; - let mbi: String = self.load(request)?; - - let scnd_request = self.get_from_mbi(format!("/ws/2/artist/{}?inc=url-rels&fmt=json", mbi))?; - let links: Vector = self.load(scnd_request)?; - - Ok(links) - } - */ } /// Album endpoints. @@ -459,7 +425,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-an-album/ pub fn get_album(&self, id: &str) -> Result>, Error> { let request = self - .get(format!("v1/albums/{}", id))? + .get(format!("v1/albums/{}", id), None)? .query("market", "from_token"); let result = self.load_cached(request, "album", id)?; Ok(result) @@ -479,7 +445,7 @@ impl WebApi { } let request = self - .get("v1/episodes")? + .get("v1/episodes", None)? .query("ids", &ids.into_iter().map(|id| id.0.to_base62()).join(",")) .query("market", "from_token"); let result: Episodes = self.load(request)?; @@ -489,7 +455,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-a-shows-episodes pub fn get_show_episodes(&self, id: &str) -> Result>, Error> { let request = self - .get(format!("v1/shows/{}/episodes", id))? + .get(format!("v1/shows/{}/episodes", id), None)? .query("market", "from_token"); let mut results = Vector::new(); @@ -511,7 +477,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-track pub fn get_track(&self, id: &str) -> Result, Error> { let request = self - .get(format!("v1/tracks/{}", id))? + .get(format!("v1/tracks/{}", id), None)? .query("market", "from_token"); let result = self.load(request)?; Ok(result) @@ -527,7 +493,7 @@ impl WebApi { album: Arc, } - let request = self.get("v1/me/albums")?.query("market", "from_token"); + let request = self.get("v1/me/albums", None)?.query("market", "from_token"); Ok(self .load_all_pages(request)? @@ -538,14 +504,14 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/save-albums-user/ pub fn save_album(&self, id: &str) -> Result<(), Error> { - let request = self.put("v1/me/albums")?.query("ids", id); + let request = self.put("v1/me/albums", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } // https://developer.spotify.com/documentation/web-api/reference/remove-albums-user/ pub fn unsave_album(&self, id: &str) -> Result<(), Error> { - let request = self.delete("v1/me/albums")?.query("ids", id); + let request = self.delete("v1/me/albums", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } @@ -557,7 +523,7 @@ impl WebApi { track: Arc, } - let request = self.get("v1/me/tracks")?.query("market", "from_token"); + let request = self.get("v1/me/tracks", None)?.query("market", "from_token"); Ok(self .load_all_pages(request)? @@ -573,7 +539,7 @@ impl WebApi { show: Arc, } - let request = self.get("v1/me/shows")?.query("market", "from_token"); + let request = self.get("v1/me/shows", None)?.query("market", "from_token"); Ok(self .load_all_pages(request)? @@ -584,44 +550,319 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/save-tracks-user/ pub fn save_track(&self, id: &str) -> Result<(), Error> { - let request = self.put("v1/me/tracks")?.query("ids", id); + let request = self.put("v1/me/tracks", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } // https://developer.spotify.com/documentation/web-api/reference/remove-tracks-user/ pub fn unsave_track(&self, id: &str) -> Result<(), Error> { - let request = self.delete("v1/me/tracks")?.query("ids", id); + let request = self.delete("v1/me/tracks", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } // https://developer.spotify.com/documentation/web-api/reference/save-shows-user pub fn save_show(&self, id: &str) -> Result<(), Error> { - let request = self.put("v1/me/shows")?.query("ids", id); + let request = self.put("v1/me/shows", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } // https://developer.spotify.com/documentation/web-api/reference/remove-shows-user pub fn unsave_show(&self, id: &str) -> Result<(), Error> { - let request = self.delete("v1/me/shows")?.query("ids", id); + let request = self.delete("v1/me/shows", None)?.query("ids", id); self.send_empty_json(request)?; Ok(()) } } + +/* +impl WebApi { + // https://developer.spotify.com/documentation/web-api/reference/search/ + pub fn search( + &self, + query: &str, + topics: &[SearchTopic], + limit: usize, + ) -> Result { + #[derive(Deserialize)] + struct ApiSearchResults { + artists: Option>, + albums: Option>>, + tracks: Option>>, + playlists: Option>, + shows: Option>>, + } + + let topics = topics.iter().map(SearchTopic::as_str).join(","); + let request = self + .get("v1/search")? + .query("q", query) + .query("type", &topics) + .query("limit", &limit.to_string()) + .query("marker", "from_token"); + let result: ApiSearchResults = self.load(request)?; + + let artists = result.artists.map_or_else(Vector::new, |page| page.items); + let albums = result.albums.map_or_else(Vector::new, |page| page.items); + let tracks = result.tracks.map_or_else(Vector::new, |page| page.items); + let playlists = result.playlists.map_or_else(Vector::new, |page| page.items); + let shows = result.shows.map_or_else(Vector::new, |page| page.items); + Ok(SearchResults { + query: query.into(), + artists, + albums, + tracks, + playlists, + shows, + }) + } +} +*/ /// View endpoints. impl WebApi { - pub fn get_made_for_you(&self) -> Result, Error> { + pub fn get_made_for_you(&self) -> Result { #[derive(Deserialize)] - struct View { - content: Page, + pub struct Welcome { + data: WelcomeData, + extensions: Extensions, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct WelcomeData { + home_sections: HomeSections, + } + + #[derive(Deserialize)] + pub struct HomeSections { + #[serde(rename = "__typename")] + typename: String, + sections: Vec
, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Section { + #[serde(rename = "__typename")] + typename: String, + data: SectionData, + section_items: SectionItems, + uri: String, + } + + #[derive(Deserialize)] + pub struct SectionData { + #[serde(rename = "__typename")] + typename: String, + subtitle: Subtitle, + title: Title, + } + + #[derive(Deserialize)] + pub struct Subtitle { + text: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Title { + original_label: OriginalLabel, + text: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct OriginalLabel { + text_attributes: TextAttributes, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct TextAttributes { + text_format_arguments: Vec, + } + + #[derive(Deserialize)] + pub struct TextFormatArgument { + uri: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct SectionItems { + items: Vec, + paging_info: PagingInfo, + total_count: i64, + } + + #[derive(Deserialize)] + pub struct SectionItemsItem { + data: Option, + content: Content, + uri: String, + } + + #[derive(Deserialize)] + pub struct Content { + #[serde(rename = "__typename")] + typename: String, + data: ContentData, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ContentData { + // This needs to be variable, based off the type name + #[serde(rename = "__typename")] + typename: String, + attributes: Option>, + description: Option, + format: Option, + images: Option, + name: Option, + owner_v2: Option, + uri: String, + artists: Option, + cover_art: Option, + album_type: Option, + profile: Option, + media_type: Option, + publisher: Option, } + #[derive(Deserialize)] + pub struct Artists { + items: Vec, + } + + #[derive(Deserialize)] + pub struct ArtistsItem { + profile: Profile, + uri: String, + } + + #[derive(Deserialize)] + pub struct Profile { + name: String, + } + + #[derive(Deserialize)] + pub struct Attribute { + key: String, + value: String, + } + + #[derive(Deserialize)] + pub struct Images { + items: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ImagesItem { + extracted_colors: ExtractedColors, + sources: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ExtractedColors { + color_dark: ColorDark, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ColorDark { + hex: String, + is_fallback: bool, + } + + #[derive(Deserialize)] + pub struct Source { + height: Option, + url: String, + width: Option, + } + + #[derive(Deserialize)] + pub struct OwnerV2 { + data: OwnerV2Data, + } + + #[derive(Deserialize)] + pub struct OwnerV2Data { + #[serde(rename = "__typename")] + typename: String, + name: String, + uri: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PagingInfo { + next_offset: Option, + } + + #[derive(Deserialize)] + pub struct Extensions {} + + let request = self.get("pathfinder/v1/query?operationName=homeSection&variables=%7B%22uri%22%3A%22spotify%3Asection%3A0JQ5DAUnp4wcj0bCb3wh3S%22%2C%22timeZone%22%3A%22Europe%2FLondon%22%2C%22sp_t%22%3A%223c7e9795a8ab85165839a5e905d6f10c%22%2C%22country%22%3A%22GB%22%2C%22sectionItemsOffset%22%3A0%2C%22sectionItemsLimit%22%3A20%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%224da53a78e4e98d4f3fa55698af5b751fe05ca3a1a4a526ff8147e8866ccfa49f%22%7D%7D", Some("api-partner.spotify.com"))?; + + // Extract the playlists + let result: Welcome = self.load(request)?; + + let playlist: Vector = result.data.home_sections.sections + .iter() + .flat_map(|section| { + section.section_items.items.iter().map(|item| { + let uri = item.uri.clone(); + let id = uri.split(':').last().unwrap_or("").to_string(); + + Playlist { + id: id.into(), + name: item.content.data.name.clone().unwrap_or_default().into(), + images: item.content.data.images.as_ref().map(|images| + images.items.iter().map(|img| data::utils::Image { + url: img.sources.first().map(|s| s.url.clone()).unwrap_or_default().into(), + width: None, + height: None, + }).collect() + ), + description: item.content.data.description.clone().unwrap_or_default().into(), + track_count: Some(10), + owner: PublicUser { + id: "".into(), + + display_name: item.content.data.owner_v2.as_ref() + .map(|owner| owner.data.name.clone()) + .unwrap_or_default() + .into(), + }, + collaborative: false, + } + }) + }) + .collect(); + + Ok(MixedView { + playlists: playlist, + artists: Vector::new(), + albums: Vector::new(), + shows: Vector::new(), + }) + } + + pub fn podcasts_and_more(&self) -> Result, Error> { + #[derive(Deserialize)] + struct View { + content: Page, + } let request = self - .get("v1/views/made-for-x")? - .query("types", "playlist") + .get("v1/views/podcasts-and-more", None)? + .query("types", "Show") .query("limit", "20") .query("offset", "0"); let result: View = self.load(request)?; @@ -633,26 +874,26 @@ impl WebApi { impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-a-list-of-current-users-playlists pub fn get_playlists(&self) -> Result, Error> { - let request = self.get("v1/me/playlists")?; + let request = self.get("v1/me/playlists", None)?; let result = self.load_all_pages(request)?; Ok(result) } pub fn follow_playlist(&self, id: &str) -> Result<(), Error> { - let request = self.put(format!("v1/playlists/{}/followers", id))?; + let request = self.put(format!("v1/playlists/{}/followers", id), None)?; request.send_json(json!({"public": false,}))?; Ok(()) } pub fn unfollow_playlist(&self, id: &str) -> Result<(), Error> { - let request = self.delete(format!("v1/playlists/{}/followers", id))?; + let request = self.delete(format!("v1/playlists/{}/followers", id), None)?; self.send_empty_json(request)?; Ok(()) } // https://developer.spotify.com/documentation/web-api/reference/get-playlist pub fn get_playlist(&self, id: &str) -> Result { - let request = self.get(format!("v1/me/playlists/{}", id))?; + let request = self.get(format!("v1/me/playlists/{}", id), None)?; let result = self.load(request)?; Ok(result) } @@ -675,7 +916,7 @@ impl WebApi { } let request = self - .get(format!("v1/playlists/{}/tracks", id))? + .get(format!("v1/playlists/{}/tracks", id), None)? .query("marker", "from_token") .query("additional_types", "track"); let result: Vector = self.load_all_pages(request)?; @@ -697,7 +938,7 @@ impl WebApi { } pub fn change_playlist_details(&self, id: &str, name: &str) -> Result<(), Error> { - let request = self.put(format!("v1/playlists/{}", id))?; + let request = self.put(format!("v1/playlists/{}", id), None)?; request.send_json(json!({ "name": name }))?; Ok(()) } @@ -705,7 +946,7 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/add-tracks-to-playlist pub fn add_track_to_playlist(&self, playlist_id: &str, track_uri: &str) -> Result<(), Error> { let request = self - .post(format!("v1/playlists/{}/tracks", playlist_id))? + .post(format!("v1/playlists/{}/tracks", playlist_id), None)? .query("uris", track_uri); self.send_empty_json(request) } @@ -716,7 +957,7 @@ impl WebApi { playlist_id: &str, track_pos: usize, ) -> Result<(), Error> { - self.delete(format!("v1/playlists/{}/tracks", playlist_id))? + self.delete(format!("v1/playlists/{}/tracks", playlist_id), None)? .send_json(ureq::json!({ "positions": [track_pos] }))?; Ok(()) } @@ -742,7 +983,7 @@ impl WebApi { let topics = topics.iter().map(SearchTopic::as_str).join(","); let request = self - .get("v1/search")? + .get("v1/search", None)? .query("q", query) .query("type", &topics) .query("limit", &limit.to_string()) @@ -752,14 +993,14 @@ impl WebApi { let artists = result.artists.map_or_else(Vector::new, |page| page.items); let albums = result.albums.map_or_else(Vector::new, |page| page.items); let tracks = result.tracks.map_or_else(Vector::new, |page| page.items); - let playlists = result.playlists.map_or_else(Vector::new, |page| page.items); + let playlist = result.playlists.map_or_else(Vector::new, |page| page.items); let shows = result.shows.map_or_else(Vector::new, |page| page.items); Ok(SearchResults { query: query.into(), artists, albums, tracks, - playlists, + playlists: playlist, shows, }) } @@ -796,7 +1037,7 @@ impl WebApi { .join(", "); let mut request = self - .get("v1/recommendations")? + .get("v1/recommendations", None)? .query("marker", "from_token") .query("limit", "100") .query("seed_artists", &seed_artists) @@ -839,7 +1080,7 @@ impl WebApi { impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis/ pub fn _get_audio_analysis(&self, track_id: &str) -> Result { - let request = self.get(format!("v1/audio-analysis/{}", track_id))?; + let request = self.get(format!("v1/audio-analysis/{}", track_id), None)?; let result = self.load_cached(request, "audio-analysis", track_id)?; Ok(result.data) } From a075bfb16f355ede8e289c6b3482b4eb5ea2acc8 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 12 Sep 2024 13:49:29 +0100 Subject: [PATCH 05/30] Update to be more dynamic --- psst-gui/src/data/home.rs | 71 ----------------------------------- psst-gui/src/webapi/client.rs | 34 ++++++++++++++--- 2 files changed, 28 insertions(+), 77 deletions(-) delete mode 100644 psst-gui/src/data/home.rs diff --git a/psst-gui/src/data/home.rs b/psst-gui/src/data/home.rs deleted file mode 100644 index 33334e2e..00000000 --- a/psst-gui/src/data/home.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::sync::Arc; - -use druid::{im::Vector, Data, Lens}; -use serde::{Deserialize, Serialize}; - -use crate::data::{Album, Cached, Image, Promise, Track}; - -use super::Playlist; - -#[derive(Clone, Data, Lens)] -pub struct HomeDetail { - pub made_for_you: Promise> - pub top_tracks: Promise, - pub related_artists: Promise>, ArtistLink>, -} - -#[derive(Clone, Data, Lens, Deserialize)] -pub struct Artist { - pub id: Arc, - pub name: Arc, - pub images: Vector, -} - -impl Artist { - pub fn image(&self, width: f64, height: f64) -> Option<&Image> { - Image::at_least_of_size(&self.images, width, height) - } - - pub fn link(&self) -> ArtistLink { - ArtistLink { - id: self.id.clone(), - name: self.name.clone(), - } - } -} - -#[derive(Clone, Data, Lens)] -pub struct ArtistAlbums { - pub albums: Vector>, - pub singles: Vector>, - pub compilations: Vector>, - pub appears_on: Vector>, -} - -#[derive(Clone, Data, Lens)] -pub struct ArtistTracks { - pub id: Arc, - pub name: Arc, - pub tracks: Vector>, -} - -impl ArtistTracks { - pub fn link(&self) -> ArtistLink { - ArtistLink { - id: self.id.clone(), - name: self.name.clone(), - } - } -} - -#[derive(Clone, Debug, Data, Lens, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct ArtistLink { - pub id: Arc, - pub name: Arc, -} - -impl ArtistLink { - pub fn url(&self) -> String { - format!("https://open.spotify.com/artist/{id}", id = self.id) - } -} diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 98d21169..afd9056c 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -13,13 +13,12 @@ use druid::{ use itertools::Itertools; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use serde::{de::DeserializeOwned, Deserialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::json; use ureq::{Agent, Request, Response}; use psst_core::{ - session::{access_token::TokenProvider, SessionService}, - util::default_ureq_agent_builder, + protocol::authentication::APWelcome, session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder }; use crate::{ @@ -808,9 +807,32 @@ impl WebApi { #[derive(Deserialize)] pub struct Extensions {} - - let request = self.get("pathfinder/v1/query?operationName=homeSection&variables=%7B%22uri%22%3A%22spotify%3Asection%3A0JQ5DAUnp4wcj0bCb3wh3S%22%2C%22timeZone%22%3A%22Europe%2FLondon%22%2C%22sp_t%22%3A%223c7e9795a8ab85165839a5e905d6f10c%22%2C%22country%22%3A%22GB%22%2C%22sectionItemsOffset%22%3A0%2C%22sectionItemsLimit%22%3A20%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%224da53a78e4e98d4f3fa55698af5b751fe05ca3a1a4a526ff8147e8866ccfa49f%22%7D%7D", Some("api-partner.spotify.com"))?; - + + let extensions = json!({ + "persistedQuery": { + "version": 1, + "sha256Hash": "4da53a78e4e98d4f3fa55698af5b751fe05ca3a1a4a526ff8147e8866ccfa49f" + } + }); + + let variables = json!( { + "uri": "spotify:section:0JQ5DAUnp4wcj0bCb3wh3S", + "timeZone": "Europe/London", + "sp_t": self.access_token()?, // Assuming this returns a Result + "country": "GB", + "sectionItemsOffset": 0, + "sectionItemsLimit": 20, + }); + + let variables_json = serde_json::to_string(&variables)?; + let extensions_json = serde_json::to_string(&extensions)?; + + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &format!("{}", variables_json)) + .query("extensions", &format!("{}", extensions_json)); + + log::info!("Request: {:?}", request); // Extract the playlists let result: Welcome = self.load(request)?; From 47be312f575893ea87b9403ed5482268d53d8575 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 12 Sep 2024 14:12:56 +0100 Subject: [PATCH 06/30] Rearrange & Lint --- psst-gui/src/ui/home.rs | 8 +- psst-gui/src/webapi/client.rs | 489 +++++++++++++++++----------------- 2 files changed, 255 insertions(+), 242 deletions(-) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 864b49cc..25bd928e 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -4,7 +4,7 @@ use druid::im::Vector; use druid::widget::{Either, Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; -use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Playlist, Show, Track, WithCtx}; +use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Show, Track, WithCtx}; use crate::widget::Empty; use crate::{ data::AppState, @@ -12,7 +12,7 @@ use crate::{ widget::{Async, MyWidgetExt}, }; -use super::{album, artist, playable, show, theme, track}; +use super::{artist, playable, theme, track}; use super::{ playlist, utils::{error_widget, spinner_widget}, @@ -129,7 +129,7 @@ fn user_top_artists_widget() -> impl Widget { spinner_widget, || Scroll::new( List::new( - || artist::horizontal_recommended_artist_widget() + artist::horizontal_recommended_artist_widget ).horizontal() // TODO Add a function which allows people to scroll with their scroll wheel!!! ).horizontal(), @@ -162,7 +162,7 @@ fn top_tracks_widget() -> impl Widget>>> { fn user_top_tracks_widget() -> impl Widget { Async::new( spinner_widget, - || top_tracks_widget(), + top_tracks_widget, error_widget, ) .lens( diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index afd9056c..f313384d 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -8,22 +8,22 @@ use std::{ }; use druid::{ - im::Vector, image::{self, ImageFormat}, kurbo::MinDistance, Data, ImageBuf + im::Vector, image::{self, ImageFormat}, Data, ImageBuf }; use itertools::Itertools; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize}; use serde_json::json; use ureq::{Agent, Request, Response}; use psst_core::{ - protocol::authentication::APWelcome, session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder + session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder }; use crate::{ data::{ - self, library_derived_lenses::playlists, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile + self, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile }, error::Error, }; @@ -282,6 +282,236 @@ impl WebApi { .and_then(|sections| sections.get(0)) .cloned() } + + fn load_and_return_home_section(&self, request: Request) -> Result { + #[derive(Deserialize)] + pub struct Welcome { + data: WelcomeData, + extensions: Extensions, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct WelcomeData { + home_sections: HomeSections, + } + + #[derive(Deserialize)] + pub struct HomeSections { + #[serde(rename = "__typename")] + typename: String, + sections: Vec
, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Section { + #[serde(rename = "__typename")] + typename: String, + data: SectionData, + section_items: SectionItems, + uri: String, + } + + #[derive(Deserialize)] + pub struct SectionData { + #[serde(rename = "__typename")] + typename: String, + subtitle: Subtitle, + title: Title, + } + + #[derive(Deserialize)] + pub struct Subtitle { + text: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Title { + original_label: OriginalLabel, + text: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct OriginalLabel { + text_attributes: TextAttributes, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct TextAttributes { + text_format_arguments: Vec, + } + + #[derive(Deserialize)] + pub struct TextFormatArgument { + uri: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct SectionItems { + items: Vec, + paging_info: PagingInfo, + total_count: i64, + } + + #[derive(Deserialize)] + pub struct SectionItemsItem { + data: Option, + content: Content, + uri: String, + } + + #[derive(Deserialize)] + pub struct Content { + #[serde(rename = "__typename")] + typename: String, + data: ContentData, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ContentData { + // This needs to be variable, based off the type name + #[serde(rename = "__typename")] + typename: String, + attributes: Option>, + description: Option, + format: Option, + images: Option, + name: Option, + owner_v2: Option, + uri: String, + artists: Option, + cover_art: Option, + album_type: Option, + profile: Option, + media_type: Option, + publisher: Option, + } + + #[derive(Deserialize)] + pub struct Artists { + items: Vec, + } + + #[derive(Deserialize)] + pub struct ArtistsItem { + profile: Profile, + uri: String, + } + + #[derive(Deserialize)] + pub struct Profile { + name: String, + } + + #[derive(Deserialize)] + pub struct Attribute { + key: String, + value: String, + } + + #[derive(Deserialize)] + pub struct Images { + items: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ImagesItem { + extracted_colors: ExtractedColors, + sources: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ExtractedColors { + color_dark: ColorDark, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ColorDark { + hex: String, + is_fallback: bool, + } + + #[derive(Deserialize)] + pub struct Source { + height: Option, + url: String, + width: Option, + } + + #[derive(Deserialize)] + pub struct OwnerV2 { + data: OwnerV2Data, + } + + #[derive(Deserialize)] + pub struct OwnerV2Data { + #[serde(rename = "__typename")] + typename: String, + name: String, + uri: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PagingInfo { + next_offset: Option, + } + + #[derive(Deserialize)] + pub struct Extensions {} + + // Extract the playlists + let result: Welcome = self.load(request)?; + + let playlist: Vector = result.data.home_sections.sections + .iter() + .flat_map(|section| { + section.section_items.items.iter().map(|item| { + let uri = item.uri.clone(); + let id = uri.split(':').last().unwrap_or("").to_string(); + + Playlist { + id: id.into(), + name: item.content.data.name.clone().unwrap_or_default().into(), + images: item.content.data.images.as_ref().map(|images| + images.items.iter().map(|img| data::utils::Image { + url: img.sources.first().map(|s| s.url.clone()).unwrap_or_default().into(), + width: None, + height: None, + }).collect() + ), + description: item.content.data.description.clone().unwrap_or_default().into(), + track_count: Some(10), + owner: PublicUser { + id: "".into(), + + display_name: item.content.data.owner_v2.as_ref() + .map(|owner| owner.data.name.clone()) + .unwrap_or_default() + .into(), + }, + collaborative: false, + } + }) + }) + .collect(); + + Ok(MixedView { + playlists: playlist, + artists: Vector::new(), + albums: Vector::new(), + shows: Vector::new(), + }) + } } static GLOBAL_WEBAPI: OnceCell> = OnceCell::new(); @@ -622,192 +852,7 @@ impl WebApi { */ /// View endpoints. impl WebApi { - pub fn get_made_for_you(&self) -> Result { - #[derive(Deserialize)] - pub struct Welcome { - data: WelcomeData, - extensions: Extensions, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct WelcomeData { - home_sections: HomeSections, - } - - #[derive(Deserialize)] - pub struct HomeSections { - #[serde(rename = "__typename")] - typename: String, - sections: Vec
, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Section { - #[serde(rename = "__typename")] - typename: String, - data: SectionData, - section_items: SectionItems, - uri: String, - } - - #[derive(Deserialize)] - pub struct SectionData { - #[serde(rename = "__typename")] - typename: String, - subtitle: Subtitle, - title: Title, - } - - #[derive(Deserialize)] - pub struct Subtitle { - text: String, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Title { - original_label: OriginalLabel, - text: String, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct OriginalLabel { - text_attributes: TextAttributes, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct TextAttributes { - text_format_arguments: Vec, - } - - #[derive(Deserialize)] - pub struct TextFormatArgument { - uri: Option, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct SectionItems { - items: Vec, - paging_info: PagingInfo, - total_count: i64, - } - - #[derive(Deserialize)] - pub struct SectionItemsItem { - data: Option, - content: Content, - uri: String, - } - - #[derive(Deserialize)] - pub struct Content { - #[serde(rename = "__typename")] - typename: String, - data: ContentData, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ContentData { - // This needs to be variable, based off the type name - #[serde(rename = "__typename")] - typename: String, - attributes: Option>, - description: Option, - format: Option, - images: Option, - name: Option, - owner_v2: Option, - uri: String, - artists: Option, - cover_art: Option, - album_type: Option, - profile: Option, - media_type: Option, - publisher: Option, - } - - #[derive(Deserialize)] - pub struct Artists { - items: Vec, - } - - #[derive(Deserialize)] - pub struct ArtistsItem { - profile: Profile, - uri: String, - } - - #[derive(Deserialize)] - pub struct Profile { - name: String, - } - - #[derive(Deserialize)] - pub struct Attribute { - key: String, - value: String, - } - - #[derive(Deserialize)] - pub struct Images { - items: Vec, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ImagesItem { - extracted_colors: ExtractedColors, - sources: Vec, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ExtractedColors { - color_dark: ColorDark, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ColorDark { - hex: String, - is_fallback: bool, - } - - #[derive(Deserialize)] - pub struct Source { - height: Option, - url: String, - width: Option, - } - - #[derive(Deserialize)] - pub struct OwnerV2 { - data: OwnerV2Data, - } - - #[derive(Deserialize)] - pub struct OwnerV2Data { - #[serde(rename = "__typename")] - typename: String, - name: String, - uri: String, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PagingInfo { - next_offset: Option, - } - - #[derive(Deserialize)] - pub struct Extensions {} - + fn build_home_request(&self, section_uri: &str) -> (String, String) { let extensions = json!({ "persistedQuery": { "version": 1, @@ -816,65 +861,33 @@ impl WebApi { }); let variables = json!( { - "uri": "spotify:section:0JQ5DAUnp4wcj0bCb3wh3S", + "uri": section_uri, "timeZone": "Europe/London", - "sp_t": self.access_token()?, // Assuming this returns a Result + "sp_t": self.access_token().unwrap(), // Assuming this returns a Result "country": "GB", "sectionItemsOffset": 0, "sectionItemsLimit": 20, }); - let variables_json = serde_json::to_string(&variables)?; - let extensions_json = serde_json::to_string(&extensions)?; + let variables_json = serde_json::to_string(&variables); + let extensions_json = serde_json::to_string(&extensions); + (variables_json.unwrap(), extensions_json.unwrap()) + } + + pub fn get_made_for_you(&self) -> Result { + // 0JQ5DAUnp4wcj0bCb3wh3S -> Daily mixes + // 0JQ5DAnM3wGh0gz1MXnu89 -> Top mixes + let json_query = self.build_home_request("spotify:section:0JQ5DAUnp4wcj0bCb3wh3S"); let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") - .query("variables", &format!("{}", variables_json)) - .query("extensions", &format!("{}", extensions_json)); + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); - log::info!("Request: {:?}", request); // Extract the playlists - let result: Welcome = self.load(request)?; - - let playlist: Vector = result.data.home_sections.sections - .iter() - .flat_map(|section| { - section.section_items.items.iter().map(|item| { - let uri = item.uri.clone(); - let id = uri.split(':').last().unwrap_or("").to_string(); - - Playlist { - id: id.into(), - name: item.content.data.name.clone().unwrap_or_default().into(), - images: item.content.data.images.as_ref().map(|images| - images.items.iter().map(|img| data::utils::Image { - url: img.sources.first().map(|s| s.url.clone()).unwrap_or_default().into(), - width: None, - height: None, - }).collect() - ), - description: item.content.data.description.clone().unwrap_or_default().into(), - track_count: Some(10), - owner: PublicUser { - id: "".into(), - - display_name: item.content.data.owner_v2.as_ref() - .map(|owner| owner.data.name.clone()) - .unwrap_or_default() - .into(), - }, - collaborative: false, - } - }) - }) - .collect(); - - Ok(MixedView { - playlists: playlist, - artists: Vector::new(), - albums: Vector::new(), - shows: Vector::new(), - }) + let result = self.load_and_return_home_section(request)?; + + Ok(result) } pub fn podcasts_and_more(&self) -> Result, Error> { From e69a39cdb7e0c69db8f89b7a28be82463e7bf2f1 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 12 Sep 2024 14:35:17 +0100 Subject: [PATCH 07/30] Update hompage --- psst-gui/src/data/mod.rs | 8 +++++--- psst-gui/src/ui/home.rs | 38 +++++++++++++++++++++++++++++------ psst-gui/src/webapi/client.rs | 30 ++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 76f8067c..a23db90a 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -133,9 +133,10 @@ impl AppState { }, home_detail: HomeDetail { made_for_you: Promise::Empty, + user_top_mixes: Promise::Empty, + jump_back_in: Promise::Empty, user_top_tracks: Promise::Empty, user_top_artists: Promise::Empty, - made_for_x_hub: Promise::Empty, }, album_detail: AlbumDetail { album: Promise::Empty, @@ -516,10 +517,11 @@ pub type WithCtx = Ctx, T>; #[derive(Clone, Data, Lens)] pub struct HomeDetail { - pub made_for_you: Promise>, + pub made_for_you: Promise, + pub user_top_mixes: Promise, + pub jump_back_in: Promise, pub user_top_tracks: Promise>>, pub user_top_artists: Promise>, - pub made_for_x_hub: Promise, } #[derive(Clone, Data, Lens)] diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 25bd928e..52b8a744 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -24,7 +24,11 @@ pub fn home_widget() -> impl Widget { Flex::column() .with_child(Label::new("Made for you").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) .with_default_spacer() - .with_child(results_widget()) + .with_child(made_for_you()) + .with_default_spacer() + .with_child(Label::new("Your top mixes").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_default_spacer() + .with_child(user_top_mixes()) .with_default_spacer() .with_child(Label::new("Your top artists").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) .with_default_spacer() @@ -35,24 +39,46 @@ pub fn home_widget() -> impl Widget { .with_child(user_top_tracks_widget()) } -pub fn results_widget() -> impl Widget { +pub fn made_for_you() -> impl Widget { Async::new( spinner_widget, - loaded_results_widget, + loaded_results_widget.clone(), error_widget, ) .lens( Ctx::make( AppState::common_ctx, - AppState::home_detail.then(HomeDetail::made_for_x_hub), + AppState::home_detail.then(HomeDetail::made_for_you), ) .then(Ctx::in_promise()), ) .on_command_async( LOAD_MADE_FOR_YOU, |q| WebApi::global().get_made_for_you(), - |_, data, q| data.home_detail.made_for_x_hub.defer(q), - |_, data, r| data.home_detail.made_for_x_hub.update(r), + |_, data, q| data.home_detail.made_for_you.defer(q), + |_, data, r| data.home_detail.made_for_you.update(r), + ) +} + +pub fn user_top_mixes() -> impl Widget { + // We need a way to parse HTML + Async::new( + spinner_widget, + loaded_results_widget.clone(), + error_widget, + ) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::user_top_mixes), + ) + .then(Ctx::in_promis_e()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + || WebApi::global().get_top_mixes(), + |_, data, q| data.home_detail.user_top_mixes.defer(q), + |_, data, r| data.home_detail.user_top_mixes.update(r), ) } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index f313384d..b05bc898 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -877,7 +877,6 @@ impl WebApi { pub fn get_made_for_you(&self) -> Result { // 0JQ5DAUnp4wcj0bCb3wh3S -> Daily mixes - // 0JQ5DAnM3wGh0gz1MXnu89 -> Top mixes let json_query = self.build_home_request("spotify:section:0JQ5DAUnp4wcj0bCb3wh3S"); let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") @@ -889,7 +888,36 @@ impl WebApi { Ok(result) } + + pub fn get_top_mixes(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu89 -> Top mixes + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu89"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + + pub fn jump_back_in(&self) -> Result { + // 0JQ5DAIiKWzVFULQfUm85X -> Jump back in + // This is where we need to get the laod and return to return everything!!! + let json_query = self.build_home_request("spotify:section:0JQ5DAIiKWzVFULQfUm85X"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + pub fn podcasts_and_more(&self) -> Result, Error> { #[derive(Deserialize)] struct View { From 3725ecfbed54d9b04742b466713a1639c3d70584 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 12 Sep 2024 14:44:18 +0100 Subject: [PATCH 08/30] Fix --- psst-gui/src/ui/home.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 52b8a744..91cdbbd0 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -54,7 +54,7 @@ pub fn made_for_you() -> impl Widget { ) .on_command_async( LOAD_MADE_FOR_YOU, - |q| WebApi::global().get_made_for_you(), + |_| WebApi::global().get_made_for_you(), |_, data, q| data.home_detail.made_for_you.defer(q), |_, data, r| data.home_detail.made_for_you.update(r), ) @@ -72,11 +72,11 @@ pub fn user_top_mixes() -> impl Widget { AppState::common_ctx, AppState::home_detail.then(HomeDetail::user_top_mixes), ) - .then(Ctx::in_promis_e()), + .then(Ctx::in_promise()), ) .on_command_async( LOAD_MADE_FOR_YOU, - || WebApi::global().get_top_mixes(), + |_| WebApi::global().get_top_mixes(), |_, data, q| data.home_detail.user_top_mixes.defer(q), |_, data, r| data.home_detail.user_top_mixes.update(r), ) From 8db4b7acd54624dfc43ce123df595fbbc9839999 Mon Sep 17 00:00:00 2001 From: Jackson Goode Date: Thu, 12 Sep 2024 11:39:31 -0700 Subject: [PATCH 09/30] Sanitize html in descriptions, truncate to 65 chars, make playlist & artist same height/width --- Cargo.lock | 192 ++++++++++++++++++++++++++++++++++ psst-gui/Cargo.toml | 3 +- psst-gui/src/ui/artist.rs | 14 +-- psst-gui/src/ui/home.rs | 166 ++++++++++++++--------------- psst-gui/src/ui/playlist.rs | 9 +- psst-gui/src/webapi/client.rs | 33 ++++-- 6 files changed, 313 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5c3da16..6ef92630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -2080,6 +2090,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "http" version = "0.2.12" @@ -2601,6 +2625,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "mach" version = "0.3.2" @@ -2628,6 +2658,32 @@ dependencies = [ "libc", ] +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "matches" version = "0.1.10" @@ -3108,6 +3164,63 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "piet" version = "0.6.2" @@ -3298,6 +3411,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3445,6 +3564,7 @@ dependencies = [ "rand", "raw-window-handle", "regex", + "sanitize_html", "serde", "serde_json", "souvlaki", @@ -3863,6 +3983,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize_html" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4383365aa34a33238e8f6c7c1cd87be0ff89cebe6fce9476fc5f78fbf1c80bc0" +dependencies = [ + "html5ever", + "lazy_static", + "markup5ever_rcdom", + "regex", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4061,6 +4193,12 @@ dependencies = [ "quote", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -4150,6 +4288,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4321,6 +4485,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -4755,6 +4930,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_lit" version = "2.0.2" @@ -5355,6 +5536,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "zbus" version = "3.15.2" diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index ed1b752d..3a1f52d2 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -53,6 +53,7 @@ souvlaki = { version = "0.7.3", default-features = false, features = [ "use_zbus", ] } webbrowser = { version = "1.0.1" } +sanitize_html = "0.8.1" [target.'cfg(windows)'.build-dependencies] winres = { version = "0.1.12" } image = { version = "0.25.2" } @@ -68,4 +69,4 @@ category = "Music" short_description = "Fast Spotify client with native GUI" long_description = """ Small and efficient graphical music player for Spotify network. -""" \ No newline at end of file +""" diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index b5f5f1df..f8cce5bb 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -96,7 +96,7 @@ fn async_related_widget() -> impl Widget { } pub fn artist_widget() -> impl Widget { - let artist_image = artist_cover_widget(theme::grid(21.0)); + let artist_image = artist_cover_widget(theme::grid(18.0)); Flex::row() .with_child(artist_image) .with_child( @@ -105,14 +105,14 @@ pub fn artist_widget() -> impl Widget { "Coldplay are a British rock band formed in London in 1997, consisting of vocalist and pianist Chris Martin, lead guitarist Jonny Buckland, bassist Guy Berryman, drummer and percussionist Will Champion, and manager Phil Harvey. They are best known for their live performances, having also impacted popular culture with their artistry, advocacy and achievements. \n The members of the band initially met at University College London, calling themselves Big Fat Noises and changing to Starfish, before settling on the current name. After releasing Safety (1998) independently, Coldplay signed with Parlophone in 1999 and wrote their debut album, Parachutes (2000). It featured breakthrough single \"Yellow\" and received a Brit Award for British Album of the Year and a Grammy Award for Best Alternative Music Album. The group's follow-up, A Rush of Blood to the Head (2002), won the same accolades. X&Y (2005) later saw the completion of what they considered a trilogy, being nominated for Best Rock Album as well. Its successor, Viva la Vida or Death and All His Friends (2008), prevailed in the category. Both albums were the best-selling of their years, topping the charts in over 30 countries. Viva la Vida's title track also became the first British act single to lead the Billboard Hot 100 and UK Singles Chart simultaneously in the 21st century." ) .with_line_break_mode(LineBreaking::WordWrap) - .fix_width(theme::grid(35.0)) + .fix_width(theme::grid(18.0)) ) - .fix_size(theme::grid(35.0), theme::grid(21.0)) + .fix_size(theme::grid(20.0), theme::grid(20.0)) ) .context_menu(|artist| artist_menu(&artist.link())) } pub fn horizontal_recommended_artist_widget() -> impl Widget { - let artist_image = cover_widget(theme::grid(21.0)); + let artist_image = cover_widget(theme::grid(16.0)); let artist_label = Label::raw() .with_font(theme::UI_FONT_MEDIUM) .lens(Artist::name); @@ -121,8 +121,10 @@ pub fn horizontal_recommended_artist_widget() -> impl Widget { .with_default_spacer() .with_child(artist_label); artist - .padding(theme::grid(0.5)) + .padding(theme::grid(1.0)) + .fix_width(theme::grid(20.0)) .link() + .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, artist, _| { ctx.submit_command(cmd::NAVIGATE.with(Nav::ArtistDetail(artist.link()))); }) @@ -227,4 +229,4 @@ fn artist_menu(artist: &ArtistLink) -> Menu { ); menu -} +} \ No newline at end of file diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 91cdbbd0..35497937 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -22,64 +22,76 @@ pub const LOAD_MADE_FOR_YOU: Selector = Selector::new("app.home.load-made-for-yo pub fn home_widget() -> impl Widget { Flex::column() - .with_child(Label::new("Made for you").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_child( + Label::new("Made for you") + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), + ) .with_default_spacer() .with_child(made_for_you()) .with_default_spacer() - .with_child(Label::new("Your top mixes").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_child( + Label::new("Your top mixes") + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), + ) .with_default_spacer() .with_child(user_top_mixes()) .with_default_spacer() - .with_child(Label::new("Your top artists").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_child( + Label::new("Your top artists") + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), + ) .with_default_spacer() .with_child(user_top_artists_widget()) .with_default_spacer() - .with_child(Label::new("Your top tracks").with_text_size(theme::grid(2.5)).align_left().padding((theme::grid(1.5), 0.0))) + .with_child( + Label::new("Your top tracks") + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), + ) .with_default_spacer() .with_child(user_top_tracks_widget()) } pub fn made_for_you() -> impl Widget { - Async::new( - spinner_widget, - loaded_results_widget.clone(), - error_widget, - ) - .lens( - Ctx::make( - AppState::common_ctx, - AppState::home_detail.then(HomeDetail::made_for_you), + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::made_for_you), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().get_made_for_you(), + |_, data, q| data.home_detail.made_for_you.defer(q), + |_, data, r| data.home_detail.made_for_you.update(r), ) - .then(Ctx::in_promise()), - ) - .on_command_async( - LOAD_MADE_FOR_YOU, - |_| WebApi::global().get_made_for_you(), - |_, data, q| data.home_detail.made_for_you.defer(q), - |_, data, r| data.home_detail.made_for_you.update(r), - ) } pub fn user_top_mixes() -> impl Widget { // We need a way to parse HTML - Async::new( - spinner_widget, - loaded_results_widget.clone(), - error_widget, - ) - .lens( - Ctx::make( - AppState::common_ctx, - AppState::home_detail.then(HomeDetail::user_top_mixes), + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::user_top_mixes), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().get_top_mixes(), + |_, data, q| data.home_detail.user_top_mixes.defer(q), + |_, data, r| data.home_detail.user_top_mixes.update(r), ) - .then(Ctx::in_promise()), - ) - .on_command_async( - LOAD_MADE_FOR_YOU, - |_| WebApi::global().get_top_mixes(), - |_, data, q| data.home_detail.user_top_mixes.defer(q), - |_, data, r| data.home_detail.user_top_mixes.update(r), - ) } fn loaded_results_widget() -> impl Widget> { @@ -106,8 +118,7 @@ fn artist_results_widget() -> impl Widget> { Either::new( |artists: &Vector, _| artists.is_empty(), Empty, - Flex::column() - .with_child(List::new(artist::recommended_artist_widget)), + Flex::column().with_child(List::new(artist::recommended_artist_widget)), ) .lens(Ctx::data().then(MixedView::artists)) } @@ -116,8 +127,7 @@ fn album_results_widget() -> impl Widget> { Either::new( |albums: &Vector, _| albums.is_empty(), Empty, - Flex::column() - .with_child(Label::new("not implemented")), + Flex::column().with_child(Label::new("not implemented")), ) .lens(Ctx::data().then(MixedView::albums)) } @@ -126,17 +136,15 @@ fn playlist_results_widget() -> impl Widget> { Either::new( |playlists: &WithCtx, _| playlists.data.playlists.is_empty(), Empty, - Flex::column() - .with_child( - // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), - // May be nicer - Scroll::new( - List::new( - || playlist::horizontal_playlist_widget(false, true) - ).horizontal() - ).horizontal() - .lens(Ctx::map(MixedView::playlists)), - ), + Flex::column().with_child( + // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), + // May be nicer + Scroll::new( + List::new(|| playlist::horizontal_playlist_widget(false, true)).horizontal(), + ) + .horizontal() + .lens(Ctx::map(MixedView::playlists)), + ), ) } @@ -144,8 +152,7 @@ fn show_results_widget() -> impl Widget> { Either::new( |shows: &Vector, _| shows.is_empty(), Empty, - Flex::column() - .with_child(Label::new("not implemented")), + Flex::column().with_child(Label::new("not implemented")), ) .lens(Ctx::data().then(MixedView::shows)) } @@ -153,24 +160,21 @@ fn show_results_widget() -> impl Widget> { fn user_top_artists_widget() -> impl Widget { Async::new( spinner_widget, - || Scroll::new( - List::new( - artist::horizontal_recommended_artist_widget - ).horizontal() - // TODO Add a function which allows people to scroll with their scroll wheel!!! - ).horizontal(), + || { + Scroll::new( + List::new(artist::horizontal_recommended_artist_widget).horizontal(), // TODO Add a function which allows people to scroll with their scroll wheel!!! + ) + .horizontal() + }, error_widget, ) - .lens( - AppState::home_detail.then(HomeDetail::user_top_artists) - ) + .lens(AppState::home_detail.then(HomeDetail::user_top_artists)) .on_command_async( LOAD_MADE_FOR_YOU, |_| WebApi::global().get_user_top_artist(), |_, data, d| data.home_detail.user_top_artists.defer(d), |_, data, r| data.home_detail.user_top_artists.update(r), ) - } fn top_tracks_widget() -> impl Widget>>> { @@ -186,22 +190,18 @@ fn top_tracks_widget() -> impl Widget>>> { } fn user_top_tracks_widget() -> impl Widget { - Async::new( - spinner_widget, - top_tracks_widget, - error_widget, - ) - .lens( - Ctx::make( - AppState::common_ctx, - AppState::home_detail.then(HomeDetail::user_top_tracks), + Async::new(spinner_widget, top_tracks_widget, error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::user_top_tracks), + ) + .then(Ctx::in_promise()), ) - .then(Ctx::in_promise()), - ) - .on_command_async( - LOAD_MADE_FOR_YOU, - |_| WebApi::global().get_user_top_tracks(), - |_, data, d| data.home_detail.user_top_tracks.defer(d), - |_, data, r| data.home_detail.user_top_tracks.update(r), - ) -} \ No newline at end of file + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().get_user_top_tracks(), + |_, data, d| data.home_detail.user_top_tracks.defer(d), + |_, data, r| data.home_detail.user_top_tracks.update(r), + ) +} diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index 4ca093c8..c7813ed7 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -318,8 +318,11 @@ fn information_section(title_msg: &str, description_msg: &str) -> impl Widget impl Widget> { - let playlist_image = rounded_cover_widget(theme::grid(21.0)).lens(Ctx::data()); +pub fn horizontal_playlist_widget( + show_name: bool, + show_description: bool, +) -> impl Widget> { + let playlist_image = rounded_cover_widget(theme::grid(16.0)).lens(Ctx::data()); let mut playlist = Flex::column() .with_child(playlist_image) @@ -350,7 +353,7 @@ pub fn horizontal_playlist_widget(show_name: bool, show_description: bool) -> im .on_left_click(|ctx, _, playlist, _| { ctx.submit_command(cmd::NAVIGATE.with(Nav::PlaylistDetail(playlist.data.link()))); }) - .fix_width(theme::grid(25.0)) + .fix_width(theme::grid(20.0)) .context_menu(playlist_menu_ctx) } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index b05bc898..a3184019 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -16,6 +16,8 @@ use parking_lot::Mutex; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::json; use ureq::{Agent, Request, Response}; +use sanitize_html::sanitize_str; +use sanitize_html::rules::predefined::DEFAULT; use psst_core::{ session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder @@ -481,26 +483,35 @@ impl WebApi { Playlist { id: id.into(), - name: item.content.data.name.clone().unwrap_or_default().into(), - images: item.content.data.images.as_ref().map(|images| + name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), + images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| images.items.iter().map(|img| data::utils::Image { - url: img.sources.first().map(|s| s.url.clone()).unwrap_or_default().into(), + url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), width: None, height: None, }).collect() + )), + description: { + let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); + // This is roughly 3 lines of description, truncated if too long + if desc.chars().count() > 65 { + desc.chars().take(62).collect::() + "..." + } else { + desc + }.into() + }, + track_count: item.content.data.attributes.as_ref().and_then(|attrs| + attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) ), - description: item.content.data.description.clone().unwrap_or_default().into(), - track_count: Some(10), owner: PublicUser { - id: "".into(), - + id: Arc::from(""), display_name: item.content.data.owner_v2.as_ref() - .map(|owner| owner.data.name.clone()) - .unwrap_or_default() - .into(), + .map(|owner| Arc::from(owner.data.name.as_str())) + .unwrap_or_else(|| Arc::from("")), }, collaborative: false, } + }) }) .collect(); @@ -1210,4 +1221,4 @@ impl From for Error { fn from(err: image::ImageError) -> Self { Error::WebApiError(err.to_string()) } -} +} \ No newline at end of file From 09d8681ac3320630b45f2a8d297ca8b6531e3915 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Fri, 13 Sep 2024 18:50:34 +0100 Subject: [PATCH 10/30] Grab the homepage things --- .gitignore | 5 +- psst-gui/src/data/mod.rs | 5 + psst-gui/src/ui/home.rs | 80 ++++++++++-- psst-gui/src/webapi/client.rs | 238 +++++++++++++++++++--------------- 4 files changed, 205 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index eeb9b2af..16d663e1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,4 @@ cache .env *.iml rust-toolchain -*.ico -psst-gui/src/ui/artist.rs -psst-gui/src/ui/search.rs -psst-gui/src/webapi/client.rs +*.ico \ No newline at end of file diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index a23db90a..fd452c9c 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -134,6 +134,8 @@ impl AppState { home_detail: HomeDetail { made_for_you: Promise::Empty, user_top_mixes: Promise::Empty, + recommended_stations: Promise::Empty, + uniquely_yours: Promise::Empty, jump_back_in: Promise::Empty, user_top_tracks: Promise::Empty, user_top_artists: Promise::Empty, @@ -519,6 +521,8 @@ pub type WithCtx = Ctx, T>; pub struct HomeDetail { pub made_for_you: Promise, pub user_top_mixes: Promise, + pub recommended_stations: Promise, + pub uniquely_yours: Promise, pub jump_back_in: Promise, pub user_top_tracks: Promise>>, pub user_top_artists: Promise>, @@ -526,6 +530,7 @@ pub struct HomeDetail { #[derive(Clone, Data, Lens)] pub struct MixedView { + pub title: Arc, pub playlists: Vector, pub artists: Vector, pub albums: Vector, diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 35497937..a4c6ec9e 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -4,6 +4,8 @@ use druid::im::Vector; use druid::widget::{Either, Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; +use crate::data::config::authentication_derived_lenses::result; +use crate::data::mixed_view_derived_lenses::title; use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Show, Track, WithCtx}; use crate::widget::Empty; use crate::{ @@ -22,24 +24,18 @@ pub const LOAD_MADE_FOR_YOU: Selector = Selector::new("app.home.load-made-for-yo pub fn home_widget() -> impl Widget { Flex::column() - .with_child( - Label::new("Made for you") - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)), - ) - .with_default_spacer() .with_child(made_for_you()) - .with_default_spacer() + .with_child(user_top_mixes()) + .with_child(recommended_stations()) + // turn this into a function as it is like title_label(title: &str){}!!! .with_child( - Label::new("Your top mixes") + Label::new("Uniquely yours") .with_text_size(theme::grid(2.5)) .align_left() .padding((theme::grid(1.5), 0.0)), ) .with_default_spacer() - .with_child(user_top_mixes()) - .with_default_spacer() + .with_child(uniquely_yours()) .with_child( Label::new("Your top artists") .with_text_size(theme::grid(2.5)) @@ -76,6 +72,40 @@ pub fn made_for_you() -> impl Widget { ) } +pub fn recommended_stations() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::recommended_stations), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().recommended_stations(), + |_, data, q| data.home_detail.recommended_stations.defer(q), + |_, data, r| data.home_detail.recommended_stations.update(r), + ) +} + +pub fn uniquely_yours() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::uniquely_yours), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().uniquely_yours(), + |_, data, q| data.home_detail.uniquely_yours.defer(q), + |_, data, r| data.home_detail.uniquely_yours.update(r), + ) +} + pub fn user_top_mixes() -> impl Widget { // We need a way to parse HTML Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) @@ -108,17 +138,36 @@ fn loaded_results_widget() -> impl Widget> { .padding(theme::grid(6.0)) .center(), Flex::column() + .with_child(title_label()) .with_child(artist_results_widget()) .with_child(album_results_widget()) .with_child(playlist_results_widget()) .with_child(show_results_widget()), ) } + +fn title_label() -> impl Widget> { + Either::new( + |title_check: &Arc, _| title_check.is_empty(), + Empty, + Flex::column() + .with_default_spacer() + .with_child(Label::raw() + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)),) + .with_default_spacer() + .align_left() + ) + .lens(Ctx::data().then(MixedView::title)) +} + fn artist_results_widget() -> impl Widget> { Either::new( |artists: &Vector, _| artists.is_empty(), Empty, - Flex::column().with_child(List::new(artist::recommended_artist_widget)), + Flex::column().with_child(List::new(artist::recommended_artist_widget)) + .align_left(), ) .lens(Ctx::data().then(MixedView::artists)) } @@ -127,7 +176,8 @@ fn album_results_widget() -> impl Widget> { Either::new( |albums: &Vector, _| albums.is_empty(), Empty, - Flex::column().with_child(Label::new("not implemented")), + Flex::column().with_child(Label::new("not implemented")) + .align_left(), ) .lens(Ctx::data().then(MixedView::albums)) } @@ -143,6 +193,7 @@ fn playlist_results_widget() -> impl Widget> { List::new(|| playlist::horizontal_playlist_widget(false, true)).horizontal(), ) .horizontal() + .align_left() .lens(Ctx::map(MixedView::playlists)), ), ) @@ -152,7 +203,8 @@ fn show_results_widget() -> impl Widget> { Either::new( |shows: &Vector, _| shows.is_empty(), Empty, - Flex::column().with_child(Label::new("not implemented")), + Flex::column().with_child(Label::new("not implemented")) + .align_left(), ) .lens(Ctx::data().then(MixedView::shows)) } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index a3184019..c3d999b2 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -25,9 +25,9 @@ use psst_core::{ use crate::{ data::{ - self, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile + self, library_derived_lenses::playlists, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile }, - error::Error, + error::Error, ui::album, }; use super::{cache::WebApiCache, local::LocalTrackManager}; @@ -277,13 +277,6 @@ impl WebApi { log::error!("failed to read local tracks: {}", err); } } - pub fn transform_section_item_json_map(item: &serde_json::Value) -> Option { - item.get("data") - .and_then(|data| data.get("homeSections")) - .and_then(|home_sections| home_sections.get("sections")) - .and_then(|sections| sections.get(0)) - .cloned() - } fn load_and_return_home_section(&self, request: Request) -> Result { #[derive(Deserialize)] @@ -471,59 +464,92 @@ impl WebApi { #[derive(Deserialize)] pub struct Extensions {} - // Extract the playlists - let result: Welcome = self.load(request)?; + // Extract the playlists + let result: Welcome = self.load(request)?; - let playlist: Vector = result.data.home_sections.sections + let mut title: Arc = Arc::from(""); + let mut playlist: Vector = Vector::new(); + let mut album: Vector = Vector::new(); + let mut artist: Vector = Vector::new(); + let mut show: Vector = Vector::new(); + + result.data.home_sections.sections .iter() - .flat_map(|section| { - section.section_items.items.iter().map(|item| { + .for_each(|section| { + title = section.data.title.text.clone().into(); + + section.section_items.items.iter().for_each(|item| { let uri = item.uri.clone(); let id = uri.split(':').last().unwrap_or("").to_string(); - Playlist { - id: id.into(), - name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), - images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| - images.items.iter().map(|img| data::utils::Image { - url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), - width: None, - height: None, - }).collect() - )), - description: { - let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); - // This is roughly 3 lines of description, truncated if too long - if desc.chars().count() > 65 { - desc.chars().take(62).collect::() + "..." - } else { - desc - }.into() - }, - track_count: item.content.data.attributes.as_ref().and_then(|attrs| - attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) - ), - owner: PublicUser { - id: Arc::from(""), - display_name: item.content.data.owner_v2.as_ref() - .map(|owner| Arc::from(owner.data.name.as_str())) - .unwrap_or_else(|| Arc::from("")), - }, - collaborative: false, + if uri.contains("playlist") { + playlist.push_back(Playlist { + id: id.into(), + name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), + images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| + images.items.iter().map(|img| data::utils::Image { + url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), + width: None, + height: None, + }).collect() + )), + description: { + let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); + // This is roughly 3 lines of description, truncated if too long + if desc.chars().count() > 65 { + desc.chars().take(62).collect::() + "..." + } else { + desc + }.into() + }, + track_count: item.content.data.attributes.as_ref().and_then(|attrs| + attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) + ), + owner: PublicUser { + id: Arc::from(""), + display_name: item.content.data.owner_v2.as_ref() + .map(|owner| Arc::from(owner.data.name.as_str())) + .unwrap_or_else(|| Arc::from("")), + }, + collaborative: false, + }); + } else if uri.contains("episode") { + show.push_back(Show { + /* + - id + - name + - images + - publisher + - description + */ + id: id.into(), + name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), + images: item.content.data.images.as_ref().map_or_else(Vector::new, |images| + images.items.iter().map(|img| data::utils::Image { + url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), + width: None, + height: None, + }).collect() + ), + publisher: item.content.data.publisher.as_ref().map(|p| p.name.clone()).unwrap_or_default().into(), + description: item.content.data.description.as_deref().unwrap_or_default().into(), + }) } - }) - }) - .collect(); + else { + log::info!("adkj"); + } + }); + }); Ok(MixedView { + title: title, playlists: playlist, artists: Vector::new(), albums: Vector::new(), - shows: Vector::new(), + shows: show, }) - } -} + }} static GLOBAL_WEBAPI: OnceCell> = OnceCell::new(); @@ -817,50 +843,6 @@ impl WebApi { } } - -/* -impl WebApi { - // https://developer.spotify.com/documentation/web-api/reference/search/ - pub fn search( - &self, - query: &str, - topics: &[SearchTopic], - limit: usize, - ) -> Result { - #[derive(Deserialize)] - struct ApiSearchResults { - artists: Option>, - albums: Option>>, - tracks: Option>>, - playlists: Option>, - shows: Option>>, - } - - let topics = topics.iter().map(SearchTopic::as_str).join(","); - let request = self - .get("v1/search")? - .query("q", query) - .query("type", &topics) - .query("limit", &limit.to_string()) - .query("marker", "from_token"); - let result: ApiSearchResults = self.load(request)?; - - let artists = result.artists.map_or_else(Vector::new, |page| page.items); - let albums = result.albums.map_or_else(Vector::new, |page| page.items); - let tracks = result.tracks.map_or_else(Vector::new, |page| page.items); - let playlists = result.playlists.map_or_else(Vector::new, |page| page.items); - let shows = result.shows.map_or_else(Vector::new, |page| page.items); - Ok(SearchResults { - query: query.into(), - artists, - albums, - tracks, - playlists, - shows, - }) - } -} -*/ /// View endpoints. impl WebApi { fn build_home_request(&self, section_uri: &str) -> (String, String) { @@ -913,6 +895,49 @@ impl WebApi { Ok(result) } + + pub fn recommended_stations(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu3R -> Recommended stations + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3R"); + + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + + pub fn uniquely_yours(&self) -> Result { + // 0JQ5DAqAJXkJGsa2DyEjKi -> Uniquely yours + let json_query = self.build_home_request("spotify:section:0JQ5DAqAJXkJGsa2DyEjKi"); + + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + + pub fn best_of_artists(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu3n -> Best of artists + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3n"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } pub fn jump_back_in(&self) -> Result { // 0JQ5DAIiKWzVFULQfUm85X -> Jump back in @@ -929,19 +954,22 @@ impl WebApi { Ok(result) } - pub fn podcasts_and_more(&self) -> Result, Error> { - #[derive(Deserialize)] - struct View { - content: Page, - } - let request = self - .get("v1/views/podcasts-and-more", None)? - .query("types", "Show") - .query("limit", "20") - .query("offset", "0"); - let result: View = self.load(request)?; - Ok(result.content.items) + /* + pub fn podcasts_and_more(&self) -> Result, Error> { + // 0JQ5DAnM3wGh0gz1MXnu9e -> Jump back in + // This is where we need to get the laod and return to return everything!!! + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu9e"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result.episodes) } + */ } /// Playlist endpoints. From d628e8d17fab8ff068ba441348f1852e2333c418 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Sat, 14 Sep 2024 17:43:46 +0100 Subject: [PATCH 11/30] Grab shows and show them, next to get is artist, album, new_episodes and episodes for you! Also remove changes to uneeded places --- psst-gui/src/data/mod.rs | 6 +- psst-gui/src/ui/artist.rs | 46 +---- psst-gui/src/ui/home.rs | 59 +++++-- psst-gui/src/ui/search.rs | 16 +- psst-gui/src/ui/show.rs | 34 ++++ psst-gui/src/webapi/client.rs | 318 +++++++++++++++++++--------------- 6 files changed, 270 insertions(+), 209 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index fd452c9c..eedaf2da 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -135,6 +135,8 @@ impl AppState { made_for_you: Promise::Empty, user_top_mixes: Promise::Empty, recommended_stations: Promise::Empty, + your_shows: Promise::Empty, + shows_that_you_might_like: Promise::Empty, uniquely_yours: Promise::Empty, jump_back_in: Promise::Empty, user_top_tracks: Promise::Empty, @@ -523,6 +525,8 @@ pub struct HomeDetail { pub user_top_mixes: Promise, pub recommended_stations: Promise, pub uniquely_yours: Promise, + pub your_shows: Promise, + pub shows_that_you_might_like: Promise, pub jump_back_in: Promise, pub user_top_tracks: Promise>>, pub user_top_artists: Promise>, @@ -534,7 +538,7 @@ pub struct MixedView { pub playlists: Vector, pub artists: Vector, pub albums: Vector, - pub shows: Vector, + pub shows: Vector>, } static ALERT_ID: AtomicUsize = AtomicUsize::new(0); diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index f8cce5bb..a44ebb03 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -18,26 +18,10 @@ pub const LOAD_DETAIL: Selector = Selector::new("app.artist.load-det pub fn detail_widget() -> impl Widget { Flex::column() - .with_child(async_main_artist_widget()) .with_child(async_top_tracks_widget()) .with_child(async_albums_widget().padding((theme::grid(1.0), 0.0))) .with_child(async_related_widget().padding((theme::grid(1.0), 0.0))) } -//WebApi::global().get_musicbrainz_artist(&d.url()), -fn async_main_artist_widget() -> impl Widget { - Async::new( - utils::spinner_widget, - artist_widget, - utils::error_widget, - ) - .lens(AppState::artist_detail.then(ArtistDetail::artist)) - .on_command_async( - LOAD_DETAIL, - |d| WebApi::global().get_artist(&d.id), - |_, data, d| data.artist_detail.artist.defer(d), - |_, data, r| data.artist_detail.artist.update(r), - ) -} fn async_top_tracks_widget() -> impl Widget { Async::new( @@ -95,23 +79,7 @@ fn async_related_widget() -> impl Widget { ) } -pub fn artist_widget() -> impl Widget { - let artist_image = artist_cover_widget(theme::grid(18.0)); - Flex::row() - .with_child(artist_image) - .with_child( - Scroll::new( - Label::new( - "Coldplay are a British rock band formed in London in 1997, consisting of vocalist and pianist Chris Martin, lead guitarist Jonny Buckland, bassist Guy Berryman, drummer and percussionist Will Champion, and manager Phil Harvey. They are best known for their live performances, having also impacted popular culture with their artistry, advocacy and achievements. \n The members of the band initially met at University College London, calling themselves Big Fat Noises and changing to Starfish, before settling on the current name. After releasing Safety (1998) independently, Coldplay signed with Parlophone in 1999 and wrote their debut album, Parachutes (2000). It featured breakthrough single \"Yellow\" and received a Brit Award for British Album of the Year and a Grammy Award for Best Alternative Music Album. The group's follow-up, A Rush of Blood to the Head (2002), won the same accolades. X&Y (2005) later saw the completion of what they considered a trilogy, being nominated for Best Rock Album as well. Its successor, Viva la Vida or Death and All His Friends (2008), prevailed in the category. Both albums were the best-selling of their years, topping the charts in over 30 countries. Viva la Vida's title track also became the first British act single to lead the Billboard Hot 100 and UK Singles Chart simultaneously in the 21st century." - ) - .with_line_break_mode(LineBreaking::WordWrap) - .fix_width(theme::grid(18.0)) - ) - .fix_size(theme::grid(20.0), theme::grid(20.0)) - ) - .context_menu(|artist| artist_menu(&artist.link())) -} -pub fn horizontal_recommended_artist_widget() -> impl Widget { +pub fn horizontal_artist_widget() -> impl Widget { let artist_image = cover_widget(theme::grid(16.0)); let artist_label = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -130,7 +98,7 @@ pub fn horizontal_recommended_artist_widget() -> impl Widget { }) .context_menu(|artist| artist_menu(&artist.link())) } -pub fn recommended_artist_widget() -> impl Widget { +pub fn artist_widget() -> impl Widget { let artist_image = cover_widget(theme::grid(7.0)); let artist_label = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -160,14 +128,6 @@ pub fn link_widget() -> impl Widget { .context_menu(artist_menu) } -pub fn artist_cover_widget(size: f64) -> impl Widget { - RemoteImage::new(utils::placeholder_widget(), move |artist: &Artist, _| { - artist.image(size, size).map(|image| image.url.clone()) - }) - .fix_size(size, size) - .clip(Size::new(size, size).to_rounded_rect(4.0)) -} - pub fn cover_widget(size: f64) -> impl Widget { let radius = size / 2.0; RemoteImage::new(utils::placeholder_widget(), move |artist: &Artist, _| { @@ -206,7 +166,7 @@ fn related_widget() -> impl Widget>> { Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(header_widget("Related Artists")) - .with_child(List::new(recommended_artist_widget)) + .with_child(List::new(artist_widget)) .lens(Cached::data) } diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index a4c6ec9e..b532489c 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -4,8 +4,6 @@ use druid::im::Vector; use druid::widget::{Either, Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; -use crate::data::config::authentication_derived_lenses::result; -use crate::data::mixed_view_derived_lenses::title; use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Show, Track, WithCtx}; use crate::widget::Empty; use crate::{ @@ -14,7 +12,7 @@ use crate::{ widget::{Async, MyWidgetExt}, }; -use super::{artist, playable, theme, track}; +use super::{artist, playable, show, theme, track}; use super::{ playlist, utils::{error_widget, spinner_widget}, @@ -36,6 +34,8 @@ pub fn home_widget() -> impl Widget { ) .with_default_spacer() .with_child(uniquely_yours()) + .with_child(your_shows()) + .with_child(shows_that_you_might_like()) .with_child( Label::new("Your top artists") .with_text_size(theme::grid(2.5)) @@ -107,7 +107,6 @@ pub fn uniquely_yours() -> impl Widget { } pub fn user_top_mixes() -> impl Widget { - // We need a way to parse HTML Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) .lens( Ctx::make( @@ -124,6 +123,40 @@ pub fn user_top_mixes() -> impl Widget { ) } +pub fn your_shows() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::your_shows), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().your_shows(), + |_, data, q| data.home_detail.your_shows.defer(q), + |_, data, r| data.home_detail.your_shows.update(r), + ) +} + +pub fn shows_that_you_might_like() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::shows_that_you_might_like), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().shows_that_you_might_like(), + |_, data, q| data.home_detail.shows_that_you_might_like.defer(q), + |_, data, r| data.home_detail.shows_that_you_might_like.update(r), + ) +} + fn loaded_results_widget() -> impl Widget> { Either::new( |results: &WithCtx, _| { @@ -166,7 +199,7 @@ fn artist_results_widget() -> impl Widget> { Either::new( |artists: &Vector, _| artists.is_empty(), Empty, - Flex::column().with_child(List::new(artist::recommended_artist_widget)) + Flex::column().with_child(List::new(artist::artist_widget)) .align_left(), ) .lens(Ctx::data().then(MixedView::artists)) @@ -187,8 +220,6 @@ fn playlist_results_widget() -> impl Widget> { |playlists: &WithCtx, _| playlists.data.playlists.is_empty(), Empty, Flex::column().with_child( - // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), - // May be nicer Scroll::new( List::new(|| playlist::horizontal_playlist_widget(false, true)).horizontal(), ) @@ -201,12 +232,16 @@ fn playlist_results_widget() -> impl Widget> { fn show_results_widget() -> impl Widget> { Either::new( - |shows: &Vector, _| shows.is_empty(), + |shows: &WithCtx>>, _| shows.data.is_empty(), Empty, - Flex::column().with_child(Label::new("not implemented")) - .align_left(), + Flex::column().with_child( + Scroll::new( + List::new(|| show::horizontal_show_widget()).horizontal(), + ) + .align_left() + ), ) - .lens(Ctx::data().then(MixedView::shows)) + .lens(Ctx::map(MixedView::shows)) } fn user_top_artists_widget() -> impl Widget { @@ -214,7 +249,7 @@ fn user_top_artists_widget() -> impl Widget { spinner_widget, || { Scroll::new( - List::new(artist::horizontal_recommended_artist_widget).horizontal(), // TODO Add a function which allows people to scroll with their scroll wheel!!! + List::new(artist::horizontal_artist_widget).horizontal(), // TODO Add a function which allows people to scroll with their scroll wheel!!! ) .horizontal() }, diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index f5784758..4627b3c9 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use druid::{ im::Vector, - widget::{CrossAxisAlignment, Either, Flex, Label, LabelText, List, Scroll, TextBox}, + widget::{CrossAxisAlignment, Either, Flex, Label, LabelText, List, TextBox}, Data, LensExt, Selector, Widget, WidgetExt, }; @@ -10,8 +10,7 @@ use crate::{ cmd, controller::InputController, data::{ - Album, AppState, Artist, Ctx, Nav, Search, SearchResults, SearchTopic, Show, SpotifyUrl, - WithCtx, + Album, AppState, Artist, Ctx, Nav, Search, SearchResults, SearchTopic, Show, SpotifyUrl, WithCtx }, ui::show, webapi::WebApi, @@ -101,7 +100,7 @@ fn artist_results_widget() -> impl Widget> { Empty, Flex::column() .with_child(header_widget("Artists")) - .with_child(List::new(artist::recommended_artist_widget)), + .with_child(List::new(artist::artist_widget)), ) .lens(Ctx::data().then(SearchResults::artists)) } @@ -142,14 +141,7 @@ fn playlist_results_widget() -> impl Widget> { Flex::column() .with_child(header_widget("Playlists")) .with_child( - // List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), - // May be nicer - Scroll::new( - List::new( - || playlist::horizontal_playlist_widget(false, true) - ).horizontal() - ).horizontal() - .lens(Ctx::map(SearchResults::playlists)), + List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), ), ) } diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index 653ed4a3..20119454 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -76,6 +76,40 @@ fn async_episodes_widget() -> impl Widget { ) } +pub fn horizontal_show_widget() -> impl Widget>> { + let show_image = rounded_cover_widget(theme::grid(16.0)); + + let show_name = Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::Clip) + .lens(Show::name.in_arc()); + + let show_publisher = Label::raw() + .with_text_size(theme::TEXT_SIZE_SMALL) + .with_text_color(theme::PLACEHOLDER_COLOR) + .lens(Show::publisher.in_arc()); + + let show_info = Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(show_name) + .with_spacer(1.0) + .with_child(show_publisher); + + let show = Flex::column() + .with_child(show_image) + .with_default_spacer() + .with_child(show_info) + .lens(Ctx::data()); + + show.padding(theme::grid(1.0)) + .link() + .on_left_click(|ctx, _, show, _| { + ctx.submit_command(cmd::NAVIGATE.with(Nav::ShowDetail(show.data.link()))); + }) + .fix_width(theme::grid(20.0)) + .context_menu(show_ctx_menu) +} + pub fn show_widget() -> impl Widget>> { let show_image = rounded_cover_widget(theme::grid(6.0)); diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index c3d999b2..791fd5a6 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -1,10 +1,5 @@ use std::{ - fmt::Display, - io::{self, Read}, - path::PathBuf, - sync::Arc, - thread, - time::Duration, + fmt::Display, io::{self, Read}, path::PathBuf, string, sync::Arc, thread, time::Duration }; use druid::{ @@ -279,190 +274,194 @@ impl WebApi { } fn load_and_return_home_section(&self, request: Request) -> Result { - #[derive(Deserialize)] + use serde::{Serialize, Deserialize}; + + #[derive(Serialize, Deserialize)] pub struct Welcome { data: WelcomeData, - extensions: Extensions, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WelcomeData { home_sections: HomeSections, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct HomeSections { - #[serde(rename = "__typename")] - typename: String, sections: Vec
, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Section { - #[serde(rename = "__typename")] - typename: String, data: SectionData, section_items: SectionItems, - uri: String, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct SectionData { - #[serde(rename = "__typename")] - typename: String, - subtitle: Subtitle, + subtitle: Option, title: Title, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct Subtitle { text: String, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Title { - original_label: OriginalLabel, + original_label: Option, text: String, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OriginalLabel { text_attributes: TextAttributes, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextAttributes { - text_format_arguments: Vec, + text_format_arguments: Vec>, } - - #[derive(Deserialize)] - pub struct TextFormatArgument { - uri: Option, - } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SectionItems { - items: Vec, + items: Vec, paging_info: PagingInfo, total_count: i64, } - - #[derive(Deserialize)] - pub struct SectionItemsItem { + + #[derive(Serialize, Deserialize)] + pub struct Item { data: Option, content: Content, uri: String, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct Content { #[serde(rename = "__typename")] - typename: String, + typename: ContentTypename, data: ContentData, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContentData { - // This needs to be variable, based off the type name #[serde(rename = "__typename")] - typename: String, + typename: DataTypename, + cover_art: Option, + media_type: Option, + name: String, + publisher: Option, + uri: String, + // Playlist-specific fields attributes: Option>, description: Option, - format: Option, images: Option, - name: Option, owner_v2: Option, - uri: String, - artists: Option, - cover_art: Option, - album_type: Option, - profile: Option, - media_type: Option, - publisher: Option, - } - - #[derive(Deserialize)] - pub struct Artists { - items: Vec, - } - - #[derive(Deserialize)] - pub struct ArtistsItem { - profile: Profile, - uri: String, - } - - #[derive(Deserialize)] - pub struct Profile { - name: String, } - #[derive(Deserialize)] - pub struct Attribute { - key: String, - value: String, - } - - #[derive(Deserialize)] - pub struct Images { - items: Vec, - } - - #[derive(Deserialize)] + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] - pub struct ImagesItem { + pub struct CoverArt { extracted_colors: ExtractedColors, sources: Vec, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExtractedColors { color_dark: ColorDark, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ColorDark { hex: String, is_fallback: bool, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct Source { - height: Option, + height: Option, url: String, - width: Option, + width: Option, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] + pub enum MediaType { + #[serde(rename = "AUDIO")] + Audio, + #[serde(rename = "MIXED")] + Mixed, + } + + #[derive(Serialize, Deserialize)] + pub struct Publisher { + name: String, + } + + #[derive(Serialize, Deserialize)] + pub enum DataTypename { + Podcast, + Playlist, + } + + #[derive(Serialize, Deserialize)] + pub enum ContentTypename { + #[serde(rename = "PodcastOrAudiobookResponseWrapper")] + PodcastOrAudiobookResponseWrapper, + #[serde(rename = "PlaylistResponseWrapper")] + PlaylistResponseWrapper, + } + + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PagingInfo { + next_offset: Option, + } + + #[derive(Serialize, Deserialize)] + pub struct Extensions {} + + // Playlist-specific structures + #[derive(Serialize, Deserialize)] + pub struct Attribute { + key: String, + value: String, + } + + #[derive(Serialize, Deserialize)] + pub struct Images { + items: Vec, + } + + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct ImagesItem { + sources: Vec, + extracted_colors: ExtractedColors, + } + + #[derive(Serialize, Deserialize)] pub struct OwnerV2 { data: OwnerV2Data, } - - #[derive(Deserialize)] + + #[derive(Serialize, Deserialize)] pub struct OwnerV2Data { #[serde(rename = "__typename")] typename: String, name: String, uri: String, } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PagingInfo { - next_offset: Option, - } - - #[derive(Deserialize)] - pub struct Extensions {} // Extract the playlists let result: Welcome = self.load(request)?; @@ -471,7 +470,7 @@ impl WebApi { let mut playlist: Vector = Vector::new(); let mut album: Vector = Vector::new(); let mut artist: Vector = Vector::new(); - let mut show: Vector = Vector::new(); + let mut show: Vector> = Vector::new(); result.data.home_sections.sections .iter() @@ -479,13 +478,13 @@ impl WebApi { title = section.data.title.text.clone().into(); section.section_items.items.iter().for_each(|item| { - let uri = item.uri.clone(); + let uri = item.content.data.uri.clone(); let id = uri.split(':').last().unwrap_or("").to_string(); if uri.contains("playlist") { playlist.push_back(Playlist { id: id.into(), - name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), + name: Arc::from(item.content.data.name.clone()), images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| images.items.iter().map(|img| data::utils::Image { url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), @@ -513,27 +512,20 @@ impl WebApi { }, collaborative: false, }); - } else if uri.contains("episode") { - show.push_back(Show { - /* - - id - - name - - images - - publisher - - description - */ + } else if uri.contains("show") { + show.push_back(Arc::new(Show { id: id.into(), - name: Arc::from(item.content.data.name.as_deref().unwrap_or_default()), - images: item.content.data.images.as_ref().map_or_else(Vector::new, |images| - images.items.iter().map(|img| data::utils::Image { - url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), + name: Arc::from(item.content.data.name.clone()), + images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| + images.sources.iter().map(|src| data::utils::Image { + url: Arc::from(src.url.clone()), width: None, height: None, }).collect() ), - publisher: item.content.data.publisher.as_ref().map(|p| p.name.clone()).unwrap_or_default().into(), - description: item.content.data.description.as_deref().unwrap_or_default().into(), - }) + publisher: Arc::from(item.content.data.publisher.as_ref().unwrap().name.clone()), + description: "".into(), + })) } else { @@ -543,14 +535,14 @@ impl WebApi { }); Ok(MixedView { - title: title, + title, playlists: playlist, artists: Vector::new(), albums: Vector::new(), shows: show, }) - }} - + } +} static GLOBAL_WEBAPI: OnceCell> = OnceCell::new(); /// Global instance. @@ -939,9 +931,9 @@ impl WebApi { Ok(result) } + // Need to make a mix of it! pub fn jump_back_in(&self) -> Result { // 0JQ5DAIiKWzVFULQfUm85X -> Jump back in - // This is where we need to get the laod and return to return everything!!! let json_query = self.build_home_request("spotify:section:0JQ5DAIiKWzVFULQfUm85X"); let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") @@ -953,12 +945,25 @@ impl WebApi { Ok(result) } - - /* - pub fn podcasts_and_more(&self) -> Result, Error> { - // 0JQ5DAnM3wGh0gz1MXnu9e -> Jump back in - // This is where we need to get the laod and return to return everything!!! - let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu9e"); + + // Shows + pub fn your_shows(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu3N -> Your shows + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3N"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + + pub fn shows_that_you_might_like(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu3P -> Shows that you might like + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3P"); let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) @@ -967,7 +972,38 @@ impl WebApi { // Extract the playlists let result = self.load_and_return_home_section(request)?; - Ok(result.episodes) + Ok(result) + } + + /* EPISODES + // Episodes for you, this needs a different thing, mixedview could include this + pub fn new_episodes(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu3K -> New episodes + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3K"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + + Ok(result) + } + + // Episodes for you, this needs a different thing, mixedview could include this + // + pub fn episode_for_you(&self) -> Result { + // 0JQ5DAnM3wGh0gz1MXnu9e -> Episodes for you + let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu9e"); + let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + .query("operationName", "homeSection") + .query("variables", &json_query.0.to_string()) + .query("extensions", &json_query.1.to_string()); + + // Extract the playlists + let result = self.load_and_return_home_section(request)?; + Ok(result) } */ } From 933b2b0789c283ae76677332e588712437fff2a4 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Sat, 14 Sep 2024 19:52:29 +0100 Subject: [PATCH 12/30] Start adding jump back in --- psst-gui/src/ui/home.rs | 28 ++++- psst-gui/src/webapi/client.rs | 229 ++++++++++++++++++++-------------- 2 files changed, 160 insertions(+), 97 deletions(-) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index b532489c..adea77bc 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -34,6 +34,7 @@ pub fn home_widget() -> impl Widget { ) .with_default_spacer() .with_child(uniquely_yours()) + .with_child(jump_back_in()) .with_child(your_shows()) .with_child(shows_that_you_might_like()) .with_child( @@ -140,6 +141,23 @@ pub fn your_shows() -> impl Widget { ) } +pub fn jump_back_in() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::jump_back_in), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().jump_back_in(), + |_, data, q| data.home_detail.jump_back_in.defer(q), + |_, data, r| data.home_detail.jump_back_in.update(r), + ) +} + pub fn shows_that_you_might_like() -> impl Widget { Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) .lens( @@ -199,8 +217,10 @@ fn artist_results_widget() -> impl Widget> { Either::new( |artists: &Vector, _| artists.is_empty(), Empty, - Flex::column().with_child(List::new(artist::artist_widget)) - .align_left(), + Scroll::new( + List::new(artist::horizontal_artist_widget).horizontal(), + ) + .horizontal().align_left(), ) .lens(Ctx::data().then(MixedView::artists)) } @@ -221,7 +241,7 @@ fn playlist_results_widget() -> impl Widget> { Empty, Flex::column().with_child( Scroll::new( - List::new(|| playlist::horizontal_playlist_widget(false, true)).horizontal(), + List::new(|| playlist::horizontal_playlist_widget(true, true)).horizontal(), ) .horizontal() .align_left() @@ -249,7 +269,7 @@ fn user_top_artists_widget() -> impl Widget { spinner_widget, || { Scroll::new( - List::new(artist::horizontal_artist_widget).horizontal(), // TODO Add a function which allows people to scroll with their scroll wheel!!! + List::new(artist::horizontal_artist_widget).horizontal(), ) .horizontal() }, diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 791fd5a6..cbb2d3ee 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -273,63 +273,62 @@ impl WebApi { } } + /// VERY MUCH NEED TO MAKE THE CODE LOOK NICER & MORE CONSISTANT fn load_and_return_home_section(&self, request: Request) -> Result { - use serde::{Serialize, Deserialize}; - - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Welcome { data: WelcomeData, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct WelcomeData { home_sections: HomeSections, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct HomeSections { sections: Vec
, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Section { data: SectionData, section_items: SectionItems, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct SectionData { subtitle: Option, title: Title, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Subtitle { text: String, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Title { original_label: Option, text: String, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct OriginalLabel { text_attributes: TextAttributes, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextAttributes { text_format_arguments: Vec>, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SectionItems { items: Vec, @@ -337,65 +336,102 @@ impl WebApi { total_count: i64, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Item { data: Option, content: Content, uri: String, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Content { #[serde(rename = "__typename")] typename: ContentTypename, data: ContentData, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContentData { #[serde(rename = "__typename")] typename: DataTypename, - cover_art: Option, - media_type: Option, - name: String, - publisher: Option, + name: Option, uri: String, + // Playlist-specific fields attributes: Option>, description: Option, images: Option, owner_v2: Option, + + // Artist-specific fields + artists: Option, + profile: Option, + visuals: Option, + album_type: Option, + + // Show-specific fields + cover_art: Option, + media_type: Option, + publisher: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Visuals { + avatar_image: CoverArt, + } + + #[derive(Deserialize)] + pub struct Artists { + items: Vec, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] + pub struct ArtistsItem { + profile: Profile, + uri: String, + } + + #[derive(Deserialize)] + pub struct Profile { + name: String, + } + + #[derive(Deserialize)] + pub struct Attribute { + key: String, + value: String, + } + + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CoverArt { extracted_colors: ExtractedColors, sources: Vec, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExtractedColors { color_dark: ColorDark, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ColorDark { hex: String, is_fallback: bool, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Source { height: Option, url: String, width: Option, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub enum MediaType { #[serde(rename = "AUDIO")] Audio, @@ -403,59 +439,55 @@ impl WebApi { Mixed, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Publisher { name: String, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub enum DataTypename { Podcast, Playlist, + Artist, + Album, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub enum ContentTypename { #[serde(rename = "PodcastOrAudiobookResponseWrapper")] PodcastOrAudiobookResponseWrapper, #[serde(rename = "PlaylistResponseWrapper")] PlaylistResponseWrapper, + #[serde(rename = "AlbumResponseWrapper")] + AlbumResponseWrapper, + #[serde(rename = "ArtistResponseWrapper")] + ArtistResponseWrapper, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct PagingInfo { next_offset: Option, } - #[derive(Serialize, Deserialize)] - pub struct Extensions {} - - // Playlist-specific structures - #[derive(Serialize, Deserialize)] - pub struct Attribute { - key: String, - value: String, - } - - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct Images { items: Vec, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImagesItem { sources: Vec, extracted_colors: ExtractedColors, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct OwnerV2 { data: OwnerV2Data, } - #[derive(Serialize, Deserialize)] + #[derive(Deserialize)] pub struct OwnerV2Data { #[serde(rename = "__typename")] typename: String, @@ -481,55 +513,68 @@ impl WebApi { let uri = item.content.data.uri.clone(); let id = uri.split(':').last().unwrap_or("").to_string(); - if uri.contains("playlist") { - playlist.push_back(Playlist { - id: id.into(), - name: Arc::from(item.content.data.name.clone()), - images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| - images.items.iter().map(|img| data::utils::Image { - url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), - width: None, - height: None, - }).collect() - )), - description: { - let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); - // This is roughly 3 lines of description, truncated if too long - if desc.chars().count() > 65 { - desc.chars().take(62).collect::() + "..." - } else { - desc - }.into() - }, - track_count: item.content.data.attributes.as_ref().and_then(|attrs| - attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) - ), - owner: PublicUser { - id: Arc::from(""), - display_name: item.content.data.owner_v2.as_ref() - .map(|owner| Arc::from(owner.data.name.as_str())) - .unwrap_or_else(|| Arc::from("")), - }, - collaborative: false, - }); - } else if uri.contains("show") { - show.push_back(Arc::new(Show { - id: id.into(), - name: Arc::from(item.content.data.name.clone()), - images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| - images.sources.iter().map(|src| data::utils::Image { - url: Arc::from(src.url.clone()), - width: None, - height: None, - }).collect() - ), - publisher: Arc::from(item.content.data.publisher.as_ref().unwrap().name.clone()), - description: "".into(), - })) - } - - else { - log::info!("adkj"); + match item.content.data.typename { + DataTypename::Playlist => { + playlist.push_back(Playlist { + id: id.into(), + name: Arc::from(item.content.data.name.clone().unwrap()), + images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| + images.items.iter().map(|img| data::utils::Image { + url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), + width: None, + height: None, + }).collect() + )), + description: { + let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); + // This is roughly 3 lines of description, truncated if too long + if desc.chars().count() > 65 { + desc.chars().take(62).collect::() + "..." + } else { + desc + }.into() + }, + track_count: item.content.data.attributes.as_ref().and_then(|attrs| + attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) + ), + owner: PublicUser { + id: Arc::from(""), + display_name: item.content.data.owner_v2.as_ref() + .map(|owner| Arc::from(owner.data.name.as_str())) + .unwrap_or_else(|| Arc::from("")), + }, + collaborative: false, + }); + }, + DataTypename::Artist => { + artist.push_back(Artist { + id: id.into(), + name: Arc::from(item.content.data.profile.as_ref().unwrap().name.clone()), + images: item.content.data.visuals.as_ref().map_or_else(Vector::new, |images| + images.avatar_image.sources.iter().map(|img| data::utils::Image { + url: Arc::from(img.url.as_str()), + width: None, + height: None, + }).collect() + ), + })}, + + DataTypename::Podcast => { + show.push_back(Arc::new(Show { + id: id.into(), + name: Arc::from(item.content.data.name.clone().unwrap()), + images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| + images.sources.iter().map(|src| data::utils::Image { + url: Arc::from(src.url.clone()), + width: None, + height: None, + }).collect() + ), + publisher: Arc::from(item.content.data.publisher.as_ref().unwrap().name.clone()), + description: "".into(), + })) + }, + _ => {} } }); }); @@ -955,7 +1000,6 @@ impl WebApi { .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); - // Extract the playlists let result = self.load_and_return_home_section(request)?; Ok(result) @@ -969,7 +1013,6 @@ impl WebApi { .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); - // Extract the playlists let result = self.load_and_return_home_section(request)?; Ok(result) From 01712ee101e3d875ea0510f5f0392d0491bd8cbd Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Sat, 14 Sep 2024 20:50:13 +0100 Subject: [PATCH 13/30] Implement the jump back in section, then I NEED to clean up the code! --- psst-gui/src/data/mod.rs | 2 +- psst-gui/src/ui/album.rs | 58 ++++++++++++++++++++++++++++++++++- psst-gui/src/ui/artist.rs | 2 +- psst-gui/src/ui/home.rs | 47 ++++++++++++++++------------ psst-gui/src/webapi/client.rs | 42 +++++++++++++++++++------ 5 files changed, 119 insertions(+), 32 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index eedaf2da..9e09653d 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -537,7 +537,7 @@ pub struct MixedView { pub title: Arc, pub playlists: Vector, pub artists: Vector, - pub albums: Vector, + pub albums: Vector>, pub shows: Vector>, } diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 515549f6..d44742cf 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -99,9 +99,65 @@ fn rounded_cover_widget(size: f64) -> impl Widget> { cover_widget(size).clip(Size::new(size, size).to_rounded_rect(4.0)) } +pub fn horizontal_album_widget() -> impl Widget>> { + let album_cover = rounded_cover_widget(theme::grid(16.0)); + + let album_name = Flex::column() + .with_child( + Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::Clip) + .lens(Album::name.in_arc()), + ) + .with_spacer(theme::grid(0.5)) + .with_child(ViewSwitcher::new( + |album: &Arc, _| album.has_explicit(), + |selector: &bool, _, _| match selector { + true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), + false => Box::new(Flex::column()), + }, + )) + .center(); + + let album_artists = List::new(|| { + Label::raw() + .with_text_size(theme::TEXT_SIZE_SMALL) + .with_line_break_mode(LineBreaking::Clip) + .lens(ArtistLink::name) + }) + .horizontal() + .with_spacing(theme::grid(1.0)) + .lens(Album::artists.in_arc()); + + let album_date = Label::>::dynamic(|album, _| album.release_year()) + .with_text_size(theme::TEXT_SIZE_SMALL) + .with_text_color(theme::PLACEHOLDER_COLOR); + + let album_info = Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(album_name) + .with_spacer(1.0) + .with_child(album_artists); + + let album = Flex::column() + .with_child(album_cover) + .with_default_spacer() + .with_child(album_info) + .padding(theme::grid(1.0)) + .lens(Ctx::data()); + + album + .link() + .rounded(theme::BUTTON_BORDER_RADIUS) + .on_left_click(|ctx, _, album, _| { + ctx.submit_command(cmd::NAVIGATE.with(Nav::AlbumDetail(album.data.link()))); + }) + .context_menu(album_ctx_menu) +} + pub fn album_widget() -> impl Widget>> { let album_cover = rounded_cover_widget(theme::grid(6.0)); - + let album_name = Flex::row() .with_child( Label::raw() diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index a44ebb03..3a709bca 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -1,5 +1,5 @@ use druid::{ - im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List, Scroll}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, Widget, WidgetExt + im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, Widget, WidgetExt }; use crate::{ diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index adea77bc..71fc07d1 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -4,7 +4,7 @@ use druid::im::Vector; use druid::widget::{Either, Flex, Label, Scroll}; use druid::{widget::List, LensExt, Selector, Widget, WidgetExt}; -use crate::data::{Album, Artist, Ctx, HomeDetail, MixedView, Show, Track, WithCtx}; +use crate::data::{Artist, Ctx, HomeDetail, MixedView, Show, Track, WithCtx}; use crate::widget::Empty; use crate::{ data::AppState, @@ -12,7 +12,7 @@ use crate::{ widget::{Async, MyWidgetExt}, }; -use super::{artist, playable, show, theme, track}; +use super::{album, artist, playable, show, theme, track}; use super::{ playlist, utils::{error_widget, spinner_widget}, @@ -57,7 +57,7 @@ pub fn home_widget() -> impl Widget { } pub fn made_for_you() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -74,7 +74,7 @@ pub fn made_for_you() -> impl Widget { } pub fn recommended_stations() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -91,7 +91,7 @@ pub fn recommended_stations() -> impl Widget { } pub fn uniquely_yours() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -108,7 +108,7 @@ pub fn uniquely_yours() -> impl Widget { } pub fn user_top_mixes() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -125,7 +125,7 @@ pub fn user_top_mixes() -> impl Widget { } pub fn your_shows() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -142,7 +142,7 @@ pub fn your_shows() -> impl Widget { } pub fn jump_back_in() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -159,7 +159,7 @@ pub fn jump_back_in() -> impl Widget { } pub fn shows_that_you_might_like() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget.clone(), error_widget) + Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( Ctx::make( AppState::common_ctx, @@ -189,11 +189,14 @@ fn loaded_results_widget() -> impl Widget> { .padding(theme::grid(6.0)) .center(), Flex::column() - .with_child(title_label()) - .with_child(artist_results_widget()) - .with_child(album_results_widget()) - .with_child(playlist_results_widget()) - .with_child(show_results_widget()), + .with_child(title_label()) + .with_child(Scroll::new(Flex::row() + .with_child(artist_results_widget()) + .with_child(album_results_widget()) + .with_child(playlist_results_widget()) + .with_child(show_results_widget())) + .align_left(), + ) ) } @@ -227,12 +230,16 @@ fn artist_results_widget() -> impl Widget> { fn album_results_widget() -> impl Widget> { Either::new( - |albums: &Vector, _| albums.is_empty(), + |playlists: &WithCtx, _| playlists.data.albums.is_empty(), Empty, - Flex::column().with_child(Label::new("not implemented")) - .align_left(), - ) - .lens(Ctx::data().then(MixedView::albums)) + Flex::column().with_child( + Scroll::new( + List::new(album::horizontal_album_widget).horizontal(), + ) + .horizontal() + .align_left() + .lens(Ctx::map(MixedView::albums)), + )) } fn playlist_results_widget() -> impl Widget> { @@ -256,7 +263,7 @@ fn show_results_widget() -> impl Widget> { Empty, Flex::column().with_child( Scroll::new( - List::new(|| show::horizontal_show_widget()).horizontal(), + List::new(show::horizontal_show_widget).horizontal(), ) .align_left() ), diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index cbb2d3ee..350cba81 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -1,5 +1,5 @@ use std::{ - fmt::Display, io::{self, Read}, path::PathBuf, string, sync::Arc, thread, time::Duration + fmt::Display, io::{self, Read}, path::PathBuf, sync::Arc, thread, time::Duration }; use druid::{ @@ -20,9 +20,9 @@ use psst_core::{ use crate::{ data::{ - self, library_derived_lenses::playlists, Album, AlbumType, Artist, ArtistAlbums, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile + self, Album, AlbumType, Artist, ArtistAlbums, ArtistLink, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile }, - error::Error, ui::album, + error::Error, }; use super::{cache::WebApiCache, local::LocalTrackManager}; @@ -500,7 +500,7 @@ impl WebApi { let mut title: Arc = Arc::from(""); let mut playlist: Vector = Vector::new(); - let mut album: Vector = Vector::new(); + let mut album: Vector> = Vector::new(); let mut artist: Vector = Vector::new(); let mut show: Vector> = Vector::new(); @@ -557,8 +557,32 @@ impl WebApi { height: None, }).collect() ), - })}, - + }) + }, + DataTypename::Album => { + album.push_back(Arc::new(Album { + id: id.into(), + name: Arc::from(item.content.data.name.clone().unwrap()), + album_type: AlbumType::Album, + images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| + images.sources.iter().map(|src| data::utils::Image { + url: Arc::from(src.url.clone()), + width: None, + height: None, + }).collect() + ), + artists: item.content.data.artists.as_ref().map_or_else(Vector::new, |artists| + artists.items.iter().map(|artist| ArtistLink { + id: Arc::from(artist.uri.split(':').last().unwrap_or("").to_string()), + name: Arc::from(artist.profile.name.clone()), + }).collect()), + copyrights: Vector::new(), + label: "".into(), + tracks: Vector::new(), + release_date: None, + release_date_precision: None, + })) + }, DataTypename::Podcast => { show.push_back(Arc::new(Show { id: id.into(), @@ -582,8 +606,8 @@ impl WebApi { Ok(MixedView { title, playlists: playlist, - artists: Vector::new(), - albums: Vector::new(), + artists: artist, + albums: album, shows: show, }) } @@ -1018,7 +1042,7 @@ impl WebApi { Ok(result) } - /* EPISODES + /* EPISODES, IMPLEMENT THIS TO REDO THE PODCAST SECTION // Episodes for you, this needs a different thing, mixedview could include this pub fn new_episodes(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3K -> New episodes From ffcfd199d944e11fe51b95ad58a9ca56936520a8 Mon Sep 17 00:00:00 2001 From: so9010 Date: Mon, 16 Sep 2024 19:24:54 +0100 Subject: [PATCH 14/30] Neaten code for show and playlist, the rest will come soon! --- psst-gui/src/ui/home.rs | 5 +-- psst-gui/src/ui/library.rs | 2 +- psst-gui/src/ui/playlist.rs | 26 +++++++++++---- psst-gui/src/ui/search.rs | 4 +-- psst-gui/src/ui/show.rs | 63 ++++++++++++++----------------------- 5 files changed, 49 insertions(+), 51 deletions(-) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 71fc07d1..0f33d979 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -195,6 +195,7 @@ fn loaded_results_widget() -> impl Widget> { .with_child(album_results_widget()) .with_child(playlist_results_widget()) .with_child(show_results_widget())) + // We want it to allign vertially too so it looks neater! .align_left(), ) ) @@ -248,7 +249,7 @@ fn playlist_results_widget() -> impl Widget> { Empty, Flex::column().with_child( Scroll::new( - List::new(|| playlist::horizontal_playlist_widget(true, true)).horizontal(), + List::new(|| playlist::playlist_widget(true)).horizontal(), ) .horizontal() .align_left() @@ -263,7 +264,7 @@ fn show_results_widget() -> impl Widget> { Empty, Flex::column().with_child( Scroll::new( - List::new(show::horizontal_show_widget).horizontal(), + List::new(|| show::show_widget(true)).horizontal(), ) .align_left() ), diff --git a/psst-gui/src/ui/library.rs b/psst-gui/src/ui/library.rs index b5891ded..ba77c27e 100644 --- a/psst-gui/src/ui/library.rs +++ b/psst-gui/src/ui/library.rs @@ -165,7 +165,7 @@ pub fn saved_albums_widget() -> impl Widget { pub fn saved_shows_widget() -> impl Widget { Async::new( utils::spinner_widget, - || List::new(show::show_widget).lens(Ctx::map(SavedShows::shows)), + || List::new(|| show::show_widget(false)).lens(Ctx::map(SavedShows::shows)), utils::error_widget, ) .lens( diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index c7813ed7..5463274b 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -357,8 +357,11 @@ pub fn horizontal_playlist_widget( .context_menu(playlist_menu_ctx) } -pub fn playlist_widget() -> impl Widget> { - let playlist_image = rounded_cover_widget(theme::grid(6.0)).lens(Ctx::data()); +pub fn playlist_widget(horizontal: bool) -> impl Widget> { + let playlist_image = if horizontal {rounded_cover_widget(theme::grid(16.0)).lens(Ctx::data())} else {rounded_cover_widget(theme::grid(6.0)).lens(Ctx::data())}; + + let mut playlist_info = if horizontal {Flex::column()} else {Flex::row()}; + let mut playlist = if horizontal {Flex::column()} else {Flex::row()}; let playlist_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -371,16 +374,27 @@ pub fn playlist_widget() -> impl Widget> { .with_text_size(theme::TEXT_SIZE_SMALL) .lens(Ctx::data().then(Playlist::description)); - let playlist_info = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) + let playlist_description = if horizontal { + playlist_description.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + } else { + playlist_description.align_left() + }; + + let playlist_name = if horizontal { + playlist_name.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + } else { + playlist_name.align_left() + }; + + playlist_info = playlist_info .with_child(playlist_name) .with_spacer(2.0) .with_child(playlist_description); - let playlist = Flex::row() + let playlist = playlist .with_child(playlist_image) .with_default_spacer() - .with_flex_child(playlist_info, 1.0) + .with_child(playlist_info) .padding(theme::grid(1.0)); playlist diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index 4627b3c9..052e5a75 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -141,7 +141,7 @@ fn playlist_results_widget() -> impl Widget> { Flex::column() .with_child(header_widget("Playlists")) .with_child( - List::new(playlist::playlist_widget).lens(Ctx::map(SearchResults::playlists)), + List::new(|| playlist::playlist_widget(false)).lens(Ctx::map(SearchResults::playlists)), ), ) } @@ -152,7 +152,7 @@ fn show_results_widget() -> impl Widget> { Empty, Flex::column() .with_child(header_widget("Podcasts")) - .with_child(List::new(show::show_widget)), + .with_child(List::new(|| show::show_widget(false))), ) .lens(Ctx::map(SearchResults::shows)) } diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index 20119454..702023bd 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -76,8 +76,11 @@ fn async_episodes_widget() -> impl Widget { ) } -pub fn horizontal_show_widget() -> impl Widget>> { - let show_image = rounded_cover_widget(theme::grid(16.0)); +pub fn show_widget(horizontal: bool) -> impl Widget>> { + let show_image = if horizontal {rounded_cover_widget(theme::grid(16.0))} else {rounded_cover_widget(theme::grid(6.0))}; + + let mut show_info = if horizontal {Flex::column()} else {Flex::row()}; + let mut show = if horizontal {Flex::column()} else {Flex::row()}; let show_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -85,58 +88,38 @@ pub fn horizontal_show_widget() -> impl Widget>> { .lens(Show::name.in_arc()); let show_publisher = Label::raw() + .with_line_break_mode(LineBreaking::WordWrap) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR) .lens(Show::publisher.in_arc()); - let show_info = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(show_name) - .with_spacer(1.0) - .with_child(show_publisher); - - let show = Flex::column() - .with_child(show_image) - .with_default_spacer() - .with_child(show_info) - .lens(Ctx::data()); - - show.padding(theme::grid(1.0)) - .link() - .on_left_click(|ctx, _, show, _| { - ctx.submit_command(cmd::NAVIGATE.with(Nav::ShowDetail(show.data.link()))); - }) - .fix_width(theme::grid(20.0)) - .context_menu(show_ctx_menu) -} - -pub fn show_widget() -> impl Widget>> { - let show_image = rounded_cover_widget(theme::grid(6.0)); - - let show_name = Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::Clip) - .lens(Show::name.in_arc()); + let show_name = if horizontal { + show_name.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + } else { + show_name.align_left() + }; - let show_publisher = Label::raw() - .with_text_size(theme::TEXT_SIZE_SMALL) - .with_text_color(theme::PLACEHOLDER_COLOR) - .lens(Show::publisher.in_arc()); + let show_publisher = if horizontal { + show_publisher.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + } else { + show_publisher.align_left() + }; - let show_info = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) + show_info = show_info .with_child(show_name) - .with_spacer(1.0) + .with_spacer(2.0) .with_child(show_publisher); - let show = Flex::row() + let show = show .with_child(show_image) .with_default_spacer() - .with_flex_child(show_info, 1.0) + .with_child(show_info) + .padding(theme::grid(1.0)) .lens(Ctx::data()); - show.padding(theme::grid(1.0)) + show .link() + .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, show, _| { ctx.submit_command(cmd::NAVIGATE.with(Nav::ShowDetail(show.data.link()))); }) From c41216152217ae28b01771f726b4c479188c93eb Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 17 Sep 2024 23:06:15 +0100 Subject: [PATCH 15/30] Finish condensing views code, get the user data from a simple api call --- psst-gui/src/ui/album.rs | 119 ++++++++++++---------------------- psst-gui/src/ui/artist.rs | 66 +++++++++---------- psst-gui/src/ui/home.rs | 8 +-- psst-gui/src/ui/library.rs | 2 +- psst-gui/src/ui/playlist.rs | 48 +------------- psst-gui/src/ui/search.rs | 4 +- psst-gui/src/ui/show.rs | 3 +- psst-gui/src/webapi/client.rs | 93 +++++++------------------- 8 files changed, 105 insertions(+), 238 deletions(-) diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index d44742cf..caed3e03 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -99,85 +99,32 @@ fn rounded_cover_widget(size: f64) -> impl Widget> { cover_widget(size).clip(Size::new(size, size).to_rounded_rect(4.0)) } -pub fn horizontal_album_widget() -> impl Widget>> { - let album_cover = rounded_cover_widget(theme::grid(16.0)); - - let album_name = Flex::column() - .with_child( - Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::Clip) - .lens(Album::name.in_arc()), - ) - .with_spacer(theme::grid(0.5)) - .with_child(ViewSwitcher::new( - |album: &Arc, _| album.has_explicit(), - |selector: &bool, _, _| match selector { - true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), - false => Box::new(Flex::column()), - }, - )) - .center(); +pub fn album_widget(horizontal: bool) -> impl Widget>> { + let album_cover = if horizontal { rounded_cover_widget(theme::grid(16.0)) } else { rounded_cover_widget(theme::grid(6.0)) }; - let album_artists = List::new(|| { + let album_name = if horizontal { + Flex::column() + } else { + Flex::row() } + .with_child( Label::raw() - .with_text_size(theme::TEXT_SIZE_SMALL) - .with_line_break_mode(LineBreaking::Clip) - .lens(ArtistLink::name) - }) - .horizontal() - .with_spacing(theme::grid(1.0)) - .lens(Album::artists.in_arc()); - - let album_date = Label::>::dynamic(|album, _| album.release_year()) - .with_text_size(theme::TEXT_SIZE_SMALL) - .with_text_color(theme::PLACEHOLDER_COLOR); - - let album_info = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(album_name) - .with_spacer(1.0) - .with_child(album_artists); - - let album = Flex::column() - .with_child(album_cover) - .with_default_spacer() - .with_child(album_info) - .padding(theme::grid(1.0)) - .lens(Ctx::data()); - - album - .link() - .rounded(theme::BUTTON_BORDER_RADIUS) - .on_left_click(|ctx, _, album, _| { - ctx.submit_command(cmd::NAVIGATE.with(Nav::AlbumDetail(album.data.link()))); - }) - .context_menu(album_ctx_menu) -} - -pub fn album_widget() -> impl Widget>> { - let album_cover = rounded_cover_widget(theme::grid(6.0)); - - let album_name = Flex::row() - .with_child( - Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::Clip) - .lens(Album::name.in_arc()), - ) - .with_spacer(theme::grid(0.5)) - .with_child(ViewSwitcher::new( - |album: &Arc, _| album.has_explicit(), - |selector: &bool, _, _| match selector { - true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), - false => Box::new(Flex::column()), - }, - )); + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::WordWrap) + .lens(Album::name.in_arc()), + ) + .with_spacer(theme::grid(0.5)) + .with_child(ViewSwitcher::new( + |album: &Arc, _| album.has_explicit(), + |selector: &bool, _, _| match selector { + true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), + false => Box::new(Flex::column()), + }, + )); let album_artists = List::new(|| { Label::raw() .with_text_size(theme::TEXT_SIZE_SMALL) - .with_line_break_mode(LineBreaking::Clip) + .with_line_break_mode(LineBreaking::WordWrap) .lens(ArtistLink::name) }) .horizontal() @@ -185,6 +132,7 @@ pub fn album_widget() -> impl Widget>> { .lens(Album::artists.in_arc()); let album_date = Label::>::dynamic(|album, _| album.release_year()) + .with_line_break_mode(LineBreaking::WordWrap) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR); @@ -194,16 +142,29 @@ pub fn album_widget() -> impl Widget>> { .with_spacer(1.0) .with_child(album_artists) .with_spacer(1.0) - .with_child(album_date); - - let album = Flex::row() + .with_child(album_date) + .fix_width(theme::grid(16.0)); + + let album_layout = if horizontal { + Flex::column() + .with_child(album_cover) + .with_default_spacer() + .with_child(album_info) + .fix_width(theme::grid(16.0)) + .padding_horizontal(theme::grid(1.0)) + .align_left() + } else { + Flex::row() .with_child(album_cover) .with_default_spacer() .with_flex_child(album_info, 1.0) - .padding(theme::grid(1.0)) - .lens(Ctx::data()); + .with_spacer(1.0) + .align_left() + }; - album + album_layout + .padding(theme::grid(1.0)) + .lens(Ctx::data()) .link() .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, album, _| { diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 3a709bca..46a7999e 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -5,8 +5,7 @@ use druid::{ use crate::{ cmd, data::{ - AppState, Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks, Cached, Ctx, Nav, - WithCtx, + AppState, Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks, Cached, Ctx, Nav, WithCtx }, webapi::WebApi, widget::{Async, MyWidgetExt, RemoteImage}, @@ -79,36 +78,31 @@ fn async_related_widget() -> impl Widget { ) } -pub fn horizontal_artist_widget() -> impl Widget { - let artist_image = cover_widget(theme::grid(16.0)); - let artist_label = Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .lens(Artist::name); - let artist = Flex::column() - .with_child(artist_image) - .with_default_spacer() - .with_child(artist_label); - artist - .padding(theme::grid(1.0)) - .fix_width(theme::grid(20.0)) - .link() - .rounded(theme::BUTTON_BORDER_RADIUS) - .on_left_click(|ctx, _, artist, _| { - ctx.submit_command(cmd::NAVIGATE.with(Nav::ArtistDetail(artist.link()))); - }) - .context_menu(|artist| artist_menu(&artist.link())) -} -pub fn artist_widget() -> impl Widget { - let artist_image = cover_widget(theme::grid(7.0)); - let artist_label = Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .lens(Artist::name); - let artist = Flex::row() +pub fn artist_widget(horizontal: bool) -> impl Widget { + let mut artist = if horizontal { Flex::column() } else { Flex::row() }; + + let artist_image = cover_widget(if horizontal { theme::grid(16.0) } else { theme::grid(6.0) }); + artist = artist .with_child(artist_image) - .with_default_spacer() - .with_flex_child(artist_label, 1.0); - artist - .padding(theme::grid(0.5)) + .with_default_spacer(); + + artist = if horizontal { + artist.with_child(Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .lens(Artist::name)) + } else { + artist.with_flex_child(Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .lens(Artist::name), 1.0) + }; + + let artist = if horizontal { + artist.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + } else { + artist.align_left() + }; + + artist.padding(theme::grid(1.0)) .link() .on_left_click(|ctx, _, artist, _| { ctx.submit_command(cmd::NAVIGATE.with(Nav::ArtistDetail(artist.link()))); @@ -153,20 +147,20 @@ fn albums_widget() -> impl Widget> { Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(header_widget("Albums")) - .with_child(List::new(album::album_widget).lens(Ctx::map(ArtistAlbums::albums))) + .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::albums))) .with_child(header_widget("Singles")) - .with_child(List::new(album::album_widget).lens(Ctx::map(ArtistAlbums::singles))) + .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::singles))) .with_child(header_widget("Compilations")) - .with_child(List::new(album::album_widget).lens(Ctx::map(ArtistAlbums::compilations))) + .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::compilations))) .with_child(header_widget("Appears On")) - .with_child(List::new(album::album_widget).lens(Ctx::map(ArtistAlbums::appears_on))) + .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::appears_on))) } fn related_widget() -> impl Widget>> { Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(header_widget("Related Artists")) - .with_child(List::new(artist_widget)) + .with_child(List::new(|| artist_widget(false))) .lens(Cached::data) } diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 0f33d979..fcdef990 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -195,7 +195,7 @@ fn loaded_results_widget() -> impl Widget> { .with_child(album_results_widget()) .with_child(playlist_results_widget()) .with_child(show_results_widget())) - // We want it to allign vertially too so it looks neater! + // Figure out a way to algin these widgets! .align_left(), ) ) @@ -222,7 +222,7 @@ fn artist_results_widget() -> impl Widget> { |artists: &Vector, _| artists.is_empty(), Empty, Scroll::new( - List::new(artist::horizontal_artist_widget).horizontal(), + List::new(|| artist::artist_widget(true)).horizontal(), ) .horizontal().align_left(), ) @@ -235,7 +235,7 @@ fn album_results_widget() -> impl Widget> { Empty, Flex::column().with_child( Scroll::new( - List::new(album::horizontal_album_widget).horizontal(), + List::new(|| album::album_widget(true)).horizontal(), ) .horizontal() .align_left() @@ -277,7 +277,7 @@ fn user_top_artists_widget() -> impl Widget { spinner_widget, || { Scroll::new( - List::new(artist::horizontal_artist_widget).horizontal(), + List::new(|| artist::artist_widget(true)).horizontal(), ) .horizontal() }, diff --git a/psst-gui/src/ui/library.rs b/psst-gui/src/ui/library.rs index ba77c27e..c341456f 100644 --- a/psst-gui/src/ui/library.rs +++ b/psst-gui/src/ui/library.rs @@ -104,7 +104,7 @@ pub fn saved_tracks_widget() -> impl Widget { pub fn saved_albums_widget() -> impl Widget { Async::new( utils::spinner_widget, - || List::new(album::album_widget).lens(Ctx::map(SavedAlbums::albums)), + || List::new(|| album::album_widget(true)).lens(Ctx::map(SavedAlbums::albums)), utils::error_widget, ) .lens( diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index 5463274b..8684316a 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -5,7 +5,7 @@ use std::{cmp::Ordering, sync::Arc}; use druid::widget::{Button, LensWrap, TextBox}; use druid::{ im::Vector, - widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List}, + widget::{Flex, Label, LineBreaking, List}, Insets, Lens, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, Widget, WidgetExt, WindowDesc, }; @@ -318,51 +318,9 @@ fn information_section(title_msg: &str, description_msg: &str) -> impl Widget impl Widget> { - let playlist_image = rounded_cover_widget(theme::grid(16.0)).lens(Ctx::data()); - - let mut playlist = Flex::column() - .with_child(playlist_image) - .with_default_spacer(); - - if show_name { - let playlist_name = Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::Clip) - .lens(Ctx::data().then(Playlist::name)); - playlist.add_child(playlist_name); - playlist.add_spacer(2.0); - } - - if show_description { - let playlist_description = Label::raw() - .with_line_break_mode(LineBreaking::WordWrap) - .with_text_color(theme::PLACEHOLDER_COLOR) - .with_text_size(theme::TEXT_SIZE_SMALL) - .lens(Ctx::data().then(Playlist::description)); - playlist.add_child(playlist_description); - } - - playlist - .padding(theme::grid(1.0)) - .link() - .rounded(theme::BUTTON_BORDER_RADIUS) - .on_left_click(|ctx, _, playlist, _| { - ctx.submit_command(cmd::NAVIGATE.with(Nav::PlaylistDetail(playlist.data.link()))); - }) - .fix_width(theme::grid(20.0)) - .context_menu(playlist_menu_ctx) -} - pub fn playlist_widget(horizontal: bool) -> impl Widget> { let playlist_image = if horizontal {rounded_cover_widget(theme::grid(16.0)).lens(Ctx::data())} else {rounded_cover_widget(theme::grid(6.0)).lens(Ctx::data())}; - let mut playlist_info = if horizontal {Flex::column()} else {Flex::row()}; - let mut playlist = if horizontal {Flex::column()} else {Flex::row()}; - let playlist_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) .with_line_break_mode(LineBreaking::Clip) @@ -386,12 +344,12 @@ pub fn playlist_widget(horizontal: bool) -> impl Widget> { playlist_name.align_left() }; - playlist_info = playlist_info + let playlist_info = if horizontal {Flex::column()} else {Flex::row()} .with_child(playlist_name) .with_spacer(2.0) .with_child(playlist_description); - let playlist = playlist + let playlist = if horizontal { Flex::column() } else { Flex::row() } .with_child(playlist_image) .with_default_spacer() .with_child(playlist_info) diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index 052e5a75..d3754196 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -100,7 +100,7 @@ fn artist_results_widget() -> impl Widget> { Empty, Flex::column() .with_child(header_widget("Artists")) - .with_child(List::new(artist::artist_widget)), + .with_child(List::new(|| artist::artist_widget(false))), ) .lens(Ctx::data().then(SearchResults::artists)) } @@ -111,7 +111,7 @@ fn album_results_widget() -> impl Widget> { Empty, Flex::column() .with_child(header_widget("Albums")) - .with_child(List::new(album::album_widget)), + .with_child(List::new(|| album::album_widget(false))), ) .lens(Ctx::map(SearchResults::albums)) } diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index 702023bd..b2174752 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -80,7 +80,7 @@ pub fn show_widget(horizontal: bool) -> impl Widget>> { let show_image = if horizontal {rounded_cover_widget(theme::grid(16.0))} else {rounded_cover_widget(theme::grid(6.0))}; let mut show_info = if horizontal {Flex::column()} else {Flex::row()}; - let mut show = if horizontal {Flex::column()} else {Flex::row()}; + let show = if horizontal {Flex::column()} else {Flex::row()}; let show_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -118,6 +118,7 @@ pub fn show_widget(horizontal: bool) -> impl Widget>> { .lens(Ctx::data()); show + .align_left() .link() .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, show, _| { diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 350cba81..9f3f3f05 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -273,7 +273,6 @@ impl WebApi { } } - /// VERY MUCH NEED TO MAKE THE CODE LOOK NICER & MORE CONSISTANT fn load_and_return_home_section(&self, request: Request) -> Result { #[derive(Deserialize)] pub struct Welcome { @@ -300,53 +299,28 @@ impl WebApi { #[derive(Deserialize)] pub struct SectionData { - subtitle: Option, title: Title, } - #[derive(Deserialize)] - pub struct Subtitle { - text: String, - } - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Title { - original_label: Option, text: String, } - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct OriginalLabel { - text_attributes: TextAttributes, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct TextAttributes { - text_format_arguments: Vec>, - } - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SectionItems { items: Vec, - paging_info: PagingInfo, - total_count: i64, } #[derive(Deserialize)] pub struct Item { - data: Option, content: Content, - uri: String, } #[derive(Deserialize)] pub struct Content { - #[serde(rename = "__typename")] - typename: ContentTypename, data: ContentData, } @@ -368,11 +342,9 @@ impl WebApi { artists: Option, profile: Option, visuals: Option, - album_type: Option, // Show-specific fields cover_art: Option, - media_type: Option, publisher: Option, } @@ -407,28 +379,12 @@ impl WebApi { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CoverArt { - extracted_colors: ExtractedColors, sources: Vec, } - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ExtractedColors { - color_dark: ColorDark, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct ColorDark { - hex: String, - is_fallback: bool, - } - #[derive(Deserialize)] pub struct Source { - height: Option, url: String, - width: Option, } #[derive(Deserialize)] @@ -452,24 +408,6 @@ impl WebApi { Album, } - #[derive(Deserialize)] - pub enum ContentTypename { - #[serde(rename = "PodcastOrAudiobookResponseWrapper")] - PodcastOrAudiobookResponseWrapper, - #[serde(rename = "PlaylistResponseWrapper")] - PlaylistResponseWrapper, - #[serde(rename = "AlbumResponseWrapper")] - AlbumResponseWrapper, - #[serde(rename = "ArtistResponseWrapper")] - ArtistResponseWrapper, - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PagingInfo { - next_offset: Option, - } - #[derive(Deserialize)] pub struct Images { items: Vec, @@ -479,7 +417,6 @@ impl WebApi { #[serde(rename_all = "camelCase")] pub struct ImagesItem { sources: Vec, - extracted_colors: ExtractedColors, } #[derive(Deserialize)] @@ -490,9 +427,7 @@ impl WebApi { #[derive(Deserialize)] pub struct OwnerV2Data { #[serde(rename = "__typename")] - typename: String, name: String, - uri: String, } // Extract the playlists @@ -597,8 +532,7 @@ impl WebApi { publisher: Arc::from(item.content.data.publisher.as_ref().unwrap().name.clone()), description: "".into(), })) - }, - _ => {} + } } }); }); @@ -906,6 +840,25 @@ impl WebApi { /// View endpoints. impl WebApi { + pub fn get_user_info(&self) -> Result<(String, String), Error> { + #[derive(Deserialize)] + struct User { + region: String, + timezone: String, + } + let token = self.access_token()?; + let request = self + .agent + .request("GET", &format!("http://{}/{}", "ip-api.com", "json")) + .query("fields", "260") + .set("Authorization", &format!("Bearer {}", &token)); + + let result: User = self.load(request)?; + + Ok((result.region, result.timezone)) + + } + fn build_home_request(&self, section_uri: &str) -> (String, String) { let extensions = json!({ "persistedQuery": { @@ -913,12 +866,12 @@ impl WebApi { "sha256Hash": "4da53a78e4e98d4f3fa55698af5b751fe05ca3a1a4a526ff8147e8866ccfa49f" } }); - + let variables = json!( { "uri": section_uri, - "timeZone": "Europe/London", + "timeZone": self.get_user_info().unwrap().0, "sp_t": self.access_token().unwrap(), // Assuming this returns a Result - "country": "GB", + "country": self.get_user_info().unwrap().1, "sectionItemsOffset": 0, "sectionItemsLimit": 20, }); From d2e72414d2df78bf4f769ee654d1b592c390f579 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 17 Sep 2024 23:11:33 +0100 Subject: [PATCH 16/30] Remove comments --- psst-gui/src/webapi/client.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 9f3f3f05..9487368f 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -187,7 +187,6 @@ impl WebApi { } /// Very similar to `for_all_pages`, but only returns a certain number of results - /// TODO: test properly fn for_some_pages( &self, request: Request, @@ -572,7 +571,6 @@ impl WebApi { } // https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks - // TODO Cache this. pub fn get_user_top_tracks(&self) -> Result>, Error> { let request = self.get("v1/me/top/tracks", None)? .query("market", "from_token"); @@ -582,7 +580,6 @@ impl WebApi { Ok(result) } - // TODO Cache this. pub fn get_user_top_artist(&self) -> Result, Error> { #[derive(Clone, Data, Deserialize)] struct Artists { From d6db64d494e810dae07b46c388074f5e895a29e3 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 17 Sep 2024 23:16:31 +0100 Subject: [PATCH 17/30] Code neatness --- psst-gui/src/ui/home.rs | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index fcdef990..474a97d0 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -23,39 +23,30 @@ pub const LOAD_MADE_FOR_YOU: Selector = Selector::new("app.home.load-made-for-yo pub fn home_widget() -> impl Widget { Flex::column() .with_child(made_for_you()) + .with_child(jump_back_in()) .with_child(user_top_mixes()) .with_child(recommended_stations()) - // turn this into a function as it is like title_label(title: &str){}!!! - .with_child( - Label::new("Uniquely yours") - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)), - ) + .with_child(simple_title_label("Uniquely yours")) .with_default_spacer() .with_child(uniquely_yours()) - .with_child(jump_back_in()) .with_child(your_shows()) .with_child(shows_that_you_might_like()) - .with_child( - Label::new("Your top artists") - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)), - ) + .with_child(simple_title_label("Your top artists")) .with_default_spacer() .with_child(user_top_artists_widget()) .with_default_spacer() - .with_child( - Label::new("Your top tracks") - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)), - ) + .with_child(simple_title_label("Your top tracks")) .with_default_spacer() .with_child(user_top_tracks_widget()) } +fn simple_title_label(title: &str) -> impl Widget { + Label::new(title) + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)) +} + pub fn made_for_you() -> impl Widget { Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( From 41ef4bcf66dfbc030c526b4458f98ca3a63fcd2e Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Wed, 18 Sep 2024 10:42:04 +0100 Subject: [PATCH 18/30] Make album widget small for library --- psst-gui/src/ui/library.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psst-gui/src/ui/library.rs b/psst-gui/src/ui/library.rs index c341456f..67f92a20 100644 --- a/psst-gui/src/ui/library.rs +++ b/psst-gui/src/ui/library.rs @@ -104,7 +104,7 @@ pub fn saved_tracks_widget() -> impl Widget { pub fn saved_albums_widget() -> impl Widget { Async::new( utils::spinner_widget, - || List::new(|| album::album_widget(true)).lens(Ctx::map(SavedAlbums::albums)), + || List::new(|| album::album_widget(false)).lens(Ctx::map(SavedAlbums::albums)), utils::error_widget, ) .lens( From ef49f9320ecc50cba5ed4a3162da5a0b29665cd1 Mon Sep 17 00:00:00 2001 From: SO9010 Date: Thu, 19 Sep 2024 15:24:32 +0100 Subject: [PATCH 19/30] Rearage Loaded resulst widget to make it look nicer! --- psst-gui/src/data/mod.rs | 2 ++ psst-gui/src/ui/album.rs | 5 +---- psst-gui/src/ui/artist.rs | 1 + psst-gui/src/ui/home.rs | 23 ++++++++++++++++++++--- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 9e09653d..eb05f4bb 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -134,6 +134,7 @@ impl AppState { home_detail: HomeDetail { made_for_you: Promise::Empty, user_top_mixes: Promise::Empty, + best_of_artists: Promise::Empty, recommended_stations: Promise::Empty, your_shows: Promise::Empty, shows_that_you_might_like: Promise::Empty, @@ -523,6 +524,7 @@ pub type WithCtx = Ctx, T>; pub struct HomeDetail { pub made_for_you: Promise, pub user_top_mixes: Promise, + pub best_of_artists: Promise, pub recommended_stations: Promise, pub uniquely_yours: Promise, pub your_shows: Promise, diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index caed3e03..601ac9e9 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -102,10 +102,7 @@ fn rounded_cover_widget(size: f64) -> impl Widget> { pub fn album_widget(horizontal: bool) -> impl Widget>> { let album_cover = if horizontal { rounded_cover_widget(theme::grid(16.0)) } else { rounded_cover_widget(theme::grid(6.0)) }; - let album_name = if horizontal { - Flex::column() - } else { - Flex::row() } + let album_name = if horizontal { Flex::column() } else { Flex::row() } .with_child( Label::raw() .with_font(theme::UI_FONT_MEDIUM) diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 46a7999e..06057dee 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -104,6 +104,7 @@ pub fn artist_widget(horizontal: bool) -> impl Widget { artist.padding(theme::grid(1.0)) .link() + .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, artist, _| { ctx.submit_command(cmd::NAVIGATE.with(Nav::ArtistDetail(artist.link()))); }) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 474a97d0..6fdc3d5f 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -26,6 +26,7 @@ pub fn home_widget() -> impl Widget { .with_child(jump_back_in()) .with_child(user_top_mixes()) .with_child(recommended_stations()) + .with_child(best_of_artists()) .with_child(simple_title_label("Uniquely yours")) .with_default_spacer() .with_child(uniquely_yours()) @@ -115,6 +116,23 @@ pub fn user_top_mixes() -> impl Widget { ) } +pub fn best_of_artists() -> impl Widget { + Async::new(spinner_widget, loaded_results_widget, error_widget) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::home_detail.then(HomeDetail::best_of_artists), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_MADE_FOR_YOU, + |_| WebApi::global().best_of_artists(), + |_, data, q| data.home_detail.best_of_artists.defer(q), + |_, data, r| data.home_detail.best_of_artists.update(r), + ) +} + pub fn your_shows() -> impl Widget { Async::new(spinner_widget, loaded_results_widget, error_widget) .lens( @@ -182,11 +200,10 @@ fn loaded_results_widget() -> impl Widget> { Flex::column() .with_child(title_label()) .with_child(Scroll::new(Flex::row() - .with_child(artist_results_widget()) - .with_child(album_results_widget()) .with_child(playlist_results_widget()) + .with_child(album_results_widget()) + .with_child(artist_results_widget()) .with_child(show_results_widget())) - // Figure out a way to algin these widgets! .align_left(), ) ) From 689085659beb60f32748f1fc66033315505a9af2 Mon Sep 17 00:00:00 2001 From: SO9010 Date: Sat, 21 Sep 2024 17:21:24 +0100 Subject: [PATCH 20/30] Neaten code, match widgets with original ones! --- psst-gui/src/controller/playback.rs | 2 +- psst-gui/src/data/album.rs | 2 +- psst-gui/src/ui/album.rs | 58 ++++++++++++------------ psst-gui/src/ui/artist.rs | 29 ++++++------ psst-gui/src/ui/home.rs | 6 +-- psst-gui/src/ui/playlist.rs | 45 ++++++++++++------- psst-gui/src/ui/show.rs | 69 +++++++++++++++++------------ psst-gui/src/webapi/client.rs | 9 ++-- 8 files changed, 120 insertions(+), 100 deletions(-) diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 79b8b811..75869218 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -154,7 +154,7 @@ impl PlaybackController { fn handle_media_control_event(event: MediaControlEvent, sender: &Sender) { let cmd = match event { - MediaControlEvent::Play => PlayerEvent::Command(PlayerCommand::Resume), + MediaControlEvent::Play(_) => PlayerEvent::Command(PlayerCommand::Resume), MediaControlEvent::Pause => PlayerEvent::Command(PlayerCommand::Pause), MediaControlEvent::Toggle => PlayerEvent::Command(PlayerCommand::PauseOrResume), MediaControlEvent::Next => PlayerEvent::Command(PlayerCommand::Next), diff --git a/psst-gui/src/data/album.rs b/psst-gui/src/data/album.rs index bdc81094..ff353686 100644 --- a/psst-gui/src/data/album.rs +++ b/psst-gui/src/data/album.rs @@ -59,7 +59,7 @@ impl Album { self.release_date .as_ref() .map(|date| date.format(format).expect("invalid format")) - .unwrap_or_else(|| '-'.to_string()) + .unwrap_or_else(|| "".to_string()) } pub fn image(&self, width: f64, height: f64) -> Option<&Image> { diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 601ac9e9..d08580a8 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -8,7 +8,7 @@ use druid::{ use crate::{ cmd, data::{ - Album, AlbumDetail, AlbumLink, AppState, ArtistLink, Cached, Ctx, Library, Nav, WithCtx, + Album, AlbumDetail, AlbumLink, AppState, ArtistLink, Cached, Ctx, Library, Nav, WithCtx }, webapi::WebApi, widget::{icons, Async, MyWidgetExt, RemoteImage}, @@ -100,24 +100,25 @@ fn rounded_cover_widget(size: f64) -> impl Widget> { } pub fn album_widget(horizontal: bool) -> impl Widget>> { - let album_cover = if horizontal { rounded_cover_widget(theme::grid(16.0)) } else { rounded_cover_widget(theme::grid(6.0)) }; - - let album_name = if horizontal { Flex::column() } else { Flex::row() } - .with_child( - Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::WordWrap) - .lens(Album::name.in_arc()), - ) - .with_spacer(theme::grid(0.5)) - .with_child(ViewSwitcher::new( - |album: &Arc, _| album.has_explicit(), - |selector: &bool, _, _| match selector { - true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), - false => Box::new(Flex::column()), - }, - )); - + let (album_cover_size, album_name_layout) = if horizontal { (16.0, Flex::column()) } else { (6.0, Flex::row()) }; + let album_cover = rounded_cover_widget(theme::grid(album_cover_size)); + + let album_name = album_name_layout + .with_child( + Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::WordWrap) + .lens(Album::name.in_arc()), + ) + .with_spacer(theme::grid(0.5)) + .with_child(ViewSwitcher::new( + |album: &Arc, _| album.has_explicit(), + |selector: &bool, _, _| match selector { + true => icons::EXPLICIT.scale(theme::ICON_SIZE_TINY).boxed(), + false => Box::new(Flex::column()), + }, + )); + let album_artists = List::new(|| { Label::raw() .with_text_size(theme::TEXT_SIZE_SMALL) @@ -127,12 +128,12 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { .horizontal() .with_spacing(theme::grid(1.0)) .lens(Album::artists.in_arc()); - + let album_date = Label::>::dynamic(|album, _| album.release_year()) .with_line_break_mode(LineBreaking::WordWrap) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR); - + let album_info = Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(album_name) @@ -141,24 +142,25 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { .with_spacer(1.0) .with_child(album_date) .fix_width(theme::grid(16.0)); - + let album_layout = if horizontal { Flex::column() .with_child(album_cover) .with_default_spacer() .with_child(album_info) + .with_spacer(theme::grid(2.0)) .fix_width(theme::grid(16.0)) .padding_horizontal(theme::grid(1.0)) .align_left() } else { Flex::row() - .with_child(album_cover) - .with_default_spacer() - .with_flex_child(album_info, 1.0) - .with_spacer(1.0) - .align_left() + .with_child(album_cover) + .with_default_spacer() + .with_flex_child(album_info, 1.0) + .padding(theme::grid(1.0)) + .align_left() }; - + album_layout .padding(theme::grid(1.0)) .lens(Ctx::data()) diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index 06057dee..e1e819fe 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -79,28 +79,27 @@ fn async_related_widget() -> impl Widget { } pub fn artist_widget(horizontal: bool) -> impl Widget { - let mut artist = if horizontal { Flex::column() } else { Flex::row() }; - - let artist_image = cover_widget(if horizontal { theme::grid(16.0) } else { theme::grid(6.0) }); - artist = artist - .with_child(artist_image) - .with_default_spacer(); + let ( mut artist, artist_image) = if horizontal { (Flex::column(), cover_widget(theme::grid(16.0))) } else { (Flex::row(), cover_widget(theme::grid(6.0))) }; artist = if horizontal { - artist.with_child(Label::raw() - .with_font(theme::UI_FONT_MEDIUM) + artist + .with_child(artist_image) + .with_default_spacer() + .with_child(Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .center() + .fix_width(theme::grid(16.0)) + .padding_horizontal(theme::grid(1.0)) .lens(Artist::name)) + .with_spacer(theme::grid(2.0)) } else { - artist.with_flex_child(Label::raw() + artist + .with_child(artist_image) + .with_default_spacer() + .with_flex_child(Label::raw() .with_font(theme::UI_FONT_MEDIUM) .lens(Artist::name), 1.0) }; - - let artist = if horizontal { - artist.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() - } else { - artist.align_left() - }; artist.padding(theme::grid(1.0)) .link() diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 6fdc3d5f..bfc4eef0 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -192,11 +192,7 @@ fn loaded_results_widget() -> impl Widget> { && results.data.playlists.is_empty() && results.data.shows.is_empty() }, - Label::new("No results") - .with_text_size(theme::TEXT_SIZE_LARGE) - .with_text_color(theme::PLACEHOLDER_COLOR) - .padding(theme::grid(6.0)) - .center(), + Empty, Flex::column() .with_child(title_label()) .with_child(Scroll::new(Flex::row() diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index 8684316a..d90badfc 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -319,7 +319,8 @@ fn information_section(title_msg: &str, description_msg: &str) -> impl Widget impl Widget> { - let playlist_image = if horizontal {rounded_cover_widget(theme::grid(16.0)).lens(Ctx::data())} else {rounded_cover_widget(theme::grid(6.0)).lens(Ctx::data())}; + let playlist_image_size = if horizontal { theme::grid(16.0) } else { theme::grid(6.0) }; + let playlist_image = rounded_cover_widget(playlist_image_size).lens(Ctx::data()); let playlist_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) @@ -332,29 +333,39 @@ pub fn playlist_widget(horizontal: bool) -> impl Widget> { .with_text_size(theme::TEXT_SIZE_SMALL) .lens(Ctx::data().then(Playlist::description)); - let playlist_description = if horizontal { - playlist_description.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + let (playlist_name, playlist_description) = if horizontal { + (playlist_name + .fix_width(playlist_image_size) + .padding_horizontal(theme::grid(1.0)) + .align_left(), + playlist_description + .fix_width(playlist_image_size) + .padding_horizontal(theme::grid(1.0)) + .align_left()) } else { - playlist_description.align_left() + (playlist_name.align_left(), playlist_description.align_left()) }; - let playlist_name = if horizontal { - playlist_name.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() - } else { - playlist_name.align_left() - }; - - let playlist_info = if horizontal {Flex::column()} else {Flex::row()} + let playlist_info = Flex::column() .with_child(playlist_name) .with_spacer(2.0) .with_child(playlist_description); - let playlist = if horizontal { Flex::column() } else { Flex::row() } - .with_child(playlist_image) - .with_default_spacer() - .with_child(playlist_info) - .padding(theme::grid(1.0)); - + let playlist = if horizontal { + Flex::column() + .with_child(playlist_image) + .with_default_spacer() + .with_child(playlist_info) + .with_spacer(theme::grid(2.0)) + .padding(theme::grid(1.0))} + else { + Flex::row() + .with_child(playlist_image) + .with_default_spacer() + .with_flex_child(playlist_info, 1.0) + .padding(theme::grid(1.0)) + }; + playlist .link() .rounded(theme::BUTTON_BORDER_RADIUS) diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index b2174752..3425400e 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -77,48 +77,59 @@ fn async_episodes_widget() -> impl Widget { } pub fn show_widget(horizontal: bool) -> impl Widget>> { - let show_image = if horizontal {rounded_cover_widget(theme::grid(16.0))} else {rounded_cover_widget(theme::grid(6.0))}; - - let mut show_info = if horizontal {Flex::column()} else {Flex::row()}; - let show = if horizontal {Flex::column()} else {Flex::row()}; + let image_size = theme::grid(if horizontal { 16.0 } else { 6.0 }); + let show_image = rounded_cover_widget(image_size); let show_name = Label::raw() .with_font(theme::UI_FONT_MEDIUM) .with_line_break_mode(LineBreaking::Clip) - .lens(Show::name.in_arc()); + .lens(Show::name.in_arc()) + .align_left(); let show_publisher = Label::raw() - .with_line_break_mode(LineBreaking::WordWrap) + .with_line_break_mode(LineBreaking::Clip) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR) - .lens(Show::publisher.in_arc()); - - let show_name = if horizontal { - show_name.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() - } else { - show_name.align_left() - }; - - let show_publisher = if horizontal { - show_publisher.fix_width(theme::grid(16.0)).padding_horizontal(theme::grid(1.0)).align_left() + .lens(Show::publisher.in_arc()) + .align_left(); + + let (show_name, show_publisher) = if horizontal { + (show_name + .fix_width(image_size) + .padding_horizontal(theme::grid(1.0)) + .align_left(), + show_publisher + .fix_width(image_size) + .padding_horizontal(theme::grid(1.0)) + .align_left()) } else { - show_publisher.align_left() + (show_name.align_left(), show_publisher.align_left()) }; - show_info = show_info - .with_child(show_name) + let show_info = Flex::column() + .with_child(show_name.padding_horizontal(theme::grid(1.0))) .with_spacer(2.0) - .with_child(show_publisher); - - let show = show - .with_child(show_image) - .with_default_spacer() - .with_child(show_info) - .padding(theme::grid(1.0)) - .lens(Ctx::data()); + .with_child(show_publisher.padding_horizontal(theme::grid(1.0))) + .align_left(); + + let show = if horizontal { + Flex::column() + .with_child(show_image) + .with_default_spacer() + .with_child(show_info) + .with_spacer(theme::grid(2.0)) + .padding(theme::grid(1.0)) + .lens(Ctx::data())} + else { + Flex::row() + .with_child(show_image) + .with_default_spacer() + .with_flex_child(show_info, 1.0) + .padding(theme::grid(1.0)) + .lens(Ctx::data()) + }; - show - .align_left() + show.align_left() .link() .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, show, _| { diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 9487368f..7ef153a3 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -837,6 +837,8 @@ impl WebApi { /// View endpoints. impl WebApi { + // This is getting called too often! It only needs to be called once ever really. + // Maybe we could call it once then allow the user to change it in the settings? pub fn get_user_info(&self) -> Result<(String, String), Error> { #[derive(Deserialize)] struct User { @@ -992,8 +994,8 @@ impl WebApi { Ok(result) } - /* EPISODES, IMPLEMENT THIS TO REDO THE PODCAST SECTION - // Episodes for you, this needs a different thing, mixedview could include this + /* + // TODO: Episodes for you, implement this to redesign the podcast page pub fn new_episodes(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3K -> New episodes let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3K"); @@ -1008,8 +1010,7 @@ impl WebApi { Ok(result) } - // Episodes for you, this needs a different thing, mixedview could include this - // + // Episodes for you, this needs to have its own thing or be part of a mixed view as it is in episode form pub fn episode_for_you(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu9e -> Episodes for you let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu9e"); From 8244562608c328098db0b943f498b855bf7ef213 Mon Sep 17 00:00:00 2001 From: SO9010 Date: Sat, 21 Sep 2024 17:26:17 +0100 Subject: [PATCH 21/30] Lint and remove hack to build on my machine --- psst-gui/src/controller/playback.rs | 2 +- psst-gui/src/data/album.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 75869218..79b8b811 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -154,7 +154,7 @@ impl PlaybackController { fn handle_media_control_event(event: MediaControlEvent, sender: &Sender) { let cmd = match event { - MediaControlEvent::Play(_) => PlayerEvent::Command(PlayerCommand::Resume), + MediaControlEvent::Play => PlayerEvent::Command(PlayerCommand::Resume), MediaControlEvent::Pause => PlayerEvent::Command(PlayerCommand::Pause), MediaControlEvent::Toggle => PlayerEvent::Command(PlayerCommand::PauseOrResume), MediaControlEvent::Next => PlayerEvent::Command(PlayerCommand::Next), diff --git a/psst-gui/src/data/album.rs b/psst-gui/src/data/album.rs index ff353686..c04664a4 100644 --- a/psst-gui/src/data/album.rs +++ b/psst-gui/src/data/album.rs @@ -59,7 +59,7 @@ impl Album { self.release_date .as_ref() .map(|date| date.format(format).expect("invalid format")) - .unwrap_or_else(|| "".to_string()) + .unwrap_or_default() } pub fn image(&self, width: f64, height: f64) -> Option<&Image> { From 9149122cd8ee8f52db73955b8febcc7fdc801839 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 10:36:02 +0100 Subject: [PATCH 22/30] Cache user info --- psst-gui/src/webapi/client.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 7ef153a3..6a66a79f 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -835,12 +835,11 @@ impl WebApi { } } + /// View endpoints. impl WebApi { - // This is getting called too often! It only needs to be called once ever really. - // Maybe we could call it once then allow the user to change it in the settings? pub fn get_user_info(&self) -> Result<(String, String), Error> { - #[derive(Deserialize)] + #[derive(Deserialize, Clone, Data)] struct User { region: String, timezone: String, @@ -852,10 +851,9 @@ impl WebApi { .query("fields", "260") .set("Authorization", &format!("Bearer {}", &token)); - let result: User = self.load(request)?; - - Ok((result.region, result.timezone)) - + let result: Cached = self.load_cached(request, "User_info", "usrinfo")?; + + Ok((result.data.region.clone(), result.data.timezone.clone())) } fn build_home_request(&self, section_uri: &str) -> (String, String) { From 199c04e8b4baa3e205aed917ddbb738194a03a51 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 12:22:32 +0100 Subject: [PATCH 23/30] Revert to spotube hash --- psst-gui/src/webapi/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 6a66a79f..69a84bde 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -860,7 +860,8 @@ impl WebApi { let extensions = json!({ "persistedQuery": { "version": 1, - "sha256Hash": "4da53a78e4e98d4f3fa55698af5b751fe05ca3a1a4a526ff8147e8866ccfa49f" + // From https://github.com/KRTirtho/spotube/blob/9b024120601c0d381edeab4460cb22f87149d0f8/lib%2Fservices%2Fcustom_spotify_endpoints%2Fspotify_endpoints.dart keep and eye on this and change accordingly + "sha256Hash": "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be" } }); From 34652d1504a69ba347b770c47dde6a82bb870769 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 19:21:19 +0100 Subject: [PATCH 24/30] Unify widgets --- psst-gui/src/ui/album.rs | 43 +++++++++++++++++++------------------ psst-gui/src/ui/artist.rs | 11 +++++----- psst-gui/src/ui/playlist.rs | 26 +++++++++++----------- psst-gui/src/ui/show.rs | 38 +++++++++++--------------------- 4 files changed, 53 insertions(+), 65 deletions(-) diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index d08580a8..8473d161 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use druid::{ - widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List, ViewSwitcher}, - LensExt, LocalizedString, Menu, MenuItem, Selector, Size, Widget, WidgetExt, + widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List, ViewSwitcher}, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt }; use crate::{ @@ -124,40 +123,42 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { .with_text_size(theme::TEXT_SIZE_SMALL) .with_line_break_mode(LineBreaking::WordWrap) .lens(ArtistLink::name) - }) - .horizontal() - .with_spacing(theme::grid(1.0)) - .lens(Album::artists.in_arc()); + }) + .horizontal() + .with_spacing(theme::grid(1.0)) + .lens(Album::artists.in_arc()); let album_date = Label::>::dynamic(|album, _| album.release_year()) .with_line_break_mode(LineBreaking::WordWrap) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR); - let album_info = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(album_name) - .with_spacer(1.0) - .with_child(album_artists) - .with_spacer(1.0) - .with_child(album_date) - .fix_width(theme::grid(16.0)); - let album_layout = if horizontal { Flex::column() .with_child(album_cover) .with_default_spacer() - .with_child(album_info) - .with_spacer(theme::grid(2.0)) - .fix_width(theme::grid(16.0)) - .padding_horizontal(theme::grid(1.0)) + .with_child(Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(album_name) + .with_spacer(1.0) + .with_child(album_artists) + .with_spacer(1.0) + .with_child(album_date) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(8.5))) .align_left() } else { Flex::row() .with_child(album_cover) .with_default_spacer() - .with_flex_child(album_info, 1.0) - .padding(theme::grid(1.0)) + .with_flex_child(Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(album_name) + .with_spacer(1.0) + .with_child(album_artists) + .with_spacer(1.0) + .with_child(album_date), 1.0) .align_left() }; diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index e1e819fe..acae643d 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -1,5 +1,5 @@ use druid::{ - im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, Widget, WidgetExt + im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, UnitPoint, Widget, WidgetExt }; use crate::{ @@ -86,12 +86,11 @@ pub fn artist_widget(horizontal: bool) -> impl Widget { .with_child(artist_image) .with_default_spacer() .with_child(Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .center() - .fix_width(theme::grid(16.0)) - .padding_horizontal(theme::grid(1.0)) + .with_font(theme::UI_FONT_MEDIUM) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(8.5)) .lens(Artist::name)) - .with_spacer(theme::grid(2.0)) } else { artist .with_child(artist_image) diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index d90badfc..e3267a6b 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use std::{cmp::Ordering, sync::Arc}; use druid::widget::{Button, LensWrap, TextBox}; +use druid::UnitPoint; use druid::{ im::Vector, widget::{Flex, Label, LineBreaking, List}, @@ -336,33 +337,34 @@ pub fn playlist_widget(horizontal: bool) -> impl Widget> { let (playlist_name, playlist_description) = if horizontal { (playlist_name .fix_width(playlist_image_size) - .padding_horizontal(theme::grid(1.0)) .align_left(), playlist_description .fix_width(playlist_image_size) - .padding_horizontal(theme::grid(1.0)) .align_left()) } else { (playlist_name.align_left(), playlist_description.align_left()) }; - let playlist_info = Flex::column() - .with_child(playlist_name) - .with_spacer(2.0) - .with_child(playlist_description); - let playlist = if horizontal { Flex::column() .with_child(playlist_image) .with_default_spacer() - .with_child(playlist_info) - .with_spacer(theme::grid(2.0)) - .padding(theme::grid(1.0))} - else { + .with_child(Flex::column() + .with_child(playlist_name) + .with_spacer(2.0) + .with_child(playlist_description) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(8.5))) + .padding(theme::grid(1.0)) + } else { Flex::row() .with_child(playlist_image) .with_default_spacer() - .with_flex_child(playlist_info, 1.0) + .with_flex_child(Flex::column() + .with_child(playlist_name) + .with_spacer(2.0) + .with_child(playlist_description), 1.0) .padding(theme::grid(1.0)) }; diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index 3425400e..bcef25e0 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use druid::{ - widget::{CrossAxisAlignment, Flex, Label, LineBreaking}, - LensExt, LocalizedString, Menu, MenuItem, Selector, Size, Widget, WidgetExt, + widget::{CrossAxisAlignment, Flex, Label, LineBreaking}, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt }; use crate::{ @@ -93,38 +92,25 @@ pub fn show_widget(horizontal: bool) -> impl Widget>> { .lens(Show::publisher.in_arc()) .align_left(); - let (show_name, show_publisher) = if horizontal { - (show_name - .fix_width(image_size) - .padding_horizontal(theme::grid(1.0)) - .align_left(), - show_publisher - .fix_width(image_size) - .padding_horizontal(theme::grid(1.0)) - .align_left()) - } else { - (show_name.align_left(), show_publisher.align_left()) - }; - - let show_info = Flex::column() - .with_child(show_name.padding_horizontal(theme::grid(1.0))) - .with_spacer(2.0) - .with_child(show_publisher.padding_horizontal(theme::grid(1.0))) - .align_left(); - let show = if horizontal { Flex::column() .with_child(show_image) .with_default_spacer() - .with_child(show_info) - .with_spacer(theme::grid(2.0)) + .with_child(Flex::column() + .with_child(show_name) + .with_child(show_publisher) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(8.5))) .padding(theme::grid(1.0)) - .lens(Ctx::data())} - else { + .lens(Ctx::data()) + } else { Flex::row() .with_child(show_image) .with_default_spacer() - .with_flex_child(show_info, 1.0) + .with_flex_child(Flex::column() + .with_child(show_name) + .with_child(show_publisher), 1.0) .padding(theme::grid(1.0)) .lens(Ctx::data()) }; From 08983ddcff9e46ac47fa97750db564cbdaa2ff44 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 19:59:57 +0100 Subject: [PATCH 25/30] revert linebreak mode --- psst-gui/src/ui/album.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 8473d161..8ec47b97 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -106,7 +106,7 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { .with_child( Label::raw() .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::WordWrap) + .with_line_break_mode(LineBreaking::Clip) .lens(Album::name.in_arc()), ) .with_spacer(theme::grid(0.5)) From cc3eacb914b9a413443546d15125e56fcf6882f9 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 20:09:04 +0100 Subject: [PATCH 26/30] Home icon --- psst-gui/src/ui/playback.rs | 2 +- psst-gui/src/widget/icons.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/psst-gui/src/ui/playback.rs b/psst-gui/src/ui/playback.rs index b4f5a7b5..91d1ca41 100644 --- a/psst-gui/src/ui/playback.rs +++ b/psst-gui/src/ui/playback.rs @@ -173,7 +173,7 @@ fn cover_widget(size: f64) -> impl Widget { fn playback_origin_icon(origin: &PlaybackOrigin) -> &'static SvgIcon { match origin { // TODO add home widget - PlaybackOrigin::Home => &icons::PODCAST, + PlaybackOrigin::Home => &icons::HOME, PlaybackOrigin::Library => &icons::HEART, PlaybackOrigin::Album { .. } => &icons::ALBUM, PlaybackOrigin::Artist { .. } => &icons::ARTIST, diff --git a/psst-gui/src/widget/icons.rs b/psst-gui/src/widget/icons.rs index ac82958e..93f68b76 100644 --- a/psst-gui/src/widget/icons.rs +++ b/psst-gui/src/widget/icons.rs @@ -131,6 +131,12 @@ pub static PLAYLIST: SvgIcon = SvgIcon { svg_size: Size::new(22.0, 22.0), op: PaintOp::Fill, }; +// SFSymbols - house +pub static HOME: SvgIcon = SvgIcon { + svg_path: "M17.2 41.5H8.9c-2 0-2.8-.8-2.8-2.8V16l16-14.6c.3-.3.6-.5 1-.5s.7.2 1 .5L40 16V38.8c0 2-.8 2.8-2.8 2.8H28.9V26.2c0-.7-.5-1.2-1.2-1.2H18.4c-.7 0-1.2.5-1.2 1.2Zm20.1 1.2c2.5 0 3.7-1.3 3.7-3.7V17l4.1 3.7c.2.1.3.2.5.2.3 0 .5-.2.5-.4s0-.3-.2-.4L24.9.7c-.5-.5-1.1-.8-1.8-.8s-1.3.3-1.8.8L8.6 12.4V.6c0-.5-.4-.8-.8-.8H6.9c-.5 0-.8.3-.8.8v14l-6 5.5c-.1.1-.2.2-.2.4s.2.4.5.4.4-.1.5-.2L5 16.9v22c0 2.4 1.3 3.7 3.7 3.7H37.4Z", + svg_size: Size::new(46.0, 46.0), + op: PaintOp::Fill, +}; // SF Pro Regular - mic.circle pub static PODCAST: SvgIcon = SvgIcon { svg_path: "M10.9957 20C15.9285 20 20 15.9265 20 11C20 6.0735 15.9198 2 10.987 2C6.06283 2 2 6.0735 2 11C2 15.9265 6.07153 20 10.9957 20ZM10.9957 18.207C7.00242 18.207 3.80957 14.9952 3.80957 11C3.80957 7.00484 7.00242 3.80174 10.987 3.80174C14.9802 3.80174 18.1904 7.00484 18.1991 11C18.2078 14.9952 14.9889 18.207 10.9957 18.207ZM10.9957 12.5928C11.8395 12.5928 12.4746 11.9313 12.4746 11.0348V7.42263C12.4746 6.51741 11.8395 5.8646 10.9957 5.8646C10.1431 5.8646 9.50797 6.51741 9.50797 7.42263V11.0348C9.50797 11.9313 10.1431 12.5928 10.9957 12.5928ZM8.82939 16.1789H13.1619C13.4316 16.1789 13.6752 15.9439 13.6752 15.6741C13.6752 15.3956 13.4403 15.1605 13.1619 15.1605H11.5002V14.3598C13.2228 14.1509 14.4234 12.854 14.4234 11.087V10.0077C14.4234 9.73791 14.1885 9.51161 13.9188 9.51161C13.6404 9.51161 13.4055 9.73791 13.4055 10.0077V11.0783C13.4055 12.4536 12.3876 13.4371 10.987 13.4371C9.59497 13.4371 8.57709 12.4536 8.57709 11.0783V10.0077C8.57709 9.73791 8.34219 9.51161 8.0638 9.51161C7.7941 9.51161 7.55921 9.73791 7.55921 10.0077V11.087C7.55921 12.854 8.76849 14.1596 10.4911 14.3598V15.1605H8.82939C8.55099 15.1605 8.30739 15.3956 8.30739 15.6741C8.30739 15.9526 8.55099 16.1789 8.82939 16.1789Z", From 409d432282ba65b6927dd9a198d0f4371ed611be Mon Sep 17 00:00:00 2001 From: Jackson Goode Date: Mon, 23 Sep 2024 13:25:37 -0700 Subject: [PATCH 27/30] Filled home with fixed sizing --- psst-gui/src/widget/icons.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psst-gui/src/widget/icons.rs b/psst-gui/src/widget/icons.rs index 93f68b76..18fe97ae 100644 --- a/psst-gui/src/widget/icons.rs +++ b/psst-gui/src/widget/icons.rs @@ -133,8 +133,8 @@ pub static PLAYLIST: SvgIcon = SvgIcon { }; // SFSymbols - house pub static HOME: SvgIcon = SvgIcon { - svg_path: "M17.2 41.5H8.9c-2 0-2.8-.8-2.8-2.8V16l16-14.6c.3-.3.6-.5 1-.5s.7.2 1 .5L40 16V38.8c0 2-.8 2.8-2.8 2.8H28.9V26.2c0-.7-.5-1.2-1.2-1.2H18.4c-.7 0-1.2.5-1.2 1.2Zm20.1 1.2c2.5 0 3.7-1.3 3.7-3.7V17l4.1 3.7c.2.1.3.2.5.2.3 0 .5-.2.5-.4s0-.3-.2-.4L24.9.7c-.5-.5-1.1-.8-1.8-.8s-1.3.3-1.8.8L8.6 12.4V.6c0-.5-.4-.8-.8-.8H6.9c-.5 0-.8.3-.8.8v14l-6 5.5c-.1.1-.2.2-.2.4s.2.4.5.4.4-.1.5-.2L5 16.9v22c0 2.4 1.3 3.7 3.7 3.7H37.4Z", - svg_size: Size::new(46.0, 46.0), + svg_path: "M5.47851563 25.97460938c0 .23632812.171875.4296875.47265624.4296875.23632813 0 .34375-.10742188.47265626-.23632813L27.54296874 6.89648437c.32226563-.30078124.6875-.45117187 1.05273438-.45117187.32226562 0 .6875.171875.98828124.45117188L50.703125 26.16796874c.12890625.12890625.23632813.23632813.47265625.23632813.27929688 0 .47265625-.19335938.47265625-.4296875 0-.19335938-.06445313-.30078126-.19335938-.4296875l-5.95117187-5.45703126V6.12304689c0-.45117188-.32226563-.75195313-.75195313-.75195313h-.90234375c-.4296875 0-.75195312.30078125-.75195312.75195313v11.7734375L30.3359375 6.16601561c-.55859375-.49414062-1.18164063-.73046874-1.8046875-.73046874-.6015625 0-1.203125.23632812-1.74023438.73046874L5.671875 25.54492189c-.15039063.12890624-.19335938.23632812-.19335938.4296875ZM10.52734374 44.515625c0 2.25585938 1.33203125 3.58789063 3.609375 3.58789063h9.13085938V33c0-.70898438.47265624-1.16015625 1.18164062-1.16015625h8.22851563c.70898437 0 1.16015625.45117188 1.16015625 1.16015625v15.10351563h9.15234375c2.25585937 0 3.58789062-1.33203125 3.58789062-3.58789063V25.28710937L29.79882812 10.44140626c-.47265624-.40820313-.90234374-.6015625-1.33203124-.6015625-.4296875 0-.88085938.19335938-1.31054688.6015625L10.52734375 25.84570313Z", + svg_size: Size::new(57.0, 53.0), op: PaintOp::Fill, }; // SF Pro Regular - mic.circle From 15c888e28e3f6ebb7aaa70f5fb72d87331d83d80 Mon Sep 17 00:00:00 2001 From: Jackson Goode Date: Mon, 23 Sep 2024 13:42:37 -0700 Subject: [PATCH 28/30] Filled house icon, vertical space in title separators, trim more chars to keep to 3 lines, decrease height of widget, linting --- psst-gui/src/data/mod.rs | 2 +- psst-gui/src/ui/album.rs | 68 ++++--- psst-gui/src/ui/artist.rs | 52 +++-- psst-gui/src/ui/home.rs | 73 ++++--- psst-gui/src/ui/playable.rs | 4 +- psst-gui/src/ui/playlist.rs | 56 +++--- psst-gui/src/ui/search.rs | 6 +- psst-gui/src/ui/show.rs | 350 +++++++++++++++++----------------- psst-gui/src/webapi/client.rs | 340 +++++++++++++++++++++------------ 9 files changed, 542 insertions(+), 409 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index eb05f4bb..685b4e81 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -56,7 +56,7 @@ pub use crate::data::{ show::{Episode, EpisodeId, EpisodeLink, Show, ShowDetail, ShowEpisodes, ShowLink}, slider_scroll_scale::SliderScrollScale, track::{AudioAnalysis, Track, TrackId}, - user::{UserProfile, PublicUser}, + user::{PublicUser, UserProfile}, utils::{Cached, Float64, Image, Page}, }; diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 8ec47b97..6897a60d 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use druid::{ - widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List, ViewSwitcher}, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt + widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List, ViewSwitcher}, + LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt, }; use crate::{ cmd, data::{ - Album, AlbumDetail, AlbumLink, AppState, ArtistLink, Cached, Ctx, Library, Nav, WithCtx + Album, AlbumDetail, AlbumLink, AppState, ArtistLink, Cached, Ctx, Library, Nav, WithCtx, }, webapi::WebApi, widget::{icons, Async, MyWidgetExt, RemoteImage}, @@ -99,9 +100,13 @@ fn rounded_cover_widget(size: f64) -> impl Widget> { } pub fn album_widget(horizontal: bool) -> impl Widget>> { - let (album_cover_size, album_name_layout) = if horizontal { (16.0, Flex::column()) } else { (6.0, Flex::row()) }; + let (album_cover_size, album_name_layout) = if horizontal { + (16.0, Flex::column()) + } else { + (6.0, Flex::row()) + }; let album_cover = rounded_cover_widget(theme::grid(album_cover_size)); - + let album_name = album_name_layout .with_child( Label::raw() @@ -117,51 +122,56 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { false => Box::new(Flex::column()), }, )); - + let album_artists = List::new(|| { Label::raw() .with_text_size(theme::TEXT_SIZE_SMALL) .with_line_break_mode(LineBreaking::WordWrap) .lens(ArtistLink::name) - }) - .horizontal() - .with_spacing(theme::grid(1.0)) - .lens(Album::artists.in_arc()); - + }) + .horizontal() + .with_spacing(theme::grid(1.0)) + .lens(Album::artists.in_arc()); + let album_date = Label::>::dynamic(|album, _| album.release_year()) .with_line_break_mode(LineBreaking::WordWrap) .with_text_size(theme::TEXT_SIZE_SMALL) .with_text_color(theme::PLACEHOLDER_COLOR); - + let album_layout = if horizontal { Flex::column() .with_child(album_cover) .with_default_spacer() - .with_child(Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(album_name) - .with_spacer(1.0) - .with_child(album_artists) - .with_spacer(1.0) - .with_child(album_date) - .align_horizontal(UnitPoint::CENTER) - .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(8.5))) + .with_child( + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(album_name) + .with_spacer(1.0) + .with_child(album_artists) + .with_spacer(1.0) + .with_child(album_date) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(7.0)), + ) .align_left() } else { Flex::row() .with_child(album_cover) .with_default_spacer() - .with_flex_child(Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(album_name) - .with_spacer(1.0) - .with_child(album_artists) - .with_spacer(1.0) - .with_child(album_date), 1.0) + .with_flex_child( + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(album_name) + .with_spacer(1.0) + .with_child(album_artists) + .with_spacer(1.0) + .with_child(album_date), + 1.0, + ) .align_left() }; - + album_layout .padding(theme::grid(1.0)) .lens(Ctx::data()) diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index acae643d..c4f8b621 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -1,11 +1,15 @@ use druid::{ - im::Vector, kurbo::Circle, widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, UnitPoint, Widget, WidgetExt + im::Vector, + kurbo::Circle, + widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List}, + Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, UnitPoint, Widget, WidgetExt, }; use crate::{ cmd, data::{ - AppState, Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks, Cached, Ctx, Nav, WithCtx + AppState, Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks, Cached, Ctx, Nav, + WithCtx, }, webapi::WebApi, widget::{Async, MyWidgetExt, RemoteImage}, @@ -79,28 +83,38 @@ fn async_related_widget() -> impl Widget { } pub fn artist_widget(horizontal: bool) -> impl Widget { - let ( mut artist, artist_image) = if horizontal { (Flex::column(), cover_widget(theme::grid(16.0))) } else { (Flex::row(), cover_widget(theme::grid(6.0))) }; + let (mut artist, artist_image) = if horizontal { + (Flex::column(), cover_widget(theme::grid(16.0))) + } else { + (Flex::row(), cover_widget(theme::grid(6.0))) + }; artist = if horizontal { artist .with_child(artist_image) .with_default_spacer() - .with_child(Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .align_horizontal(UnitPoint::CENTER) - .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(8.5)) - .lens(Artist::name)) + .with_child( + Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(7.0)) + .lens(Artist::name), + ) } else { artist - .with_child(artist_image) + .with_child(artist_image) .with_default_spacer() - .with_flex_child(Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .lens(Artist::name), 1.0) + .with_flex_child( + Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .lens(Artist::name), + 1.0, + ) }; - artist.padding(theme::grid(1.0)) + artist + .padding(theme::grid(1.0)) .link() .rounded(theme::BUTTON_BORDER_RADIUS) .on_left_click(|ctx, _, artist, _| { @@ -150,9 +164,13 @@ fn albums_widget() -> impl Widget> { .with_child(header_widget("Singles")) .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::singles))) .with_child(header_widget("Compilations")) - .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::compilations))) + .with_child( + List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::compilations)), + ) .with_child(header_widget("Appears On")) - .with_child(List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::appears_on))) + .with_child( + List::new(|| album::album_widget(false)).lens(Ctx::map(ArtistAlbums::appears_on)), + ) } fn related_widget() -> impl Widget>> { @@ -182,4 +200,4 @@ fn artist_menu(artist: &ArtistLink) -> Menu { ); menu -} \ No newline at end of file +} diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index bfc4eef0..91022290 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -193,15 +193,16 @@ fn loaded_results_widget() -> impl Widget> { && results.data.shows.is_empty() }, Empty, - Flex::column() - .with_child(title_label()) - .with_child(Scroll::new(Flex::row() - .with_child(playlist_results_widget()) - .with_child(album_results_widget()) - .with_child(artist_results_widget()) - .with_child(show_results_widget())) - .align_left(), + Flex::column().with_child(title_label()).with_child( + Scroll::new( + Flex::row() + .with_child(playlist_results_widget()) + .with_child(album_results_widget()) + .with_child(artist_results_widget()) + .with_child(show_results_widget()), ) + .align_left(), + ), ) } @@ -211,13 +212,15 @@ fn title_label() -> impl Widget> { Empty, Flex::column() .with_default_spacer() - .with_child(Label::raw() - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)),) + .with_child( + Label::raw() + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), theme::grid(0.5))), + ) .with_default_spacer() - .align_left() - ) + .align_left(), + ) .lens(Ctx::data().then(MixedView::title)) } @@ -225,10 +228,9 @@ fn artist_results_widget() -> impl Widget> { Either::new( |artists: &Vector, _| artists.is_empty(), Empty, - Scroll::new( - List::new(|| artist::artist_widget(true)).horizontal(), - ) - .horizontal().align_left(), + Scroll::new(List::new(|| artist::artist_widget(true)).horizontal()) + .horizontal() + .align_left(), ) .lens(Ctx::data().then(MixedView::artists)) } @@ -238,13 +240,12 @@ fn album_results_widget() -> impl Widget> { |playlists: &WithCtx, _| playlists.data.albums.is_empty(), Empty, Flex::column().with_child( - Scroll::new( - List::new(|| album::album_widget(true)).horizontal(), - ) - .horizontal() - .align_left() - .lens(Ctx::map(MixedView::albums)), - )) + Scroll::new(List::new(|| album::album_widget(true)).horizontal()) + .horizontal() + .align_left() + .lens(Ctx::map(MixedView::albums)), + ), + ) } fn playlist_results_widget() -> impl Widget> { @@ -252,12 +253,10 @@ fn playlist_results_widget() -> impl Widget> { |playlists: &WithCtx, _| playlists.data.playlists.is_empty(), Empty, Flex::column().with_child( - Scroll::new( - List::new(|| playlist::playlist_widget(true)).horizontal(), - ) - .horizontal() - .align_left() - .lens(Ctx::map(MixedView::playlists)), + Scroll::new(List::new(|| playlist::playlist_widget(true)).horizontal()) + .horizontal() + .align_left() + .lens(Ctx::map(MixedView::playlists)), ), ) } @@ -267,10 +266,7 @@ fn show_results_widget() -> impl Widget> { |shows: &WithCtx>>, _| shows.data.is_empty(), Empty, Flex::column().with_child( - Scroll::new( - List::new(|| show::show_widget(true)).horizontal(), - ) - .align_left() + Scroll::new(List::new(|| show::show_widget(true)).horizontal()).align_left(), ), ) .lens(Ctx::map(MixedView::shows)) @@ -279,12 +275,7 @@ fn show_results_widget() -> impl Widget> { fn user_top_artists_widget() -> impl Widget { Async::new( spinner_widget, - || { - Scroll::new( - List::new(|| artist::artist_widget(true)).horizontal(), - ) - .horizontal() - }, + || Scroll::new(List::new(|| artist::artist_widget(true)).horizontal()).horizontal(), error_widget, ) .lens(AppState::home_detail.then(HomeDetail::user_top_artists)) diff --git a/psst-gui/src/ui/playable.rs b/psst-gui/src/ui/playable.rs index 1dff8536..caf11270 100644 --- a/psst-gui/src/ui/playable.rs +++ b/psst-gui/src/ui/playable.rs @@ -12,7 +12,9 @@ use druid::{ use crate::{ cmd, data::{ - Album, ArtistTracks, CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, PlaybackPayload, PlaylistTracks, Recommendations, SavedTracks, SearchResults, ShowEpisodes, Track, WithCtx + Album, ArtistTracks, CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, + PlaybackPayload, PlaylistTracks, Recommendations, SavedTracks, SearchResults, ShowEpisodes, + Track, WithCtx, }, ui::theme, }; diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index e3267a6b..72e21426 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -320,7 +320,11 @@ fn information_section(title_msg: &str, description_msg: &str) -> impl Widget impl Widget> { - let playlist_image_size = if horizontal { theme::grid(16.0) } else { theme::grid(6.0) }; + let playlist_image_size = if horizontal { + theme::grid(16.0) + } else { + theme::grid(6.0) + }; let playlist_image = rounded_cover_widget(playlist_image_size).lens(Ctx::data()); let playlist_name = Label::raw() @@ -335,39 +339,47 @@ pub fn playlist_widget(horizontal: bool) -> impl Widget> { .lens(Ctx::data().then(Playlist::description)); let (playlist_name, playlist_description) = if horizontal { - (playlist_name - .fix_width(playlist_image_size) - .align_left(), - playlist_description - .fix_width(playlist_image_size) - .align_left()) + ( + playlist_name.fix_width(playlist_image_size).align_left(), + playlist_description + .fix_width(playlist_image_size) + .align_left(), + ) } else { - (playlist_name.align_left(), playlist_description.align_left()) + ( + playlist_name.align_left(), + playlist_description.align_left(), + ) }; let playlist = if horizontal { - Flex::column() + Flex::column() .with_child(playlist_image) .with_default_spacer() - .with_child(Flex::column() - .with_child(playlist_name) - .with_spacer(2.0) - .with_child(playlist_description) - .align_horizontal(UnitPoint::CENTER) - .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(8.5))) + .with_child( + Flex::column() + .with_child(playlist_name) + .with_spacer(2.0) + .with_child(playlist_description) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(7.0)), + ) .padding(theme::grid(1.0)) } else { - Flex::row() + Flex::row() .with_child(playlist_image) .with_default_spacer() - .with_flex_child(Flex::column() - .with_child(playlist_name) - .with_spacer(2.0) - .with_child(playlist_description), 1.0) + .with_flex_child( + Flex::column() + .with_child(playlist_name) + .with_spacer(2.0) + .with_child(playlist_description), + 1.0, + ) .padding(theme::grid(1.0)) }; - + playlist .link() .rounded(theme::BUTTON_BORDER_RADIUS) diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index d3754196..b5245db6 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -10,7 +10,8 @@ use crate::{ cmd, controller::InputController, data::{ - Album, AppState, Artist, Ctx, Nav, Search, SearchResults, SearchTopic, Show, SpotifyUrl, WithCtx + Album, AppState, Artist, Ctx, Nav, Search, SearchResults, SearchTopic, Show, SpotifyUrl, + WithCtx, }, ui::show, webapi::WebApi, @@ -141,7 +142,8 @@ fn playlist_results_widget() -> impl Widget> { Flex::column() .with_child(header_widget("Playlists")) .with_child( - List::new(|| playlist::playlist_widget(false)).lens(Ctx::map(SearchResults::playlists)), + List::new(|| playlist::playlist_widget(false)) + .lens(Ctx::map(SearchResults::playlists)), ), ) } diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index bcef25e0..88bfd713 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -1,172 +1,178 @@ -use std::sync::Arc; - -use druid::{ - widget::{CrossAxisAlignment, Flex, Label, LineBreaking}, LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt -}; - -use crate::{ - cmd, - data::{AppState, Ctx, Library, Nav, Show, ShowDetail, ShowEpisodes, ShowLink, WithCtx}, - webapi::WebApi, - widget::{Async, MyWidgetExt, RemoteImage}, -}; - -use super::{library, playable, theme, track, utils}; - -pub const LOAD_DETAIL: Selector = Selector::new("app.show.load-detail"); - -pub fn detail_widget() -> impl Widget { - Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - // .with_child(async_info_widget()) - // .with_default_spacer() - .with_child(async_episodes_widget()) -} - -// fn async_info_widget() -> impl Widget { -// Async::new(utils::spinner_widget, info_widget, utils::error_widget) -// .lens( -// Ctx::make( -// AppState::common_ctx, -// AppState::show_detail.then(ShowDetail::show), -// ) -// .then(Ctx::in_promise()), -// ) -// .on_command_async( -// LOAD_DETAIL, -// |d| WebApi::global().get_show(&d.id), -// |_, data, d| data.show_detail.show.defer(d), -// |_, data, (d, r)| data.show_detail.show.update((d, r)), -// ) -// } - -// fn info_widget() -> impl Widget>> { -// Label::raw().lens(Ctx::data().then(Show::description.in_arc())) -// } - -fn async_episodes_widget() -> impl Widget { - Async::new( - utils::spinner_widget, - || { - playable::list_widget(playable::Display { - track: track::Display::empty(), - }) - }, - utils::error_widget, - ) - .lens( - Ctx::make( - AppState::common_ctx, - AppState::show_detail.then(ShowDetail::episodes), - ) - .then(Ctx::in_promise()), - ) - .on_command_async( - LOAD_DETAIL, - |d| WebApi::global().get_show_episodes(&d.id), - |_, data, d| data.show_detail.episodes.defer(d), - |_, data, (d, r)| { - let r = r.map(|episodes| ShowEpisodes { - show: d.clone(), - episodes, - }); - data.show_detail.episodes.update((d, r)) - }, - ) -} - -pub fn show_widget(horizontal: bool) -> impl Widget>> { - let image_size = theme::grid(if horizontal { 16.0 } else { 6.0 }); - let show_image = rounded_cover_widget(image_size); - - let show_name = Label::raw() - .with_font(theme::UI_FONT_MEDIUM) - .with_line_break_mode(LineBreaking::Clip) - .lens(Show::name.in_arc()) - .align_left(); - - let show_publisher = Label::raw() - .with_line_break_mode(LineBreaking::Clip) - .with_text_size(theme::TEXT_SIZE_SMALL) - .with_text_color(theme::PLACEHOLDER_COLOR) - .lens(Show::publisher.in_arc()) - .align_left(); - - let show = if horizontal { - Flex::column() - .with_child(show_image) - .with_default_spacer() - .with_child(Flex::column() - .with_child(show_name) - .with_child(show_publisher) - .align_horizontal(UnitPoint::CENTER) - .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(8.5))) - .padding(theme::grid(1.0)) - .lens(Ctx::data()) - } else { - Flex::row() - .with_child(show_image) - .with_default_spacer() - .with_flex_child(Flex::column() - .with_child(show_name) - .with_child(show_publisher), 1.0) - .padding(theme::grid(1.0)) - .lens(Ctx::data()) - }; - - show.align_left() - .link() - .rounded(theme::BUTTON_BORDER_RADIUS) - .on_left_click(|ctx, _, show, _| { - ctx.submit_command(cmd::NAVIGATE.with(Nav::ShowDetail(show.data.link()))); - }) - .context_menu(show_ctx_menu) -} - -fn cover_widget(size: f64) -> impl Widget> { - RemoteImage::new(utils::placeholder_widget(), move |show: &Arc, _| { - show.image(size, size).map(|image| image.url.clone()) - }) - .fix_size(size, size) -} - -fn rounded_cover_widget(size: f64) -> impl Widget> { - // TODO: Take the radius from theme. - cover_widget(size).clip(Size::new(size, size).to_rounded_rect(4.0)) -} - -fn show_ctx_menu(show: &WithCtx>) -> Menu { - show_menu(&show.data, &show.ctx.library) -} - -fn show_menu(show: &Arc, library: &Arc) -> Menu { - let mut menu = Menu::empty(); - - menu = menu.entry( - MenuItem::new( - LocalizedString::new("menu-item-copy-link").with_placeholder("Copy Link to Show"), - ) - .command(cmd::COPY.with(show.link().url())), - ); - - menu = menu.separator(); - - if library.contains_show(show) { - menu = menu.entry( - MenuItem::new( - LocalizedString::new("menu-item-remove-from-library").with_placeholder("Unfollow"), - ) - .command(library::UNSAVE_SHOW.with(show.link())), - ); - } else { - menu = menu.entry( - MenuItem::new( - LocalizedString::new("menu-item-save-to-library").with_placeholder("Follow"), - ) - .command(library::SAVE_SHOW.with(show.clone())), - ); - } - - menu -} +use std::sync::Arc; + +use druid::{ + widget::{CrossAxisAlignment, Flex, Label, LineBreaking}, + LensExt, LocalizedString, Menu, MenuItem, Selector, Size, UnitPoint, Widget, WidgetExt, +}; + +use crate::{ + cmd, + data::{AppState, Ctx, Library, Nav, Show, ShowDetail, ShowEpisodes, ShowLink, WithCtx}, + webapi::WebApi, + widget::{Async, MyWidgetExt, RemoteImage}, +}; + +use super::{library, playable, theme, track, utils}; + +pub const LOAD_DETAIL: Selector = Selector::new("app.show.load-detail"); + +pub fn detail_widget() -> impl Widget { + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + // .with_child(async_info_widget()) + // .with_default_spacer() + .with_child(async_episodes_widget()) +} + +// fn async_info_widget() -> impl Widget { +// Async::new(utils::spinner_widget, info_widget, utils::error_widget) +// .lens( +// Ctx::make( +// AppState::common_ctx, +// AppState::show_detail.then(ShowDetail::show), +// ) +// .then(Ctx::in_promise()), +// ) +// .on_command_async( +// LOAD_DETAIL, +// |d| WebApi::global().get_show(&d.id), +// |_, data, d| data.show_detail.show.defer(d), +// |_, data, (d, r)| data.show_detail.show.update((d, r)), +// ) +// } + +// fn info_widget() -> impl Widget>> { +// Label::raw().lens(Ctx::data().then(Show::description.in_arc())) +// } + +fn async_episodes_widget() -> impl Widget { + Async::new( + utils::spinner_widget, + || { + playable::list_widget(playable::Display { + track: track::Display::empty(), + }) + }, + utils::error_widget, + ) + .lens( + Ctx::make( + AppState::common_ctx, + AppState::show_detail.then(ShowDetail::episodes), + ) + .then(Ctx::in_promise()), + ) + .on_command_async( + LOAD_DETAIL, + |d| WebApi::global().get_show_episodes(&d.id), + |_, data, d| data.show_detail.episodes.defer(d), + |_, data, (d, r)| { + let r = r.map(|episodes| ShowEpisodes { + show: d.clone(), + episodes, + }); + data.show_detail.episodes.update((d, r)) + }, + ) +} + +pub fn show_widget(horizontal: bool) -> impl Widget>> { + let image_size = theme::grid(if horizontal { 16.0 } else { 6.0 }); + let show_image = rounded_cover_widget(image_size); + + let show_name = Label::raw() + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::Clip) + .lens(Show::name.in_arc()) + .align_left(); + + let show_publisher = Label::raw() + .with_line_break_mode(LineBreaking::Clip) + .with_text_size(theme::TEXT_SIZE_SMALL) + .with_text_color(theme::PLACEHOLDER_COLOR) + .lens(Show::publisher.in_arc()) + .align_left(); + + let show = if horizontal { + Flex::column() + .with_child(show_image) + .with_default_spacer() + .with_child( + Flex::column() + .with_child(show_name) + .with_child(show_publisher) + .align_horizontal(UnitPoint::CENTER) + .align_vertical(UnitPoint::TOP) + .fix_size(theme::grid(16.0), theme::grid(7.0)), + ) + .padding(theme::grid(1.0)) + .lens(Ctx::data()) + } else { + Flex::row() + .with_child(show_image) + .with_default_spacer() + .with_flex_child( + Flex::column() + .with_child(show_name) + .with_child(show_publisher), + 1.0, + ) + .padding(theme::grid(1.0)) + .lens(Ctx::data()) + }; + + show.align_left() + .link() + .rounded(theme::BUTTON_BORDER_RADIUS) + .on_left_click(|ctx, _, show, _| { + ctx.submit_command(cmd::NAVIGATE.with(Nav::ShowDetail(show.data.link()))); + }) + .context_menu(show_ctx_menu) +} + +fn cover_widget(size: f64) -> impl Widget> { + RemoteImage::new(utils::placeholder_widget(), move |show: &Arc, _| { + show.image(size, size).map(|image| image.url.clone()) + }) + .fix_size(size, size) +} + +fn rounded_cover_widget(size: f64) -> impl Widget> { + // TODO: Take the radius from theme. + cover_widget(size).clip(Size::new(size, size).to_rounded_rect(4.0)) +} + +fn show_ctx_menu(show: &WithCtx>) -> Menu { + show_menu(&show.data, &show.ctx.library) +} + +fn show_menu(show: &Arc, library: &Arc) -> Menu { + let mut menu = Menu::empty(); + + menu = menu.entry( + MenuItem::new( + LocalizedString::new("menu-item-copy-link").with_placeholder("Copy Link to Show"), + ) + .command(cmd::COPY.with(show.link().url())), + ); + + menu = menu.separator(); + + if library.contains_show(show) { + menu = menu.entry( + MenuItem::new( + LocalizedString::new("menu-item-remove-from-library").with_placeholder("Unfollow"), + ) + .command(library::UNSAVE_SHOW.with(show.link())), + ); + } else { + menu = menu.entry( + MenuItem::new( + LocalizedString::new("menu-item-save-to-library").with_placeholder("Follow"), + ) + .command(library::SAVE_SHOW.with(show.clone())), + ); + } + + menu +} diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 69a84bde..56e8774f 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -1,26 +1,36 @@ use std::{ - fmt::Display, io::{self, Read}, path::PathBuf, sync::Arc, thread, time::Duration + fmt::Display, + io::{self, Read}, + path::PathBuf, + sync::Arc, + thread, + time::Duration, }; use druid::{ - im::Vector, image::{self, ImageFormat}, Data, ImageBuf + im::Vector, + image::{self, ImageFormat}, + Data, ImageBuf, }; use itertools::Itertools; use once_cell::sync::OnceCell; use parking_lot::Mutex; +use sanitize_html::rules::predefined::DEFAULT; +use sanitize_html::sanitize_str; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::json; use ureq::{Agent, Request, Response}; -use sanitize_html::sanitize_str; -use sanitize_html::rules::predefined::DEFAULT; use psst_core::{ - session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder + session::{access_token::TokenProvider, SessionService}, + util::default_ureq_agent_builder, }; use crate::{ data::{ - self, Album, AlbumType, Artist, ArtistAlbums, ArtistLink, AudioAnalysis, Cached, Episode, EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile + self, Album, AlbumType, Artist, ArtistAlbums, ArtistLink, AudioAnalysis, Cached, Episode, + EpisodeId, EpisodeLink, MixedView, Nav, Page, Playlist, PublicUser, Range, Recommendations, + RecommendationsRequest, SearchResults, SearchTopic, Show, SpotifyUrl, Track, UserProfile, }, error::Error, }; @@ -62,8 +72,9 @@ impl WebApi { Ok(token.token) } - fn build_request(&self, - method: &str, + fn build_request( + &self, + method: &str, base_url: &str, path: impl Display, ) -> Result { @@ -80,7 +91,8 @@ impl WebApi { } fn get(&self, path: impl Display, base_url: Option<&str>) -> Result { - self.request("GET", base_url.unwrap_or("api.spotify.com"), path) } + self.request("GET", base_url.unwrap_or("api.spotify.com"), path) + } fn put(&self, path: impl Display, base_url: Option<&str>) -> Result { self.request("GET", base_url.unwrap_or("api.spotify.com"), path) @@ -211,14 +223,14 @@ impl WebApi { .clone() .query("limit", &limit.to_string()) .query("offset", &offset.to_string()); - + let page: Page = self.load(req)?; - - let page_total = limit/lim; + + let page_total = limit / lim; let page_offset = page.offset; let page_limit = page.limit; func(page)?; - + if page_total > offset && offset < self.paginated_limit { limit = page_limit; offset = page_offset + page_limit; @@ -346,13 +358,13 @@ impl WebApi { cover_art: Option, publisher: Option, } - + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Visuals { avatar_image: CoverArt, } - + #[derive(Deserialize)] pub struct Artists { items: Vec, @@ -431,110 +443,178 @@ impl WebApi { // Extract the playlists let result: Welcome = self.load(request)?; - + let mut title: Arc = Arc::from(""); let mut playlist: Vector = Vector::new(); let mut album: Vector> = Vector::new(); let mut artist: Vector = Vector::new(); let mut show: Vector> = Vector::new(); - result.data.home_sections.sections - .iter() - .for_each(|section| { - title = section.data.title.text.clone().into(); - - section.section_items.items.iter().for_each(|item| { - let uri = item.content.data.uri.clone(); - let id = uri.split(':').last().unwrap_or("").to_string(); - - match item.content.data.typename { - DataTypename::Playlist => { - playlist.push_back(Playlist { + result + .data + .home_sections + .sections + .iter() + .for_each(|section| { + title = section.data.title.text.clone().into(); + + section.section_items.items.iter().for_each(|item| { + let uri = item.content.data.uri.clone(); + let id = uri.split(':').last().unwrap_or("").to_string(); + + match item.content.data.typename { + DataTypename::Playlist => { + playlist.push_back(Playlist { + id: id.into(), + name: Arc::from(item.content.data.name.clone().unwrap()), + images: Some(item.content.data.images.as_ref().map_or_else( + Vector::new, + |images| { + images + .items + .iter() + .map(|img| data::utils::Image { + url: Arc::from( + img.sources + .first() + .map(|s| s.url.as_str()) + .unwrap_or_default(), + ), + width: None, + height: None, + }) + .collect() + }, + )), + description: { + let desc = sanitize_str( + &DEFAULT, + item.content + .data + .description + .as_deref() + .unwrap_or_default(), + ) + .unwrap_or_default(); + // This is roughly 3 lines of description, truncated if too long + if desc.chars().count() > 55 { + desc.chars().take(52).collect::() + "..." + } else { + desc + } + .into() + }, + track_count: item.content.data.attributes.as_ref().and_then( + |attrs| { + attrs + .iter() + .find(|attr| attr.key == "track_count") + .and_then(|attr| attr.value.parse().ok()) + }, + ), + owner: PublicUser { + id: Arc::from(""), + display_name: item + .content + .data + .owner_v2 + .as_ref() + .map(|owner| Arc::from(owner.data.name.as_str())) + .unwrap_or_else(|| Arc::from("")), + }, + collaborative: false, + }); + } + DataTypename::Artist => artist.push_back(Artist { id: id.into(), - name: Arc::from(item.content.data.name.clone().unwrap()), - images: Some(item.content.data.images.as_ref().map_or_else(Vector::new, |images| - images.items.iter().map(|img| data::utils::Image { - url: Arc::from(img.sources.first().map(|s| s.url.as_str()).unwrap_or_default()), - width: None, - height: None, - }).collect() - )), - description: { - let desc = sanitize_str(&DEFAULT, item.content.data.description.as_deref().unwrap_or_default()).unwrap_or_default(); - // This is roughly 3 lines of description, truncated if too long - if desc.chars().count() > 65 { - desc.chars().take(62).collect::() + "..." - } else { - desc - }.into() - }, - track_count: item.content.data.attributes.as_ref().and_then(|attrs| - attrs.iter().find(|attr| attr.key == "track_count").and_then(|attr| attr.value.parse().ok()) + name: Arc::from( + item.content.data.profile.as_ref().unwrap().name.clone(), ), - owner: PublicUser { - id: Arc::from(""), - display_name: item.content.data.owner_v2.as_ref() - .map(|owner| Arc::from(owner.data.name.as_str())) - .unwrap_or_else(|| Arc::from("")), - }, - collaborative: false, - }); - }, - DataTypename::Artist => { - artist.push_back(Artist { - id: id.into(), - name: Arc::from(item.content.data.profile.as_ref().unwrap().name.clone()), - images: item.content.data.visuals.as_ref().map_or_else(Vector::new, |images| - images.avatar_image.sources.iter().map(|img| data::utils::Image { - url: Arc::from(img.url.as_str()), - width: None, - height: None, - }).collect() + images: item.content.data.visuals.as_ref().map_or_else( + Vector::new, + |images| { + images + .avatar_image + .sources + .iter() + .map(|img| data::utils::Image { + url: Arc::from(img.url.as_str()), + width: None, + height: None, + }) + .collect() + }, ), - }) - }, - DataTypename::Album => { - album.push_back(Arc::new(Album { + }), + DataTypename::Album => album.push_back(Arc::new(Album { id: id.into(), name: Arc::from(item.content.data.name.clone().unwrap()), album_type: AlbumType::Album, - images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| - images.sources.iter().map(|src| data::utils::Image { - url: Arc::from(src.url.clone()), - width: None, - height: None, - }).collect() + images: item.content.data.cover_art.as_ref().map_or_else( + Vector::new, + |images| { + images + .sources + .iter() + .map(|src| data::utils::Image { + url: Arc::from(src.url.clone()), + width: None, + height: None, + }) + .collect() + }, + ), + artists: item.content.data.artists.as_ref().map_or_else( + Vector::new, + |artists| { + artists + .items + .iter() + .map(|artist| ArtistLink { + id: Arc::from( + artist + .uri + .split(':') + .last() + .unwrap_or("") + .to_string(), + ), + name: Arc::from(artist.profile.name.clone()), + }) + .collect() + }, ), - artists: item.content.data.artists.as_ref().map_or_else(Vector::new, |artists| - artists.items.iter().map(|artist| ArtistLink { - id: Arc::from(artist.uri.split(':').last().unwrap_or("").to_string()), - name: Arc::from(artist.profile.name.clone()), - }).collect()), copyrights: Vector::new(), label: "".into(), tracks: Vector::new(), release_date: None, release_date_precision: None, - })) - }, - DataTypename::Podcast => { - show.push_back(Arc::new(Show { + })), + DataTypename::Podcast => show.push_back(Arc::new(Show { id: id.into(), name: Arc::from(item.content.data.name.clone().unwrap()), - images: item.content.data.cover_art.as_ref().map_or_else(Vector::new, |images| - images.sources.iter().map(|src| data::utils::Image { - url: Arc::from(src.url.clone()), - width: None, - height: None, - }).collect() + images: item.content.data.cover_art.as_ref().map_or_else( + Vector::new, + |images| { + images + .sources + .iter() + .map(|src| data::utils::Image { + url: Arc::from(src.url.clone()), + width: None, + height: None, + }) + .collect() + }, + ), + publisher: Arc::from( + item.content.data.publisher.as_ref().unwrap().name.clone(), ), - publisher: Arc::from(item.content.data.publisher.as_ref().unwrap().name.clone()), description: "".into(), - })) + })), } - } + }); }); - }); Ok(MixedView { title, @@ -572,7 +652,8 @@ impl WebApi { // https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks pub fn get_user_top_tracks(&self) -> Result>, Error> { - let request = self.get("v1/me/top/tracks", None)? + let request = self + .get("v1/me/top/tracks", None)? .query("market", "from_token"); let result: Vector> = self.load_some_pages(request, 30)?; @@ -751,7 +832,9 @@ impl WebApi { album: Arc, } - let request = self.get("v1/me/albums", None)?.query("market", "from_token"); + let request = self + .get("v1/me/albums", None)? + .query("market", "from_token"); Ok(self .load_all_pages(request)? @@ -781,7 +864,9 @@ impl WebApi { track: Arc, } - let request = self.get("v1/me/tracks", None)?.query("market", "from_token"); + let request = self + .get("v1/me/tracks", None)? + .query("market", "from_token"); Ok(self .load_all_pages(request)? @@ -835,7 +920,6 @@ impl WebApi { } } - /// View endpoints. impl WebApi { pub fn get_user_info(&self) -> Result<(String, String), Error> { @@ -852,7 +936,7 @@ impl WebApi { .set("Authorization", &format!("Bearer {}", &token)); let result: Cached = self.load_cached(request, "User_info", "usrinfo")?; - + Ok((result.data.region.clone(), result.data.timezone.clone())) } @@ -864,7 +948,7 @@ impl WebApi { "sha256Hash": "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be" } }); - + let variables = json!( { "uri": section_uri, "timeZone": self.get_user_info().unwrap().0, @@ -883,86 +967,92 @@ impl WebApi { pub fn get_made_for_you(&self) -> Result { // 0JQ5DAUnp4wcj0bCb3wh3S -> Daily mixes let json_query = self.build_home_request("spotify:section:0JQ5DAUnp4wcj0bCb3wh3S"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } - + pub fn get_top_mixes(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu89 -> Top mixes let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu89"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } pub fn recommended_stations(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3R -> Recommended stations let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3R"); - - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } pub fn uniquely_yours(&self) -> Result { // 0JQ5DAqAJXkJGsa2DyEjKi -> Uniquely yours let json_query = self.build_home_request("spotify:section:0JQ5DAqAJXkJGsa2DyEjKi"); - - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } pub fn best_of_artists(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3n -> Best of artists let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3n"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); let result = self.load_and_return_home_section(request)?; - + Ok(result) } - + // Need to make a mix of it! pub fn jump_back_in(&self) -> Result { // 0JQ5DAIiKWzVFULQfUm85X -> Jump back in let json_query = self.build_home_request("spotify:section:0JQ5DAIiKWzVFULQfUm85X"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } @@ -970,30 +1060,32 @@ impl WebApi { pub fn your_shows(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3N -> Your shows let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3N"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); let result = self.load_and_return_home_section(request)?; - + Ok(result) } pub fn shows_that_you_might_like(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3P -> Shows that you might like let json_query = self.build_home_request("spotify:section:0JQ5DAnM3wGh0gz1MXnu3P"); - let request = self.get("pathfinder/v1/query", Some("api-partner.spotify.com"))? + let request = self + .get("pathfinder/v1/query", Some("api-partner.spotify.com"))? .query("operationName", "homeSection") .query("variables", &json_query.0.to_string()) .query("extensions", &json_query.1.to_string()); let result = self.load_and_return_home_section(request)?; - + Ok(result) } - /* + /* // TODO: Episodes for you, implement this to redesign the podcast page pub fn new_episodes(&self) -> Result { // 0JQ5DAnM3wGh0gz1MXnu3K -> New episodes @@ -1005,7 +1097,7 @@ impl WebApi { // Extract the playlists let result = self.load_and_return_home_section(request)?; - + Ok(result) } @@ -1302,4 +1394,4 @@ impl From for Error { fn from(err: image::ImageError) -> Self { Error::WebApiError(err.to_string()) } -} \ No newline at end of file +} From 63fb9e0ea2d7b6e8885e95590085bfc6762cbf3a Mon Sep 17 00:00:00 2001 From: Jackson Goode Date: Mon, 23 Sep 2024 13:43:58 -0700 Subject: [PATCH 29/30] Update name --- psst-gui/src/widget/icons.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psst-gui/src/widget/icons.rs b/psst-gui/src/widget/icons.rs index 18fe97ae..23956f61 100644 --- a/psst-gui/src/widget/icons.rs +++ b/psst-gui/src/widget/icons.rs @@ -131,7 +131,7 @@ pub static PLAYLIST: SvgIcon = SvgIcon { svg_size: Size::new(22.0, 22.0), op: PaintOp::Fill, }; -// SFSymbols - house +// SFSymbols - house.fill pub static HOME: SvgIcon = SvgIcon { svg_path: "M5.47851563 25.97460938c0 .23632812.171875.4296875.47265624.4296875.23632813 0 .34375-.10742188.47265626-.23632813L27.54296874 6.89648437c.32226563-.30078124.6875-.45117187 1.05273438-.45117187.32226562 0 .6875.171875.98828124.45117188L50.703125 26.16796874c.12890625.12890625.23632813.23632813.47265625.23632813.27929688 0 .47265625-.19335938.47265625-.4296875 0-.19335938-.06445313-.30078126-.19335938-.4296875l-5.95117187-5.45703126V6.12304689c0-.45117188-.32226563-.75195313-.75195313-.75195313h-.90234375c-.4296875 0-.75195312.30078125-.75195312.75195313v11.7734375L30.3359375 6.16601561c-.55859375-.49414062-1.18164063-.73046874-1.8046875-.73046874-.6015625 0-1.203125.23632812-1.74023438.73046874L5.671875 25.54492189c-.15039063.12890624-.19335938.23632812-.19335938.4296875ZM10.52734374 44.515625c0 2.25585938 1.33203125 3.58789063 3.609375 3.58789063h9.13085938V33c0-.70898438.47265624-1.16015625 1.18164062-1.16015625h8.22851563c.70898437 0 1.16015625.45117188 1.16015625 1.16015625v15.10351563h9.15234375c2.25585937 0 3.58789062-1.33203125 3.58789062-3.58789063V25.28710937L29.79882812 10.44140626c-.47265624-.40820313-.90234374-.6015625-1.33203124-.6015625-.4296875 0-.88085938.19335938-1.31054688.6015625L10.52734375 25.84570313Z", svg_size: Size::new(57.0, 53.0), From 7d5c5c753952be6b769a1c018929c20e14870804 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 23 Sep 2024 22:03:11 +0100 Subject: [PATCH 30/30] Lift size by one to encounter scroll bar hight --- psst-gui/src/ui/album.rs | 2 +- psst-gui/src/ui/artist.rs | 2 +- psst-gui/src/ui/playlist.rs | 2 +- psst-gui/src/ui/show.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/psst-gui/src/ui/album.rs b/psst-gui/src/ui/album.rs index 6897a60d..f53ad678 100644 --- a/psst-gui/src/ui/album.rs +++ b/psst-gui/src/ui/album.rs @@ -152,7 +152,7 @@ pub fn album_widget(horizontal: bool) -> impl Widget>> { .with_child(album_date) .align_horizontal(UnitPoint::CENTER) .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(7.0)), + .fix_size(theme::grid(16.0), theme::grid(8.0)), ) .align_left() } else { diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index c4f8b621..7a92aae4 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -98,7 +98,7 @@ pub fn artist_widget(horizontal: bool) -> impl Widget { .with_font(theme::UI_FONT_MEDIUM) .align_horizontal(UnitPoint::CENTER) .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(7.0)) + .fix_size(theme::grid(16.0), theme::grid(8.0)) .lens(Artist::name), ) } else { diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index 72e21426..31ee3639 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -363,7 +363,7 @@ pub fn playlist_widget(horizontal: bool) -> impl Widget> { .with_child(playlist_description) .align_horizontal(UnitPoint::CENTER) .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(7.0)), + .fix_size(theme::grid(16.0), theme::grid(8.0)), ) .padding(theme::grid(1.0)) } else { diff --git a/psst-gui/src/ui/show.rs b/psst-gui/src/ui/show.rs index 88bfd713..1643220f 100644 --- a/psst-gui/src/ui/show.rs +++ b/psst-gui/src/ui/show.rs @@ -103,7 +103,7 @@ pub fn show_widget(horizontal: bool) -> impl Widget>> { .with_child(show_publisher) .align_horizontal(UnitPoint::CENTER) .align_vertical(UnitPoint::TOP) - .fix_size(theme::grid(16.0), theme::grid(7.0)), + .fix_size(theme::grid(16.0), theme::grid(8.0)), ) .padding(theme::grid(1.0)) .lens(Ctx::data())