Skip to content

Commit

Permalink
add playlist folders support (#518)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Thang Pham <phamducthang1234@gmail.com>
  • Loading branch information
aNNiMON and aome510 authored Aug 18, 2024
1 parent 04f1763 commit 2ebc027
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 84 deletions.
28 changes: 25 additions & 3 deletions spotify_player/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,15 @@ impl Client {
}
ClientRequest::GetUserPlaylists => {
let playlists = self.current_user_playlists().await?;
let node = state.data.read().user_data.playlist_folder_node.clone();
let playlists = if let Some(node) = node.filter(|n| !n.children.is_empty()) {
crate::playlist_folders::structurize(playlists, node.children)
} else {
playlists
.into_iter()
.map(PlaylistFolderItem::Playlist)
.collect()
};
store_data_into_file_cache(
FileCacheKey::Playlists,
&config::get_config().cache_folder,
Expand Down Expand Up @@ -1085,7 +1094,12 @@ impl Client {
if !follows[0] {
self.playlist_follow(playlist.id.as_ref(), None).await?;
// update the in-memory `user_data`
state.data.write().user_data.playlists.insert(0, playlist);
state
.data
.write()
.user_data
.playlists
.insert(0, PlaylistFolderItem::Playlist(playlist));
}
}
}
Expand Down Expand Up @@ -1125,7 +1139,10 @@ impl Client {
.write()
.user_data
.playlists
.retain(|p| p.id != id);
.retain(|item| match item {
PlaylistFolderItem::Playlist(p) => p.id != id,
_ => true,
});
self.playlist_unfollow(id).await?;
}
}
Expand Down Expand Up @@ -1478,7 +1495,12 @@ impl Client {
playlist.name,
playlist.id
);
state.data.write().user_data.playlists.insert(0, playlist);
state
.data
.write()
.user_data
.playlists
.insert(0, PlaylistFolderItem::Playlist(playlist));
Ok(())
}

Expand Down
25 changes: 23 additions & 2 deletions spotify_player/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::state::{Album, Artist, DataReadGuard, Playlist, Track};
use crate::state::{
Album, Artist, DataReadGuard, Playlist, PlaylistFolder, PlaylistFolderItem, Track,
};
use serde::Deserialize;

#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -105,6 +107,9 @@ pub enum ActionContext {
Album(Album),
Artist(Artist),
Playlist(Playlist),
#[allow(dead_code)]
// TODO: support actions for playlist folders
PlaylistFolder(PlaylistFolder),
}

pub enum CommandOrAction {
Expand Down Expand Up @@ -136,13 +141,24 @@ impl From<Playlist> for ActionContext {
}
}

impl From<PlaylistFolderItem> for ActionContext {
fn from(value: PlaylistFolderItem) -> Self {
match value {
PlaylistFolderItem::Playlist(p) => ActionContext::Playlist(p),
PlaylistFolderItem::Folder(f) => ActionContext::PlaylistFolder(f),
}
}
}

impl ActionContext {
pub fn get_available_actions(&self, data: &DataReadGuard) -> Vec<Action> {
match self {
Self::Track(track) => construct_track_actions(track, data),
Self::Album(album) => construct_album_actions(album, data),
Self::Artist(artist) => construct_artist_actions(artist, data),
Self::Playlist(playlist) => construct_playlist_actions(playlist, data),
// TODO: support actions for playlist folders
Self::PlaylistFolder(_) => vec![],
}
}
}
Expand Down Expand Up @@ -207,7 +223,12 @@ pub fn construct_artist_actions(artist: &Artist, data: &DataReadGuard) -> Vec<Ac
pub fn construct_playlist_actions(playlist: &Playlist, data: &DataReadGuard) -> Vec<Action> {
let mut actions = vec![Action::GoToRadio, Action::CopyLink];

if data.user_data.playlists.iter().any(|a| a.id == playlist.id) {
if data
.user_data
.playlists
.iter()
.any(|item| matches!(item, PlaylistFolderItem::Playlist(p) if p.id == playlist.id))
{
actions.push(Action::DeleteFromLibrary);
} else {
actions.push(Action::AddToLibrary);
Expand Down
9 changes: 7 additions & 2 deletions spotify_player/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ pub fn handle_action_in_context(
Action::AddToPlaylist => {
client_pub.send(ClientRequest::GetUserPlaylists)?;
ui.popup = Some(PopupState::UserPlaylistList(
PlaylistPopupAction::AddTrack(track.id),
PlaylistPopupAction::AddTrack {
folder_id: 0,
track_id: track.id,
},
ListState::default(),
));
}
Expand Down Expand Up @@ -304,6 +307,8 @@ pub fn handle_action_in_context(
}
_ => {}
},
// TODO: support actions for playlist folders
ActionContext::PlaylistFolder(_) => {}
}

Ok(())
Expand Down Expand Up @@ -444,7 +449,7 @@ fn handle_global_command(
Command::BrowseUserPlaylists => {
client_pub.send(ClientRequest::GetUserPlaylists)?;
ui.popup = Some(PopupState::UserPlaylistList(
PlaylistPopupAction::Browse,
PlaylistPopupAction::Browse { folder_id: 0 },
ListState::default(),
));
}
Expand Down
41 changes: 31 additions & 10 deletions spotify_player/src/event/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,17 @@ fn handle_action_for_library_page(
state: &SharedState,
) -> Result<bool> {
let data = state.data.read();
let focus_state = match ui.current_page() {
PageState::Library { state } => state.focus,
let (focus_state, folder_id) = match ui.current_page() {
PageState::Library { state } => (state.focus, state.playlist_folder_id),
_ => anyhow::bail!("expect a library page state"),
};
match focus_state {
LibraryFocusState::Playlists => window::handle_action_for_selected_item(
action,
ui.search_filtered_items(&data.user_data.playlists),
ui.search_filtered_items(&data.user_data.folder_playlists_items(folder_id))
.into_iter()
.cloned()
.collect(),
&data,
ui,
client_pub,
Expand Down Expand Up @@ -92,14 +95,17 @@ fn handle_command_for_library_page(
}
_ => {
let data = state.data.read();
let focus_state = match ui.current_page() {
PageState::Library { state } => state.focus,
let (focus_state, folder_id) = match ui.current_page() {
PageState::Library { state } => (state.focus, state.playlist_folder_id),
_ => anyhow::bail!("expect a library page state"),
};
match focus_state {
LibraryFocusState::Playlists => window::handle_command_for_playlist_list_window(
command,
ui.search_filtered_items(&data.user_data.playlists),
ui.search_filtered_items(&data.user_data.folder_playlists_items(folder_id))
.into_iter()
.cloned()
.collect(),
&data,
ui,
),
Expand Down Expand Up @@ -214,16 +220,31 @@ fn handle_key_sequence_for_search_page(
}
}
SearchFocusState::Playlists => {
let playlists = search_results
.map(|s| s.playlists.iter().collect())
let playlists: Vec<PlaylistFolderItem> = search_results
.map(|s| {
s.playlists
.iter()
.map(|p| PlaylistFolderItem::Playlist(p.clone()))
.collect()
})
.unwrap_or_default();
let playlist_refs = playlists.iter().collect();

match found_keymap {
CommandOrAction::Command(command) => {
window::handle_command_for_playlist_list_window(command, playlists, &data, ui)
window::handle_command_for_playlist_list_window(
command,
playlist_refs,
&data,
ui,
)
}
CommandOrAction::Action(action) => window::handle_action_for_selected_item(
action, playlists, &data, ui, client_pub,
action,
playlist_refs,
&data,
ui,
client_pub,
),
}
}
Expand Down
89 changes: 59 additions & 30 deletions spotify_player/src/event/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,45 +89,74 @@ pub fn handle_key_sequence_for_popup(
)
}
PopupState::UserPlaylistList(action, _) => match action {
PlaylistPopupAction::Browse => {
let playlist_uris = state
.data
.read()
.user_data
.playlists
.iter()
.map(|p| p.id.uri())
.collect::<Vec<_>>();
PlaylistPopupAction::Browse { folder_id } => {
let data = state.data.read();
let items = data.user_data.folder_playlists_items(*folder_id);

handle_command_for_context_browsing_list_popup(
handle_command_for_list_popup(
command,
ui,
playlist_uris,
rspotify_model::Type::Playlist,
items.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
match items[id] {
PlaylistFolderItem::Folder(f) => {
ui.popup = Some(PopupState::UserPlaylistList(
PlaylistPopupAction::Browse {
folder_id: f.target_id,
},
ListState::default(),
));
}
PlaylistFolderItem::Playlist(p) => {
let context_id = ContextId::Playlist(
PlaylistId::from_uri(&crate::utils::parse_uri(&p.id.uri()))?
.into_static(),
);
ui.new_page(PageState::Context {
id: None,
context_page_type: ContextPageType::Browsing(context_id),
state: None,
});
}
}
Ok(())
},
|ui: &mut UIStateGuard| {
ui.popup = None;
},
)
}
PlaylistPopupAction::AddTrack(track_id) => {
PlaylistPopupAction::AddTrack {
folder_id,
track_id,
} => {
let track_id = track_id.clone();
let playlist_ids = state
.data
.read()
.user_data
.modifiable_playlists()
.into_iter()
.map(|p| p.id.clone())
.collect::<Vec<_>>();
let data = state.data.read();
let items = data.user_data.modifiable_playlist_items(Some(*folder_id));

handle_command_for_list_popup(
command,
ui,
playlist_ids.len(),
items.len(),
|_, _| {},
|ui: &mut UIStateGuard, id: usize| -> Result<()> {
client_pub.send(ClientRequest::AddTrackToPlaylist(
playlist_ids[id].clone(),
track_id.clone(),
))?;
ui.popup = None;
ui.popup = match items[id] {
PlaylistFolderItem::Folder(f) => Some(PopupState::UserPlaylistList(
PlaylistPopupAction::AddTrack {
folder_id: f.target_id,
track_id,
},
ListState::default(),
)),
PlaylistFolderItem::Playlist(p) => {
client_pub.send(ClientRequest::AddTrackToPlaylist(
p.id.clone(),
track_id,
))?;
None
}
};
Ok(())
},
|ui: &mut UIStateGuard| {
Expand Down Expand Up @@ -370,9 +399,9 @@ fn handle_command_for_list_popup(
command: Command,
ui: &mut UIStateGuard,
n_items: usize,
on_select_func: impl Fn(&mut UIStateGuard, usize),
on_choose_func: impl Fn(&mut UIStateGuard, usize) -> Result<()>,
on_close_func: impl Fn(&mut UIStateGuard),
on_select_func: impl FnOnce(&mut UIStateGuard, usize),
on_choose_func: impl FnOnce(&mut UIStateGuard, usize) -> Result<()>,
on_close_func: impl FnOnce(&mut UIStateGuard),
) -> Result<bool> {
let popup = ui.popup.as_mut().with_context(|| "expect a popup")?;
let current_id = popup.list_selected().unwrap_or_default();
Expand Down
Loading

0 comments on commit 2ebc027

Please sign in to comment.