diff --git a/src/api.rs b/src/api.rs index ce27028..8731039 100644 --- a/src/api.rs +++ b/src/api.rs @@ -6,7 +6,7 @@ use futures_util::lock::Mutex; use librespot_core::Session; use librespot_oauth::OAuthToken; use reqwest::StatusCode; -use rspotify::model::PrivateUser; +use rspotify::model::{Page, PrivateUser, SimplifiedPlaylist, UserId}; use rspotify::prelude::*; use rspotify::{AuthCodeSpotify, ClientError, ClientResult, Config, Token}; use rspotify::http::HttpError; @@ -108,6 +108,10 @@ impl SpotifyContext { self.api_with_retry(|api| api.current_user()).await.ok_or(()) } + pub async fn current_user_playlists(&self, limit: Option, offset: Option) -> Result, ()> { + self.api_with_retry(|api| api.current_user_playlists_manual(limit, offset)).await.ok_or(()) + } + } fn librespot_token_to_rspotify(token: &OAuthToken) -> Token { diff --git a/src/main.rs b/src/main.rs index c6783fb..db18b22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,10 @@ use api::SpotifyContext; use auth::get_token; use clap::Parser; use cli::Args; -use cushy::{window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime}; +use cushy::{value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime}; use librespot_core::{authentication::Credentials, Session, SessionConfig}; use librespot_playback::{audio_backend, config::{AudioFormat, PlayerConfig}, mixer::NoOpVolume, player::Player}; +use widgets::{library::playlist::playlists_widget, ActivePage}; mod vibrancy; mod theme; @@ -64,9 +65,14 @@ fn main() -> cushy::Result { dbg!(&user); let userid = user.id; - format!("Hello, {}!", user.display_name.unwrap()) + let playlists = context.current_user_playlists(None, None).await.unwrap(); + + let selected_page = Dynamic::new(ActivePage::default()); + + playlists_widget(playlists.items, selected_page) .make_window() - .open(&mut app).unwrap(); + .open(&mut app) + .unwrap(); }); drop(guard); diff --git a/src/widgets/image.rs b/src/widgets/image.rs index d4d1c69..94e9230 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use cushy::{kludgine::{AnyTexture, LazyTexture}, value::{CallbackHandle, Destination, Dynamic, Source, Value}, widgets::Image}; +use cushy::{kludgine::{AnyTexture, LazyTexture}, value::{CallbackDisconnected, CallbackHandle, Destination, Dynamic, Source, Value}, widgets::Image}; use futures_util::lock::Mutex; use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions}; use image::imageops::FilterType; @@ -10,11 +10,17 @@ use tokio::task::JoinHandle; use crate::rt::tokio_runtime; - -trait ImageExt { +pub trait ImageExt { fn new_empty() -> Self; fn load_url(&mut self, url: Dynamic>) -> CallbackHandle; + + fn with_url(mut self, url: Dynamic>) -> Self + where Self: Sized + { + self.load_url(url).persist(); + self + } } impl ImageExt for Image { @@ -22,6 +28,8 @@ impl ImageExt for Image { Image::new(Dynamic::new(get_empty_texture())) } + /// Makes the image connected to a URL + /// Calling this multiple times on a single image may cause memory leaks fn load_url(&mut self, url: Dynamic>) -> CallbackHandle { let client = ClientBuilder::new(Client::new()) .with(Cache(HttpCache { @@ -41,12 +49,15 @@ impl ImageExt for Image { Value::Dynamic(dynamic) => dynamic, _ => unreachable!() }; - let texture = texture.clone(); let prev_request_join = Arc::new(Mutex::new(None::>)); - url.for_each({ + url.for_each_try({ let texture = texture.clone(); move |url| { + let texture_count = texture.instances(); + if texture_count <= 1 { + return Err(CallbackDisconnected); + } let guard = tokio_runtime().enter(); let url = url.clone(); let prev_request_join = prev_request_join.clone(); @@ -74,6 +85,7 @@ impl ImageExt for Image { } }); drop(guard); + Ok(()) } }) } diff --git a/src/widgets/library/mod.rs b/src/widgets/library/mod.rs new file mode 100644 index 0000000..500127d --- /dev/null +++ b/src/widgets/library/mod.rs @@ -0,0 +1,3 @@ +use rspotify::model::SimplifiedPlaylist; + +pub mod playlist; diff --git a/src/widgets/library/playlist.rs b/src/widgets/library/playlist.rs new file mode 100644 index 0000000..bec6556 --- /dev/null +++ b/src/widgets/library/playlist.rs @@ -0,0 +1,78 @@ +use cushy::{styles::components::WidgetBackground, value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}, widget::{MakeWidget, WidgetList}, widgets::{grid::Orientation, Image, Stack}}; +use rspotify::model::SimplifiedPlaylist; +use cushy::kludgine::Color; + +use crate::widgets::{image::ImageExt, ActivePage, SelectedPage}; + +fn playlist_entry(playlist: impl IntoValue, selected_page: SelectedPage) -> impl MakeWidget { + let playlist: Value = playlist.into_value(); + let id = playlist.map(|p| p.id.clone()); + let background = selected_page.map_each(move |page| { + match page { + ActivePage::Playlist(p) if p.id == id => { + Color(0xFFFFFF10) + } + _ => Color::CLEAR_WHITE + } + }); + Image::new_empty() + .with_url( + playlist + .map_each(|playlist| playlist.images.first().map(|image| image.url.clone())) + .into_dynamic() + ) + + .and( + playlist + .map_each(|p| p.name.clone()) + .align_left() + .expand() + ) + .into_columns() + .into_button() + .on_click(move |_| { + selected_page.set(ActivePage::Playlist(playlist.get())); + }) + .with(&WidgetBackground, background) +} + +pub fn playlists_widget(playlists: impl IntoValue>, selected_page: SelectedPage) -> impl MakeWidget { + let playlists: Value> = playlists.into_value(); + Stack::new( + Orientation::Row, + playlists + .map_each(move |t| { + let mut list = t.clone().into_iter().map(|playlist| playlist_entry(playlist, selected_page.clone())).collect::(); + list.insert(0, liked_songs_entry(selected_page.clone())); + list + }) + ) + .vertical_scroll() + .expand() +} + +pub fn liked_songs_entry(selected_page: SelectedPage) -> impl MakeWidget { + let background = selected_page.map_each(move |page| { + match page { + ActivePage::LikedSongs => { + Color(0xFFFFFF10) + } + _ => Color::CLEAR_WHITE + } + }); + Image::new_empty() + .with_url( + Dynamic::new(Some("https://misc.scdn.co/liked-songs/liked-songs-300.png".to_string())) + ) + .and( + "Liked Songs" + .align_left() + .expand() + ) + .into_columns() + .into_button() + .on_click(move |_| { + selected_page.set(ActivePage::LikedSongs); + }) + .with(&WidgetBackground, background) +} \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b1b6b4f..81daad8 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1 +1,15 @@ -pub mod image; \ No newline at end of file +use cushy::value::Dynamic; +use rspotify::model::{SimplifiedAlbum, SimplifiedPlaylist}; + +pub mod image; +pub mod library; + +#[derive(PartialEq, Debug, Default)] +pub enum ActivePage { + #[default] + LikedSongs, + Playlist(SimplifiedPlaylist), + Album(SimplifiedAlbum) +} + +type SelectedPage = Dynamic; \ No newline at end of file