Files
SpotiFLAC-Mobile/lib/controllers/completion_bridge_tracker.dart
T
zarzet 5b3a2b9575 refactor(queue): move the completion bridge into CompletionBridgeTracker
The completion bridge maps, the precache set, the History row matcher and the
bodies of the queue lookup and History index listeners lived on the Library
tab State. CompletionBridgeTracker in lib/controllers now owns them. The two
ref.listen registrations stay in build at the same positions and call into
the tracker; the State forwards the old field names as getters so the part
files read the same collections.

Guards, the setState that drops removed items, the file check refresh order
(file cache, then playable probe), the offset reset and the mounted rebuild
after a History index change are unchanged.
2026-10-19 01:12:21 +07:00

107 lines
3.8 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:spotiflac_android/models/download_history.dart';
import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/providers/download_queue_state.dart';
/// Just-completed queue items that the Library keeps showing until their
/// History row lands in the paged Library query.
///
/// The owning State registers the `ref.listen` callbacks in `build` and calls
/// into this tracker; `shouldRetainCompletionBridge` in
/// queue_library_refresh_policy.dart decides when a bridged item expires.
class CompletionBridgeTracker {
/// Bridged queue items by queue item id.
final Map<String, DownloadItem> items = {};
/// When each bridged item completed.
final Map<String, DateTime> addedAt = {};
/// Bridged items whose cover precache has already been scheduled.
final Set<String> precacheStarted = {};
/// Finds the History row for a bridged [item] by file path, id, track id
/// and then ISRC.
static DownloadHistoryItem? historyItemFor(
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;
}
/// Handles a queue lookup change: drops bridged items that left the queue
/// (rebuilding through [setState]) and bridges items that just completed,
/// refreshing their file checks through [refreshPath] first.
void onQueueLookupChanged(
DownloadQueueLookup? previous,
DownloadQueueLookup next, {
required void Function(VoidCallback fn) setState,
required void Function(String? path) refreshPath,
}) {
if (previous == null) return;
if (items.keys.any((id) => !next.byItemId.containsKey(id))) {
setState(() {
items.removeWhere((id, _) => !next.byItemId.containsKey(id));
addedAt.removeWhere((id, _) => !next.byItemId.containsKey(id));
precacheStarted.removeWhere((id) => !next.byItemId.containsKey(id));
});
}
if (identical(previous.notCompletedItemIds, next.notCompletedItemIds)) {
return;
}
for (final id in previous.notCompletedItemIds) {
final prevItem = previous.byItemId[id];
final nextItem = next.byItemId[id];
if (prevItem == null) continue;
final wasActive =
prevItem.status == DownloadStatus.downloading ||
prevItem.status == DownloadStatus.finalizing ||
prevItem.status == DownloadStatus.queued;
final nowCompleted =
nextItem != null && nextItem.status == DownloadStatus.completed;
if (wasActive && nowCompleted) {
refreshPath(nextItem.filePath);
items[id] = nextItem;
addedAt[id] = DateTime.now();
}
}
}
/// Refreshes the file checks of every bridged item and of its History row
/// after History reloads its index.
void refreshPaths(
List<DownloadHistoryItem> historyItems, {
required void Function(String? path) refreshPath,
}) {
for (final bridgeItem in items.values) {
refreshPath(bridgeItem.filePath);
final historyPath = historyItemFor(bridgeItem, historyItems)?.filePath;
refreshPath(historyPath);
}
}
}