mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-06 14:44:00 +02:00
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:
@@ -564,12 +564,12 @@ class DownloadService : Service() {
|
||||
nativeWorkerCurrentItemId = request.itemId
|
||||
currentTrackName = request.trackName
|
||||
currentArtistName = request.artistName
|
||||
currentStatus = "downloading"
|
||||
currentStatus = "preparing"
|
||||
lastProgress = 0L
|
||||
lastTotal = 0L
|
||||
updateNotification(0, 0)
|
||||
updateNativeWorkerItem(request.itemId) {
|
||||
it.status = "downloading"
|
||||
it.status = "preparing"
|
||||
it.progress = 0.0
|
||||
it.bytesReceived = 0L
|
||||
it.bytesTotal = 0L
|
||||
@@ -580,7 +580,7 @@ class DownloadService : Service() {
|
||||
isRunning = true,
|
||||
isPaused = false,
|
||||
currentItemId = request.itemId,
|
||||
message = "Downloading",
|
||||
message = "Preparing",
|
||||
settingsJson = settingsJson,
|
||||
includeItems = true
|
||||
)
|
||||
@@ -1100,14 +1100,34 @@ class DownloadService : Service() {
|
||||
val root = JSONObject(raw)
|
||||
val items = root.optJSONObject("items") ?: return
|
||||
val progress = items.optJSONObject(itemId) ?: return
|
||||
val backendStatus = progress.optString("status", "downloading")
|
||||
val bytesReceived = progress.optLong("bytes_received", 0L)
|
||||
val bytesTotal = progress.optLong("bytes_total", 0L)
|
||||
if (backendStatus == "preparing") {
|
||||
currentStatus = "preparing"
|
||||
updateNativeWorkerItem(itemId) {
|
||||
it.status = "preparing"
|
||||
it.progress = 0.0
|
||||
it.bytesReceived = 0L
|
||||
it.bytesTotal = 0L
|
||||
}
|
||||
lastProgress = 0L
|
||||
lastTotal = 0L
|
||||
updateNotification(0L, 0L)
|
||||
return
|
||||
}
|
||||
val progressValue = if (bytesTotal > 0L) {
|
||||
bytesReceived.toDouble() / bytesTotal.toDouble()
|
||||
} else {
|
||||
progress.optDouble("progress", 0.0)
|
||||
}.coerceIn(0.0, 1.0)
|
||||
currentStatus = if (backendStatus == "finalizing") {
|
||||
"finalizing"
|
||||
} else {
|
||||
"downloading"
|
||||
}
|
||||
updateNativeWorkerItem(itemId) {
|
||||
it.status = currentStatus
|
||||
it.progress = progressValue
|
||||
it.bytesReceived = bytesReceived
|
||||
it.bytesTotal = bytesTotal
|
||||
|
||||
@@ -213,8 +213,8 @@ func StartItemProgress(itemID string) {
|
||||
BytesTotal: 0,
|
||||
BytesReceived: 0,
|
||||
Progress: 0,
|
||||
IsDownloading: true,
|
||||
Status: itemProgressStatusDownloading,
|
||||
IsDownloading: false,
|
||||
Status: itemProgressStatusPreparing,
|
||||
revision: nextMultiProgressSeqLocked(),
|
||||
}
|
||||
delete(removedProgressSeq, itemID)
|
||||
@@ -316,14 +316,19 @@ func SetItemProgress(itemID string, progress float64, bytesReceived, bytesTotal
|
||||
|
||||
if item, ok := multiProgress.Items[itemID]; ok {
|
||||
before := itemProgressBridgeState(item)
|
||||
item.Progress = progress
|
||||
hasByteProgress := bytesReceived > 0 || bytesTotal > 0
|
||||
if item.Status != itemProgressStatusPreparing || hasByteProgress || progress >= 1 {
|
||||
item.Progress = progress
|
||||
} else {
|
||||
item.Progress = 0
|
||||
}
|
||||
if bytesReceived > 0 {
|
||||
item.BytesReceived = bytesReceived
|
||||
}
|
||||
if bytesTotal > 0 {
|
||||
item.BytesTotal = bytesTotal
|
||||
}
|
||||
if progress > 0 || bytesReceived > 0 || bytesTotal > 0 {
|
||||
if hasByteProgress || progress >= 1 || item.Status == itemProgressStatusDownloading {
|
||||
item.IsDownloading = true
|
||||
item.Status = itemProgressStatusDownloading
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ func TestItemProgressPreparingAndDownloadingStatuses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
SetItemProgress(itemID, 0.37, 0, 0)
|
||||
SetItemProgress(itemID, 0.05, 0, 0)
|
||||
if item := multiProgress.Items[itemID]; item == nil {
|
||||
t.Fatal("expected item progress entry to exist after update")
|
||||
} else if item.Status != itemProgressStatusDownloading {
|
||||
t.Fatalf("status after progress update = %q, want %q", item.Status, itemProgressStatusDownloading)
|
||||
} else if item.Status != itemProgressStatusPreparing {
|
||||
t.Fatalf("status after synthetic pre-download progress = %q, want %q", item.Status, itemProgressStatusPreparing)
|
||||
} else if item.Progress != 0 {
|
||||
t.Fatalf("progress after synthetic pre-download progress = %v, want 0", item.Progress)
|
||||
}
|
||||
|
||||
SetItemDownloading(itemID)
|
||||
@@ -37,6 +39,15 @@ func TestItemProgressPreparingAndDownloadingStatuses(t *testing.T) {
|
||||
} else if item.Status != itemProgressStatusDownloading {
|
||||
t.Fatalf("status after download start = %q, want %q", item.Status, itemProgressStatusDownloading)
|
||||
}
|
||||
|
||||
SetItemProgress(itemID, 0.37, 0, 0)
|
||||
if item := multiProgress.Items[itemID]; item == nil {
|
||||
t.Fatal("expected item progress entry to exist after real update")
|
||||
} else if item.Status != itemProgressStatusDownloading {
|
||||
t.Fatalf("status after real progress update = %q, want %q", item.Status, itemProgressStatusDownloading)
|
||||
} else if item.Progress != 0.37 {
|
||||
t.Fatalf("progress after real update = %v, want 0.37", item.Progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemProgressFinalizingAndCompletedStatuses(t *testing.T) {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user