Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add playlist folders support #518

Merged
merged 11 commits into from
Aug 18, 2024
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
Loading