import 'package:flutter/material.dart'; import 'package:spotiflac_android/screens/track_history_snapshot.dart'; import 'package:spotiflac_android/widgets/album_detail_header.dart'; import 'package:spotiflac_android/theme/cover_palette.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/recent_access_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/provider_resource_ids.dart'; import 'package:spotiflac_android/utils/ttl_cache.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/home_tab.dart' show ExtensionAlbumScreen; import 'package:spotiflac_android/utils/local_playback.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; import 'package:spotiflac_android/widgets/error_card.dart'; import 'package:spotiflac_android/widgets/selection_bottom_bar.dart'; import 'package:spotiflac_android/widgets/in_library_badge.dart'; import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/screens/selection_mode_mixin.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; import 'package:spotiflac_android/widgets/motion_header_banner.dart'; import 'package:spotiflac_android/widgets/cross_extension_share_sheet.dart'; import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart'; import 'package:spotiflac_android/widgets/downloadable_cover.dart'; part 'artist_screen_widgets.dart'; class _ArtistCache { static final _cache = TtlCache<_CacheEntry>( const Duration(minutes: 10), maxEntries: 24, ); static _CacheEntry? get(String artistId) => _cache.get(artistId); static void set( String artistId, { required List albums, List? releases, List? topTracks, String? headerImageUrl, String? headerVideoUrl, int? monthlyListeners, }) { _cache.set( artistId, _CacheEntry( albums: albums, releases: releases, topTracks: topTracks, headerImageUrl: headerImageUrl, headerVideoUrl: headerVideoUrl, monthlyListeners: monthlyListeners, ), ); } } class _CacheEntry { final List albums; final List? releases; final List? topTracks; final String? headerImageUrl; final String? headerVideoUrl; final int? monthlyListeners; _CacheEntry({ required this.albums, this.releases, this.topTracks, this.headerImageUrl, this.headerVideoUrl, this.monthlyListeners, }); } class ArtistScreen extends ConsumerStatefulWidget { final String artistId; final String artistName; final String? coverUrl; final String? headerImageUrl; final String? headerVideoUrl; final int? monthlyListeners; final List? albums; final List? topTracks; final String? extensionId; const ArtistScreen({ super.key, required this.artistId, required this.artistName, this.coverUrl, this.headerImageUrl, this.headerVideoUrl, this.monthlyListeners, this.albums, this.topTracks, this.extensionId, }); @override ConsumerState createState() => _ArtistScreenState(); } class _ArtistScreenState extends ConsumerState with SelectionModeMixin { bool _isLoadingDiscography = false; final _historySnapshot = TrackHistorySnapshot(); List? _albums; List? _releases; List? _topTracks; String? _headerImageUrl; String? _headerVideoUrl; int? _monthlyListeners; String? _error; bool _showTitleInAppBar = false; final ScrollController _scrollController = ScrollController(); final PageController _popularPageController = PageController(); final SelectionOverlayController _selectionOverlay = SelectionOverlayController(); int _popularCurrentPage = 0; bool _isFetchingDiscography = false; List? _albumBucketSource; List _albumsOnlyBucket = const []; List _singlesBucket = const []; List _compilationsBucket = const []; double _responsiveScale({ double min = 0.82, double max = 1.08, double baseShortestSide = 390, }) { final shortestSide = MediaQuery.sizeOf(context).shortestSide; final scale = shortestSide / baseShortestSide; if (scale < min) return min; if (scale > max) return max; return scale; } double _effectiveTextScale() { final textScale = MediaQuery.textScalerOf(context).scale(1.0); if (textScale < 1.0) return 1.0; if (textScale > 1.4) return 1.4; return textScale; } double _artistAlbumTileSize() { final scale = _responsiveScale(min: 0.82, max: 1.05); final textScale = _effectiveTextScale(); return 140 * scale * (1 + (textScale - 1) * 0.12); } double _artistAlbumSectionHeight() { final tileSize = _artistAlbumTileSize(); final textScale = _effectiveTextScale(); return tileSize + 64 + ((textScale - 1) * 14); } String? _recommendedDownloadService() { return _directMetadataProviderId(); } String _effectiveMetadataProviderIdFromArtistId() { if (widget.extensionId != null && widget.extensionId!.isNotEmpty) { return widget.extensionId!; } return resolveEffectiveMetadataProvider( legacyProviderIdFromResourceId(widget.artistId) ?? 'spotify', ref.read(extensionProvider), ); } String? _directMetadataProviderId() { final providerId = _effectiveMetadataProviderIdFromArtistId(); return providerId.isEmpty ? null : providerId; } String _metadataResourceId(String providerId) { return stripPrefixedResourceId(widget.artistId); } @override void initState() { super.initState(); _scrollController.addListener(_onScroll); WidgetsBinding.instance.addPostFrameCallback((_) { final providerId = _effectiveMetadataProviderIdFromArtistId(); ref .read(recentAccessProvider.notifier) .recordArtistAccess( id: widget.artistId, name: widget.artistName, imageUrl: widget.coverUrl, providerId: providerId, ); }); if (widget.extensionId != null) { _albums = widget.albums; _topTracks = widget.topTracks; _headerImageUrl = widget.headerImageUrl; _headerVideoUrl = widget.headerVideoUrl; _monthlyListeners = widget.monthlyListeners; if ((_albums == null || _albums!.isEmpty) || (_topTracks == null || _topTracks!.isEmpty)) { _fetchDiscography(); } return; } final cached = _ArtistCache.get(widget.artistId); if (widget.albums != null) { _albums = widget.albums; _topTracks = widget.topTracks; _headerImageUrl = widget.headerImageUrl; _headerVideoUrl = widget.headerVideoUrl; _monthlyListeners = widget.monthlyListeners; if (_topTracks == null || _topTracks!.isEmpty) { _fetchDiscography(); } } else if (cached != null) { _albums = cached.albums; _releases = cached.releases; _topTracks = cached.topTracks; _headerImageUrl = cached.headerImageUrl; _headerVideoUrl = cached.headerVideoUrl; _monthlyListeners = cached.monthlyListeners; if (_topTracks == null || _topTracks!.isEmpty) { _fetchDiscography(); } } else { _fetchDiscography(); } } void _onScroll() { final shouldShow = _scrollController.offset > 280; if (shouldShow != _showTitleInAppBar) { setState(() => _showTitleInAppBar = shouldShow); } } @override void dispose() { _selectionOverlay.dispose(); _scrollController.removeListener(_onScroll); _scrollController.dispose(); _popularPageController.dispose(); super.dispose(); } Future _fetchDiscography() async { setState(() { _isLoadingDiscography = true; _error = null; }); try { List albums; List? releases; List? topTracks; String? headerImage; String? headerVideo; int? listeners; if (_directMetadataProviderId() != null) { final providerId = _directMetadataProviderId()!; final artistData = await PlatformBridge.getProviderMetadata( providerId, 'artist', _metadataResourceId(providerId), ); final albumsList = artistData['albums'] as List? ?? []; albums = albumsList .map((a) => _parseArtistAlbum(a as Map)) .toList(); final releasesList = artistData['releases'] as List? ?? []; if (releasesList.isNotEmpty) { releases = releasesList .map((a) => _parseArtistAlbum(a as Map)) .toList(); } final topTracksList = artistData['top_tracks'] as List? ?? []; if (topTracksList.isNotEmpty) { topTracks = topTracksList .map((t) => _parseTrack(t as Map)) .toList(); } final artistInfo = artistData['artist_info'] as Map?; headerImage = artistInfo?['images'] as String? ?? artistInfo?['header_image'] as String? ?? artistInfo?['cover_url'] as String? ?? artistData['header_image'] as String? ?? artistData['cover_url'] as String? ?? artistData['image_url'] as String?; headerVideo = artistInfo?['header_video'] as String? ?? artistData['header_video'] as String?; listeners = artistInfo?['listeners'] as int? ?? artistData['listeners'] as int?; } else { final url = 'https://open.spotify.com/artist/${widget.artistId}'; final result = await PlatformBridge.handleURLWithExtension(url); if (result != null && result['artist'] != null) { final artistData = result['artist'] as Map; final albumsList = artistData['albums'] as List? ?? []; albums = albumsList .map((a) => _parseArtistAlbum(a as Map)) .toList(); final topTracksList = artistData['top_tracks'] as List? ?? []; if (topTracksList.isNotEmpty) { topTracks = topTracksList .map((t) => _parseTrack(t as Map)) .toList(); } headerImage = artistData['header_image'] as String?; headerVideo = artistData['header_video'] as String?; listeners = artistData['listeners'] as int?; } else { throw StateError('Failed to load artist metadata from extension'); } } final finalHeaderImage = headerImage ?? _headerImageUrl ?? widget.headerImageUrl; final finalHeaderVideo = headerVideo ?? _headerVideoUrl ?? widget.headerVideoUrl; final finalListeners = listeners ?? _monthlyListeners ?? widget.monthlyListeners; _ArtistCache.set( widget.artistId, albums: albums, releases: releases, topTracks: topTracks, headerImageUrl: finalHeaderImage, headerVideoUrl: finalHeaderVideo, monthlyListeners: finalListeners, ); if (mounted) { setState(() { _albums = albums; _releases = releases; _topTracks = topTracks; _headerImageUrl = finalHeaderImage; _headerVideoUrl = finalHeaderVideo; _monthlyListeners = finalListeners; _error = null; _isLoadingDiscography = false; }); } } catch (e) { if (mounted) { setState(() { _error = context.friendlyError(e); _isLoadingDiscography = false; }); } } } Track _parseTrack(Map data, {ArtistAlbum? album}) { int durationMs = 0; final durationValue = data['duration_ms']; if (durationValue is int) { durationMs = durationValue; } else if (durationValue is double) { durationMs = durationValue.toInt(); } final spotifyId = (data['spotify_id'] ?? '').toString(); final nativeId = (data['id'] ?? '').toString(); return Track( id: spotifyId.isNotEmpty ? spotifyId : nativeId, name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? data['album'] ?? album?.name ?? '') .toString(), albumArtist: normalizeOptionalString(data['album_artist']?.toString()), artistId: (data['artist_id'] ?? data['artistId'])?.toString() ?? widget.artistId, albumId: data['album_id']?.toString() ?? album?.id, coverUrl: normalizeCoverReference( (data['cover_url'] ?? data['images'] ?? album?.coverUrl)?.toString(), ), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, discNumber: data['disc_number'] as int?, totalDiscs: data['total_discs'] as int?, releaseDate: data['release_date']?.toString(), albumType: normalizeOptionalString(data['album_type']?.toString()) ?? album?.albumType, totalTracks: data['total_tracks'] as int? ?? album?.totalTracks, composer: data['composer']?.toString(), source: data['provider_id']?.toString() ?? widget.extensionId, previewUrl: data['preview_url']?.toString(), explicit: parseExplicitFlag(data['explicit']), ); } ArtistAlbum _parseArtistAlbum(Map data) { final totalTracksValue = data['total_tracks']; final totalTracks = totalTracksValue is int ? totalTracksValue : int.tryParse(totalTracksValue?.toString() ?? '') ?? 0; return ArtistAlbum( id: data['id'] as String? ?? '', name: (data['name'] ?? data['title'] ?? '').toString(), releaseDate: (data['release_date'] ?? '').toString(), totalTracks: totalTracks, coverUrl: normalizeCoverReference( (data['cover_url'] ?? data['images'] ?? data['cover_art'])?.toString(), ), albumType: (data['album_type'] ?? data['type'] ?? 'album').toString(), artists: (data['artists'] ?? data['artist'] ?? widget.artistName) .toString(), providerId: data['provider_id']?.toString() ?? widget.extensionId, ); } void _ensureAlbumBuckets(List albums) { if (identical(albums, _albumBucketSource)) return; _albumBucketSource = albums; _albumsOnlyBucket = albums .where((a) => a.albumType == 'album') .toList(growable: false); _singlesBucket = albums .where((a) => a.albumType == 'single' || a.albumType == 'ep') .toList(growable: false); _compilationsBucket = albums .where((a) => a.albumType == 'compilation') .toList(growable: false); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final albums = _albums ?? []; _ensureAlbumBuckets(albums); final releases = _releases ?? const []; final albumsOnly = _albumsOnlyBucket; final singles = _singlesBucket; final compilations = _compilationsBucket; final bottomInset = context.navBarBottomInset; final hasDiscography = !_isLoadingDiscography && _error == null && albums.isNotEmpty; if (isSelectionMode || _selectionOverlay.isVisible) { final bottomPadding = MediaQuery.paddingOf(context).bottom; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _syncSelectionOverlay(albums: albums, bottomPadding: bottomPadding); }); } return PopScope( canPop: !isSelectionMode, onPopInvokedWithResult: (didPop, result) { if (!didPop && isSelectionMode) { exitSelectionMode(); } }, child: Scaffold( body: Stack( children: [ CustomScrollView( controller: _scrollController, slivers: [ _buildHeader( context, colorScheme, albums: albums, hasDiscography: hasDiscography, ), if (_isLoadingDiscography) SliverToBoxAdapter( child: ArtistScreenSkeleton( showCoverHeader: (_headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl) == null, showPopularSection: !widget.artistId.startsWith('deezer:') && !widget.artistId.startsWith('qobuz:') && !widget.artistId.startsWith('tidal:'), ), ), if (_error != null) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: ErrorCard( error: _error!, colorScheme: colorScheme, onRetry: _fetchDiscography, ), ), ), if (!_isLoadingDiscography && _error == null) ...[ if (_topTracks != null && _topTracks!.isNotEmpty) SliverToBoxAdapter( child: _buildPopularSection(colorScheme), ), if (releases.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( context.l10n.artistReleases, releases, colorScheme, ), ), if (albumsOnly.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( context.l10n.artistAlbums, albumsOnly, colorScheme, ), ), if (singles.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( context.l10n.artistSingles, singles, colorScheme, showTypeBadge: true, ), ), if (compilations.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( context.l10n.artistCompilations, compilations, colorScheme, ), ), ], SliverToBoxAdapter( child: SizedBox(height: isSelectionMode ? 120 : 32), ), SliverToBoxAdapter(child: SizedBox(height: bottomInset)), ], ), ], ), ), ); } void _syncSelectionOverlay({ required List albums, required double bottomPadding, }) { if (!isSelectionMode) { _selectionOverlay.hide(); return; } _selectionOverlay.show( context, (overlayContext) => _buildSelectionBar( overlayContext, Theme.of(overlayContext).colorScheme, albums, bottomPadding, ), ); } @override void exitSelectionMode() { HapticFeedback.lightImpact(); super.exitSelectionMode(); } @override void toggleSelection(String itemId) { HapticFeedback.selectionClick(); super.toggleSelection(itemId); } void _deselectAll() { setState(() { selectedIds.clear(); }); } Widget _buildSelectionBar( BuildContext context, ColorScheme colorScheme, List allAlbums, double bottomPadding, ) { final allSelected = selectedIds.length == allAlbums.length && allAlbums.isNotEmpty; final selectedCount = selectedIds.length; final selectedAlbums = allAlbums .where((a) => selectedIds.contains(a.id)) .toList(); final totalTracks = selectedAlbums.fold( 0, (sum, a) => sum + a.totalTracks, ); return SelectionBottomBar( selectedCount: selectedCount, allSelected: allSelected, onClose: exitSelectionMode, onToggleSelectAll: allSelected ? _deselectAll : () => selectAll(allAlbums.map((a) => a.id)), bottomPadding: bottomPadding, allSelectedLabel: context.l10n.tracksCount(totalTracks), tapToSelectLabel: selectedCount > 0 ? context.l10n.tracksCount(totalTracks) : context.l10n.discographySelectAlbumsSubtitle, children: [ Row( children: [ Expanded( child: FilledButton.icon( onPressed: selectedCount > 0 ? () => _downloadSelectedAlbums(context, selectedAlbums) : null, icon: const Icon(Icons.download, size: 18), label: Text(context.l10n.discographyDownloadSelected), ), ), ], ), ], ); } void _showDiscographyOptions( BuildContext context, ColorScheme colorScheme, List albums, ) { final albumsOnly = albums.where((a) => a.albumType == 'album').toList(); final singles = albums .where((a) => a.albumType == 'single' || a.albumType == 'ep') .toList(); final totalTracks = albums.fold(0, (sum, a) => sum + a.totalTracks); final albumTracks = albumsOnly.fold( 0, (sum, a) => sum + a.totalTracks, ); final singleTracks = singles.fold(0, (sum, a) => sum + a.totalTracks); showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, builder: (context) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ const AppSheetHandle(), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Row( children: [ Icon(Icons.download, color: colorScheme.primary), const SizedBox(width: 12), Text( context.l10n.discographyDownload, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), ), const Divider(height: 1), if (albums.isNotEmpty) _DiscographyOptionTile( icon: Icons.library_music, title: context.l10n.discographyDownloadAll, subtitle: context.l10n.discographyDownloadAllSubtitle( totalTracks, albums.length, ), onTap: () { Navigator.pop(context); _downloadAlbums(context, albums); }, ), if (albumsOnly.isNotEmpty) _DiscographyOptionTile( icon: Icons.album, title: context.l10n.discographyAlbumsOnly, subtitle: context.l10n.discographyAlbumsOnlySubtitle( albumTracks, albumsOnly.length, ), onTap: () { Navigator.pop(context); _downloadAlbums(context, albumsOnly); }, ), if (singles.isNotEmpty) _DiscographyOptionTile( icon: Icons.music_note, title: context.l10n.discographySinglesOnly, subtitle: context.l10n.discographySinglesOnlySubtitle( singleTracks, singles.length, ), onTap: () { Navigator.pop(context); _downloadAlbums(context, singles); }, ), _DiscographyOptionTile( icon: Icons.checklist, title: context.l10n.discographySelectAlbums, subtitle: context.l10n.discographySelectAlbumsSubtitle, onTap: () { Navigator.pop(context); enterSelectionMode(albums.first.id); }, ), const SizedBox(height: 8), ], ), ), ), ); } Future _downloadAlbums( BuildContext context, List albums, ) async { final settings = ref.read(settingsProvider); if (settings.askQualityBeforeDownload || settings.allowQualityVariants) { DownloadServicePicker.show( context, recommendedService: _recommendedDownloadService(), onSelect: (quality, service) { _fetchAndQueueAlbums(albums, service, quality); }, ); } else { _fetchAndQueueAlbums(albums, settings.defaultService, null); } } Future _downloadSelectedAlbums( BuildContext context, List albums, ) async { exitSelectionMode(); await _downloadAlbums(context, albums); } Future _toggleFavoriteArtist(BuildContext context) async { final providerId = _directMetadataProviderId(); final imageUrl = _headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl; final added = await ref .read(libraryCollectionsProvider.notifier) .toggleFavoriteArtist( artistId: _metadataResourceId(providerId ?? ''), providerId: providerId, name: widget.artistName, imageUrl: imageUrl, ); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( added ? context.l10n.collectionAddedToFavoriteArtists(widget.artistName) : context.l10n.collectionRemovedFromFavoriteArtists( widget.artistName, ), ), ), ); } Future _fetchAndQueueAlbums( List albums, String service, String? qualityOverride, ) async { if (_isFetchingDiscography) return; setState(() => _isFetchingDiscography = true); if (!mounted) { setState(() => _isFetchingDiscography = false); return; } final progressDialogKey = GlobalKey<_FetchingProgressDialogState>(); showDialog( context: context, barrierDismissible: false, builder: (ctx) => _FetchingProgressDialog( key: progressDialogKey, totalAlbums: albums.length, onCancel: () { setState(() => _isFetchingDiscography = false); Navigator.pop(ctx); }, ), ); final allTracks = []; int fetchedCount = 0; int failedCount = 0; for (final album in albums) { if (!_isFetchingDiscography) break; try { final tracks = await _fetchAlbumTracks(album); allTracks.addAll(tracks); } catch (e) { failedCount++; } fetchedCount++; if (mounted) { progressDialogKey.currentState?.updateProgress( fetchedCount, albums.length, ); } } setState(() => _isFetchingDiscography = false); if (mounted) { Navigator.of(context, rootNavigator: true).pop(); } if (failedCount > 0 && mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.discographyFailedToFetch)), ); } if (allTracks.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.discographyNoAlbums)), ); } return; } final settings = ref.read(settingsProvider); final skipExisting = settings.deduplicateDownloads && !settings.allowQualityVariants; final historyLookups = allTracks .map(historyLookupForTrack) .toList(growable: false); final existingHistoryKeys = skipExisting ? await ref.read( downloadHistoryBatchExistsProvider( HistoryBatchLookupRequest(historyLookups), ).future, ) : const {}; final tracksToQueue = []; int skippedCount = 0; for (var i = 0; i < allTracks.length; i++) { final track = allTracks[i]; final isDownloaded = skipExisting && existingHistoryKeys.contains(historyLookups[i].lookupKey); if (!isDownloaded) { tracksToQueue.add(track); } else { skippedCount++; } } if (tracksToQueue.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( context.l10n.discographySkippedDownloaded(0, skippedCount), ), ), ); } return; } ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( tracksToQueue, service, qualityOverride: qualityOverride, ); if (mounted) { final message = skippedCount > 0 ? context.l10n.discographySkippedDownloaded( tracksToQueue.length, skippedCount, ) : context.l10n.discographyAddedToQueue(tracksToQueue.length); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), action: buildViewQueueSnackBarAction(context), ), ); } } Future> _fetchAlbumTracks(ArtistAlbum album) async { final providerId = album.providerId; if (providerId != null && providerId.isNotEmpty) { final resourceId = stripPrefixedResourceId(album.id); final metadata = await PlatformBridge.getProviderMetadata( providerId, 'album', resourceId, ); if (metadata['track_list'] != null) { final tracksList = metadata['track_list'] as List; return tracksList .map((t) => _parseTrack(t as Map, album: album)) .toList(); } } else { final url = 'https://open.spotify.com/album/${album.id}'; final result = await PlatformBridge.handleURLWithExtension(url); if (result != null && result['tracks'] != null) { final tracksList = result['tracks'] as List; return tracksList .map((t) => _parseTrack(t as Map, album: album)) .toList(); } } return []; } }