Add IsolateService for improved asynchronous operations and refactor service calls to utilize it

This commit is contained in:
Moustapha Kodjo Amadou 2025-11-07 16:48:42 +01:00
parent 1569c1bcd1
commit ea50cc91ca
10 changed files with 327 additions and 127 deletions

View file

@ -32,6 +32,7 @@ import 'package:mangayomi/router/router.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart'; import 'package:mangayomi/l10n/generated/app_localizations.dart';
import 'package:mangayomi/services/http/m_client.dart'; import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:mangayomi/src/rust/frb_generated.dart'; import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/discord_rpc.dart'; import 'package:mangayomi/utils/discord_rpc.dart';
import 'package:mangayomi/utils/log/logger.dart'; import 'package:mangayomi/utils/log/logger.dart';
@ -54,6 +55,7 @@ void main(List<String> args) async {
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
await RustLib.init(); await RustLib.init();
await imgCropIsolate.start(); await imgCropIsolate.start();
await getIsolateService.start();
if (!(Platform.isAndroid || Platform.isIOS)) { if (!(Platform.isAndroid || Platform.isIOS)) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
} }

View file

@ -15,13 +15,16 @@ Future<void> fetchItemSourcesList(
if (ref.watch(checkForExtensionsUpdateStateProvider) || reFresh) { if (ref.watch(checkForExtensionsUpdateStateProvider) || reFresh) {
final repos = ref.watch(extensionsRepoStateProvider(itemType)); final repos = ref.watch(extensionsRepoStateProvider(itemType));
for (Repo repo in repos) { for (Repo repo in repos) {
await fetchSourcesList( try {
repo: repo, await fetchSourcesList(
refresh: reFresh, repo: repo,
id: id, refresh: reFresh,
ref: ref, id: id,
itemType: itemType, androidProxyServer: ref.watch(androidProxyServerStateProvider),
); autoUpdateExtensions: ref.watch(autoUpdateExtensionsStateProvider),
itemType: itemType,
);
} catch (_) {}
} }
} }
} }

View file

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_interceptor/http_interceptor.dart'; import 'package:http_interceptor/http_interceptor.dart';
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart'; import 'package:mangayomi/eval/lib.dart';
@ -9,14 +8,14 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/http/m_client.dart'; import 'package:mangayomi/services/http/m_client.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
Future<void> fetchSourcesList({ Future<void> fetchSourcesList({
int? id, int? id,
required bool refresh, required bool refresh,
required Ref ref, required String androidProxyServer,
required bool autoUpdateExtensions,
required ItemType itemType, required ItemType itemType,
required Repo? repo, required Repo? repo,
}) async { }) async {
@ -126,21 +125,21 @@ Future<void> fetchSourcesList({
orElse: () => Source(), orElse: () => Source(),
); );
if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) { if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) {
await _updateSource(matchingSource, ref, repo, itemType); await _updateSource(matchingSource, androidProxyServer, repo, itemType);
} }
} else { } else {
for (var source in sourceList) { for (var source in sourceList) {
final existingSource = await isar.sources.get(source.id!); final existingSource = await isar.sources.get(source.id!);
if (existingSource == null) { if (existingSource == null) {
await _addNewSource(source, ref, repo, itemType); await _addNewSource(source, repo, itemType);
continue; continue;
} }
final shouldUpdate = final shouldUpdate =
existingSource.isAdded! && existingSource.isAdded! &&
compareVersions(existingSource.version!, source.version!) < 0; compareVersions(existingSource.version!, source.version!) < 0;
if (!shouldUpdate) continue; if (!shouldUpdate) continue;
if (ref.read(autoUpdateExtensionsStateProvider)) { if (autoUpdateExtensions) {
await _updateSource(source, ref, repo, itemType); await _updateSource(source, androidProxyServer, repo, itemType);
} else { } else {
await isar.writeTxn(() async { await isar.writeTxn(() async {
isar.sources.put(existingSource..versionLast = source.version); isar.sources.put(existingSource..versionLast = source.version);
@ -149,12 +148,12 @@ Future<void> fetchSourcesList({
} }
} }
checkIfSourceIsObsolete(sourceList, repo!, itemType, ref); checkIfSourceIsObsolete(sourceList, repo!, itemType);
} }
Future<void> _updateSource( Future<void> _updateSource(
Source source, Source source,
Ref ref, String androidProxyServer,
Repo? repo, Repo? repo,
ItemType itemType, ItemType itemType,
) async { ) async {
@ -163,7 +162,7 @@ Future<void> _updateSource(
final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon
? base64.encode(req.bodyBytes) ? base64.encode(req.bodyBytes)
: req.body; : req.body;
final androidProxyServer = ref.read(androidProxyServerStateProvider);
Map<String, String> headers = {}; Map<String, String> headers = {};
bool? supportLatest; bool? supportLatest;
FilterList? filterList; FilterList? filterList;
@ -232,12 +231,7 @@ Future<void> _updateSource(
await isar.writeTxn(() async => isar.sources.put(updatedSource)); await isar.writeTxn(() async => isar.sources.put(updatedSource));
} }
Future<void> _addNewSource( Future<void> _addNewSource(Source source, Repo? repo, ItemType itemType) async {
Source source,
Ref ref,
Repo? repo,
ItemType itemType,
) async {
final newSource = Source() final newSource = Source()
..sourceCodeUrl = source.sourceCodeUrl ..sourceCodeUrl = source.sourceCodeUrl
..id = source.id ..id = source.id
@ -269,7 +263,6 @@ Future<void> checkIfSourceIsObsolete(
List<Source> sourceList, List<Source> sourceList,
Repo repo, Repo repo,
ItemType itemType, ItemType itemType,
Ref ref,
) async { ) async {
if (sourceList.isEmpty) return; if (sourceList.isEmpty) return;

View file

@ -1,7 +1,8 @@
import 'package:mangayomi/eval/lib.dart'; import 'dart:async';
import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_detail.g.dart'; part 'get_detail.g.dart';
@ -11,8 +12,12 @@ Future<MManga> getDetail(
required String url, required String url,
required Source source, required Source source,
}) async { }) async {
return getExtensionService( final proxyServer = ref.read(androidProxyServerStateProvider);
source,
ref.read(androidProxyServerStateProvider), return getIsolateService.get<MManga>(
).getDetail(url); url: url,
source: source,
serviceType: 'getDetail',
proxyServer: proxyServer,
);
} }

View file

@ -1,13 +1,12 @@
import 'dart:math'; import 'dart:math';
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_latest_updates.g.dart'; part 'get_latest_updates.g.dart';
@ -38,8 +37,10 @@ Future<MPages?> getLatestUpdates(
.toList(); .toList();
return MPages(list: result, hasNextPage: true); return MPages(list: result, hasNextPage: true);
} }
return getExtensionService( return getIsolateService.get<MPages?>(
source, page: page,
ref.read(androidProxyServerStateProvider), source: source,
).getLatestUpdates(page); serviceType: 'getLatestUpdates',
proxyServer: ref.read(androidProxyServerStateProvider),
);
} }

View file

@ -1,13 +1,12 @@
import 'dart:math'; import 'dart:math';
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_popular.g.dart'; part 'get_popular.g.dart';
@ -38,8 +37,11 @@ Future<MPages?> getPopular(
.toList(); .toList();
return MPages(list: result, hasNextPage: true); return MPages(list: result, hasNextPage: true);
} }
return getExtensionService(
source, return getIsolateService.get<MPages?>(
ref.read(androidProxyServerStateProvider), page: page,
).getPopular(page); source: source,
serviceType: 'getPopular',
proxyServer: ref.read(androidProxyServerStateProvider),
);
} }

View file

@ -68,7 +68,7 @@ class MClient {
); );
return InterceptedClient.build( return InterceptedClient.build(
client: httpClient(settings: clientSettings, reqcopyWith: reqcopyWith), client: httpClient(settings: clientSettings, reqcopyWith: reqcopyWith),
retryPolicy: ResolveCloudFlareChallenge(showCloudFlareError),
interceptors: [ interceptors: [
MCookieManager(reqcopyWith), MCookieManager(reqcopyWith),
LoggerInterceptor(showCloudFlareError), LoggerInterceptor(showCloudFlareError),
@ -235,11 +235,15 @@ class LoggerInterceptor extends InterceptorContract {
Logger.add(LoggerLevel.info, content); Logger.add(LoggerLevel.info, content);
} }
if (cloudflare) { if (cloudflare) {
botToast( try {
"${response.statusCode} Failed to bypass Cloudflare", botToast(
hasCloudFlare: cloudflare, "${response.statusCode} Failed to bypass Cloudflare",
url: response.request!.url.toString(), hasCloudFlare: cloudflare,
); url: response.request!.url.toString(),
);
} catch (e) {
throw "${response.statusCode} Failed to bypass Cloudflare";
}
} }
} }
@ -252,78 +256,78 @@ bool isCloudflare(BaseResponse response) {
["cloudflare-nginx", "cloudflare"].contains(response.headers["server"]); ["cloudflare-nginx", "cloudflare"].contains(response.headers["server"]);
} }
class ResolveCloudFlareChallenge extends RetryPolicy { // class ResolveCloudFlareChallenge extends RetryPolicy {
bool showCloudFlareError; // bool showCloudFlareError;
ResolveCloudFlareChallenge(this.showCloudFlareError); // ResolveCloudFlareChallenge(this.showCloudFlareError);
@override // @override
int get maxRetryAttempts => 2; // int get maxRetryAttempts => 2;
@override // @override
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async { // Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
if (!showCloudFlareError || Platform.isLinux) return false; // if (!showCloudFlareError || Platform.isLinux) return false;
flutter_inappwebview.HeadlessInAppWebView? headlessWebView; // flutter_inappwebview.HeadlessInAppWebView? headlessWebView;
int time = 0; // int time = 0;
bool timeOut = false; // bool timeOut = false;
bool isCloudFlare = isCloudflare(response); // bool isCloudFlare = isCloudflare(response);
if (isCloudFlare) { // if (isCloudFlare) {
headlessWebView = flutter_inappwebview.HeadlessInAppWebView( // headlessWebView = flutter_inappwebview.HeadlessInAppWebView(
webViewEnvironment: webViewEnvironment, // webViewEnvironment: webViewEnvironment,
initialUrlRequest: flutter_inappwebview.URLRequest( // initialUrlRequest: flutter_inappwebview.URLRequest(
url: flutter_inappwebview.WebUri(response.request!.url.toString()), // url: flutter_inappwebview.WebUri(response.request!.url.toString()),
), // ),
onLoadStop: (controller, url) async { // onLoadStop: (controller, url) async {
try { // try {
isCloudFlare = await controller.platform.evaluateJavascript( // isCloudFlare = await controller.platform.evaluateJavascript(
source: // source:
"document.head.innerHTML.includes('#challenge-success-text')", // "document.head.innerHTML.includes('#challenge-success-text')",
); // );
} catch (_) { // } catch (_) {
isCloudFlare = false; // isCloudFlare = false;
} // }
await Future.doWhile(() async { // await Future.doWhile(() async {
if (!timeOut && isCloudFlare) { // if (!timeOut && isCloudFlare) {
try { // try {
isCloudFlare = await controller.platform.evaluateJavascript( // isCloudFlare = await controller.platform.evaluateJavascript(
source: // source:
"document.head.innerHTML.includes('#challenge-success-text')", // "document.head.innerHTML.includes('#challenge-success-text')",
); // );
} catch (_) { // } catch (_) {
isCloudFlare = false; // isCloudFlare = false;
} // }
} // }
if (isCloudFlare) await Future.delayed(Duration(milliseconds: 300)); // if (isCloudFlare) await Future.delayed(Duration(milliseconds: 300));
return isCloudFlare; // return isCloudFlare;
}); // });
if (!timeOut) { // if (!timeOut) {
final ua = // final ua =
await controller.evaluateJavascript( // await controller.evaluateJavascript(
source: "navigator.userAgent", // source: "navigator.userAgent",
) ?? // ) ??
""; // "";
await MClient.setCookie(url.toString(), ua, controller); // await MClient.setCookie(url.toString(), ua, controller);
} // }
}, // },
); // );
headlessWebView.run(); // headlessWebView.run();
await Future.doWhile(() async { // await Future.doWhile(() async {
timeOut = time == 15; // timeOut = time == 15;
if (!isCloudFlare || timeOut) { // if (!isCloudFlare || timeOut) {
return false; // return false;
} // }
await Future.delayed(const Duration(seconds: 1)); // await Future.delayed(const Duration(seconds: 1));
time++; // time++;
return true; // return true;
}); // });
try { // try {
headlessWebView.dispose(); // headlessWebView.dispose();
} catch (_) {} // } catch (_) {}
return true; // return true;
} // }
return false; // return false;
} // }
} // }

View file

@ -0,0 +1,183 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/providers/storage_provider.dart';
class _IsolateData {
final SendPort sendPort;
final RootIsolateToken rootIsolateToken;
_IsolateData({required this.sendPort, required this.rootIsolateToken});
}
class GetIsolateService {
bool _isRunning = false;
Isolate? _getIsolateService;
ReceivePort? _receivePort;
SendPort? _sendPort;
Future<void> start() async {
if (!_isRunning) {
try {
await _initGetIsolateService();
} catch (_) {
await stop();
}
}
}
Future<void> _initGetIsolateService() async {
_receivePort = ReceivePort();
final rootToken = RootIsolateToken.instance!;
_getIsolateService = await Isolate.spawn(
_getIsolateServiceEntryPoint,
_IsolateData(
sendPort: _receivePort!.sendPort,
rootIsolateToken: rootToken,
),
);
final completer = Completer<SendPort>();
_receivePort!.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
_sendPort = await completer.future;
_isRunning = true;
}
static Future<void> _getIsolateServiceEntryPoint(
_IsolateData isolateData,
) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(
isolateData.rootIsolateToken,
);
await initializeDateFormatting();
isar = await StorageProvider().initDB(null, inspector: kDebugMode);
final receivePort = ReceivePort();
isolateData.sendPort.send(receivePort.sendPort);
await for (var message in receivePort) {
if (message is Map<String, dynamic>) {
try {
final url = message['url'] as String?;
final page = message['page'] as int?;
final query = message['query'] as String?;
final filterList = message['filterList'] as List?;
final source = message['source'] as Source?;
final proxyServer = message['proxyServer'] as String?;
final serviceType = message['serviceType'] as String?;
final responsePort = message['responsePort'] as SendPort;
if (serviceType == 'getDetail') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getDetail(url!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getPopular') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getPopular(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'getLatestUpdates') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).getLatestUpdates(page!);
responsePort.send({'success': true, 'data': result});
} else if (serviceType == 'search') {
final result = await getExtensionService(
source!,
proxyServer ?? '',
).search(query!, page!, filterList!);
responsePort.send({'success': true, 'data': result});
}
} catch (e) {
final responsePort = message['responsePort'] as SendPort;
responsePort.send({'success': false, 'error': e.toString()});
}
} else if (message == 'dispose') {
break;
}
}
}
Future<T> get<T>({
int? id,
bool? refresh,
ItemType? itemType,
Repo? repo,
String? url,
int? page,
String? query,
List<dynamic>? filterList,
Source? source,
String? serviceType,
String? proxyServer,
bool? autoUpdateExtensions,
String? androidProxyServer,
}) async {
if (_sendPort == null) {
throw Exception('Isolate not running');
}
final responsePort = ReceivePort();
final completer = Completer<T>();
responsePort.listen((response) {
responsePort.close();
if (response is Map<String, dynamic>) {
if (response['success'] == true) {
completer.complete(response['data'] as T);
} else {
completer.completeError(response['error']);
}
}
});
_sendPort!.send({
'url': url,
'page': page,
'query': query,
'filterList': filterList,
'serviceType': serviceType,
'source': source,
'proxyServer': proxyServer,
'responsePort': responsePort.sendPort,
});
return completer.future;
}
Future<void> stop() async {
if (!_isRunning) {
return;
}
_sendPort?.send('dispose');
_getIsolateService?.kill(priority: Isolate.immediate);
_receivePort?.close();
_sendPort = null;
_getIsolateService = null;
_receivePort = null;
_isRunning = false;
}
}
final getIsolateService = GetIsolateService();

View file

@ -1,13 +1,12 @@
import 'dart:math'; import 'dart:math';
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search.g.dart'; part 'search.g.dart';
@ -40,8 +39,12 @@ Future<MPages?> search(
.toList(); .toList();
return MPages(list: result, hasNextPage: true); return MPages(list: result, hasNextPage: true);
} }
return getExtensionService( return getIsolateService.get<MPages?>(
source, query: query,
ref.read(androidProxyServerStateProvider), filterList: filterList,
).search(query, page, filterList); source: source,
page: page,
serviceType: 'search',
proxyServer: ref.read(androidProxyServerStateProvider),
);
} }

View file

@ -1,11 +1,11 @@
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/isolate_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search_.g.dart'; part 'search_.g.dart';
@ -37,8 +37,12 @@ Future<MPages?> search(
.toList(); .toList();
return MPages(list: result, hasNextPage: true); return MPages(list: result, hasNextPage: true);
} }
return getExtensionService( return getIsolateService.get<MPages?>(
source, query: query,
ref.read(androidProxyServerStateProvider), filterList: filterList,
).search(query, page, filterList); source: source,
page: page,
serviceType: 'search',
proxyServer: ref.read(androidProxyServerStateProvider),
);
} }