From 485be2f4680acd7ae997d3d6a1a45be585a347d9 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:43:25 +0700 Subject: [PATCH] refactor(artist): move the discography download into a part artist_screen.dart mixed the artist page with the whole discography download flow (option sheet, album fetching, queueing and its progress dialog). Moving that flow into its own part keeps one responsibility per file and shrinks the screen file. Bodies are moved verbatim. --- lib/screens/artist_screen.dart | 340 +------------- lib/screens/artist_screen_discography.dart | 521 +++++++++++++++++++++ lib/screens/artist_screen_widgets.dart | 178 ------- lib/widgets/batch_progress_dialog.dart | 2 +- 4 files changed, 523 insertions(+), 518 deletions(-) create mode 100644 lib/screens/artist_screen_discography.dart diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 0c7231c0..e3199d47 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -60,6 +60,7 @@ import 'package:spotiflac_android/theme/app_motion.dart'; import 'package:spotiflac_android/theme/material_expressive.dart'; part 'artist_screen_widgets.dart'; +part 'artist_screen_discography.dart'; class _ArtistCache { static final _cache = TtlCache<_CacheEntry>( @@ -879,157 +880,6 @@ class _ArtistScreenState extends ConsumerState ); } - 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); - final pageContext = context; - - Widget buildSheet(BuildContext context) => AppBottomSheet( - maxHeightFactor: 0.85, - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Row( - children: [ - if (!context.isMornye) ...[ - Icon(Icons.download, color: colorScheme.primary), - const SizedBox(width: 12), - ], - Expanded( - child: Text( - context.l10n.discographyDownload, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: context.isMornye - ? FontWeight.w600 - : 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(pageContext, albums); - }, - ), - if (albumsOnly.isNotEmpty) - _DiscographyOptionTile( - icon: Icons.album, - title: context.l10n.discographyAlbumsOnly, - subtitle: context.l10n.discographyAlbumsOnlySubtitle( - albumTracks, - albumsOnly.length, - ), - onTap: () { - Navigator.pop(context); - _downloadAlbums(pageContext, 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(pageContext, singles); - }, - ), - _DiscographyOptionTile( - icon: Icons.checklist, - title: context.l10n.discographySelectAlbums, - subtitle: context.l10n.discographySelectAlbumsSubtitle, - showDivider: false, - onTap: () { - Navigator.pop(context); - enterSelectionMode(albums.first.id); - }, - ), - const SizedBox(height: 8), - ], - ), - ), - ); - - showModalBottomSheet( - context: context, - sheetAnimationStyle: AppMotion.surfaceEntrance(context), - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: context.isMornye - ? Colors.transparent - : colorScheme.surfaceContainerHigh, - builder: (sheetContext) => sheetContext.isMornye - ? Theme( - data: MornyeTheme.fromContext( - sheetContext, - brightness: Brightness.dark, - ), - child: MornyeGlassPanel( - tintOpacity: 0.78, - child: Builder(builder: buildSheet), - ), - ) - : buildSheet(sheetContext), - ); - } - - 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 = @@ -1055,192 +905,4 @@ class _ArtistScreenState extends ConsumerState ), ); } - - Future _fetchAndQueueAlbums( - List albums, - String service, - String? qualityOverride, { - bool addToPlaylist = false, - }) async { - if (!mounted || _isFetchingDiscography) return; - - final generation = ++_discographyFetchGeneration; - setState(() => _isFetchingDiscography = true); - - final progressDialogKey = GlobalKey<_FetchingProgressDialogState>(); - unawaited( - showAppDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => _FetchingProgressDialog( - key: progressDialogKey, - totalAlbums: albums.length, - onCancel: () { - _discographyFetchGeneration++; - setState(() => _isFetchingDiscography = false); - Navigator.pop(ctx); - }, - ), - ).whenComplete(() { - if (mounted && - generation == _discographyFetchGeneration && - _isFetchingDiscography) { - _discographyFetchGeneration++; - setState(() => _isFetchingDiscography = false); - } - }), - ); - - final allTracks = []; - int fetchedCount = 0; - int failedCount = 0; - - for (final album in albums) { - if (!mounted || - generation != _discographyFetchGeneration || - !_isFetchingDiscography) { - break; - } - - try { - final tracks = await _fetchAlbumTracks(album); - if (!mounted || generation != _discographyFetchGeneration) return; - allTracks.addAll(tracks); - } catch (e) { - failedCount++; - } - if (!mounted || generation != _discographyFetchGeneration) return; - - fetchedCount++; - - if (mounted) { - progressDialogKey.currentState?.updateProgress( - fetchedCount, - albums.length, - ); - } - } - - if (!mounted || generation != _discographyFetchGeneration) return; - final cancelled = !_isFetchingDiscography; - setState(() => _isFetchingDiscography = false); - if (cancelled) return; - - 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; - } - - if (addToPlaylist) { - await showAddTracksToPlaylistSheet(context, ref, allTracks); - 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); - - showAppSnackBar( - context, - 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 []; - } } diff --git a/lib/screens/artist_screen_discography.dart b/lib/screens/artist_screen_discography.dart new file mode 100644 index 00000000..9bb54f02 --- /dev/null +++ b/lib/screens/artist_screen_discography.dart @@ -0,0 +1,521 @@ +// ignore_for_file: invalid_use_of_protected_member +part of 'artist_screen.dart'; + +extension _ArtistScreenDiscography on _ArtistScreenState { + 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); + final pageContext = context; + + Widget buildSheet(BuildContext context) => AppBottomSheet( + maxHeightFactor: 0.85, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Row( + children: [ + if (!context.isMornye) ...[ + Icon(Icons.download, color: colorScheme.primary), + const SizedBox(width: 12), + ], + Expanded( + child: Text( + context.l10n.discographyDownload, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: context.isMornye + ? FontWeight.w600 + : 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(pageContext, albums); + }, + ), + if (albumsOnly.isNotEmpty) + _DiscographyOptionTile( + icon: Icons.album, + title: context.l10n.discographyAlbumsOnly, + subtitle: context.l10n.discographyAlbumsOnlySubtitle( + albumTracks, + albumsOnly.length, + ), + onTap: () { + Navigator.pop(context); + _downloadAlbums(pageContext, 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(pageContext, singles); + }, + ), + _DiscographyOptionTile( + icon: Icons.checklist, + title: context.l10n.discographySelectAlbums, + subtitle: context.l10n.discographySelectAlbumsSubtitle, + showDivider: false, + onTap: () { + Navigator.pop(context); + enterSelectionMode(albums.first.id); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + + showModalBottomSheet( + context: context, + sheetAnimationStyle: AppMotion.surfaceEntrance(context), + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: context.isMornye + ? Colors.transparent + : colorScheme.surfaceContainerHigh, + builder: (sheetContext) => sheetContext.isMornye + ? Theme( + data: MornyeTheme.fromContext( + sheetContext, + brightness: Brightness.dark, + ), + child: MornyeGlassPanel( + tintOpacity: 0.78, + child: Builder(builder: buildSheet), + ), + ) + : buildSheet(sheetContext), + ); + } + + 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 _fetchAndQueueAlbums( + List albums, + String service, + String? qualityOverride, { + bool addToPlaylist = false, + }) async { + if (!mounted || _isFetchingDiscography) return; + + final generation = ++_discographyFetchGeneration; + setState(() => _isFetchingDiscography = true); + + final progressDialogKey = GlobalKey<_FetchingProgressDialogState>(); + unawaited( + showAppDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => _FetchingProgressDialog( + key: progressDialogKey, + totalAlbums: albums.length, + onCancel: () { + _discographyFetchGeneration++; + setState(() => _isFetchingDiscography = false); + Navigator.pop(ctx); + }, + ), + ).whenComplete(() { + if (mounted && + generation == _discographyFetchGeneration && + _isFetchingDiscography) { + _discographyFetchGeneration++; + setState(() => _isFetchingDiscography = false); + } + }), + ); + + final allTracks = []; + int fetchedCount = 0; + int failedCount = 0; + + for (final album in albums) { + if (!mounted || + generation != _discographyFetchGeneration || + !_isFetchingDiscography) { + break; + } + + try { + final tracks = await _fetchAlbumTracks(album); + if (!mounted || generation != _discographyFetchGeneration) return; + allTracks.addAll(tracks); + } catch (e) { + failedCount++; + } + if (!mounted || generation != _discographyFetchGeneration) return; + + fetchedCount++; + + if (mounted) { + progressDialogKey.currentState?.updateProgress( + fetchedCount, + albums.length, + ); + } + } + + if (!mounted || generation != _discographyFetchGeneration) return; + final cancelled = !_isFetchingDiscography; + setState(() => _isFetchingDiscography = false); + if (cancelled) return; + + 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; + } + + if (addToPlaylist) { + await showAddTracksToPlaylistSheet(context, ref, allTracks); + 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); + + showAppSnackBar( + context, + 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 []; + } +} + +class _DiscographyOptionTile extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + final VoidCallback onTap; + final bool showDivider; + + const _DiscographyOptionTile({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + this.showDivider = true, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + if (context.isMornye) { + final textTheme = Theme.of(context).textTheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoButton( + padding: const EdgeInsets.symmetric(vertical: 16), + onPressed: onTap, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: textTheme.bodyLarge), + const SizedBox(height: 4), + Text( + subtitle, + style: textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface.withValues( + alpha: 0.72, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Icon( + mornyeIconFor(icon), + size: 22, + color: colorScheme.onSurface, + ), + ], + ), + ), + if (showDivider) const Divider(height: 0.5, thickness: 0.5), + ], + ), + ); + } + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: colorScheme.onPrimaryContainer, size: 24), + ), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Text( + subtitle, + style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12), + ), + trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), + onTap: onTap, + ); + } +} + +class _FetchingProgressDialog extends StatefulWidget { + final int totalAlbums; + final VoidCallback onCancel; + + const _FetchingProgressDialog({ + super.key, + required this.totalAlbums, + required this.onCancel, + }); + + @override + State<_FetchingProgressDialog> createState() => + _FetchingProgressDialogState(); +} + +class _FetchingProgressDialogState extends State<_FetchingProgressDialog> { + int _current = 0; + int _total = 0; + + @override + void initState() { + super.initState(); + _total = widget.totalAlbums; + } + + void updateProgress(int current, int total) { + if (mounted) { + setState(() { + _current = current; + _total = total; + }); + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final progress = _total > 0 ? _current / _total : 0.0; + + return AppAlertDialog( + backgroundColor: colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + SizedBox( + width: 64, + height: 64, + child: Stack( + alignment: Alignment.center, + children: [ + AppProgressIndicator.circular( + value: progress > 0 ? progress : null, + strokeWidth: 4, + trackColor: colorScheme.surfaceContainerHighest, + ), + Icon(Icons.library_music, color: colorScheme.primary, size: 24), + ], + ), + ), + const SizedBox(height: 20), + Text( + context.l10n.discographyFetchingTracks, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Text( + context.l10n.discographyFetchingAlbum(_current, _total), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: AppProgressIndicator.linear( + value: progress > 0 ? progress : null, + trackColor: colorScheme.surfaceContainerHighest, + minHeight: 6, + ), + ), + ], + ), + actions: [ + AppDialogAction( + onPressed: widget.onCancel, + child: Text(context.l10n.dialogCancel), + ), + ], + ); + } +} diff --git a/lib/screens/artist_screen_widgets.dart b/lib/screens/artist_screen_widgets.dart index 28206dd0..d814cb91 100644 --- a/lib/screens/artist_screen_widgets.dart +++ b/lib/screens/artist_screen_widgets.dart @@ -1111,181 +1111,3 @@ extension _ArtistScreenSections on _ArtistScreenState { } } } - -class _DiscographyOptionTile extends StatelessWidget { - final IconData icon; - final String title; - final String subtitle; - final VoidCallback onTap; - final bool showDivider; - - const _DiscographyOptionTile({ - required this.icon, - required this.title, - required this.subtitle, - required this.onTap, - this.showDivider = true, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - if (context.isMornye) { - final textTheme = Theme.of(context).textTheme; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CupertinoButton( - padding: const EdgeInsets.symmetric(vertical: 16), - onPressed: onTap, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: textTheme.bodyLarge), - const SizedBox(height: 4), - Text( - subtitle, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface.withValues( - alpha: 0.72, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 16), - Icon( - mornyeIconFor(icon), - size: 22, - color: colorScheme.onSurface, - ), - ], - ), - ), - if (showDivider) const Divider(height: 0.5, thickness: 0.5), - ], - ), - ); - } - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Icon(icon, color: colorScheme.onPrimaryContainer, size: 24), - ), - title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text( - subtitle, - style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12), - ), - trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), - onTap: onTap, - ); - } -} - -class _FetchingProgressDialog extends StatefulWidget { - final int totalAlbums; - final VoidCallback onCancel; - - const _FetchingProgressDialog({ - super.key, - required this.totalAlbums, - required this.onCancel, - }); - - @override - State<_FetchingProgressDialog> createState() => - _FetchingProgressDialogState(); -} - -class _FetchingProgressDialogState extends State<_FetchingProgressDialog> { - int _current = 0; - int _total = 0; - - @override - void initState() { - super.initState(); - _total = widget.totalAlbums; - } - - void updateProgress(int current, int total) { - if (mounted) { - setState(() { - _current = current; - _total = total; - }); - } - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final progress = _total > 0 ? _current / _total : 0.0; - - return AppAlertDialog( - backgroundColor: colorScheme.surfaceContainerHigh, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - SizedBox( - width: 64, - height: 64, - child: Stack( - alignment: Alignment.center, - children: [ - AppProgressIndicator.circular( - value: progress > 0 ? progress : null, - strokeWidth: 4, - trackColor: colorScheme.surfaceContainerHighest, - ), - Icon(Icons.library_music, color: colorScheme.primary, size: 24), - ], - ), - ), - const SizedBox(height: 20), - Text( - context.l10n.discographyFetchingTracks, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 8), - Text( - context.l10n.discographyFetchingAlbum(_current, _total), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: AppProgressIndicator.linear( - value: progress > 0 ? progress : null, - trackColor: colorScheme.surfaceContainerHighest, - minHeight: 6, - ), - ), - ], - ), - actions: [ - AppDialogAction( - onPressed: widget.onCancel, - child: Text(context.l10n.dialogCancel), - ), - ], - ); - } -} diff --git a/lib/widgets/batch_progress_dialog.dart b/lib/widgets/batch_progress_dialog.dart index a2eab43f..d668f26d 100644 --- a/lib/widgets/batch_progress_dialog.dart +++ b/lib/widgets/batch_progress_dialog.dart @@ -11,7 +11,7 @@ class _BatchProgress { /// A reusable progress dialog for batch operations like conversion and /// re-enrich. Follows the same visual style as [_FetchingProgressDialog] in -/// artist_screen.dart. +/// artist_screen_discography.dart. /// /// Uses a static [ValueNotifier] so callers do not need the dialog's /// [BuildContext] to push updates – unlike `findAncestorStateOfType` which