fix: skip tracks already in FLAC from queue-as-FLAC selection and fix local album track list widget identity

This commit is contained in:
zarzet
2026-03-17 15:00:56 +07:00
parent ac1c7d31c9
commit e003b15ffd
3 changed files with 70 additions and 48 deletions
+33 -28
View File
@@ -626,11 +626,13 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
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<LocalAlbumScreen> {
return false;
}
Future<void> _queueSelectedAsFlac(List<LocalLibraryItem> allTracks) async {
List<LocalLibraryItem> _selectedFlacEligibleItems(
List<LocalLibraryItem> allTracks,
) {
final tracksById = {for (final t in allTracks) t.id: t};
final selected = <LocalLibraryItem>[];
return _selectedIds
.map((id) => tracksById[id])
.whereType<LocalLibraryItem>()
.where(LocalTrackRedownloadService.isFlacUpgradeEligible)
.toList(growable: false);
}
for (final id in _selectedIds) {
final item = tracksById[id];
if (item != null) {
selected.add(item);
}
}
Future<void> _queueSelectedAsFlac(List<LocalLibraryItem> allTracks) async {
final selected = _selectedFlacEligibleItems(allTracks);
if (selected.isEmpty) {
return;
@@ -962,9 +967,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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,
+28 -20
View File
@@ -4484,14 +4484,21 @@ class _QueueTabState extends ConsumerState<QueueTab> {
return false;
}
List<LocalLibraryItem> _selectedFlacEligibleLocalItems(
List<UnifiedLibraryItem> allItems,
) {
final selectedItems = _selectedItemsFromAll(allItems);
return selectedItems
.map((item) => item.localItem)
.whereType<LocalLibraryItem>()
.where(LocalTrackRedownloadService.isFlacUpgradeEligible)
.toList(growable: false);
}
Future<void> _queueSelectedLocalAsFlac(
List<UnifiedLibraryItem> allItems,
) async {
final selectedItems = _selectedItemsFromAll(allItems);
final selectedLocalItems = selectedItems
.map((item) => item.localItem)
.whereType<LocalLibraryItem>()
.toList(growable: false);
final selectedLocalItems = _selectedFlacEligibleLocalItems(allItems);
if (selectedLocalItems.isEmpty) {
return;
@@ -4546,9 +4553,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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<QueueTab> {
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,
@@ -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<LocalTrackRedownloadResolution> resolveBestMatch(
LocalLibraryItem item, {
required bool includeExtensions,