fix(library): show play action for completed batch items

This commit is contained in:
zarzet
2026-08-27 17:04:58 +07:00
parent 2b117415ae
commit fca8e46412
3 changed files with 167 additions and 1 deletions
@@ -25,3 +25,17 @@ bool shouldRetainQueueLibraryPageSnapshot({
required bool cachedHasContent,
required bool activeDownloadFallbackAvailable,
}) => currentIsEmpty && cachedHasContent && activeDownloadFallbackAvailable;
/// Resolves the playable path for a just-completed download while its pinned
/// completion-bridge card is still visible. The finalized history path wins
/// because conversion or SAF publication may change the original queue path.
String? resolveCompletionBridgePlayablePath({
String? historyFilePath,
String? completedItemFilePath,
}) {
final historyPath = historyFilePath?.trim();
if (historyPath != null && historyPath.isNotEmpty) return historyPath;
final completedPath = completedItemFilePath?.trim();
return completedPath == null || completedPath.isEmpty ? null : completedPath;
}
+103 -1
View File
@@ -122,6 +122,10 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
final quality =
unifiedItem?.qualityForMode(_libraryQualityLabelMode) ??
track.audioQuality;
final playablePath = resolveCompletionBridgePlayablePath(
historyFilePath: historyItem?.filePath,
completedItemFilePath: item.filePath,
);
final cover = unifiedItem != null
? _buildUnifiedCoverImage(unifiedItem, colorScheme)
: track.coverUrl != null
@@ -132,6 +136,8 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
: 'cover_${item.id}';
final trackName = historyItem?.trackName ?? track.name;
final artistName = historyItem?.artistName ?? track.artistName;
final albumName = historyItem?.albumName ?? track.albumName;
final coverUrl = historyItem?.coverUrl ?? track.coverUrl ?? '';
return TrackGridCard(
semanticLabel: context.l10n.a11yTrackByArtist(trackName, artistName),
onTap: () => historyItem != null
@@ -143,16 +149,72 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
: _navigateToMetadataScreen(item),
cover: Hero(tag: heroTag, child: cover),
overlays: [
Positioned(
right: 4,
top: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: context.tokens.borderRadiusBadge,
),
child: Icon(
Icons.download_done,
size: 12,
color: colorScheme.onPrimaryContainer,
),
),
),
if (quality != null && quality.isNotEmpty)
Positioned(
left: 4,
right: 4,
right: 28,
top: 4,
child: Align(
alignment: Alignment.centerLeft,
child: _buildLibraryQualityBadge(context, colorScheme, quality),
),
),
if (playablePath != null)
Positioned(
right: 4,
bottom: 4,
child: ValueListenableBuilder<bool>(
valueListenable: _fileExistsListenable(playablePath),
builder: (context, fileExists, child) {
if (fileExists) {
return TrackGridPlayButton(
tooltip: context.l10n.a11yPlayTrackByArtist(
trackName,
artistName,
),
onPressed: () => _openFile(
playablePath,
title: trackName,
artist: artistName,
album: albumName,
coverUrl: coverUrl,
),
);
}
return Tooltip(
message: context.l10n.queueDownloadedFileMissing,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.error_outline,
color: colorScheme.error,
size: 14,
),
),
);
},
),
),
],
title: trackName,
subtitle: Text(artistName),
@@ -176,6 +238,10 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
final quality =
unifiedItem?.qualityForMode(_libraryQualityLabelMode) ??
track.audioQuality;
final playablePath = resolveCompletionBridgePlayablePath(
historyFilePath: historyItem?.filePath,
completedItemFilePath: item.filePath,
);
final cover = unifiedItem != null
? _buildUnifiedCoverImage(unifiedItem, colorScheme, coverSize)
: track.coverUrl != null
@@ -199,6 +265,8 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
: 'cover_${item.id}';
final trackName = historyItem?.trackName ?? track.name;
final artistName = historyItem?.artistName ?? track.artistName;
final albumName = historyItem?.albumName ?? track.albumName;
final coverUrl = historyItem?.coverUrl ?? track.coverUrl ?? '';
return TrackCard(
onTap: () => historyItem != null
? _navigateToHistoryMetadataScreen(
@@ -235,6 +303,40 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
],
],
),
trailing: playablePath == null
? null
: ValueListenableBuilder<bool>(
valueListenable: _fileExistsListenable(playablePath),
builder: (context, fileExists, child) {
if (fileExists) {
return IconButton(
onPressed: () => _openFile(
playablePath,
title: trackName,
artist: artistName,
album: albumName,
coverUrl: coverUrl,
),
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
tooltip: context.l10n.tooltipPlay,
style: IconButton.styleFrom(
minimumSize: Size.square(context.tokens.minTouchTarget),
backgroundColor: colorScheme.primaryContainer.withValues(
alpha: 0.3,
),
),
);
}
return Tooltip(
message: context.l10n.queueDownloadedFileMissing,
child: Icon(
Icons.error_outline,
color: colorScheme.error,
size: 20,
),
);
},
),
);
}
@@ -1,4 +1,7 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart';
import 'package:spotiflac_android/services/library_database.dart';
@@ -73,4 +76,51 @@ void main() {
isFalse,
);
});
test('completion bridge prefers the finalized history path', () {
expect(
resolveCompletionBridgePlayablePath(
historyFilePath: ' /music/final.flac ',
completedItemFilePath: '/music/staging.flac',
),
'/music/final.flac',
);
expect(
resolveCompletionBridgePlayablePath(
historyFilePath: ' ',
completedItemFilePath: ' /music/completed.flac ',
),
'/music/completed.flac',
);
expect(
resolveCompletionBridgePlayablePath(
historyFilePath: null,
completedItemFilePath: '',
),
isNull,
);
});
test('completion bridge cards retain Play actions during a batch', () {
final source = File(
'lib/screens/queue_tab_collection_items.dart',
).readAsStringSync();
final gridStart = source.indexOf('Widget _buildBridgeGridItem(');
final listStart = source.indexOf('Widget _buildBridgeListItem(');
final badgeStart = source.indexOf('Widget _buildLibraryQualityBadge(');
expect(gridStart, greaterThanOrEqualTo(0));
expect(listStart, greaterThan(gridStart));
expect(badgeStart, greaterThan(listStart));
final gridSource = source.substring(gridStart, listStart);
final listSource = source.substring(listStart, badgeStart);
expect(gridSource, contains('resolveCompletionBridgePlayablePath('));
expect(gridSource, contains('_fileExistsListenable(playablePath)'));
expect(gridSource, contains('TrackGridPlayButton('));
expect(listSource, contains('resolveCompletionBridgePlayablePath('));
expect(listSource, contains('_fileExistsListenable(playablePath)'));
expect(listSource, contains('Icons.play_arrow'));
});
}