diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index af667c3b..d89ffcfa 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -89,7 +89,7 @@ import Gobackend // Import Go framework } self.lastDownloadProgressPayload = payload DispatchQueue.main.async { [weak self] in - self?.downloadProgressEventSink?(payload) + self?.downloadProgressEventSink?(self?.parseJsonPayload(payload)) } } downloadProgressTimer = timer @@ -119,7 +119,7 @@ import Gobackend // Import Go framework } self.lastLibraryScanProgressPayload = payload DispatchQueue.main.async { [weak self] in - self?.libraryScanProgressEventSink?(payload) + self?.libraryScanProgressEventSink?(self?.parseJsonPayload(payload)) } } libraryScanProgressTimer = timer @@ -133,6 +133,17 @@ import Gobackend // Import Go framework libraryScanProgressEventSink = nil lastLibraryScanProgressPayload = nil } + + private func parseJsonPayload(_ payload: String) -> Any { + guard let data = payload.data(using: .utf8) else { + return payload + } + do { + return try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) + } catch { + return payload + } + } private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { @@ -169,11 +180,11 @@ import Gobackend // Import Go framework case "getDownloadProgress": let response = GobackendGetDownloadProgress() - return response + return parseJsonPayload(response as String? ?? "{}") case "getAllDownloadProgress": let response = GobackendGetAllDownloadProgress() - return response + return parseJsonPayload(response as String? ?? "{}") case "initItemProgress": let args = call.arguments as! [String: Any] @@ -933,7 +944,7 @@ import Gobackend // Import Go framework case "getLibraryScanProgress": let response = GobackendGetLibraryScanProgressJSON() - return response + return parseJsonPayload(response as String? ?? "{}") case "cancelLibraryScan": GobackendCancelLibraryScanJSON() diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 736ad0b7..4b0570af 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1103,6 +1103,7 @@ final downloadHistoryProvider = class DownloadQueueState { static const Object _noChange = Object(); final List items; + final DownloadQueueLookup lookup; final DownloadItem? currentDownload; final bool isProcessing; final bool isPaused; @@ -1115,6 +1116,7 @@ class DownloadQueueState { const DownloadQueueState({ this.items = const [], + this.lookup = const DownloadQueueLookup.empty(), this.currentDownload, this.isProcessing = false, this.isPaused = false, @@ -1128,6 +1130,7 @@ class DownloadQueueState { DownloadQueueState copyWith({ List? items, + DownloadQueueLookup? lookup, Object? currentDownload = _noChange, bool? isProcessing, bool? isPaused, @@ -1138,8 +1141,14 @@ class DownloadQueueState { bool? autoFallback, int? concurrentDownloads, }) { + final resolvedItems = items ?? this.items; return DownloadQueueState( - items: items ?? this.items, + items: resolvedItems, + lookup: + lookup ?? + (items != null + ? DownloadQueueLookup.fromItems(resolvedItems) + : this.lookup), currentDownload: identical(currentDownload, _noChange) ? this.currentDownload : currentDownload as DownloadItem?, @@ -1624,6 +1633,7 @@ class DownloadQueueNotifier extends Notifier { if (progressUpdates.isNotEmpty) { var updatedItems = currentItems; bool changed = false; + final changedIndices = []; for (final entry in progressUpdates.entries) { final index = itemIndexById[entry.key]; @@ -1652,11 +1662,19 @@ class DownloadQueueNotifier extends Notifier { changed = true; } updatedItems[index] = next; + changedIndices.add(index); } } if (changed) { - state = state.copyWith(items: updatedItems); + state = state.copyWith( + items: updatedItems, + lookup: state.lookup.updatedForIndices( + previousItems: currentItems, + nextItems: updatedItems, + changedIndices: changedIndices, + ), + ); } } @@ -5564,6 +5582,11 @@ class DownloadQueueLookup { final Map byItemId; final List itemIds; + const DownloadQueueLookup.empty() + : byTrackId = const {}, + byItemId = const {}, + itemIds = const []; + DownloadQueueLookup._({ required this.byTrackId, required this.byItemId, @@ -5585,11 +5608,53 @@ class DownloadQueueLookup { itemIds: itemIds, ); } + + DownloadQueueLookup updatedForIndices({ + required List previousItems, + required List nextItems, + required Iterable changedIndices, + }) { + if (previousItems.length != nextItems.length || + itemIds.length != nextItems.length) { + return DownloadQueueLookup.fromItems(nextItems); + } + + final normalizedChanged = []; + for (final index in changedIndices) { + if (index < 0 || index >= nextItems.length) { + return DownloadQueueLookup.fromItems(nextItems); + } + normalizedChanged.add(index); + } + if (normalizedChanged.isEmpty) return this; + + final nextByItemId = Map.from(byItemId); + Map? nextByTrackId; + + for (final index in normalizedChanged) { + final previous = previousItems[index]; + final next = nextItems[index]; + if (previous.id != next.id || previous.track.id != next.track.id) { + return DownloadQueueLookup.fromItems(nextItems); + } + + nextByItemId[next.id] = next; + if (byTrackId[next.track.id]?.id == previous.id) { + nextByTrackId ??= Map.from(byTrackId); + nextByTrackId[next.track.id] = next; + } + } + + return DownloadQueueLookup._( + byTrackId: nextByTrackId ?? byTrackId, + byItemId: nextByItemId, + itemIds: itemIds, + ); + } } final downloadQueueLookupProvider = Provider((ref) { - final items = ref.watch(downloadQueueProvider.select((s) => s.items)); - return DownloadQueueLookup.fromItems(items); + return ref.watch(downloadQueueProvider.select((s) => s.lookup)); }); // --------------------------------------------------------------------------- diff --git a/lib/services/downloaded_embedded_cover_resolver.dart b/lib/services/downloaded_embedded_cover_resolver.dart index b7a20b44..5a598149 100644 --- a/lib/services/downloaded_embedded_cover_resolver.dart +++ b/lib/services/downloaded_embedded_cover_resolver.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:collection'; import 'dart:io'; @@ -107,7 +108,7 @@ class DownloadedEmbeddedCoverResolver { _pendingPreviewValidation.remove(cleanPath); _failedExtract.remove(cleanPath); if (cached != null) { - _cleanupTempCoverPathSync(cached.previewPath); + _scheduleTempCoverCleanup(cached.previewPath); } } @@ -139,7 +140,7 @@ class DownloadedEmbeddedCoverResolver { final oldestKey = _cache.keys.first; final removed = _cache.remove(oldestKey); if (removed != null) { - _cleanupTempCoverPathSync(removed.previewPath); + _scheduleTempCoverCleanup(removed.previewPath); } _pendingExtract.remove(oldestKey); _pendingRefresh.remove(oldestKey); @@ -165,7 +166,7 @@ class DownloadedEmbeddedCoverResolver { _failedExtract.remove(cleanPath); onChanged?.call(); } - _cleanupTempCoverPathSync(entry.previewPath); + _scheduleTempCoverCleanup(entry.previewPath); } } finally { _pendingPreviewValidation.remove(cleanPath); @@ -203,7 +204,7 @@ class DownloadedEmbeddedCoverResolver { result['error'] == null && await File(outputPath).exists(); if (!hasCover) { _failedExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); + _scheduleTempCoverCleanup(outputPath); return; } @@ -217,29 +218,32 @@ class DownloadedEmbeddedCoverResolver { _trimCacheIfNeeded(); if (previous != null && previous.previewPath != outputPath) { - _cleanupTempCoverPathSync(previous.previewPath); + _scheduleTempCoverCleanup(previous.previewPath); } onChanged?.call(); } catch (_) { _failedExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); + _scheduleTempCoverCleanup(outputPath); } finally { _pendingExtract.remove(cleanPath); } }); } - static void _cleanupTempCoverPathSync(String? coverPath) { + static void _scheduleTempCoverCleanup(String? coverPath) { + unawaited(_cleanupTempCoverPath(coverPath)); + } + + static Future _cleanupTempCoverPath(String? coverPath) async { if (coverPath == null || coverPath.isEmpty) return; try { final file = File(coverPath); - if (file.existsSync()) { - file.deleteSync(); - } - final parent = file.parent; - if (parent.existsSync()) { - parent.deleteSync(recursive: true); - } + try { + await file.delete(); + } catch (_) {} + try { + await file.parent.delete(recursive: true); + } catch (_) {} } catch (_) {} } } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 7d79eaa7..5a269e84 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1181,13 +1181,13 @@ class PlatformBridge { static Map _decodeMapResult(dynamic result) { if (result is Map) { - return Map.from(result); + return result.cast(); } if (result is String) { if (result.isEmpty) return const {}; final decoded = jsonDecode(result); if (decoded is Map) { - return Map.from(decoded); + return decoded.cast(); } } return const {};