mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-21 15:36:50 +02:00
feat: unify search results display and add album search to Deezer
- Add SearchAlbumResult struct to Go backend - Add album search to Deezer SearchAll() function (returns albums alongside tracks/artists) - Change artist display from horizontal scroll to vertical list style (consistent with extension search) - Add SearchAlbum class and searchAlbums field to TrackState - Add _SearchArtistItemWidget and _SearchAlbumItemWidget for vertical list display - Add _navigateToSearchAlbum method for navigating to album details - Remove old horizontal artist scroll (_buildArtistSearchResults, _buildArtistCard) Now default search (Deezer/Spotify) shows Artists, Albums, and Songs in the same vertical list style as extension search results.
This commit is contained in:
+65
-2
@@ -186,7 +186,8 @@ type deezerPlaylistFull struct {
|
||||
func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit, artistLimit int) (*SearchAllResult, error) {
|
||||
GoLog("[Deezer] SearchAll: query=%q, trackLimit=%d, artistLimit=%d\n", query, trackLimit, artistLimit)
|
||||
|
||||
cacheKey := fmt.Sprintf("deezer:all:%s:%d:%d", query, trackLimit, artistLimit)
|
||||
albumLimit := 5 // Same as artistLimit for consistency
|
||||
cacheKey := fmt.Sprintf("deezer:all:%s:%d:%d:%d", query, trackLimit, artistLimit, albumLimit)
|
||||
|
||||
c.cacheMu.RLock()
|
||||
if entry, ok := c.searchCache[cacheKey]; ok && !entry.isExpired() {
|
||||
@@ -199,6 +200,7 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit,
|
||||
result := &SearchAllResult{
|
||||
Tracks: make([]TrackMetadata, 0, trackLimit),
|
||||
Artists: make([]SearchArtistResult, 0, artistLimit),
|
||||
Albums: make([]SearchAlbumResult, 0, albumLimit),
|
||||
}
|
||||
|
||||
// Search tracks - NO ISRC fetch for performance
|
||||
@@ -229,6 +231,7 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit,
|
||||
result.Tracks = append(result.Tracks, c.convertTrack(track))
|
||||
}
|
||||
|
||||
// Search artists
|
||||
artistURL := fmt.Sprintf("%s/artist?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), artistLimit)
|
||||
GoLog("[Deezer] Fetching artists from: %s\n", artistURL)
|
||||
|
||||
@@ -259,7 +262,67 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit,
|
||||
GoLog("[Deezer] Artist search failed: %v\n", err)
|
||||
}
|
||||
|
||||
GoLog("[Deezer] SearchAll complete: %d tracks, %d artists\n", len(result.Tracks), len(result.Artists))
|
||||
// Search albums
|
||||
albumURL := fmt.Sprintf("%s/album?q=%s&limit=%d", deezerSearchURL, url.QueryEscape(query), albumLimit)
|
||||
GoLog("[Deezer] Fetching albums from: %s\n", albumURL)
|
||||
|
||||
var albumResp struct {
|
||||
Data []struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
CoverMedium string `json:"cover_medium"`
|
||||
CoverBig string `json:"cover_big"`
|
||||
CoverXL string `json:"cover_xl"`
|
||||
NbTracks int `json:"nb_tracks"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
RecordType string `json:"record_type"`
|
||||
Artist deezerArtist `json:"artist"`
|
||||
} `json:"data"`
|
||||
Error *struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := c.getJSON(ctx, albumURL, &albumResp); err == nil {
|
||||
if albumResp.Error != nil {
|
||||
GoLog("[Deezer] Album API error: type=%s, code=%d, message=%s\n", albumResp.Error.Type, albumResp.Error.Code, albumResp.Error.Message)
|
||||
} else {
|
||||
GoLog("[Deezer] Got %d albums from API\n", len(albumResp.Data))
|
||||
for _, album := range albumResp.Data {
|
||||
coverURL := album.CoverXL
|
||||
if coverURL == "" {
|
||||
coverURL = album.CoverBig
|
||||
}
|
||||
if coverURL == "" {
|
||||
coverURL = album.CoverMedium
|
||||
}
|
||||
if coverURL == "" {
|
||||
coverURL = album.Cover
|
||||
}
|
||||
|
||||
albumType := album.RecordType
|
||||
if albumType == "compile" {
|
||||
albumType = "compilation"
|
||||
}
|
||||
|
||||
result.Albums = append(result.Albums, SearchAlbumResult{
|
||||
ID: fmt.Sprintf("deezer:%d", album.ID),
|
||||
Name: album.Title,
|
||||
Artists: album.Artist.Name,
|
||||
Images: coverURL,
|
||||
ReleaseDate: album.ReleaseDate,
|
||||
TotalTracks: album.NbTracks,
|
||||
AlbumType: albumType,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GoLog("[Deezer] Album search failed: %v\n", err)
|
||||
}
|
||||
|
||||
GoLog("[Deezer] SearchAll complete: %d tracks, %d artists, %d albums\n", len(result.Tracks), len(result.Artists), len(result.Albums))
|
||||
|
||||
c.cacheMu.Lock()
|
||||
c.searchCache[cacheKey] = &cacheEntry{
|
||||
|
||||
@@ -238,9 +238,20 @@ type SearchArtistResult struct {
|
||||
Popularity int `json:"popularity"`
|
||||
}
|
||||
|
||||
type SearchAlbumResult struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Artists string `json:"artists"`
|
||||
Images string `json:"images"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
TotalTracks int `json:"total_tracks"`
|
||||
AlbumType string `json:"album_type"`
|
||||
}
|
||||
|
||||
type SearchAllResult struct {
|
||||
Tracks []TrackMetadata `json:"tracks"`
|
||||
Artists []SearchArtistResult `json:"artists"`
|
||||
Albums []SearchAlbumResult `json:"albums"`
|
||||
}
|
||||
|
||||
type spotifyURI struct {
|
||||
|
||||
@@ -22,6 +22,7 @@ class TrackState {
|
||||
final List<ArtistAlbum>? artistAlbums; // For artist page
|
||||
final List<Track>? artistTopTracks; // Artist's popular tracks
|
||||
final List<SearchArtist>? searchArtists; // For search results
|
||||
final List<SearchAlbum>? searchAlbums; // For search results (albums)
|
||||
final bool hasSearchText; // For back button handling
|
||||
final bool isShowingRecentAccess; // For recent access mode
|
||||
final String? searchExtensionId; // Extension ID used for current search results
|
||||
@@ -42,13 +43,14 @@ class TrackState {
|
||||
this.artistAlbums,
|
||||
this.artistTopTracks,
|
||||
this.searchArtists,
|
||||
this.searchAlbums,
|
||||
this.hasSearchText = false,
|
||||
this.isShowingRecentAccess = false,
|
||||
this.searchExtensionId,
|
||||
this.selectedSearchFilter,
|
||||
});
|
||||
|
||||
bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty);
|
||||
bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty) || (searchAlbums != null && searchAlbums!.isNotEmpty);
|
||||
|
||||
TrackState copyWith({
|
||||
List<Track>? tracks,
|
||||
@@ -65,6 +67,7 @@ class TrackState {
|
||||
List<ArtistAlbum>? artistAlbums,
|
||||
List<Track>? artistTopTracks,
|
||||
List<SearchArtist>? searchArtists,
|
||||
List<SearchAlbum>? searchAlbums,
|
||||
bool? hasSearchText,
|
||||
bool? isShowingRecentAccess,
|
||||
String? searchExtensionId,
|
||||
@@ -86,6 +89,7 @@ class TrackState {
|
||||
artistAlbums: artistAlbums ?? this.artistAlbums,
|
||||
artistTopTracks: artistTopTracks ?? this.artistTopTracks,
|
||||
searchArtists: searchArtists ?? this.searchArtists,
|
||||
searchAlbums: searchAlbums ?? this.searchAlbums,
|
||||
hasSearchText: hasSearchText ?? this.hasSearchText,
|
||||
isShowingRecentAccess: isShowingRecentAccess ?? this.isShowingRecentAccess,
|
||||
searchExtensionId: searchExtensionId,
|
||||
@@ -132,6 +136,26 @@ class SearchArtist {
|
||||
});
|
||||
}
|
||||
|
||||
class SearchAlbum {
|
||||
final String id;
|
||||
final String name;
|
||||
final String artists;
|
||||
final String? imageUrl;
|
||||
final String? releaseDate;
|
||||
final int totalTracks;
|
||||
final String albumType;
|
||||
|
||||
const SearchAlbum({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.artists,
|
||||
this.imageUrl,
|
||||
this.releaseDate,
|
||||
required this.totalTracks,
|
||||
required this.albumType,
|
||||
});
|
||||
}
|
||||
|
||||
class TrackNotifier extends Notifier<TrackState> {
|
||||
int _currentRequestId = 0;
|
||||
|
||||
@@ -321,7 +345,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
if (source == 'deezer') {
|
||||
_log.d('Calling Deezer search API...');
|
||||
results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 5);
|
||||
_log.i('Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists');
|
||||
_log.i('Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums');
|
||||
} else {
|
||||
_log.d('Calling Spotify search API...');
|
||||
results = await PlatformBridge.searchSpotifyAll(query, trackLimit: 20, artistLimit: 5);
|
||||
@@ -335,8 +359,9 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
|
||||
final trackList = results['tracks'] as List<dynamic>? ?? [];
|
||||
final artistList = results['artists'] as List<dynamic>? ?? [];
|
||||
final albumList = results['albums'] as List<dynamic>? ?? [];
|
||||
|
||||
_log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists');
|
||||
_log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists, ${albumList.length} albums');
|
||||
|
||||
final tracks = <Track>[];
|
||||
|
||||
@@ -378,11 +403,26 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
_log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists parsed successfully');
|
||||
final albums = <SearchAlbum>[];
|
||||
for (int i = 0; i < albumList.length; i++) {
|
||||
final a = albumList[i];
|
||||
try {
|
||||
if (a is Map<String, dynamic>) {
|
||||
albums.add(_parseSearchAlbum(a));
|
||||
} else {
|
||||
_log.w('Album[$i] is not a Map: ${a.runtimeType}');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.e('Failed to parse album[$i]: $e', e);
|
||||
}
|
||||
}
|
||||
|
||||
_log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists, ${albums.length} albums parsed successfully');
|
||||
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
searchArtists: artists,
|
||||
searchAlbums: albums,
|
||||
isLoading: false,
|
||||
hasSearchText: state.hasSearchText,
|
||||
);
|
||||
@@ -590,6 +630,18 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
);
|
||||
}
|
||||
|
||||
SearchAlbum _parseSearchAlbum(Map<String, dynamic> data) {
|
||||
return SearchAlbum(
|
||||
id: data['id'] as String? ?? '',
|
||||
name: data['name'] as String? ?? '',
|
||||
artists: data['artists'] as String? ?? '',
|
||||
imageUrl: data['images'] as String?,
|
||||
releaseDate: data['release_date'] as String?,
|
||||
totalTracks: data['total_tracks'] as int? ?? 0,
|
||||
albumType: data['album_type'] as String? ?? 'album',
|
||||
);
|
||||
}
|
||||
|
||||
void _preWarmCacheForTracks(List<Track> tracks) {
|
||||
final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList();
|
||||
if (tracksWithIsrc.isEmpty) return;
|
||||
|
||||
+293
-79
@@ -466,6 +466,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
final tracks = ref.watch(trackProvider.select((s) => s.tracks));
|
||||
final searchArtists = ref.watch(trackProvider.select((s) => s.searchArtists));
|
||||
final searchAlbums = ref.watch(trackProvider.select((s) => s.searchAlbums));
|
||||
final isLoading = ref.watch(trackProvider.select((s) => s.isLoading));
|
||||
final error = ref.watch(trackProvider.select((s) => s.error));
|
||||
final hasSearchedBefore = ref.watch(settingsProvider.select((s) => s.hasSearchedBefore));
|
||||
@@ -481,7 +482,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
));
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasActualResults = tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty);
|
||||
final hasActualResults = tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty) || (searchAlbums != null && searchAlbums.isNotEmpty);
|
||||
final isShowingRecentAccess = ref.watch(trackProvider.select((s) => s.isShowingRecentAccess));
|
||||
final hasResults = isShowingRecentAccess || hasActualResults || isLoading;
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
@@ -681,6 +682,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
..._buildSearchResults(
|
||||
tracks: tracks,
|
||||
searchArtists: searchArtists,
|
||||
searchAlbums: searchAlbums,
|
||||
isLoading: isLoading,
|
||||
error: error,
|
||||
colorScheme: colorScheme,
|
||||
@@ -1559,6 +1561,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
List<Widget> _buildSearchResults({
|
||||
required List<Track> tracks,
|
||||
required List<SearchArtist>? searchArtists,
|
||||
required List<SearchAlbum>? searchAlbums,
|
||||
required bool isLoading,
|
||||
required String? error,
|
||||
required ColorScheme colorScheme,
|
||||
@@ -1601,9 +1604,42 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
if (isLoading)
|
||||
const SliverToBoxAdapter(child: Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: LinearProgressIndicator())),
|
||||
|
||||
// Artists from default search (Deezer/Spotify) - now in vertical list style
|
||||
if (searchArtists != null && searchArtists.isNotEmpty)
|
||||
SliverToBoxAdapter(child: _buildArtistSearchResults(searchArtists, colorScheme)),
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
if (searchArtists != null && searchArtists.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface)
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < searchArtists.length; i++)
|
||||
_SearchArtistItemWidget(
|
||||
key: ValueKey('search-artist-${searchArtists[i].id}'),
|
||||
artist: searchArtists[i],
|
||||
showDivider: i < searchArtists.length - 1,
|
||||
onTap: () => _navigateToArtist(searchArtists[i].id, searchArtists[i].name, searchArtists[i].imageUrl),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Artists from extension search
|
||||
if (artistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
@@ -1638,6 +1674,42 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Albums from default search (Deezer/Spotify)
|
||||
if (searchAlbums != null && searchAlbums.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text(context.l10n.searchAlbums, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
if (searchAlbums != null && searchAlbums.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface)
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < searchAlbums.length; i++)
|
||||
_SearchAlbumItemWidget(
|
||||
key: ValueKey('search-album-${searchAlbums[i].id}'),
|
||||
album: searchAlbums[i],
|
||||
showDivider: i < searchAlbums.length - 1,
|
||||
onTap: () => _navigateToSearchAlbum(searchAlbums[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Albums from extension search
|
||||
if (albumItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
@@ -1746,83 +1818,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildArtistSearchResults(List<SearchArtist> artists, ColorScheme colorScheme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: artists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artists[index];
|
||||
return KeyedSubtree(
|
||||
key: ValueKey(artist.id),
|
||||
child: _buildArtistCard(artist, colorScheme),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildArtistCard(SearchArtist artist, ColorScheme colorScheme) {
|
||||
final hasValidImage = artist.imageUrl != null &&
|
||||
artist.imageUrl!.isNotEmpty &&
|
||||
Uri.tryParse(artist.imageUrl!)?.hasAuthority == true;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _navigateToArtist(artist.id, artist.name, artist.imageUrl),
|
||||
child: Container(
|
||||
width: 110,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: ClipOval(
|
||||
child: hasValidImage
|
||||
? CachedNetworkImage(
|
||||
imageUrl: artist.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 200,
|
||||
memCacheHeight: 200,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
errorWidget: (context, url, error) => Icon(
|
||||
Icons.person,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 44,
|
||||
),
|
||||
)
|
||||
: Icon(Icons.person, color: colorScheme.onSurfaceVariant, size: 44),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
artist.name,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToArtist(String artistId, String artistName, String? imageUrl) {
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
@@ -1835,6 +1830,33 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
));
|
||||
}
|
||||
|
||||
void _navigateToSearchAlbum(SearchAlbum album) {
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
// Extract the numeric ID from "deezer:123" format
|
||||
String albumId = album.id;
|
||||
if (albumId.startsWith('deezer:')) {
|
||||
albumId = albumId.substring(7);
|
||||
}
|
||||
|
||||
ref.read(recentAccessProvider.notifier).recordAlbumAccess(
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artistName: album.artists,
|
||||
imageUrl: album.imageUrl,
|
||||
providerId: 'deezer',
|
||||
);
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => AlbumScreen(
|
||||
albumId: albumId,
|
||||
albumName: album.name,
|
||||
coverUrl: album.imageUrl,
|
||||
tracks: const [], // Will be fetched by AlbumScreen
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void _navigateToExtensionAlbum(Track albumItem) async {
|
||||
final extensionId = albumItem.source;
|
||||
if (extensionId == null || extensionId.isEmpty) {
|
||||
@@ -2609,6 +2631,198 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for displaying artist items from default search (Deezer/Spotify)
|
||||
class _SearchArtistItemWidget extends StatelessWidget {
|
||||
final SearchArtist artist;
|
||||
final bool showDivider;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SearchArtistItemWidget({
|
||||
super.key,
|
||||
required this.artist,
|
||||
required this.showDivider,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasValidImage = artist.imageUrl != null &&
|
||||
artist.imageUrl!.isNotEmpty &&
|
||||
Uri.tryParse(artist.imageUrl!)?.hasAuthority == true;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: hasValidImage
|
||||
? CachedNetworkImage(
|
||||
imageUrl: artist.imageUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 112,
|
||||
memCacheHeight: 112,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
artist.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Artist',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showDivider)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: 80,
|
||||
endIndent: 12,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for displaying album items from default search (Deezer/Spotify)
|
||||
class _SearchAlbumItemWidget extends StatelessWidget {
|
||||
final SearchAlbum album;
|
||||
final bool showDivider;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SearchAlbumItemWidget({
|
||||
super.key,
|
||||
required this.album,
|
||||
required this.showDivider,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasValidImage = album.imageUrl != null &&
|
||||
album.imageUrl!.isNotEmpty &&
|
||||
Uri.tryParse(album.imageUrl!)?.hasAuthority == true;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: hasValidImage
|
||||
? CachedNetworkImage(
|
||||
imageUrl: album.imageUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 112,
|
||||
memCacheHeight: 112,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.album,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
album.artists.isNotEmpty ? album.artists : 'Album',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showDivider)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: 80,
|
||||
endIndent: 12,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionAlbumScreen extends ConsumerStatefulWidget {
|
||||
final String extensionId;
|
||||
final String albumId;
|
||||
|
||||
Reference in New Issue
Block a user