fix: distinguish preparing from downloading in native worker progress

Prevent premature 'downloading' status before actual byte transfer
starts, and cache async provider values to avoid UI flicker during
queue library reloads.

Progress pipeline:
- StartItemProgress now initializes with 'preparing' status instead
  of 'downloading'
- SetItemProgress ignores synthetic pre-download progress updates
  while status is still 'preparing' (no byte data yet)
- DownloadService reads backend status field and propagates preparing/
  downloading/finalizing to native worker item snapshot
- Dart progress stream maps 'preparing' to DownloadStatus.downloading
  with progress 0.0 (indeterminate spinner)

Queue tab:
- Add _queueLibraryCountsCache and _queueLibraryPageDataCache to
  retain last successful data during FutureProvider refetches
- Prevents empty-state flash when loadedIndexVersion bumps trigger
  provider invalidation
- Caches trimmed to max 24 entries via FIFO eviction
This commit is contained in:
zarzet
2026-05-06 12:08:53 +07:00
parent 30a7cba02a
commit 83d7106e35
5 changed files with 118 additions and 31 deletions
+9 -6
View File
@@ -2099,7 +2099,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final progressFromBackend =
(itemProgress['progress'] as num?)?.toDouble() ?? 0.0;
final hasRealProgress =
bytesReceived > 0 || bytesTotal > 0 || progressFromBackend > 0;
status != 'preparing' &&
(bytesReceived > 0 || bytesTotal > 0 || progressFromBackend > 0);
if (status == 'finalizing') {
progressUpdates[itemId] = const _ProgressUpdate(
@@ -2112,7 +2113,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
continue;
}
if (status == 'preparing' && !hasRealProgress) {
if (status == 'preparing') {
progressUpdates[itemId] = const _ProgressUpdate(
status: DownloadStatus.downloading,
progress: 0.0,
@@ -2264,10 +2265,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final progressPercent =
(selectedProgress['progress'] as num?)?.toDouble() ?? 0.0;
final hasRealProgress =
bytesReceived > 0 || bytesTotal > 0 || progressPercent > 0;
if (backendStatus == 'preparing' && !hasRealProgress) {
if (backendStatus == 'preparing') {
notifProgress = 0;
notifTotal = 0;
} else if (bytesTotal <= 0) {
@@ -5400,6 +5398,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
continue;
}
if (status == 'preparing') {
updateItemStatus(itemId, DownloadStatus.downloading, progress: 0.0);
continue;
}
if (status == 'downloading') {
updateItemStatus(
itemId,
+63 -15
View File
@@ -150,6 +150,10 @@ class _QueueTabState extends ConsumerState<QueueTab> {
double? _libraryGridScaleStartExtent;
int _libraryPageLimit = _libraryPageSize;
bool _libraryPageLoadScheduled = false;
final Map<_QueueLibraryCountsRequest, QueueLibraryCounts>
_queueLibraryCountsCache = {};
final Map<_QueueLibraryPageRequest, _QueueLibraryPageData>
_queueLibraryPageDataCache = {};
double _effectiveTextScale() {
final textScale = MediaQuery.textScalerOf(context).scale(1.0);
@@ -249,6 +253,55 @@ class _QueueTabState extends ConsumerState<QueueTab> {
});
}
QueueLibraryCounts _resolveQueueLibraryCounts(
AsyncValue<QueueLibraryCounts> value,
_QueueLibraryCountsRequest request,
) {
return value.maybeWhen(
data: (counts) {
_queueLibraryCountsCache[request] = counts;
_trimQueueLibraryCaches();
return counts;
},
orElse: () =>
_queueLibraryCountsCache[request] ??
const QueueLibraryCounts(
allTrackCount: 0,
albumCount: 0,
singleTrackCount: 0,
),
);
}
_QueueLibraryPageData _resolveQueueLibraryPageData(
AsyncValue<_QueueLibraryPageData>? value,
_QueueLibraryPageRequest request,
) {
if (value == null) {
return _queueLibraryPageDataCache[request] ??
const _QueueLibraryPageData();
}
return value.maybeWhen(
data: (data) {
_queueLibraryPageDataCache[request] = data;
_trimQueueLibraryCaches();
return data;
},
orElse: () =>
_queueLibraryPageDataCache[request] ?? const _QueueLibraryPageData(),
);
}
void _trimQueueLibraryCaches() {
const maxEntries = 24;
while (_queueLibraryCountsCache.length > maxEntries) {
_queueLibraryCountsCache.remove(_queueLibraryCountsCache.keys.first);
}
while (_queueLibraryPageDataCache.length > maxEntries) {
_queueLibraryPageDataCache.remove(_queueLibraryPageDataCache.keys.first);
}
}
bool _handleLibraryScrollNotification({
required ScrollNotification notification,
required String filterMode,
@@ -2455,14 +2508,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
localLibraryEnabled: localLibraryEnabled,
);
final countsValue = ref.watch(_queueLibraryCountsProvider(countsRequest));
final queueCounts = countsValue.maybeWhen(
data: (counts) => counts,
orElse: () => const QueueLibraryCounts(
allTrackCount: 0,
albumCount: 0,
singleTrackCount: 0,
),
);
final queueCounts = _resolveQueueLibraryCounts(countsValue, countsRequest);
_QueueLibraryPageRequest pageRequest(String filterMode) =>
_QueueLibraryPageRequest(
@@ -2477,17 +2523,19 @@ class _QueueTabState extends ConsumerState<QueueTab> {
localLibraryEnabled: localLibraryEnabled,
);
final pageRequests = <String, _QueueLibraryPageRequest>{
for (final mode in _filterModes) mode: pageRequest(mode),
};
final pageValues = <String, AsyncValue<_QueueLibraryPageData>>{
for (final mode in _filterModes)
mode: ref.watch(_queueLibraryPageProvider(pageRequest(mode))),
for (final entry in pageRequests.entries)
entry.key: ref.watch(_queueLibraryPageProvider(entry.value)),
};
_QueueLibraryPageData pageData(String filterMode) =>
pageValues[filterMode]?.maybeWhen(
data: (data) => data,
orElse: () => const _QueueLibraryPageData(),
) ??
const _QueueLibraryPageData();
_resolveQueueLibraryPageData(
pageValues[filterMode],
pageRequests[filterMode]!,
);
_FilterContentData getFilterData(String filterMode) {
return pageData(filterMode).toFilterContentData(