diff --git a/lib/services/bif_thumbnail_service.dart b/lib/services/bif_thumbnail_service.dart index 4e897d30..630cfeec 100644 --- a/lib/services/bif_thumbnail_service.dart +++ b/lib/services/bif_thumbnail_service.dart @@ -1,4 +1,4 @@ -import 'dart:isolate'; +import '../utils/isolate_helper.dart'; import 'dart:typed_data'; import 'plex_client.dart'; @@ -76,7 +76,7 @@ class BifThumbnailService { appLogger.w('BIF file too large (${bytes.length} bytes), skipping'); return; } - _entries = await Isolate.run(() => _parseBifBytes(bytes)); + _entries = await tryIsolateRun(() => _parseBifBytes(bytes)); } catch (e) { appLogger.w('BIF download/parse failed', error: e); } diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index b4d9e203..9293ba7b 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -1085,6 +1085,7 @@ class DownloadManagerService { String? errorMessage, String? currentFile, }) { + if (_disposed) return; _progressController.add( DownloadProgress( globalKey: globalKey, @@ -1118,6 +1119,7 @@ class DownloadManagerService { /// Emit progress update with artwork paths so DownloadProvider can sync void _emitProgressWithArtwork(String globalKey, {String? thumbPath}) { + if (_disposed) return; // Emit a progress update containing artwork path // The status is preserved as downloading since artwork is just one step _progressController.add( @@ -1263,6 +1265,7 @@ class DownloadManagerService { /// Emit deletion progress update void _emitDeletionProgress(DeletionProgress progress) { + if (_disposed) return; _deletionProgressController.add(progress); } diff --git a/lib/services/plex_api_cache.dart b/lib/services/plex_api_cache.dart index 25a89677..442c80be 100644 --- a/lib/services/plex_api_cache.dart +++ b/lib/services/plex_api_cache.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'dart:isolate'; +import '../utils/isolate_helper.dart'; import 'package:drift/drift.dart'; @@ -42,7 +42,7 @@ class PlexApiCache { final result = await (_db.select(_db.apiCache)..where((t) => t.cacheKey.equals(key))).getSingleOrNull(); if (result != null) { - return await Isolate.run(() => jsonDecode(result.data) as Map); + return await tryIsolateRun(() => jsonDecode(result.data) as Map); } return null; } @@ -50,7 +50,7 @@ class PlexApiCache { /// Cache a response for an endpoint Future put(String serverId, String endpoint, Map data) async { final key = _buildKey(serverId, endpoint); - final encoded = await Isolate.run(() => jsonEncode(data)); + final encoded = await tryIsolateRun(() => jsonEncode(data)); await _db .into(_db.apiCache) .insertOnConflictUpdate(ApiCacheCompanion(cacheKey: Value(key), data: Value(encoded), cachedAt: Value(DateTime.now()))); @@ -145,7 +145,7 @@ class PlexApiCache { if (entries.isEmpty) return {}; - return await Isolate.run(() { + return await tryIsolateRun(() { final result = {}; for (final (serverId, ratingKey, rawData) in entries) { try { diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 387091bf..582fd0c5 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:isolate'; +import '../utils/isolate_helper.dart'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -112,7 +112,7 @@ class ConnectionTestResult { ConnectionTestResult({required this.success, required this.latencyMs, this.error}); } -// Top-level function required by compute() +// Top-level function required by tryIsolateRun() String _decodeUtf8(List bytes) { return utf8.decode(bytes, allowMalformed: true); } @@ -158,7 +158,7 @@ class PlexClient { ResponseBody responseBody, ) { if (responseBytes.length > 50 * 1024) { - return compute(_decodeUtf8, responseBytes); + return tryIsolateRun(() => _decodeUtf8(responseBytes)); } return utf8.decode(responseBytes, allowMalformed: true); } @@ -993,7 +993,7 @@ class PlexClient { final response = await _dio.get('/library/onDeck'); final sid = serverId; final sname = serverName; - return await Isolate.run(() => _processOnDeckResponse(response.data as Map, sid, sname)); + return await tryIsolateRun(() => _processOnDeckResponse(response.data as Map, sid, sname)); } /// Get children of a metadata item (e.g., seasons for a show, episodes for a season) @@ -1481,7 +1481,7 @@ class PlexClient { ); final sid = serverId; final sname = serverName; - return await Isolate.run(() => _processHubResponse(response.data as Map, sid, sname)); + return await tryIsolateRun(() => _processHubResponse(response.data as Map, sid, sname)); } catch (e) { appLogger.e('Failed to get library hubs: $e'); } @@ -1496,7 +1496,7 @@ class PlexClient { final response = await _dio.get('/hubs', queryParameters: {'count': limit, 'includeGuids': 1}); final sid = serverId; final sname = serverName; - return await Isolate.run(() => _processHubResponse(response.data as Map, sid, sname)); + return await tryIsolateRun(() => _processHubResponse(response.data as Map, sid, sname)); } catch (e) { appLogger.e('Failed to get global hubs: $e'); } diff --git a/lib/utils/isolate_helper.dart b/lib/utils/isolate_helper.dart new file mode 100644 index 00000000..2d13e42e --- /dev/null +++ b/lib/utils/isolate_helper.dart @@ -0,0 +1,14 @@ +import 'dart:isolate'; + +/// Runs [computation] in a background isolate via [Isolate.run]. +/// +/// Falls back to synchronous execution when the isolate infrastructure is +/// unavailable (e.g. iOS killed background isolates while the app was +/// suspended). +Future tryIsolateRun(R Function() computation) async { + try { + return await Isolate.run(computation); + } on StateError { + return computation(); + } +}