diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 42818a12..5fe1b77a 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -626,11 +626,13 @@ class _LocalAlbumScreenState extends ConsumerState { slivers.add( SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => - _buildTrackItem(context, colorScheme, discTracks[index]), - childCount: discTracks.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final track = discTracks[index]; + return KeyedSubtree( + key: ValueKey(track.id), + child: _buildTrackItem(context, colorScheme, track), + ); + }, childCount: discTracks.length), ), ); } @@ -900,16 +902,19 @@ class _LocalAlbumScreenState extends ConsumerState { return false; } - Future _queueSelectedAsFlac(List allTracks) async { + List _selectedFlacEligibleItems( + List allTracks, + ) { final tracksById = {for (final t in allTracks) t.id: t}; - final selected = []; + return _selectedIds + .map((id) => tracksById[id]) + .whereType() + .where(LocalTrackRedownloadService.isFlacUpgradeEligible) + .toList(growable: false); + } - for (final id in _selectedIds) { - final item = tracksById[id]; - if (item != null) { - selected.add(item); - } - } + Future _queueSelectedAsFlac(List allTracks) async { + final selected = _selectedFlacEligibleItems(allTracks); if (selected.isEmpty) { return; @@ -962,9 +967,7 @@ class _LocalAlbumScreenState extends ConsumerState { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.queueFlacFindingProgress(i + 1, total), - ), + content: Text(context.l10n.queueFlacFindingProgress(i + 1, total)), duration: const Duration(seconds: 30), ), ); @@ -1177,8 +1180,9 @@ class _LocalAlbumScreenState extends ConsumerState { String selectedFormat = formats.first; bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - String selectedBitrate = - isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); + String selectedBitrate = isLosslessTarget + ? '320k' + : (selectedFormat == 'Opus' ? '128k' : '320k'); showModalBottomSheet( context: context, @@ -1240,8 +1244,9 @@ class _LocalAlbumScreenState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -1286,11 +1291,8 @@ class _LocalAlbumScreenState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -1371,7 +1373,8 @@ class _LocalAlbumScreenState extends ConsumerState { if (currentFormat == null || currentFormat == targetFormat) continue; // Skip lossy sources when target is lossless (pointless re-encoding) final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; - final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; + final isLosslessSource = + currentFormat == 'FLAC' || currentFormat == 'M4A'; if (isLosslessTarget && !isLosslessSource) continue; selected.add(item); } @@ -1656,6 +1659,7 @@ class _LocalAlbumScreenState extends ConsumerState { double bottomPadding, ) { final selectedCount = _selectedIds.length; + final flacEligibleCount = _selectedFlacEligibleItems(tracks).length; final allSelected = selectedCount == tracks.length && tracks.isNotEmpty; return Container( @@ -1750,8 +1754,9 @@ class _LocalAlbumScreenState extends ConsumerState { Expanded( child: _LocalAlbumSelectionActionButton( icon: Icons.download_for_offline_outlined, - label: '${context.l10n.queueFlacAction} ($selectedCount)', - onPressed: selectedCount > 0 + label: + '${context.l10n.queueFlacAction} ($flacEligibleCount)', + onPressed: flacEligibleCount > 0 ? () => _queueSelectedAsFlac(tracks) : null, colorScheme: colorScheme, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 69b1aa44..eb82c984 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4484,14 +4484,21 @@ class _QueueTabState extends ConsumerState { 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 selectedItems = _selectedItemsFromAll(allItems); - final selectedLocalItems = selectedItems - .map((item) => item.localItem) - .whereType() - .toList(growable: false); + final selectedLocalItems = _selectedFlacEligibleLocalItems(allItems); if (selectedLocalItems.isEmpty) { return; @@ -4546,9 +4553,7 @@ class _QueueTabState extends ConsumerState { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.queueFlacFindingProgress(i + 1, total), - ), + content: Text(context.l10n.queueFlacFindingProgress(i + 1, total)), duration: const Duration(seconds: 30), ), ); @@ -4797,8 +4802,9 @@ class _QueueTabState extends ConsumerState { String selectedFormat = formats.first; bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - String selectedBitrate = - isLosslessTarget ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); + String selectedBitrate = isLosslessTarget + ? '320k' + : (selectedFormat == 'Opus' ? '128k' : '320k'); var didStartConversion = false; _hideSelectionOverlay(); @@ -4864,8 +4870,9 @@ class _QueueTabState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = - format == 'Opus' ? '128k' : '320k'; + selectedBitrate = format == 'Opus' + ? '128k' + : '320k'; } }); } @@ -4910,11 +4917,8 @@ class _QueueTabState extends ConsumerState { const SizedBox(width: 6), Text( context.l10n.trackConvertLosslessHint, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.primary), ), ], ), @@ -5054,7 +5058,8 @@ class _QueueTabState extends ConsumerState { int successCount = 0; final total = selectedItems.length; final historyDb = HistoryDatabase.instance; - final newQuality = (targetFormat.toUpperCase() == 'ALAC' || + final newQuality = + (targetFormat.toUpperCase() == 'ALAC' || targetFormat.toUpperCase() == 'FLAC') ? '${targetFormat.toUpperCase()} Lossless' : '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}'; @@ -5375,6 +5380,9 @@ class _QueueTabState extends ConsumerState { final allSelected = selectedCount == unifiedItems.length && unifiedItems.isNotEmpty; final localOnlySelection = _isLocalOnlySelection(unifiedItems); + final flacEligibleCount = _selectedFlacEligibleLocalItems( + unifiedItems, + ).length; return Container( decoration: BoxDecoration( @@ -5469,8 +5477,8 @@ class _QueueTabState extends ConsumerState { child: _SelectionActionButton( icon: Icons.download_for_offline_outlined, label: - '${context.l10n.queueFlacAction} ($selectedCount)', - onPressed: selectedCount > 0 + '${context.l10n.queueFlacAction} ($flacEligibleCount)', + onPressed: flacEligibleCount > 0 ? () => _queueSelectedLocalAsFlac(unifiedItems) : null, colorScheme: colorScheme, diff --git a/lib/services/local_track_redownload_service.dart b/lib/services/local_track_redownload_service.dart index 4b8500c7..d03c6c8e 100644 --- a/lib/services/local_track_redownload_service.dart +++ b/lib/services/local_track_redownload_service.dart @@ -23,6 +23,15 @@ class LocalTrackRedownloadService { static const int _minimumConfidenceScore = 85; static const int _ambiguousScoreGap = 8; + static bool isFlacUpgradeEligible(LocalLibraryItem item) { + final format = item.format?.trim().toLowerCase(); + if (format == 'flac') { + return false; + } + + return !item.filePath.toLowerCase().endsWith('.flac'); + } + static Future resolveBestMatch( LocalLibraryItem item, { required bool includeExtensions,