From 74bac570c7fd4320b9a154d4adf08de0e4bbafa3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 31 Jan 2026 12:00:29 +0700 Subject: [PATCH] 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. --- go_backend/deezer.go | 67 +++++- go_backend/spotify.go | 11 + lib/providers/track_provider.dart | 60 ++++- lib/screens/home_tab.dart | 372 +++++++++++++++++++++++------- 4 files changed, 425 insertions(+), 85 deletions(-) diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 6c2553b7..049dc6e8 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -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{ diff --git a/go_backend/spotify.go b/go_backend/spotify.go index e212c625..2423e9d7 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -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 { diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index e0f4a8c6..ebdce48d 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -22,6 +22,7 @@ class TrackState { final List? artistAlbums; // For artist page final List? artistTopTracks; // Artist's popular tracks final List? searchArtists; // For search results + final List? 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? tracks, @@ -65,6 +67,7 @@ class TrackState { List? artistAlbums, List? artistTopTracks, List? searchArtists, + List? 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 { int _currentRequestId = 0; @@ -321,7 +345,7 @@ class TrackNotifier extends Notifier { 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 { final trackList = results['tracks'] as List? ?? []; final artistList = results['artists'] as List? ?? []; + final albumList = results['albums'] as List? ?? []; - _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 = []; @@ -378,11 +403,26 @@ class TrackNotifier extends Notifier { } } - _log.i('Search complete: ${tracks.length} tracks (${extensionTracks.length} from extensions), ${artists.length} artists parsed successfully'); + final albums = []; + for (int i = 0; i < albumList.length; i++) { + final a = albumList[i]; + try { + if (a is Map) { + 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 { ); } + SearchAlbum _parseSearchAlbum(Map 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 tracks) { final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList(); if (tracksWithIsrc.isEmpty) return; diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 737caaec..db317227 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -466,6 +466,7 @@ class _HomeTabState extends ConsumerState 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 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 with AutomaticKeepAliveClient ..._buildSearchResults( tracks: tracks, searchArtists: searchArtists, + searchAlbums: searchAlbums, isLoading: isLoading, error: error, colorScheme: colorScheme, @@ -1559,6 +1561,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient List _buildSearchResults({ required List tracks, required List? searchArtists, + required List? searchAlbums, required bool isLoading, required String? error, required ColorScheme colorScheme, @@ -1601,9 +1604,42 @@ class _HomeTabState extends ConsumerState 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 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 with AutomaticKeepAliveClient ]; } - Widget _buildArtistSearchResults(List 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 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;