mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-25 01:04:11 +02:00
perf: incremental download queue lookup updates, async cover cleanup, and native JSON decoding on iOS
- Embed DownloadQueueLookup into DownloadQueueState; add updatedForIndices() for O(changed) incremental updates during frequent progress ticks instead of full O(n) rebuild - downloadQueueLookupProvider now reads pre-computed lookup from state directly - Replace sync file deletion in DownloadedEmbeddedCoverResolver with unawaited async cleanup to avoid blocking the main thread - Parse JSON payloads on iOS native side (parseJsonPayload) so event sinks and method channel responses return native objects, avoiding redundant Dart-side JSON decode - Use .cast<String, dynamic>() instead of Map.from() in _decodeMapResult for zero-copy map handling
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -1103,6 +1103,7 @@ final downloadHistoryProvider =
|
||||
class DownloadQueueState {
|
||||
static const Object _noChange = Object();
|
||||
final List<DownloadItem> 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<DownloadItem>? 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<DownloadQueueState> {
|
||||
if (progressUpdates.isNotEmpty) {
|
||||
var updatedItems = currentItems;
|
||||
bool changed = false;
|
||||
final changedIndices = <int>[];
|
||||
|
||||
for (final entry in progressUpdates.entries) {
|
||||
final index = itemIndexById[entry.key];
|
||||
@@ -1652,11 +1662,19 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
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<String, DownloadItem> byItemId;
|
||||
final List<String> 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<DownloadItem> previousItems,
|
||||
required List<DownloadItem> nextItems,
|
||||
required Iterable<int> changedIndices,
|
||||
}) {
|
||||
if (previousItems.length != nextItems.length ||
|
||||
itemIds.length != nextItems.length) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
|
||||
final normalizedChanged = <int>[];
|
||||
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<String, DownloadItem>.from(byItemId);
|
||||
Map<String, DownloadItem>? 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<String, DownloadItem>.from(byTrackId);
|
||||
nextByTrackId[next.track.id] = next;
|
||||
}
|
||||
}
|
||||
|
||||
return DownloadQueueLookup._(
|
||||
byTrackId: nextByTrackId ?? byTrackId,
|
||||
byItemId: nextByItemId,
|
||||
itemIds: itemIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final downloadQueueLookupProvider = Provider<DownloadQueueLookup>((ref) {
|
||||
final items = ref.watch(downloadQueueProvider.select((s) => s.items));
|
||||
return DownloadQueueLookup.fromItems(items);
|
||||
return ref.watch(downloadQueueProvider.select((s) => s.lookup));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<void> _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 (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,13 +1181,13 @@ class PlatformBridge {
|
||||
|
||||
static Map<String, dynamic> _decodeMapResult(dynamic result) {
|
||||
if (result is Map) {
|
||||
return Map<String, dynamic>.from(result);
|
||||
return result.cast<String, dynamic>();
|
||||
}
|
||||
if (result is String) {
|
||||
if (result.isEmpty) return const <String, dynamic>{};
|
||||
final decoded = jsonDecode(result);
|
||||
if (decoded is Map) {
|
||||
return Map<String, dynamic>.from(decoded);
|
||||
return decoded.cast<String, dynamic>();
|
||||
}
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
|
||||
Reference in New Issue
Block a user