part of 'queue_tab.dart'; extension _QueueTabBatchActions on _QueueTabState { Future _reEnrichQueueLocalTrack( LocalLibraryItem item, { List? updateFields, }) async { final durationMs = (item.duration ?? 0) * 1000; final settings = ref.read(settingsProvider); final artistTagMode = settings.artistTagMode; await ref.read(settingsProvider.notifier).syncLyricsSettingsToBackend(); final request = { 'file_path': item.filePath, 'cover_url': '', 'max_quality': true, 'embed_lyrics': settings.embedLyrics, 'lyrics_mode': settings.lyricsMode, 'artist_tag_mode': artistTagMode, 'spotify_id': '', 'track_name': item.trackName, 'artist_name': item.artistName, 'album_name': item.albumName, 'album_artist': item.albumArtist ?? '', 'track_number': item.trackNumber ?? 0, 'disc_number': item.discNumber ?? 0, 'release_date': item.releaseDate ?? '', 'isrc': item.isrc ?? '', 'genre': item.genre ?? '', 'label': '', 'copyright': '', 'duration_ms': durationMs, 'search_online': true, // ignore: use_null_aware_elements if (updateFields != null) 'update_fields': updateFields, }; final result = await PlatformBridge.reEnrichFile(request); final method = result['method'] as String?; if (method == 'native') { // Filesystem .lrc sidecar (SAF sidecar handled natively in Kotlin). await writeReEnrichSidecarLrc( audioFilePath: item.filePath, reEnrichResult: result, ); return true; } if (method == 'ffmpeg') { return applyFfmpegReEnrichResult( item: item, result: result, artistTagMode: artistTagMode, ); } return false; } List _selectedFlacEligibleLocalItems( List allItems, ) { final selectedItems = _selectedItemsFromAll(allItems); return selectedItems .map((item) => item.localItem) .whereType() .where(LocalTrackRedownloadService.isFlacUpgradeEligible) .toList(growable: false); } Future _queueSelectedLocalAsFlac( List allItems, ) async { final selectedLocalItems = _selectedFlacEligibleLocalItems(allItems); if (selectedLocalItems.isEmpty) { return; } final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.queueFlacAction), content: Text( context.l10n.queueFlacConfirmMessage(selectedLocalItems.length), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: Text(context.l10n.queueFlacAction), ), ], ), ); if (confirmed != true || !mounted) { return; } final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); final includeExtensions = settings.useExtensionProviders && extensionState.extensions.any( (ext) => ext.enabled && ext.hasMetadataProvider, ); final targetService = LocalTrackRedownloadService.preferredFlacService( settings, extensionState, ); if (targetService.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.extensionsNoDownloadProvider)), ); return; } final targetQuality = LocalTrackRedownloadService.preferredFlacQualityForService( targetService, extensionState, ); final matchedTracks = []; var skippedCount = 0; final total = selectedLocalItems.length; var cancelled = false; BatchProgressDialog.show( context: context, title: context.l10n.queueFlacAction, total: total, icon: Icons.queue_music, onCancel: () { cancelled = true; BatchProgressDialog.dismiss(context); }, ); for (var i = 0; i < total; i++) { if (!mounted || cancelled) break; BatchProgressDialog.update( current: i + 1, detail: selectedLocalItems[i].trackName, ); try { final resolution = await LocalTrackRedownloadService.resolveBestMatch( selectedLocalItems[i], includeExtensions: includeExtensions, ); if (resolution.canQueue && resolution.match != null) { matchedTracks.add(resolution.match!); } else { skippedCount++; } } catch (_) { skippedCount++; } } if (!mounted) { return; } if (!cancelled) { BatchProgressDialog.dismiss(context); } if (matchedTracks.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.queueFlacNoReliableMatches)), ); return; } ref .read(downloadQueueProvider.notifier) .addMultipleToQueue( matchedTracks, targetService, qualityOverride: targetQuality, ); final summary = skippedCount == 0 ? context.l10n.snackbarAddedTracksToQueue(matchedTracks.length) : context.l10n.queueFlacQueuedWithSkipped( matchedTracks.length, skippedCount, ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(summary))); _setState(() { _selectedIds.clear(); _isSelectionMode = false; }); } Future _reEnrichSelectedLocalFromQueue( List allItems, ) async { final selectedItems = _selectedItemsFromAll(allItems); final selectedLocalItems = selectedItems .map((item) => item.localItem) .whereType() .toList(growable: false); if (selectedLocalItems.isEmpty) { return; } // Hide the selection overlay: set the flag (prevents build() from // re-inserting via postFrameCallback) and remove the entry immediately. _setState(() => _isSelectionMode = false); _hideSelectionOverlay(); final selection = await showReEnrichFieldDialog( context, selectedCount: selectedLocalItems.length, ); if (selection == null || !mounted) { // Cancelled — restore selection mode; the next build cycle will // re-create the overlay via _syncSelectionOverlay in postFrameCallback. if (mounted) _setState(() => _isSelectionMode = true); return; } final updateFields = selection.isAll ? null : selection.fields; var successCount = 0; final total = selectedLocalItems.length; var cancelled = false; BatchProgressDialog.show( context: context, title: context.l10n.trackReEnrichProgress, total: total, icon: Icons.auto_fix_high, onCancel: () { cancelled = true; BatchProgressDialog.dismiss(context); }, ); for (var i = 0; i < total; i++) { if (!mounted || cancelled) break; final item = selectedLocalItems[i]; BatchProgressDialog.update( current: i + 1, detail: '${item.trackName} - ${item.artistName}', ); try { final ok = await _reEnrichQueueLocalTrack( item, updateFields: updateFields, ); if (ok) { successCount++; } } catch (_) {} } if (!mounted) { return; } final settings = ref.read(settingsProvider); final localLibraryPath = settings.localLibraryPath.trim(); final iosBookmark = settings.localLibraryBookmark; try { if (localLibraryPath.isNotEmpty && !ref.read(localLibraryProvider).isScanning) { await ref .read(localLibraryProvider.notifier) .startScan( localLibraryPath, iosBookmark: iosBookmark.isNotEmpty ? iosBookmark : null, ); } else { await ref.read(localLibraryProvider.notifier).reloadFromStorage(); } } catch (_) { await ref.read(localLibraryProvider.notifier).reloadFromStorage(); } _exitSelectionMode(); if (!mounted) { return; } if (!cancelled) { BatchProgressDialog.dismiss(context); } ScaffoldMessenger.of(context).clearSnackBars(); final failedCount = total - successCount; final summary = failedCount <= 0 ? '${context.l10n.trackReEnrichSuccess} ($successCount/$total)' : context.l10n.trackReEnrichSuccessWithFailures( successCount, total, failedCount, ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(summary))); } /// Share selected tracks via system share sheet Future _shareSelected(List allItems) async { final itemsById = {for (final item in allItems) item.id: item}; final safUris = []; final filesToShare = []; for (final id in _selectedIds) { final item = itemsById[id]; if (item == null) continue; final path = item.filePath; if (isContentUri(path)) { if (await fileExists(path)) safUris.add(path); } else if (await fileExists(path)) { filesToShare.add(XFile(path)); } } if (safUris.isEmpty && filesToShare.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.selectionShareNoFiles)), ); } return; } if (safUris.isNotEmpty) { try { if (safUris.length == 1) { await PlatformBridge.shareContentUri(safUris.first); } else { await PlatformBridge.shareMultipleContentUris(safUris); } } catch (_) {} } if (filesToShare.isNotEmpty) { await SharePlus.instance.share(ShareParams(files: filesToShare)); } } Future _showBatchConvertSheet( BuildContext context, List allItems, ) { return showBatchConvertSheet( context, ref, _selectedItemsFromAll(allItems), onExitSelectionMode: _exitSelectionMode, onSheetOpen: () { _suppressSelectionOverlay = true; _hideSelectionOverlay(); _hidePlaylistSelectionOverlay(); }, onSheetClosed: (didStartConversion) async { // Wait out the sheet's exit animation before restoring the toolbar so // it doesn't pop in front of the still-closing sheet. await Future.delayed(const Duration(milliseconds: 260)); if (!mounted) { _suppressSelectionOverlay = false; return; } _suppressSelectionOverlay = false; if (didStartConversion) return; if (_isSelectionMode) { _syncSelectionOverlay( items: allItems, bottomPadding: MediaQuery.of(this.context).padding.bottom, ); } else if (_isPlaylistSelectionMode) { _syncPlaylistSelectionOverlay( playlists: ref.read(libraryCollectionsProvider).playlists, bottomPadding: MediaQuery.of(this.context).padding.bottom, ); } }, ); } Future _runBatchReplayGain(List allItems) { return runBatchReplayGain( context, _selectedItemsFromAll(allItems), onExitSelectionMode: _exitSelectionMode, onConfirmOpen: () { _suppressSelectionOverlay = true; _hideSelectionOverlay(); _hidePlaylistSelectionOverlay(); }, onConfirmClosed: (confirmed) async { if (!mounted) { _suppressSelectionOverlay = false; return; } if (confirmed) { _suppressSelectionOverlay = false; return; } // Restore after the dialog's exit animation. await Future.delayed(const Duration(milliseconds: 220)); _suppressSelectionOverlay = false; if (!mounted) return; if (_isSelectionMode) { _syncSelectionOverlay( items: allItems, bottomPadding: MediaQuery.paddingOf(context).bottom, ); } }, ); } }