import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/eval/lib.dart'; import 'package:mangayomi/eval/model/filter.dart'; import 'package:mangayomi/eval/model/source_preference.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/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/services/http/m_client.dart'; import 'package:package_info_plus/package_info_plus.dart'; Future fetchSourcesList({ int? id, required bool refresh, required Ref ref, required ItemType itemType, required Repo? repo, }) async { final http = MClient.init(reqcopyWith: {'useDartHttpClient': true}); final url = repo?.jsonUrl; if (url == null) return; final req = await http.get(Uri.parse(url)); final info = await PackageInfo.fromPlatform(); final sourceList = (jsonDecode(req.body) as List) .expand((e) sync* { if (e['name'] != null && e['pkg'] != null && e['version'] != null && e['code'] != null && e['lang'] != null && e['nsfw'] != null && e['sources'] != null && e['apk'] != null) { final repoUrl = url.replaceAll("/index.min.json", ""); final sources = e['sources'] as List; for (final source in sources) { final src = Source.fromJson(e) ..apiUrl = '' ..appMinVerReq = '' ..dateFormat = '' ..dateFormatLocale = '' ..hasCloudflare = false ..headers = '' ..isActive = true ..isAdded = false ..isFullData = false ..isNsfw = e['nsfw'] == 1 ..isPinned = false ..lastUsed = false ..sourceCode = '' ..typeSource = '' ..versionLast = '0.0.1' ..isObsolete = false ..isLocal = false ..name = source['name'] ..lang = source['lang'] ..baseUrl = source['baseUrl'] ..sourceCodeUrl = "$repoUrl/apk/${e['apk']}" ..sourceCodeLanguage = SourceCodeLanguage.mihon ..itemType = (e['pkg'] as String).startsWith( "eu.kanade.tachiyomi.animeextension", ) ? ItemType.anime : ItemType.manga ..iconUrl = "$repoUrl/icon/${e['pkg']}.png" ..notes = "Requires Android Proxy Server (ApkBridge) for installing and using the extensions!"; src.id = 'mihon-${source['id']}'.hashCode; yield src; } } else if (e['id'] is String && e['name'] != null && e['site'] != null && e['lang'] != null && e['version'] != null && e['url'] != null && e['iconUrl'] != null) { final src = Source.fromJson(e) ..apiUrl = '' ..appMinVerReq = '' ..dateFormat = '' ..dateFormatLocale = '' ..hasCloudflare = false ..headers = '' ..isActive = true ..isAdded = false ..isFullData = false ..isNsfw = false ..isPinned = false ..lastUsed = false ..sourceCode = '' ..typeSource = '' ..versionLast = '0.0.1' ..isObsolete = false ..isLocal = false ..lang = _convertLang(e) ..baseUrl = e['site'] ..sourceCodeUrl = e['url'] ..sourceCodeLanguage = SourceCodeLanguage.lnreader ..itemType = ItemType.novel ..notes = "Performance might be poor due to limited engine"; src.id = 'lnreader-plugin-"${src.name}"."${src.lang}"'.hashCode; yield src; } else { yield Source.fromJson(e); } }) .where( (source) => source.itemType == itemType && source.appMinVerReq != null && compareVersions(info.version, source.appMinVerReq!) > -1, ) .toList(); if (id != null) { final matchingSource = sourceList.firstWhere( (source) => source.id == id, orElse: () => Source(), ); if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) { await _updateSource(matchingSource, ref, repo, itemType); } } else { for (var source in sourceList) { final existingSource = await isar.sources.get(source.id!); if (existingSource == null) { await _addNewSource(source, ref, repo, itemType); continue; } final shouldUpdate = existingSource.isAdded! && compareVersions(existingSource.version!, source.version!) < 0; if (!shouldUpdate) continue; if (ref.read(autoUpdateExtensionsStateProvider)) { await _updateSource(source, ref, repo, itemType); } else { await isar.writeTxn(() async { isar.sources.put(existingSource..versionLast = source.version); }); } } } checkIfSourceIsObsolete(sourceList, repo!, itemType, ref); } Future _updateSource( Source source, Ref ref, Repo? repo, ItemType itemType, ) async { final http = MClient.init(reqcopyWith: {'useDartHttpClient': true}); final req = await http.get(Uri.parse(source.sourceCodeUrl!)); final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon ? base64.encode(req.bodyBytes) : req.body; final androidProxyServer = ref.read(androidProxyServerStateProvider); Map headers = {}; bool? supportLatest; FilterList? filterList; List? preferenceList; if (source.sourceCodeLanguage == SourceCodeLanguage.mihon) { headers = await fetchHeadersDalvik( http, source..sourceCode = sourceCode, androidProxyServer, ); supportLatest = await fetchSupportLatestDalvik( http, source..sourceCode = sourceCode, androidProxyServer, ); filterList = await fetchFilterListDalvik( http, source..sourceCode = sourceCode, androidProxyServer, ); preferenceList = await fetchPreferencesDalvik( http, source..sourceCode = sourceCode, androidProxyServer, ); } else { headers = getExtensionService( source..sourceCode = sourceCode, androidProxyServer, ).getHeaders(); } final updatedSource = Source() ..headers = jsonEncode(headers) ..supportLatest = supportLatest ..filterList = filterList != null ? jsonEncode(filterList.toJson()) : null ..preferenceList = preferenceList != null ? jsonEncode(preferenceList.map((e) => e.toJson()).toList()) : null ..isAdded = true ..sourceCode = sourceCode ..sourceCodeUrl = source.sourceCodeUrl ..id = source.id ..apiUrl = source.apiUrl ..baseUrl = source.baseUrl ..dateFormat = source.dateFormat ..dateFormatLocale = source.dateFormatLocale ..hasCloudflare = source.hasCloudflare ..iconUrl = source.iconUrl ..typeSource = source.typeSource ..lang = source.lang ..isNsfw = source.isNsfw ..name = source.name ..version = source.version ..versionLast = source.version ..itemType = itemType ..isFullData = source.isFullData ?? false ..appMinVerReq = source.appMinVerReq ..sourceCodeLanguage = source.sourceCodeLanguage ..additionalParams = source.additionalParams ?? "" ..isObsolete = false ..notes = source.notes ..repo = repo ..updatedAt = DateTime.now().millisecondsSinceEpoch; await isar.writeTxn(() async => isar.sources.put(updatedSource)); } Future _addNewSource( Source source, Ref ref, Repo? repo, ItemType itemType, ) async { final newSource = Source() ..sourceCodeUrl = source.sourceCodeUrl ..id = source.id ..sourceCode = source.sourceCode ..apiUrl = source.apiUrl ..baseUrl = source.baseUrl ..dateFormat = source.dateFormat ..dateFormatLocale = source.dateFormatLocale ..hasCloudflare = source.hasCloudflare ..iconUrl = source.iconUrl ..typeSource = source.typeSource ..lang = source.lang ..isNsfw = source.isNsfw ..name = source.name ..version = source.version ..versionLast = source.version ..itemType = itemType ..sourceCodeLanguage = source.sourceCodeLanguage ..isFullData = source.isFullData ?? false ..appMinVerReq = source.appMinVerReq ..isObsolete = false ..notes = source.notes ..repo = repo ..updatedAt = DateTime.now().millisecondsSinceEpoch; await isar.writeTxn(() async => isar.sources.put(newSource)); } Future checkIfSourceIsObsolete( List sourceList, Repo repo, ItemType itemType, Ref ref, ) async { if (sourceList.isEmpty) return; final sources = await isar.sources .filter() .idIsNotNull() .itemTypeEqualTo(itemType) .and() .isLocalEqualTo(false) .findAll(); if (sources.isEmpty) return; final sourceIds = sourceList .where((e) => e.id != null) .map((e) => e.id!) .toSet(); if (sourceIds.isEmpty) return; final toUpdate = []; for (var source in sources) { final isNowObsolete = !sourceIds.contains(source.id) && source.repo?.jsonUrl == repo.jsonUrl; if (source.isObsolete != isNowObsolete) { source.isObsolete = isNowObsolete; source.updatedAt = DateTime.now().millisecondsSinceEpoch; toUpdate.add(source); } } if (toUpdate.isEmpty) return; await isar.writeTxn(() => isar.sources.putAll(toUpdate)); } int compareVersions(String version1, String version2) { final v1Parts = version1.split('.'); final v2Parts = version2.split('.'); final minLength = v1Parts.length < v2Parts.length ? v1Parts.length : v2Parts.length; for (var i = 0; i < minLength; i++) { final v1Value = int.parse(v1Parts[i].padRight(2, '0')); final v2Value = int.parse(v2Parts[i].padRight(2, '0')); final comparison = v1Value.compareTo(v2Value); if (comparison != 0) return comparison; } return v1Parts.length.compareTo(v2Parts.length); } Future> fetchHeadersDalvik( InterceptedClient client, Source source, String androidProxyServer, ) async { try { final name = source.itemType == ItemType.anime ? "Anime" : "Manga"; final res = await client.post( Uri.parse("$androidProxyServer/dalvik"), body: jsonEncode({"method": "headers$name", "data": source.sourceCode}), ); final data = jsonDecode(res.body) as List; final Map headers = {}; for (var i = 0; i + 1 < data.length; i += 2) { headers[data[i]] = data[i + 1]; } return headers; } catch (_) { return {}; } } Future fetchSupportLatestDalvik( InterceptedClient client, Source source, String androidProxyServer, ) async { try { final name = source.itemType == ItemType.anime ? "Anime" : "Manga"; final res = await client.post( Uri.parse("$androidProxyServer/dalvik"), body: jsonEncode({ "method": "supportLatest$name", "data": source.sourceCode, }), ); return res.body.trim() == "true"; } catch (_) { return false; } } Future fetchFilterListDalvik( InterceptedClient client, Source source, String androidProxyServer, ) async { try { final name = source.itemType == ItemType.anime ? "Anime" : "Manga"; final res = await client.post( Uri.parse("$androidProxyServer/dalvik"), body: jsonEncode({"method": "filters$name", "data": source.sourceCode}), ); final data = jsonDecode(res.body) as List; final filters = data.expand((e) sync* { if (e['name'] is String && e['state'] is Map && e['values'] is List) { yield SortFilter( "${e['name']}Filter", e['name'], SortState(e['state']['index'], e['state']['ascending'], null), (e['values'] as List) .map((e) => SelectFilterOption(e, e, null)) .toList(), null, ); } else if (e['name'] is String && e['state'] is int && (e['values'] is List || e['vals'] is List)) { yield SelectFilter( "${e['name']}Filter", e['name'], e['state'], e['vals'] is List ? (e['vals'] as List) .map( (e) => SelectFilterOption(e['first'], e['second'], null), ) .toList() : e['values'] is List ? (e['values'] as List) .map((e) => SelectFilterOption(e, e, null)) .toList() : [], "SelectFilter", ); } else if (e['name'] is String && e['state'] is List) { yield GroupFilter( "${e['name']}Filter", e['name'], (e['state'] as List).map((e) { if (e['included'] is bool && e['ignored'] is bool && e['excluded'] is bool) { return TriStateFilter( null, e['name'], e['id'] ?? e['name'], null, state: e['state'], ); } return CheckBoxFilter( null, e['name'], e['id'] ?? e['name'], null, state: e['state'], ); }).toList(), "GroupFilter", ); } else if (e['name'] is String && e['state'] is String) { yield TextFilter( "${e['name']}Filter", e['name'], null, state: e['state'], ); } else if (e['name'] is String && e['state'] is int) { yield HeaderFilter(e['name'], "${e['name']}Filter"); } }).toList(); return FilterList(filters); } catch (_) { return null; } } Future?> fetchPreferencesDalvik( InterceptedClient client, Source source, String androidProxyServer, ) async { try { final name = source.itemType == ItemType.anime ? "Anime" : "Manga"; final res = await client.post( Uri.parse("$androidProxyServer/dalvik"), body: jsonEncode({ "method": "preferences$name", "data": source.sourceCode, }), ); final data = jsonDecode(res.body) as List; return data .map( (e) => SourcePreference.fromJson(e) ..id = null ..sourceId = source.id, ) .toList(); } catch (_) { return null; } } String _convertLang(dynamic e) { final lang = e['lang']; if (lang is String) { switch (lang) { case "‎العربية": return "ar"; case "中文, 汉语, 漢語": return "zh"; case "English": return "en"; case "Français": return "fr"; case "Bahasa Indonesia": return "id"; case "日本語": return "ja"; case "조선말, 한국어": return "ko"; case "Polski": return "pl"; case "Português": return "pt"; case "Русский": return "ru"; case "Español": return "es"; case "ไทย": return "th"; case "Türkçe": return "tr"; case "Українська": return "uk"; case "Tiếng Việt": return "vi"; default: return "all"; } } return "all"; }