From d4178ad0369935d7fbc750f1035ba17506f8bbd5 Mon Sep 17 00:00:00 2001 From: ViscousPot Date: Sat, 14 Mar 2026 01:38:32 +0000 Subject: [PATCH] feat: add option to download multiple selected playlists --- lib/screens/queue_tab.dart | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 0a56f7a1..7e3f96f9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -31,6 +31,7 @@ import 'package:spotiflac_android/screens/local_album_screen.dart'; import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/utils/path_match_keys.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; +import 'package:spotiflac_android/widgets/download_service_picker.dart'; enum LibraryItemSource { downloaded, local } @@ -1314,6 +1315,93 @@ class _QueueTabState extends ConsumerState { }); } + Future _downloadAllSelectedPlaylists(BuildContext context) async { + final collectionsState = ref.read(libraryCollectionsProvider); + final selectedPlaylists = collectionsState.playlists + .where((p) => _selectedPlaylistIds.contains(p.id)) + .toList(); + + final totalTracks = + selectedPlaylists.fold(0, (sum, p) => sum + p.tracks.length); + + if (totalTracks == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Selected playlists have no tracks')), + ); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Download All'), + content: Text( + 'Download $totalTracks ${totalTracks == 1 ? 'track' : 'tracks'} ' + 'from ${selectedPlaylists.length} ' + '${selectedPlaylists.length == 1 ? 'playlist' : 'playlists'}?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(ctx.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Download'), + ), + ], + ), + ); + + if (confirmed != true || !context.mounted) return; + + final settings = ref.read(settingsProvider); + final queueNotifier = ref.read(downloadQueueProvider.notifier); + + void enqueueAll({String? qualityOverride, String? service}) { + final svc = service ?? settings.defaultService; + for (final playlist in selectedPlaylists) { + final tracks = playlist.tracks.map((e) => e.track).toList(); + queueNotifier.addMultipleToQueue( + tracks, + svc, + qualityOverride: qualityOverride, + playlistName: playlist.name, + ); + } + } + + if (settings.askQualityBeforeDownload) { + DownloadServicePicker.show( + context, + trackName: '$totalTracks tracks', + artistName: '${selectedPlaylists.length} playlists', + onSelect: (quality, service) { + enqueueAll(qualityOverride: quality, service: service); + if (!mounted) return; + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + }, + ); + } else { + enqueueAll(); + _exitPlaylistSelectionMode(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.snackbarAddedTracksToQueue(totalTracks), + ), + ), + ); + } + } + Future _deleteSelectedPlaylists(BuildContext context) async { final count = _selectedPlaylistIds.length; final confirmed = await showDialog( @@ -1452,6 +1540,35 @@ class _QueueTabState extends ConsumerState { const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _downloadAllSelectedPlaylists(context) + : null, + icon: const Icon(Icons.download_rounded), + label: Text( + selectedCount > 0 + ? 'Download $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' + : 'Select playlists to download', + ), + style: FilledButton.styleFrom( + backgroundColor: selectedCount > 0 + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + foregroundColor: selectedCount > 0 + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + + const SizedBox(height: 8), + SizedBox( width: double.infinity, child: FilledButton.icon(