From 0bbf59fda0061a080113be82faef66c4c1d5b890 Mon Sep 17 00:00:00 2001 From: K3vinb5 Date: Tue, 21 Oct 2025 02:09:41 +0100 Subject: [PATCH] Refactored the bridge to use load the JVM in a separate Isolate that receives messages in a queue executing them, this way it doesn't block the UI thread --- example/lib/main.dart | 6 +- lib/aniyomi_bridge.dart | 357 ++++++++++++++++++++--------------- lib/jni_isolate.dart | 325 +++++++++++++++++++++++++++++++ lib/jni_isolate_message.dart | 35 ++++ 4 files changed, 564 insertions(+), 159 deletions(-) create mode 100644 lib/jni_isolate.dart create mode 100644 lib/jni_isolate_message.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index bdcf27e..f3e4bea 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,9 +6,9 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); AniyomiBridge bridge = AniyomiBridge(); - await Future.delayed(Duration(seconds: 2)); - bridge.loadAnimeExtension("https://gitea.k3vinb5.dev/Backups/kohi-den-extensions/raw/branch/main/apk/aniyomi-en.zoro-v14.60.apk"); - List animes = bridge.getAnimeSearchResults("Shingeki", 1, "hianime"); + await Future.delayed(Duration(milliseconds: 1000)); + await bridge.loadAnimeExtension("https://gitea.k3vinb5.dev/Backups/kohi-den-extensions/raw/branch/main/apk/aniyomi-all.animeonsen-v14.7.apk"); + List animes = await bridge.getAnimeSearchResults("Shingeki", 1, "animeonsen"); print(animes); animes.forEach((anime) => print(anime.getTitle().toDartString())); diff --git a/lib/aniyomi_bridge.dart b/lib/aniyomi_bridge.dart index 6d9055d..a779c01 100644 --- a/lib/aniyomi_bridge.dart +++ b/lib/aniyomi_bridge.dart @@ -1,11 +1,15 @@ +import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; +import 'dart:math'; import 'package:flutter/services.dart'; import 'package:jni/jni.dart'; +import 'package:k3vinb5_aniyomi_bridge/jni_isolate.dart'; +import 'package:k3vinb5_aniyomi_bridge/jni_isolate_message.dart'; import 'package:k3vinb5_aniyomi_bridge/models/extension.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:k3vinb5_aniyomi_bridge/jmodels/jvideo.dart'; -import 'package:k3vinb5_aniyomi_bridge/jmodels/janiyomibridge.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jsanime.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jsepisode.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jsmanga.dart'; @@ -13,168 +17,205 @@ import 'package:k3vinb5_aniyomi_bridge/jmodels/jschapter.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jpage.dart'; class AniyomiBridge { - static const String _aniyomiBridgeDir = "aniyomibridge"; + // static consts static const String _aniyomiBridgeAnimeExtensionsDir = "animeExtensions"; static const String _aniyomiBridgeMangaExtensionsDir = "mangaExtensions"; + static const String _aniyomiBridgeDir = "aniyomibridge"; static const String _aniyomiBridgeJarName = "aniyomibridge-core.jar"; static const String _packageAssetsDir = "packages/k3vinb5_aniyomi_bridge/assets"; - static late final JAniyomiBridge _jAniyomiBridge; - static late final String _supportDirectoryPath; - bool _isReady = false; + + // static variables + static bool _isReady = false; + static late final JniIsolate _jniIsolate; + static late final ReceivePort _jniReceiverPort; + static late final SendPort _jniSenderPort; + + // instance variables + late final String _supportDirectoryPath; + final Random _random = Random(); + final Map _pendingRequests = {}; + AniyomiBridge() { _initJvm(); } Future _initJvm() async { + if (_isReady) { + return; + } Directory supportDirectory = await getApplicationSupportDirectory(); _supportDirectoryPath = supportDirectory.path; await _loadJarIfNeeded(supportDirectory); - Jni.spawnIfNotExists( - dylibDir: _getDylibDir(supportDirectory), - classPath: _getClassPath(supportDirectory), + Jni.setDylibDir(dylibDir: _getDylibDir(supportDirectory)); + + _jniIsolate = JniIsolate(); + final readyPort = ReceivePort(); + _jniReceiverPort = ReceivePort(); + (bool, SendPort) response = await _jniIsolate.initJniIsolate( + readyPort, + _jniReceiverPort, + supportDirectory, + _getDylibDir(supportDirectory), + _getClassPath(supportDirectory), ); - JAniyomiBridge.init(); - _jAniyomiBridge = JAniyomiBridge(); - _isReady = true; + _isReady = response.$1; + _jniSenderPort = response.$2; + _jniReceiverPort.listen((message) { + if (message is JniIsolateError) { + final id = message.id; + final completer = _pendingRequests.remove(id); + if (completer == null) return; + completer.completeError(message.error); + } else if (message is JniIsolateResponse) { + final id = message.id; + final completer = _pendingRequests.remove(id); + if (completer == null) return; + if (message.results == null) { + completer.complete(); + return; + } + completer.complete(message.results); + } + }); } bool isReady() { return _isReady; } - List getAnimeSearchResults(String query, int page, String source) { - JList? searchResults = _jAniyomiBridge.getAnimeSearchResults( - JString.fromString(query), - page, - JString.fromString(source), - ); - if (searchResults == null) { - return []; - } - return searchResults - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JSAnime.type)) - .toList(); - } - - List getMangaSearchResults(String query, int page, String source) { - JList? searchResults = _jAniyomiBridge.getMangaSearchResults( - JString.fromString(query), - page, - JString.fromString(source), - ); - if (searchResults == null) { - return []; - } - return searchResults - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JSManga.type)) - .toList(); - } - - List getEpisodeList(JSAnime sAnime, String source) { - JList? episodeList = _jAniyomiBridge.getEpisodeList( - sAnime, - JString.fromString(source), - ); - if (episodeList == null) { - return []; - } - return episodeList - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JSEpisode.type)) - .toList(); - } - - List getChapterList(JSManga sManga, String source) { - JList? episodeList = _jAniyomiBridge.getEpisodeList( - sManga, - JString.fromString(source), - ); - if (episodeList == null) { - return []; - } - return episodeList - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JSChapter.type)) - .toList(); - } - - List getVideoList(JSEpisode sEpisode, String source) { - JList? videoList = _jAniyomiBridge.getVideoList( - sEpisode, - JString.fromString(source), - ); - if (videoList == null) { - return []; - } - return videoList - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JVideo.type)) - .toList(); - } - - List getPageList(JSChapter sChapter, String source) { - JList? videoList = _jAniyomiBridge.getVideoList( - sChapter, - JString.fromString(source), - ); - if (videoList == null) { - return []; - } - return videoList - .cast() - .where(_jObjIsNotNull) - .map((jObj) => jObj!.as(JPage.type)) - .toList(); - } - - void loadAnimeExtension(String extensionUrl) { - _jAniyomiBridge.loadAnimeExtension( - JString.fromString(extensionUrl), - JString.fromString( - path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir), + Future> getAnimeSearchResults(String query, int page, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getAnimeSearchResults, + args: { + 'query': query, + 'page': page, + 'source': source, + }, ), ); } - void unloadAnimeExtension(String extensionName, String extensionVersion) { - _jAniyomiBridge.unloadAnimeExtension( - JString.fromString(extensionName), - JString.fromString(extensionVersion), - JString.fromString( - path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir), - ) - ); - } - - void loadMangaExtension(String extensionUrl) { - _jAniyomiBridge.loadMangaExtension( - JString.fromString(extensionUrl), - JString.fromString( - path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), + Future> getMangaSearchResults(String query, int page, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getMangaSearchResults, + args: { + 'query': query, + 'page': page, + 'source': source, + }, ), ); } - void unloadMangaExtension(String extensionName, String extensionVersion) { - _jAniyomiBridge.unloadMangaExtension( - JString.fromString(extensionName), - JString.fromString(extensionVersion), - JString.fromString( - path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), - ) + Future> getEpisodeList(JSAnime sAnime, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getEpisodeList, + args: { + 'sAnime': sAnime, + 'source': source, + }, + ), ); } - Future> getInstalledAnimeExtensions() async{ + Future> getChapterList(JSManga sManga, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getChapterList, + args: { + 'sManga': sManga, + 'source': source, + }, + ), + ); + } + + Future> getVideoList(JSEpisode sEpisode, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getVideoList, + args: { + 'sEpisode': sEpisode, + 'source': source, + }, + ), + ); + } + + Future> getPageList(JSChapter sChapter, String source) async { + return _sendJniIsolateMessage>( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.getPageList, + args: { + 'sChapter': sChapter, + 'source': source, + }, + ), + ); + } + + Future loadAnimeExtension(String extensionUrl) async { + return _sendJniIsolateMessage( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.loadAnimeExtension, + args: { + 'extensionUrl': extensionUrl, + }, + ), + ); + } + + Future unloadAnimeExtension(String extensionName, String extensionVersion) async { + return _sendJniIsolateMessage( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.unloadAnimeExtension, + args: { + 'extensionName': extensionName, + 'extensionVersion': extensionVersion, + }, + ), + ); + } + + Future loadMangaExtension(String extensionUrl) async { + return _sendJniIsolateMessage( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.loadMangaExtension, + args: { + 'extensionUrl': extensionUrl, + }, + ), + ); + } + + Future unloadMangaExtension(String extensionName, String extensionVersion) async { + return _sendJniIsolateMessage( + JniIsolateMessage( + id: _random.nextInt(1 << 32), + type: JniIsolateMessageType.unloadMangaExtension, + args: { + 'extensionName': extensionName, + 'extensionVersion': extensionVersion, + }, + ), + ); + } + + Future> getInstalledAnimeExtensions() async { Directory animeExtDir = Directory( path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir), ); @@ -193,7 +234,7 @@ class AniyomiBridge { return extensions; } - Future> getInstalledMangaExtensions() async{ + Future> getInstalledMangaExtensions() async { Directory mangaExtDir = Directory( path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), ); @@ -212,33 +253,37 @@ class AniyomiBridge { return extensions; } - List? getLoadedAnimeExtensions() { - JList? loadedExtensions = _jAniyomiBridge - .getAnimeLoadedExtensions(); - if (loadedExtensions == null) { - return null; - } - return loadedExtensions - .cast() - .map((jStr) => jStr.toDartString()) - .toList(); - } + // List? getLoadedAnimeExtensions() { + // JList? loadedExtensions = _jAniyomiBridge + // .getAnimeLoadedExtensions(); + // if (loadedExtensions == null) { + // return null; + // } + // return loadedExtensions + // .cast() + // .map((jStr) => jStr.toDartString()) + // .toList(); + // } + // + // List? getLoadedMangaExtensions() { + // JList? loadedExtensions = _jAniyomiBridge + // .getMangaLoadedExtensions(); + // if (loadedExtensions == null) { + // return null; + // } + // return loadedExtensions + // .cast() + // .map((jStr) => jStr.toDartString()) + // .toList(); + // } - List? getLoadedMangaExtensions() { - JList? loadedExtensions = _jAniyomiBridge - .getMangaLoadedExtensions(); - if (loadedExtensions == null) { - return null; - } - return loadedExtensions - .cast() - .map((jStr) => jStr.toDartString()) - .toList(); + Future _sendJniIsolateMessage(JniIsolateMessage message) { + final completer = Completer(); + _pendingRequests[message.id] = completer; + _jniSenderPort.send(message); + return completer.future; } - bool Function(JObject? jObj) get _jObjIsNotNull => - (jObj) => jObj != null; - String _getDylibDir(Directory supportDirectory) { String executablePath = File(Platform.resolvedExecutable).parent.path; if (Platform.isLinux) { diff --git a/lib/jni_isolate.dart b/lib/jni_isolate.dart new file mode 100644 index 0000000..6c7494d --- /dev/null +++ b/lib/jni_isolate.dart @@ -0,0 +1,325 @@ +import 'dart:io'; +import 'dart:isolate'; + +import 'package:jni/jni.dart'; +import 'package:k3vinb5_aniyomi_bridge/jni_isolate_message.dart'; +import 'package:path/path.dart' as path; + +import 'jmodels/janiyomibridge.dart'; +import 'jmodels/jpage.dart'; +import 'jmodels/jsanime.dart'; +import 'jmodels/jschapter.dart'; +import 'jmodels/jsepisode.dart'; +import 'jmodels/jsmanga.dart'; +import 'jmodels/jvideo.dart'; + +class JniIsolate { + // Static consts + static const String _aniyomiBridgeAnimeExtensionsDir = "animeExtensions"; + static const String _aniyomiBridgeMangaExtensionsDir = "mangaExtensions"; + // Static variables + static late Isolate _jvmIsolate; + static late SendPort _jvmIsolateSendPort; + // Instance variables + late final Directory _supportDirectory; + late final String _supportDirectoryPath; + + Future<(bool, SendPort)> initJniIsolate( + ReceivePort readyPort, + ReceivePort mainIsolateReceiverPort, + Directory supportDirectory, + String dyLibDir, + List classPath, + ) async { + _supportDirectory = supportDirectory; + _supportDirectoryPath = supportDirectory.path; + _jvmIsolate = await Isolate.spawn( + _jniWorkerEntryPoint, { + 'readyPort': readyPort.sendPort, + 'mainIsolateSendPort': mainIsolateReceiverPort.sendPort, + 'dylibDir': dyLibDir, + 'classPath': classPath, + }, debugName: 'Aniyomi-JNI-Isolate'); + final message = await readyPort.first; + readyPort.close(); + if (message is SendPort) { + _jvmIsolateSendPort = message; + } else if (message is Map && message['error'] != null) { + throw Exception('Failed to initialize JVM: ${message['error']}'); + } else { + throw Exception('Unexpected message from JVM isolate: $message'); + } + return (true, _jvmIsolateSendPort); + } + + void _jniWorkerEntryPoint(Map parameters) { + final SendPort readyPort = parameters['readyPort']; + final SendPort mainIsolateSendPort = parameters['mainIsolateSendPort']; + final String dylibDir = parameters['dylibDir']; + final List classPath = parameters['classPath']; + + final jniIsolateReceiverPort = ReceivePort(); + readyPort.send(jniIsolateReceiverPort.sendPort); + + try { + Jni.spawnIfNotExists(dylibDir: dylibDir, classPath: classPath); + JAniyomiBridge.init(); + JAniyomiBridge jAniyomiBridge = JAniyomiBridge(); + jniIsolateReceiverPort.listen((message) { + if (message is! JniIsolateMessage) { + mainIsolateSendPort.send( + JniIsolateError( + id: 0, + error: 'Invalid message type received', + stackTrace: '', + ), + ); + return; + } + try { + switch ((message).type) { + case JniIsolateMessageType.getAnimeSearchResults: + List results = _getAnimeSearchResults( + jAniyomiBridge, + message.args['query'] as String, + message.args['page'] as int, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.getMangaSearchResults: + List results = _getMangaSearchResults( + jAniyomiBridge, + message.args['query'] as String, + message.args['page'] as int, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.getEpisodeList: + List results = _getEpisodeList( + jAniyomiBridge, + message.args['sAnime'] as JSAnime, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.getChapterList: + List results = _getChapterList( + jAniyomiBridge, + message.args['sManga'] as JSManga, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.getVideoList: + List results = _getVideoList( + jAniyomiBridge, + message.args['sEpisode'] as JSEpisode, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.getPageList: + List results = _getPageList( + jAniyomiBridge, + message.args['sChapter'] as JSChapter, + message.args['source'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id, results: results), + ); + case JniIsolateMessageType.loadAnimeExtension: + _loadAnimeExtension( + jAniyomiBridge, + message.args['extensionUrl'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id), + ); + case JniIsolateMessageType.loadMangaExtension: + _loadMangaExtension( + jAniyomiBridge, + message.args['extensionUrl'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id), + ); + case JniIsolateMessageType.unloadAnimeExtension: + _unloadAnimeExtension( + jAniyomiBridge, + message.args['extensionName'] as String, + message.args['extensionVersion'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id), + ); + case JniIsolateMessageType.unloadMangaExtension: + _unloadMangaExtension( + jAniyomiBridge, + message.args['extensionName'] as String, + message.args['extensionVersion'] as String, + ); + mainIsolateSendPort.send( + JniIsolateResponse(id: message.id), + ); + } + } catch (e, stackTrace) { + mainIsolateSendPort.send( + JniIsolateError( + id: message.id, + error: e.toString(), + stackTrace: stackTrace.toString(), + ), + ); + } + }); + } catch (e, stackTrace) { + mainIsolateSendPort.send( + JniIsolateError( + id: 0, + error: e.toString(), + stackTrace: stackTrace.toString(), + ), + ); + } + } + + List _getAnimeSearchResults(JAniyomiBridge jAniyomiBridge, String query, int page, String source) { + JList? searchResults = jAniyomiBridge.getAnimeSearchResults( + JString.fromString(query), + page, + JString.fromString(source), + ); + if (searchResults == null) { + return []; + } + return searchResults + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JSAnime.type)) + .toList(); + } + + List _getMangaSearchResults(JAniyomiBridge jAniyomiBridge, String query, int page, String source) { + JList? searchResults = jAniyomiBridge.getMangaSearchResults( + JString.fromString(query), + page, + JString.fromString(source), + ); + if (searchResults == null) { + return []; + } + return searchResults + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JSManga.type)) + .toList(); + } + + List _getEpisodeList(JAniyomiBridge jAniyomiBridge, JSAnime sAnime, String source) { + JList? episodeList = jAniyomiBridge.getEpisodeList( + sAnime, + JString.fromString(source), + ); + if (episodeList == null) { + return []; + } + return episodeList + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JSEpisode.type)) + .toList(); + } + + List _getChapterList(JAniyomiBridge jAniyomiBridge, JSManga sManga, String source) { + JList? episodeList = jAniyomiBridge.getEpisodeList( + sManga, + JString.fromString(source), + ); + if (episodeList == null) { + return []; + } + return episodeList + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JSChapter.type)) + .toList(); + } + + List _getVideoList(JAniyomiBridge jAniyomiBridge, JSEpisode sEpisode, String source) { + JList? videoList = jAniyomiBridge.getVideoList( + sEpisode, + JString.fromString(source), + ); + if (videoList == null) { + return []; + } + return videoList + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JVideo.type)) + .toList(); + } + + List _getPageList(JAniyomiBridge jAniyomiBridge, JSChapter sChapter, String source) { + JList? videoList = jAniyomiBridge.getVideoList( + sChapter, + JString.fromString(source), + ); + if (videoList == null) { + return []; + } + return videoList + .cast() + .where(_jObjIsNotNull) + .map((jObj) => jObj!.as(JPage.type)) + .toList(); + } + + void _loadAnimeExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl) { + jAniyomiBridge.loadAnimeExtension( + JString.fromString(extensionUrl), + JString.fromString( + path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir), + ), + ); + } + + void _unloadAnimeExtension(JAniyomiBridge jAniyomiBridge, String extensionName, String extensionVersion) { + jAniyomiBridge.unloadAnimeExtension( + JString.fromString(extensionName), + JString.fromString(extensionVersion), + JString.fromString( + path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir), + ), + ); + } + + void _loadMangaExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl) { + jAniyomiBridge.loadMangaExtension( + JString.fromString(extensionUrl), + JString.fromString( + path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), + ), + ); + } + + void _unloadMangaExtension(JAniyomiBridge jAniyomiBridge, String extensionName, String extensionVersion) { + jAniyomiBridge.unloadMangaExtension( + JString.fromString(extensionName), + JString.fromString(extensionVersion), + JString.fromString( + path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), + ), + ); + } + + bool Function(JObject? jObj) get _jObjIsNotNull => + (jObj) => jObj != null; +} diff --git a/lib/jni_isolate_message.dart b/lib/jni_isolate_message.dart new file mode 100644 index 0000000..ef323a0 --- /dev/null +++ b/lib/jni_isolate_message.dart @@ -0,0 +1,35 @@ +class JniIsolateMessage { + final int id; + final JniIsolateMessageType type; + final Map args; + + JniIsolateMessage({required this.id, required this.type, required this.args}); +} + +class JniIsolateResponse { + final int id; + final dynamic results; + + JniIsolateResponse({required this.id, this.results}); +} + +class JniIsolateError { + final int id; + final String error; + final String stackTrace; + + JniIsolateError({required this.id, required this.error, required this.stackTrace}); +} + +enum JniIsolateMessageType { + getAnimeSearchResults, + getMangaSearchResults, + getEpisodeList, + getChapterList, + getVideoList, + getPageList, + loadAnimeExtension, + loadMangaExtension, + unloadAnimeExtension, + unloadMangaExtension, +} \ No newline at end of file