diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs index e47d1c3d..0d380f03 100644 --- a/spotify_player/src/event/window.rs +++ b/spotify_player/src/event/window.rs @@ -23,10 +23,10 @@ pub fn handle_action_for_focused_context_page( let data = state.data.read(); match data.caches.context.get(&id.uri()) { Some(Context::Artist { + artist, top_tracks, albums, related_artists, - .. }) => { let PageState::Context { state: Some(ContextPageUIState::Artist { focus, .. }), @@ -58,6 +58,18 @@ pub fn handle_action_for_focused_context_page( ui, client_pub, ), + ArtistFocusState::LikedSongs => { + let mut liked: Vec = data + .user_data + .saved_tracks + .values() + .filter(|t| t.artists.iter().any(|a| a.id == artist.id)) + .cloned() + .collect(); + liked.sort_by(|a, b| b.added_at.cmp(&a.added_at)); + let filtered = ui.search_filtered_items(&liked); + handle_action_for_selected_item(action, &filtered, &data, ui, client_pub) + } } } Some( @@ -149,10 +161,10 @@ pub fn handle_command_for_focused_context_window( match data.caches.context.get(&context_id.uri()) { Some(context) => match context { Context::Artist { + artist, top_tracks, albums, related_artists, - .. } => { let PageState::Context { state: Some(ContextPageUIState::Artist { focus, .. }), @@ -179,6 +191,19 @@ pub fn handle_command_for_focused_context_window( ArtistFocusState::TopTracks => handle_command_for_track_table_window( command, client_pub, None, top_tracks, &data, ui, state, ), + ArtistFocusState::LikedSongs => { + let mut liked: Vec = data + .user_data + .saved_tracks + .values() + .filter(|t| t.artists.iter().any(|a| a.id == artist.id)) + .cloned() + .collect(); + liked.sort_by(|a, b| b.added_at.cmp(&a.added_at)); + handle_command_for_track_table_window( + command, client_pub, None, &liked, &data, ui, state, + ) + } } } Context::Album { tracks, .. } diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs index 6239d1b0..6bdf5c75 100644 --- a/spotify_player/src/state/ui/page.rs +++ b/spotify_player/src/state/ui/page.rs @@ -88,6 +88,7 @@ pub enum ContextPageUIState { top_track_table: TableState, album_table: TableState, related_artist_list: ListState, + liked_track_table: TableState, focus: ArtistFocusState, }, Tracks { @@ -110,6 +111,7 @@ pub enum ArtistFocusState { TopTracks, Albums, RelatedArtists, + LikedSongs, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -218,6 +220,7 @@ impl PageState { top_track_table, album_table, related_artist_list, + liked_track_table, focus, } => match focus { ArtistFocusState::TopTracks => MutableWindowState::Table(top_track_table), @@ -225,6 +228,7 @@ impl PageState { ArtistFocusState::RelatedArtists => { MutableWindowState::List(related_artist_list) } + ArtistFocusState::LikedSongs => MutableWindowState::Table(liked_track_table), }, ContextPageUIState::Show { episode_table } => { MutableWindowState::Table(episode_table) @@ -303,6 +307,7 @@ impl ContextPageUIState { top_track_table: TableState::default(), album_table: TableState::default(), related_artist_list: ListState::default(), + liked_track_table: TableState::default(), focus: ArtistFocusState::TopTracks, } } @@ -424,7 +429,8 @@ impl_focusable!( impl_focusable!( ArtistFocusState, - [TopTracks, Albums], + [TopTracks, LikedSongs], + [LikedSongs, Albums], [Albums, RelatedArtists], [RelatedArtists, TopTracks] ); diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 10fb472e..d75d1b6e 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -315,10 +315,10 @@ pub fn render_context_page( match context { Context::Artist { + artist, top_tracks, albums, related_artists, - .. } => { render_artist_context_page_windows( is_active, @@ -327,6 +327,7 @@ pub fn render_context_page( ui, &data, rect, + artist, (top_tracks, albums, related_artists), ); } @@ -351,6 +352,7 @@ pub fn render_context_page( ui.search_filtered_items(tracks), ui, &data, + false, ); } Context::Tracks { tracks, .. } | Context::Album { tracks, .. } => { @@ -362,6 +364,7 @@ pub fn render_context_page( ui.search_filtered_items(tracks), ui, &data, + false, ); } Context::Show { episodes, .. } => { @@ -808,6 +811,7 @@ pub fn render_queue_page( /// Render windows for an artist context page, which includes /// - A top track table +/// - A liked songs table (tracks liked by the user from this artist) /// - An album table /// - A related artist list fn render_artist_context_page_windows( @@ -817,6 +821,7 @@ fn render_artist_context_page_windows( ui: &mut UIStateGuard, data: &DataReadGuard, rect: Rect, + artist: &Artist, artist_data: (&[Track], &[Album], &[Artist]), ) { // 1. Get data @@ -826,6 +831,17 @@ fn render_artist_context_page_windows( ui.search_filtered_items(artist_data.2), ); + // Collect liked tracks for this artist, sorted newest first + let mut liked_tracks: Vec = data + .user_data + .saved_tracks + .values() + .filter(|t| t.artists.iter().any(|a| a.id == artist.id)) + .cloned() + .collect(); + liked_tracks.sort_by(|a, b| b.added_at.cmp(&a.added_at)); + let liked_tracks_filtered = ui.search_filtered_items(&liked_tracks); + let focus_state = match ui.current_page() { PageState::Context { state: Some(ContextPageUIState::Artist { focus, .. }), @@ -834,26 +850,30 @@ fn render_artist_context_page_windows( _ => return, }; - // 2. Construct the page's layout - // top tracks window - let chunks = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).split(rect); - let top_tracks_rect = chunks[0]; - - // albums and related artitsts windows - let chunks = Layout::horizontal([Constraint::Ratio(1, 2); 2]).split(chunks[1]); + // 2. Construct the page's layout: 3-row stack + // row 1: top tracks (full width) + // row 2: liked songs (full width) + // row 3: albums (left half) | related artists (right half) + let rows = + Layout::vertical([Constraint::Fill(1), Constraint::Fill(1), Constraint::Fill(1)]) + .split(rect); + let top_tracks_rect = rows[0]; + let liked_songs_rect = + construct_and_render_block("Liked Songs", &ui.theme, Borders::TOP, frame, rows[1]); + + let bot_chunks = Layout::horizontal([Constraint::Ratio(1, 2); 2]).split(rows[2]); let albums_rect = construct_and_render_block( "Albums", &ui.theme, Borders::TOP | Borders::RIGHT, frame, - chunks[0], + bot_chunks[0], ); let related_artists_rect = - construct_and_render_block("Related Artists", &ui.theme, Borders::TOP, frame, chunks[1]); + construct_and_render_block("Related Artists", &ui.theme, Borders::TOP, frame, bot_chunks[1]); // 3. Construct the page's widgets // album table - let is_albums_active = is_active && focus_state == ArtistFocusState::Albums; let n_albums = albums.len(); let album_rows = albums @@ -910,6 +930,18 @@ fn render_artist_context_page_windows( tracks, ui, data, + false, + ); + + render_track_table( + frame, + liked_songs_rect, + is_active && focus_state == ArtistFocusState::LikedSongs, + state, + liked_tracks_filtered, + ui, + data, + true, ); let PageState::Context { @@ -943,6 +975,7 @@ fn render_track_table( tracks: Vec<&Track>, ui: &mut UIStateGuard, data: &DataReadGuard, + for_liked_songs: bool, ) { let configs = config::get_config(); // get the current playing track's URI to decorate such track (if exists) in the track table @@ -1064,8 +1097,16 @@ fn render_track_table( { let playable_table_state = match state { ContextPageUIState::Artist { - top_track_table, .. - } => top_track_table, + top_track_table, + liked_track_table, + .. + } => { + if for_liked_songs { + liked_track_table + } else { + top_track_table + } + } ContextPageUIState::Playlist { track_table } | ContextPageUIState::Album { track_table } | ContextPageUIState::Tracks { track_table } => track_table,