feat: add playlist search to Deezer default search

- Add SearchPlaylist class and parsing in track_provider.dart
- Add playlist search to Deezer SearchAll API (5 results)
- Add SearchPlaylistResult struct in Go backend
- Add _SearchPlaylistItemWidget for displaying playlists
- Add _navigateToSearchPlaylist method
- Update PlaylistScreen to support fetching tracks by playlistId
- Display playlists in search results alongside artists and albums
This commit is contained in:
zarzet
2026-01-31 15:12:10 +07:00
parent 74bac570c7
commit ff7135bf2c
5 changed files with 399 additions and 22 deletions
+48 -2
View File
@@ -23,6 +23,7 @@ class TrackState {
final List<Track>? artistTopTracks; // Artist's popular tracks
final List<SearchArtist>? searchArtists; // For search results
final List<SearchAlbum>? searchAlbums; // For search results (albums)
final List<SearchPlaylist>? searchPlaylists; // For search results (playlists)
final bool hasSearchText; // For back button handling
final bool isShowingRecentAccess; // For recent access mode
final String? searchExtensionId; // Extension ID used for current search results
@@ -44,13 +45,14 @@ class TrackState {
this.artistTopTracks,
this.searchArtists,
this.searchAlbums,
this.searchPlaylists,
this.hasSearchText = false,
this.isShowingRecentAccess = false,
this.searchExtensionId,
this.selectedSearchFilter,
});
bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty) || (searchAlbums != null && searchAlbums!.isNotEmpty);
bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty) || (searchAlbums != null && searchAlbums!.isNotEmpty) || (searchPlaylists != null && searchPlaylists!.isNotEmpty);
TrackState copyWith({
List<Track>? tracks,
@@ -68,6 +70,7 @@ class TrackState {
List<Track>? artistTopTracks,
List<SearchArtist>? searchArtists,
List<SearchAlbum>? searchAlbums,
List<SearchPlaylist>? searchPlaylists,
bool? hasSearchText,
bool? isShowingRecentAccess,
String? searchExtensionId,
@@ -90,6 +93,7 @@ class TrackState {
artistTopTracks: artistTopTracks ?? this.artistTopTracks,
searchArtists: searchArtists ?? this.searchArtists,
searchAlbums: searchAlbums ?? this.searchAlbums,
searchPlaylists: searchPlaylists ?? this.searchPlaylists,
hasSearchText: hasSearchText ?? this.hasSearchText,
isShowingRecentAccess: isShowingRecentAccess ?? this.isShowingRecentAccess,
searchExtensionId: searchExtensionId,
@@ -156,6 +160,22 @@ class SearchAlbum {
});
}
class SearchPlaylist {
final String id;
final String name;
final String owner;
final String? imageUrl;
final int totalTracks;
const SearchPlaylist({
required this.id,
required this.name,
required this.owner,
this.imageUrl,
required this.totalTracks,
});
}
class TrackNotifier extends Notifier<TrackState> {
int _currentRequestId = 0;
@@ -417,12 +437,28 @@ class TrackNotifier extends Notifier<TrackState> {
}
}
_log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums parsed successfully');
final playlistList = results['playlists'] as List<dynamic>? ?? [];
final playlists = <SearchPlaylist>[];
for (int i = 0; i < playlistList.length; i++) {
final p = playlistList[i];
try {
if (p is Map<String, dynamic>) {
playlists.add(_parseSearchPlaylist(p));
} else {
_log.w('Playlist[$i] is not a Map: ${p.runtimeType}');
}
} catch (e) {
_log.e('Failed to parse playlist[$i]: $e', e);
}
}
_log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums, ${playlists.length} playlists parsed successfully');
state = TrackState(
tracks: tracks,
searchArtists: artists,
searchAlbums: albums,
searchPlaylists: playlists,
isLoading: false,
hasSearchText: state.hasSearchText,
);
@@ -642,6 +678,16 @@ class TrackNotifier extends Notifier<TrackState> {
);
}
SearchPlaylist _parseSearchPlaylist(Map<String, dynamic> data) {
return SearchPlaylist(
id: data['id'] as String? ?? '',
name: data['name'] as String? ?? '',
owner: data['owner'] as String? ?? '',
imageUrl: data['images'] as String?,
totalTracks: data['total_tracks'] as int? ?? 0,
);
}
void _preWarmCacheForTracks(List<Track> tracks) {
final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList();
if (tracksWithIsrc.isEmpty) return;