mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
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.
This commit is contained in:
1 parent
830dee31ed
commit
5b3a2b9575
2 files changed
+131
-78
No files matched your search
@@ -0,0 +1,106 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-78
@@ -81,6 +81,7 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
||||
import 'package:spotiflac_android/screens/local_album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/mornye_library_screen.dart';
|
||||
import 'package:spotiflac_android/controllers/completion_bridge_tracker.dart';
|
||||
import 'package:spotiflac_android/controllers/queue_library_pager.dart';
|
||||
import 'package:spotiflac_android/controllers/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
@@ -106,37 +107,7 @@ part 'queue_tab_download_labels.dart';
|
||||
part 'queue_tab_download_dialogs.dart';
|
||||
part 'queue_tab_filter_sheet.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
const _historyItemForCompletionBridge = CompletionBridgeTracker.historyItemFor;
|
||||
|
||||
class QueueTab extends ConsumerStatefulWidget {
|
||||
final String? librarySection;
|
||||
@@ -204,9 +175,14 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String _searchQuery = '';
|
||||
Timer? _searchDebounce;
|
||||
final Map<String, DownloadItem> _completionBridge = {};
|
||||
final Map<String, DateTime> _completionBridgeAt = {};
|
||||
final Set<String> _bridgePrecacheStarted = {};
|
||||
final CompletionBridgeTracker _completionBridgeTracker =
|
||||
CompletionBridgeTracker();
|
||||
Map<String, DownloadItem> get _completionBridge =>
|
||||
_completionBridgeTracker.items;
|
||||
Map<String, DateTime> get _completionBridgeAt =>
|
||||
_completionBridgeTracker.addedAt;
|
||||
Set<String> get _bridgePrecacheStarted =>
|
||||
_completionBridgeTracker.precacheStarted;
|
||||
String? _filterSource;
|
||||
String? _filterQuality;
|
||||
String? _filterFormat;
|
||||
@@ -468,6 +444,11 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return _fileExistsCache.listenable(filePath);
|
||||
}
|
||||
|
||||
void _refreshCompletionBridgePath(String? filePath) {
|
||||
_fileExistsCache.refreshForPath(filePath);
|
||||
_completionBridgePlayableProbe.refreshForPath(filePath);
|
||||
}
|
||||
|
||||
int get _activeFilterCount {
|
||||
int count = 0;
|
||||
if (_filterSource != null) count++;
|
||||
@@ -506,56 +487,22 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
);
|
||||
|
||||
ref.listen(downloadQueueLookupProvider, (previous, next) {
|
||||
if (previous == null) return;
|
||||
if (_completionBridge.keys.any((id) => !next.byItemId.containsKey(id))) {
|
||||
setState(() {
|
||||
_completionBridge.removeWhere(
|
||||
(id, _) => !next.byItemId.containsKey(id),
|
||||
);
|
||||
_completionBridgeAt.removeWhere(
|
||||
(id, _) => !next.byItemId.containsKey(id),
|
||||
);
|
||||
_bridgePrecacheStarted.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) {
|
||||
_fileExistsCache.refreshForPath(nextItem.filePath);
|
||||
_completionBridgePlayableProbe.refreshForPath(nextItem.filePath);
|
||||
_completionBridge[id] = nextItem;
|
||||
_completionBridgeAt[id] = DateTime.now();
|
||||
}
|
||||
}
|
||||
_completionBridgeTracker.onQueueLookupChanged(
|
||||
previous,
|
||||
next,
|
||||
setState: setState,
|
||||
refreshPath: _refreshCompletionBridgePath,
|
||||
);
|
||||
});
|
||||
ref.listen<int>(
|
||||
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
|
||||
(previous, next) {
|
||||
if (previous == null || previous == next) return;
|
||||
final historyItems = ref.read(downloadHistoryProvider).items;
|
||||
for (final bridgeItem in _completionBridge.values) {
|
||||
_fileExistsCache.refreshForPath(bridgeItem.filePath);
|
||||
_completionBridgePlayableProbe.refreshForPath(bridgeItem.filePath);
|
||||
final historyPath = _historyItemForCompletionBridge(
|
||||
bridgeItem,
|
||||
historyItems,
|
||||
)?.filePath;
|
||||
_fileExistsCache.refreshForPath(historyPath);
|
||||
_completionBridgePlayableProbe.refreshForPath(historyPath);
|
||||
}
|
||||
_completionBridgeTracker.refreshPaths(
|
||||
historyItems,
|
||||
refreshPath: _refreshCompletionBridgePath,
|
||||
);
|
||||
// The family provider already reruns for the new revision. Retain its
|
||||
// last successful page while SQLite is loading so metadata backfills
|
||||
// and download completions cannot flash the Library as empty.
|
||||
|
||||
Reference in new issue
Block a user