feat(album): batch download selected tracks

This commit is contained in:
zarzet
2026-07-28 15:48:45 +07:00
parent fb538e9517
commit 6c89a1e177
4 changed files with 390 additions and 96 deletions
+18 -1
View File
@@ -118,6 +118,12 @@ void showQueuedSnackbar(BuildContext context, int added, int skipped) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
bool shouldShowBatchDownloadPicker({
required bool forceQualityPicker,
required bool askQualityBeforeDownload,
required bool allowQualityVariants,
}) => forceQualityPicker || askQualityBeforeDownload || allowQualityVariants;
/// Shared batch "add to queue" flow for detail screens: skips tracks already
/// present in download history or the local library, then either shows the
/// quality/service picker or resolves a default service, before enqueueing
@@ -126,6 +132,9 @@ void showQueuedSnackbar(BuildContext context, int added, int skipped) {
/// Set [resolveDefaultService] to false to match screens that pass
/// `settings.defaultService` straight through without resolving it against
/// enabled extensions (playlist / library-folder screens do this today).
/// [forceQualityPicker] lets an explicit multi-select action choose one
/// provider and quality for the complete batch. [onQueued] runs only after
/// tracks are actually added, not when the picker is merely opened.
Future<void> queueTracksSkippingDownloaded(
BuildContext context,
WidgetRef ref,
@@ -134,6 +143,8 @@ Future<void> queueTracksSkippingDownloaded(
String? recommendedService,
String? playlistName,
bool resolveDefaultService = true,
bool forceQualityPicker = false,
VoidCallback? onQueued,
}) async {
if (tracks.isEmpty) return;
@@ -198,7 +209,11 @@ Future<void> queueTracksSkippingDownloaded(
return;
}
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
if (shouldShowBatchDownloadPicker(
forceQualityPicker: forceQualityPicker,
askQualityBeforeDownload: settings.askQualityBeforeDownload,
allowQualityVariants: settings.allowQualityVariants,
)) {
DownloadServicePicker.show(
context,
trackName: '${tracksToQueue.length} tracks',
@@ -213,6 +228,7 @@ Future<void> queueTracksSkippingDownloaded(
qualityOverride: quality,
playlistName: playlistName,
);
onQueued?.call();
showQueuedSnackbar(context, tracksToQueue.length, skippedCount);
},
);
@@ -238,6 +254,7 @@ Future<void> queueTracksSkippingDownloaded(
ref
.read(downloadQueueProvider.notifier)
.addMultipleToQueue(tracksToQueue, service, playlistName: playlistName);
onQueued?.call();
showQueuedSnackbar(context, tracksToQueue.length, skippedCount);
}
+60 -26
View File
@@ -7,6 +7,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/utils/local_playback.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/in_library_badge.dart';
import 'package:spotiflac_android/widgets/preview_button.dart';
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
@@ -22,6 +23,10 @@ class TrackListTile extends ConsumerWidget {
final void Function({bool forceQualityPicker}) onDownload;
final Widget leading;
final bool clickableArtist;
final bool isSelectionMode;
final bool isSelected;
final VoidCallback? onToggleSelection;
final VoidCallback? onEnterSelectionMode;
const TrackListTile({
super.key,
@@ -30,7 +35,11 @@ class TrackListTile extends ConsumerWidget {
required this.onDownload,
required this.leading,
this.clickableArtist = false,
});
this.isSelectionMode = false,
this.isSelected = false,
this.onToggleSelection,
this.onEnterSelectionMode,
}) : assert(!isSelectionMode || onToggleSelection != null);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -65,13 +74,29 @@ class TrackListTile extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
elevation: 0,
color: Colors.transparent,
color: isSelected
? colorScheme.primaryContainer.withValues(alpha: 0.3)
: Colors.transparent,
margin: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
leading: leading,
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isSelectionMode) ...[
AnimatedSelectionCheckbox(
visible: true,
selected: isSelected,
colorScheme: colorScheme,
size: 24,
),
const SizedBox(width: 12),
],
leading,
],
),
title: Text(
track.name,
maxLines: 1,
@@ -83,7 +108,7 @@ class TrackListTile extends ConsumerWidget {
subtitle: Row(
children: [
Flexible(
child: clickableArtist
child: clickableArtist && !isSelectionMode
? ClickableArtistName(
artistName: track.artistName,
artistId: track.artistId,
@@ -112,28 +137,37 @@ class TrackListTile extends ConsumerWidget {
],
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
PreviewButton(track: track),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
],
),
onTap: () => _handleTap(
context,
ref,
isQueued: isQueued,
isInLocalLibrary: isInLocalLibrary,
),
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
context,
ref,
track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
trailing: isSelectionMode
? null
: Row(
mainAxisSize: MainAxisSize.min,
children: [
PreviewButton(track: track),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate:
isInHistory || isInLocalLibrary,
),
],
),
onTap: isSelectionMode
? onToggleSelection
: () => _handleTap(
context,
ref,
isQueued: isQueued,
isInLocalLibrary: isInLocalLibrary,
),
onLongPress: isSelectionMode
? null
: onEnterSelectionMode ??
() => TrackCollectionQuickActions.showTrackOptionsSheet(
context,
ref,
track,
hasLocalPlaybackCandidate:
isInHistory || isInLocalLibrary,
),
),
),
);