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/jsanime.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jsepisode.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jsmanga.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jschapter.dart'; import 'package:k3vinb5_aniyomi_bridge/jmodels/jpage.dart'; class 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 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.setDylibDir(dylibDir: _getDylibDir(supportDirectory)); _jniIsolate = JniIsolate(); final readyPort = ReceivePort(); _jniReceiverPort = ReceivePort(); (bool, SendPort) response = await _jniIsolate.initJniIsolate( readyPort, _jniReceiverPort, supportDirectory, _getDylibDir(supportDirectory), _getClassPath(supportDirectory), ); _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; } 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, }, ), ); } 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, }, ), ); } Future> getEpisodeList(JSAnime sAnime, String source) async { return _sendJniIsolateMessage>( JniIsolateMessage( id: _random.nextInt(1 << 32), type: JniIsolateMessageType.getEpisodeList, args: { 'sAnime': sAnime, 'source': source, }, ), ); } 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), ); Set extensions = await animeExtDir .list() .where((entityFileSystem) => entityFileSystem is File) .where((file) => path.extension(file.path) == ".jar") .map((jarFile) => path.basename(jarFile.path)) .map( (jarFileName) => Extension( name: jarFileName.split("-")[0], version: jarFileName.split("-")[1], ), ) .toSet(); return extensions; } Future> getInstalledMangaExtensions() async { Directory mangaExtDir = Directory( path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir), ); Set extensions = await mangaExtDir .list() .where((entityFileSystem) => entityFileSystem is File) .where((file) => path.extension(file.path) == ".jar") .map((jarFile) => path.basename(jarFile.path)) .map( (jarFileName) => Extension( name: jarFileName.split("-")[0], version: jarFileName.split("-")[1], ), ) .toSet(); return extensions; } // 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(); // } Future _sendJniIsolateMessage(JniIsolateMessage message) { final completer = Completer(); _pendingRequests[message.id] = completer; _jniSenderPort.send(message); return completer.future; } String _getDylibDir(Directory supportDirectory) { String executablePath = File(Platform.resolvedExecutable).parent.path; if (Platform.isLinux) { return path.join(executablePath, "jre", "customjre", "lib", "server"); } else if (Platform.isMacOS) { return path.join( executablePath, "..", "Resources", "jre", "customjre", "lib", "server", ); } else if (Platform.isWindows) { return path.join( executablePath, "..", "jre", "customjre", "lib", "server", ); } else { throw UnsupportedError("Unsupported platform"); } } List _getClassPath(Directory supportDirectory) { return [ path.join( supportDirectory.absolute.path, _aniyomiBridgeDir, _aniyomiBridgeJarName, ), ]; } Future _loadJarIfNeeded(Directory supportDirectory) async { String aniyomiBridgeCorePath = path.join( supportDirectory.path, _aniyomiBridgeDir, _aniyomiBridgeJarName, ); File aniyomiBridgeCore = File(aniyomiBridgeCorePath); if (!(await aniyomiBridgeCore.exists())) { await _copyAssetToFile( "$_packageAssetsDir/$_aniyomiBridgeJarName", aniyomiBridgeCorePath, ); } } Future _copyAssetToFile(String assetPath, String outPath) async { final byteData = await rootBundle.load(assetPath); final buffer = byteData.buffer.asUint8List(); final file = File(outPath); await file.parent.create(recursive: true); await file.writeAsBytes(buffer, flush: true); return file; } }