feat(search): integrate playlists into search page
- Add playlists to unified search results (SearchAllResults) - Add dedicated Playlists tab in jump-nav - Create SearchPlaylistCard with album-like overlay (heart/play/menu) - Add playlist menu: Play next, Add to queue, Copy to library, Share - Add offset parameter to search_playlists command - Add PlaylistGenre and extra fields to Playlist model - Update searchState with Playlist type and playlistResults UI changes: - Remove glyphs from search tabs - Reduce tab spacing (gap 8px→4px, padding 16px→12px) - Reduce filter font size (13px→12px)
This commit is contained in:
1 parent
f6d2a6bddd
commit
61dd071326
7 files changed
+531
-15
No files matched your search
@@ -723,13 +723,17 @@ impl QobuzClient {
|
||||
}
|
||||
|
||||
/// Search playlists
|
||||
pub async fn search_playlists(&self, query: &str, limit: u32) -> Result<SearchResultsPage<Playlist>> {
|
||||
pub async fn search_playlists(&self, query: &str, limit: u32, offset: u32) -> Result<SearchResultsPage<Playlist>> {
|
||||
let url = endpoints::build_url(paths::PLAYLIST_SEARCH);
|
||||
let response: Value = self
|
||||
.http
|
||||
.get(&url)
|
||||
.header("X-App-Id", self.app_id().await?)
|
||||
.query(&[("query", query), ("limit", &limit.to_string())])
|
||||
.query(&[
|
||||
("query", query),
|
||||
("limit", &limit.to_string()),
|
||||
("offset", &offset.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
|
||||
@@ -226,6 +226,12 @@ pub struct Playlist {
|
||||
pub is_public: bool,
|
||||
#[serde(default)]
|
||||
pub tracks: Option<TracksContainer>,
|
||||
// Additional fields from search results
|
||||
pub genres: Option<Vec<PlaylistGenre>>,
|
||||
pub images150: Option<Vec<String>>,
|
||||
pub images300: Option<Vec<String>>,
|
||||
pub slug: Option<String>,
|
||||
pub users_count: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -236,6 +242,13 @@ pub struct PlaylistOwner {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlaylistGenre {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
/// Image set with multiple resolutions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ImageSet {
|
||||
|
||||
@@ -48,13 +48,14 @@ pub async fn get_playlist(
|
||||
pub async fn search_playlists(
|
||||
query: String,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SearchResultsPage<Playlist>, String> {
|
||||
log::info!("Command: search_playlists \"{}\"", query);
|
||||
log::info!("Command: search_playlists \"{}\" limit={:?} offset={:?}", query, limit, offset);
|
||||
|
||||
let client = state.client.lock().await;
|
||||
client
|
||||
.search_playlists(&query, limit.unwrap_or(20))
|
||||
.search_playlists(&query, limit.unwrap_or(20), offset.unwrap_or(0))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to search playlists: {}", e))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use tauri::State;
|
||||
|
||||
use crate::api::{endpoints, endpoints::paths, Album, Artist, ArtistAlbums, LabelDetail, SearchResultsPage, Track, TracksContainer};
|
||||
use crate::api::{endpoints, endpoints::paths, Album, Artist, ArtistAlbums, LabelDetail, Playlist, SearchResultsPage, Track, TracksContainer};
|
||||
use crate::api_cache::ApiCacheState;
|
||||
use crate::AppState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,6 +22,7 @@ pub struct SearchAllResults {
|
||||
pub albums: SearchResultsPage<Album>,
|
||||
pub tracks: SearchResultsPage<Track>,
|
||||
pub artists: SearchResultsPage<Artist>,
|
||||
pub playlists: SearchResultsPage<Playlist>,
|
||||
pub most_popular: Option<MostPopularItem>,
|
||||
}
|
||||
|
||||
@@ -116,6 +117,12 @@ pub async fn search_all(
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_else(|| SearchResultsPage { items: vec![], total: 0, offset: 0, limit: 30 });
|
||||
|
||||
// Parse playlists
|
||||
let playlists: SearchResultsPage<Playlist> = response
|
||||
.get("playlists")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_else(|| SearchResultsPage { items: vec![], total: 0, offset: 0, limit: 30 });
|
||||
|
||||
// Parse most_popular - get the first item
|
||||
let most_popular: Option<MostPopularItem> = response
|
||||
.get("most_popular")
|
||||
@@ -149,6 +156,7 @@ pub async fn search_all(
|
||||
albums,
|
||||
tracks,
|
||||
artists,
|
||||
playlists,
|
||||
most_popular,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user