mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
fix(queue): bridge completed downloads into library
This commit is contained in:
@@ -35,7 +35,6 @@ import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
|
||||
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
|
||||
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
|
||||
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
|
||||
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
||||
@@ -118,6 +117,38 @@ String? _formatDownloadEta(DownloadItem item) {
|
||||
return '~${minutes}m${secs.toString().padLeft(2, '0')}s';
|
||||
}
|
||||
|
||||
DownloadHistoryItem? _historyItemForCompletionBridge(
|
||||
DownloadItem item,
|
||||
List<DownloadHistoryItem> historyItems,
|
||||
) {
|
||||
final filePath = item.filePath?.trim();
|
||||
if (filePath != null && filePath.isNotEmpty) {
|
||||
for (final historyItem in historyItems) {
|
||||
if (historyItem.filePath == filePath) return historyItem;
|
||||
}
|
||||
}
|
||||
|
||||
for (final historyItem in historyItems) {
|
||||
if (historyItem.id == item.id) return historyItem;
|
||||
}
|
||||
|
||||
final trackId = item.track.id.trim();
|
||||
if (trackId.isNotEmpty) {
|
||||
for (final historyItem in historyItems) {
|
||||
if (historyItem.spotifyId == trackId) return historyItem;
|
||||
}
|
||||
}
|
||||
|
||||
final isrc = item.track.isrc?.trim();
|
||||
if (isrc != null && isrc.isNotEmpty) {
|
||||
for (final historyItem in historyItems) {
|
||||
if (historyItem.isrc == isrc) return historyItem;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class QueueTab extends ConsumerStatefulWidget {
|
||||
final PageController? parentPageController;
|
||||
final int parentPageIndex;
|
||||
@@ -1078,6 +1109,11 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
final historyTotalCount = ref.watch(
|
||||
downloadHistoryProvider.select((state) => state.totalCount),
|
||||
);
|
||||
// Completion-bridge cells need the finalized in-memory history record
|
||||
// immediately, before the database-backed Library page refresh lands.
|
||||
final inMemoryHistoryItems = ref.watch(
|
||||
downloadHistoryProvider.select((state) => state.items),
|
||||
);
|
||||
final localLibraryTotalCount = ref.watch(
|
||||
localLibraryProvider.select((state) => state.totalCount),
|
||||
);
|
||||
@@ -1400,6 +1436,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
? hasMoreLibrary
|
||||
: false,
|
||||
isPageLoading: isLibraryPageLoading,
|
||||
inMemoryHistoryItems: inMemoryHistoryItems,
|
||||
bottomInset: bottomInset,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -131,10 +131,17 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
BuildContext context,
|
||||
DownloadItem item,
|
||||
ColorScheme colorScheme,
|
||||
DownloadHistoryItem? historyItem,
|
||||
) {
|
||||
final track = item.track;
|
||||
final radius = BorderRadius.circular(8);
|
||||
final cover = track.coverUrl != null
|
||||
final unifiedItem = historyItem == null
|
||||
? null
|
||||
: UnifiedLibraryItem.fromDownloadHistory(historyItem);
|
||||
final quality = unifiedItem?.quality ?? track.audioQuality;
|
||||
final cover = unifiedItem != null
|
||||
? _buildUnifiedCoverImage(unifiedItem, colorScheme)
|
||||
: track.coverUrl != null
|
||||
? CachedCoverImage(imageUrl: track.coverUrl!, borderRadius: radius)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -143,11 +150,18 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
),
|
||||
child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant),
|
||||
);
|
||||
final heroTag = historyItem != null
|
||||
? 'cover_lib_dl_${historyItem.id}'
|
||||
: 'cover_${item.id}';
|
||||
final trackName = historyItem?.trackName ?? track.name;
|
||||
final artistName = historyItem?.artistName ?? track.artistName;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: context.l10n.a11yTrackByArtist(track.name, track.artistName),
|
||||
label: context.l10n.a11yTrackByArtist(trackName, artistName),
|
||||
child: GestureDetector(
|
||||
onTap: () => _navigateToMetadataScreen(item),
|
||||
onTap: () => historyItem != null
|
||||
? _navigateToHistoryMetadataScreen(historyItem)
|
||||
: _navigateToMetadataScreen(item),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -156,14 +170,18 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRRect(borderRadius: radius, child: cover),
|
||||
if (track.hasAudioQuality)
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: ClipRRect(borderRadius: radius, child: cover),
|
||||
),
|
||||
if (quality != null && quality.isNotEmpty)
|
||||
Positioned(
|
||||
left: 4,
|
||||
top: 4,
|
||||
child: AudioQualityBadge(
|
||||
label: track.audioQuality!,
|
||||
colorScheme: colorScheme,
|
||||
child: _buildLibraryQualityBadge(
|
||||
context,
|
||||
colorScheme,
|
||||
quality,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -171,7 +189,7 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
track.name,
|
||||
trackName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(
|
||||
@@ -179,7 +197,7 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
track.artistName,
|
||||
artistName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
@@ -196,11 +214,18 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
BuildContext context,
|
||||
DownloadItem item,
|
||||
ColorScheme colorScheme,
|
||||
DownloadHistoryItem? historyItem,
|
||||
) {
|
||||
final track = item.track;
|
||||
final coverSize = _queueCoverSize();
|
||||
final radius = BorderRadius.circular(8);
|
||||
final cover = track.coverUrl != null
|
||||
final unifiedItem = historyItem == null
|
||||
? null
|
||||
: UnifiedLibraryItem.fromDownloadHistory(historyItem);
|
||||
final quality = unifiedItem?.quality ?? track.audioQuality;
|
||||
final cover = unifiedItem != null
|
||||
? _buildUnifiedCoverImage(unifiedItem, colorScheme, coverSize)
|
||||
: track.coverUrl != null
|
||||
? CachedCoverImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
width: coverSize,
|
||||
@@ -216,23 +241,30 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
),
|
||||
child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant),
|
||||
);
|
||||
final heroTag = historyItem != null
|
||||
? 'cover_lib_dl_${historyItem.id}'
|
||||
: 'cover_${item.id}';
|
||||
final trackName = historyItem?.trackName ?? track.name;
|
||||
final artistName = historyItem?.artistName ?? track.artistName;
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _navigateToMetadataScreen(item),
|
||||
onTap: () => historyItem != null
|
||||
? _navigateToHistoryMetadataScreen(historyItem)
|
||||
: _navigateToMetadataScreen(item),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
cover,
|
||||
Hero(tag: heroTag, child: cover),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
track.name,
|
||||
trackName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
@@ -241,13 +273,25 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
track.artistName,
|
||||
artistName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (quality != null && quality.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildLibraryQualityBadge(
|
||||
context,
|
||||
colorScheme,
|
||||
quality,
|
||||
listStyle: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -258,6 +302,38 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLibraryQualityBadge(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
String quality, {
|
||||
bool listStyle = false,
|
||||
}) {
|
||||
final isHighResolution = quality.startsWith('24');
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: listStyle ? 6 : 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isHighResolution
|
||||
? listStyle
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.primary
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
listStyle ? quality : _getQualityBadgeText(quality),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: isHighResolution
|
||||
? listStyle
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onPrimary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontSize: listStyle ? 10 : 9,
|
||||
fontWeight: listStyle ? FontWeight.w500 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollectionListItem({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
|
||||
@@ -11,6 +11,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
required LibraryCollectionsState collectionState,
|
||||
required bool hasMoreLibrary,
|
||||
required bool isPageLoading,
|
||||
required List<DownloadHistoryItem> inMemoryHistoryItems,
|
||||
double bottomInset = 0,
|
||||
}) {
|
||||
final historyItems = filterData.historyItems;
|
||||
@@ -45,6 +46,13 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
final libIdSet = <String>{
|
||||
for (final item in allFilteredUnifiedItems) item.id,
|
||||
};
|
||||
final bridgeHistoryById = <String, DownloadHistoryItem?>{
|
||||
for (final entry in _completionBridge.entries)
|
||||
entry.key: _historyItemForCompletionBridge(
|
||||
entry.value,
|
||||
inMemoryHistoryItems,
|
||||
),
|
||||
};
|
||||
List<String> bridgeIds = const [];
|
||||
if (filterMode != 'albums' && _completionBridge.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
@@ -52,7 +60,8 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
final pending = <String>[];
|
||||
final hasActiveDownloads = activeDownloadIds.isNotEmpty;
|
||||
_completionBridge.forEach((id, _) {
|
||||
final landed = libIdSet.contains('dl_$id');
|
||||
final historyId = bridgeHistoryById[id]?.id ?? id;
|
||||
final landed = libIdSet.contains('dl_$historyId');
|
||||
final addedAt = _completionBridgeAt[id];
|
||||
final expired =
|
||||
addedAt == null || now.difference(addedAt).inSeconds >= 6;
|
||||
@@ -87,15 +96,8 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
.where((id) => !_bridgePrecacheStarted.contains(id))
|
||||
.toList(growable: false);
|
||||
if (toPrecache.isNotEmpty) {
|
||||
final historyItems = ref.read(downloadHistoryProvider).items;
|
||||
for (final id in toPrecache) {
|
||||
DownloadHistoryItem? historyItem;
|
||||
for (final h in historyItems) {
|
||||
if (h.id == id) {
|
||||
historyItem = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final historyItem = bridgeHistoryById[id];
|
||||
if (historyItem == null) continue;
|
||||
_bridgePrecacheStarted.add(id);
|
||||
final coverUrl = historyItem.coverUrl;
|
||||
@@ -143,7 +145,9 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
// hide their history rows so they don't appear twice.
|
||||
List<UnifiedLibraryItem> filteredUnifiedItems = allFilteredUnifiedItems;
|
||||
if (bridgeIds.isNotEmpty) {
|
||||
final pinnedHistoryIds = <String>{for (final id in bridgeIds) 'dl_$id'};
|
||||
final pinnedHistoryIds = <String>{
|
||||
for (final id in bridgeIds) 'dl_${bridgeHistoryById[id]?.id ?? id}',
|
||||
};
|
||||
filteredUnifiedItems = allFilteredUnifiedItems
|
||||
.where((item) => !pinnedHistoryIds.contains(item.id))
|
||||
.toList(growable: false);
|
||||
@@ -192,6 +196,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
context,
|
||||
_completionBridge[bridgeId]!,
|
||||
colorScheme,
|
||||
bridgeHistoryById[bridgeId],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -213,6 +218,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
context,
|
||||
_completionBridge[bridgeId]!,
|
||||
colorScheme,
|
||||
bridgeHistoryById[bridgeId],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -907,28 +907,11 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
if (item.quality != null &&
|
||||
item.quality!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: item.quality!.startsWith('24')
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
item.quality!,
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(
|
||||
color: item.quality!.startsWith('24')
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
_buildLibraryQualityBadge(
|
||||
context,
|
||||
colorScheme,
|
||||
item.quality!,
|
||||
listStyle: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -1055,28 +1038,10 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
Positioned(
|
||||
left: 4,
|
||||
top: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: item.quality!.startsWith('24')
|
||||
? colorScheme.primary
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
_getQualityBadgeText(item.quality!),
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(
|
||||
color: item.quality!.startsWith('24')
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: _buildLibraryQualityBadge(
|
||||
context,
|
||||
colorScheme,
|
||||
item.quality!,
|
||||
),
|
||||
),
|
||||
if (!_isSelectionMode)
|
||||
|
||||
Reference in New Issue
Block a user