mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
webview & cloudflare bypass
This commit is contained in:
parent
67fd3d739c
commit
aa21f07b13
18 changed files with 656 additions and 94 deletions
|
|
@ -69,3 +69,13 @@ flutter {
|
|||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
||||
all {
|
||||
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:mangayomi/models/manga_reader.dart';
|
||||
import 'package:mangayomi/models/manga_type.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/services/webview.dart';
|
||||
import 'package:mangayomi/views/browse/browse_screen.dart';
|
||||
import 'package:mangayomi/views/browse/extension/extension_lang.dart';
|
||||
import 'package:mangayomi/views/browse/global_search_screen.dart';
|
||||
|
|
@ -232,7 +233,7 @@ class AsyncRouterNotifier extends ChangeNotifier {
|
|||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
GoRoute(
|
||||
path: "/downloadQueue",
|
||||
name: "downloadQueue",
|
||||
builder: (context, state) {
|
||||
|
|
@ -245,6 +246,27 @@ class AsyncRouterNotifier extends ChangeNotifier {
|
|||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/mangawebview",
|
||||
name: "mangawebview",
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, String>;
|
||||
return MangaWebView(
|
||||
url: data["url"]!,
|
||||
source: data["source"]!,
|
||||
);
|
||||
},
|
||||
pageBuilder: (context, state) {
|
||||
final data = state.extra as Map<String, String>;
|
||||
return CustomTransition(
|
||||
key: state.pageKey,
|
||||
child: MangaWebView(
|
||||
url: data["url"]!,
|
||||
source: data["source"]!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
109
lib/services/cloudflare/cloudflare_bypass.dart
Normal file
109
lib/services/cloudflare/cloudflare_bypass.dart
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mangayomi/services/cloudflare/cookie.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
|
||||
Future<dom.Document?> cloudflareBypassDom(
|
||||
{required String url, required bool bypass, required String source}) async {
|
||||
bool isOk = false;
|
||||
dom.Document? htmll;
|
||||
if (bypass == false) {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
htmll = dom.Document.html(response.body);
|
||||
isOk = true;
|
||||
} else {
|
||||
HeadlessInAppWebView? headlessWebViewJapScan;
|
||||
headlessWebViewJapScan = HeadlessInAppWebView(
|
||||
onLoadStop: (controller, u) async {
|
||||
String? html;
|
||||
html = await controller.evaluateJavascript(
|
||||
source:
|
||||
"window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
await Future.doWhile(() async {
|
||||
if (html == null ||
|
||||
html!.contains("Just a moment") ||
|
||||
html!.contains("https://challenges.cloudflare.com")) {
|
||||
html = await controller.evaluateJavascript(
|
||||
source:
|
||||
"window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
html = await controller.evaluateJavascript(
|
||||
source:
|
||||
"window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
htmll = dom.Document.html(html!);
|
||||
isOk = true;
|
||||
headlessWebViewJapScan!.dispose();
|
||||
},
|
||||
initialSettings: InAppWebViewSettings(
|
||||
userAgent: Hive.box(HiveConstant.hiveBoxAppSettings).get("ua",
|
||||
defaultValue:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")),
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri.uri(Uri.parse(url)),
|
||||
),
|
||||
);
|
||||
|
||||
headlessWebViewJapScan.run();
|
||||
}
|
||||
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (isOk == true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
await setCookie(source, url);
|
||||
return htmll;
|
||||
}
|
||||
|
||||
Future<String> cloudflareBypassHtml(
|
||||
{required String url, required String source}) async {
|
||||
bool isOk = false;
|
||||
String? html;
|
||||
HeadlessInAppWebView? headlessWebViewJapScan;
|
||||
headlessWebViewJapScan = HeadlessInAppWebView(
|
||||
onLoadStop: (controller, u) async {
|
||||
html = await controller.evaluateJavascript(
|
||||
source: "window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
await Future.doWhile(() async {
|
||||
if (html == null ||
|
||||
html!.contains("Just a moment") ||
|
||||
html!.contains("Un instant…")) {
|
||||
html = await controller.evaluateJavascript(
|
||||
source:
|
||||
"window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
html = await controller.evaluateJavascript(
|
||||
source: "window.document.getElementsByTagName('html')[0].outerHTML;");
|
||||
isOk = true;
|
||||
headlessWebViewJapScan!.dispose();
|
||||
},
|
||||
initialSettings: InAppWebViewSettings(
|
||||
userAgent: Hive.box(HiveConstant.hiveBoxAppSettings).get("ua",
|
||||
defaultValue:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")),
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri.uri(Uri.parse(url)),
|
||||
),
|
||||
);
|
||||
|
||||
headlessWebViewJapScan.run();
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (isOk == true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
await setCookie(source, url);
|
||||
return html!;
|
||||
}
|
||||
24
lib/services/cloudflare/cookie.dart
Normal file
24
lib/services/cloudflare/cookie.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
|
||||
setCookie(String source, String url) async {
|
||||
source = source.toLowerCase();
|
||||
final hiveSetting = Hive.box(HiveConstant.hiveBoxAppSettings);
|
||||
List<Cookie> cookies = [];
|
||||
CookieManager cookie = CookieManager.instance();
|
||||
cookies = await cookie.getCookies(url: WebUri.uri(Uri.parse(url.toString())));
|
||||
final cf_clearance =
|
||||
cookies.where((element) => element.name == "cf_clearance").toList();
|
||||
String newCookie = "";
|
||||
if (cf_clearance.isNotEmpty &&
|
||||
cf_clearance.first.name !=
|
||||
hiveSetting.get("$source-cookie", defaultValue: "")) {
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
newCookie = "$newCookie ${cookies[i].name}=${cookies[i].value};";
|
||||
}
|
||||
hiveSetting.put("$source-cookie", newCookie);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import 'package:mangayomi/models/comick/chapter_page_comick.dart';
|
|||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/providers/hive_provider.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/cloudflare/cloudflare_bypass.dart';
|
||||
import 'package:mangayomi/services/get_popular_manga.dart';
|
||||
import 'package:mangayomi/services/http_res_to_dom_html.dart';
|
||||
import 'package:mangayomi/source/source_model.dart';
|
||||
|
|
@ -15,6 +16,7 @@ import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
|||
import 'package:mangayomi/views/more/settings/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:flutter_js/flutter_js.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
part 'get_manga_chapter_url.g.dart';
|
||||
|
||||
class GetMangaChapterUrlModel {
|
||||
|
|
@ -31,8 +33,54 @@ Future<GetMangaChapterUrlModel> getMangaChapterUrl(
|
|||
required ModelManga modelManga,
|
||||
required int index,
|
||||
}) async {
|
||||
bool isOk = false;
|
||||
Directory? path;
|
||||
List urll = [];
|
||||
String? baseUrl;
|
||||
String? zjsUrl;
|
||||
zjs() async {
|
||||
final html = await cloudflareBypassHtml(
|
||||
url: zjsUrl!, source: modelManga.source!.toLowerCase());
|
||||
dom.Document htmll = dom.Document.html(baseUrl!);
|
||||
final strings = html
|
||||
.replaceAll(RegExp(r'\\[(.*?)\\]'), '')
|
||||
.split(",")
|
||||
.map((s) => s.trim().replaceAll("'", "").split('').reversed.join());
|
||||
final stringLookupTables = strings
|
||||
.where((s) =>
|
||||
s.length == 62 &&
|
||||
s.split('').toSet().toList().sorted().join() ==
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||
.toList();
|
||||
|
||||
if (stringLookupTables.length != 2) {
|
||||
throw Exception("Expected only two lookup tables in ZJS");
|
||||
}
|
||||
|
||||
final scrambledData =
|
||||
htmll.getElementById("data")!.attributes['data-data']!;
|
||||
|
||||
for (var i = 0; i <= 1; i++) {
|
||||
final otherIndex = i == 0 ? 1 : 0;
|
||||
|
||||
final lookupTable = Map.fromIterables(stringLookupTables[i].split(''),
|
||||
stringLookupTables[otherIndex].split(''));
|
||||
try {
|
||||
final unscrambledData = scrambledData
|
||||
.split('')
|
||||
.map((char) => lookupTable[char] ?? char)
|
||||
.join();
|
||||
final decoded = utf8.decode(base64.decode(unscrambledData));
|
||||
final data = jsonDecode(decoded);
|
||||
urll = data["imagesLink"].map((it) => it).toList();
|
||||
ref.watch(hiveBoxMangaInfo).put(
|
||||
"${modelManga.lang}-${modelManga.source}/${modelManga.name}/${modelManga.chapters![index].name}-pageurl",
|
||||
urll);
|
||||
} catch (e) {}
|
||||
}
|
||||
isOk = true;
|
||||
}
|
||||
|
||||
List<bool> isLocaleList = [];
|
||||
String source = modelManga.source!.toLowerCase();
|
||||
List pagesUrl = ref.watch(hiveBoxMangaInfo).get(
|
||||
|
|
@ -80,21 +128,21 @@ Future<GetMangaChapterUrlModel> getMangaChapterUrl(
|
|||
/**************/
|
||||
|
||||
else if (getWpMangTypeSource(source) == TypeSource.mangathemesia) {
|
||||
final htmll =
|
||||
await httpResToDom(url: modelManga.chapters![index].url!, headers: {});
|
||||
|
||||
if (htmll.querySelectorAll('#readerarea').isNotEmpty) {
|
||||
final ta = htmll
|
||||
.querySelectorAll('#readerarea')
|
||||
.map((e) => e.outerHtml)
|
||||
.toList();
|
||||
final dom = await cloudflareBypassDom(
|
||||
url: modelManga.chapters![index].url!,
|
||||
bypass: true,
|
||||
source: source,
|
||||
);
|
||||
if (dom!.querySelectorAll('#readerarea').isNotEmpty) {
|
||||
final ta =
|
||||
dom.querySelectorAll('#readerarea').map((e) => e.outerHtml).toList();
|
||||
final RegExp regex = RegExp(r'<img[^>]+src="([^"]+)"');
|
||||
final Iterable<Match> matches = regex.allMatches(ta.first);
|
||||
|
||||
final List<String?> urls = matches.map((m) => m.group(1)).toList();
|
||||
Iterable<Match> matchess = [];
|
||||
if (htmll.querySelectorAll(' #select-paged ').isNotEmpty) {
|
||||
final ee = htmll
|
||||
if (dom.querySelectorAll(' #select-paged ').isNotEmpty) {
|
||||
final ee = dom
|
||||
.querySelectorAll(' #select-paged ')
|
||||
.map((e) => e.outerHtml)
|
||||
.toList();
|
||||
|
|
@ -133,7 +181,7 @@ Future<GetMangaChapterUrlModel> getMangaChapterUrl(
|
|||
/*mangakawaii*/
|
||||
/***********/
|
||||
|
||||
else if (modelManga.source == 'mangakawaii') {
|
||||
else if (source == 'mangakawaii') {
|
||||
final response =
|
||||
await http.get(Uri.parse(modelManga.chapters![index].url!));
|
||||
var chapterSlug = RegExp("""var chapter_slug = "([^"]*)";""")
|
||||
|
|
@ -311,6 +359,23 @@ Future<GetMangaChapterUrlModel> getMangaChapterUrl(
|
|||
urll);
|
||||
}
|
||||
}
|
||||
} else if (source == 'japscan') {
|
||||
final html = await cloudflareBypassHtml(
|
||||
url: modelManga.chapters![index].url!,
|
||||
source: modelManga.source!.toLowerCase());
|
||||
RegExp regex = RegExp(r'<script src="/zjs/(.*?)"');
|
||||
Match? match = regex.firstMatch(html);
|
||||
String zjsurl = match!.group(1)!;
|
||||
baseUrl = html;
|
||||
zjsUrl = "https://www.japscan.lol/zjs/$zjsurl";
|
||||
zjs();
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (isOk == true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (urll.isNotEmpty) {
|
||||
for (var i = 0; i < urll.length; i++) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:mangayomi/models/comick/manga_chapter_detail.dart';
|
||||
import 'package:mangayomi/models/comick/manga_detail_comick.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/services/cloudflare/cloudflare_bypass.dart';
|
||||
import 'package:mangayomi/services/get_popular_manga.dart';
|
||||
import 'package:mangayomi/services/http_res_to_dom_html.dart';
|
||||
import 'package:mangayomi/source/source_model.dart';
|
||||
|
|
@ -407,8 +408,12 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
/***********/
|
||||
|
||||
else if (getWpMangTypeSource(source.toLowerCase()) == TypeSource.mmrcms) {
|
||||
final dom = await httpResToDom(url: url, headers: {});
|
||||
description = dom
|
||||
final dom = await cloudflareBypassDom(
|
||||
url: url,
|
||||
bypass: true,
|
||||
source: source,
|
||||
);
|
||||
description = dom!
|
||||
.querySelectorAll('.row .well p')
|
||||
.map((e) => e.text.trim())
|
||||
.toList()
|
||||
|
|
@ -590,6 +595,89 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
genre.add(ok);
|
||||
}
|
||||
}
|
||||
} else if (source.toLowerCase() == "japscan") {
|
||||
final htmll = await cloudflareBypassDom(
|
||||
url: url,
|
||||
bypass: true,
|
||||
source: source,
|
||||
);
|
||||
if (htmll!.querySelectorAll('.col-7 > p').isNotEmpty) {
|
||||
final images =
|
||||
htmll.querySelectorAll('.col-5 ').map((e) => e.outerHtml).toList();
|
||||
RegExp exp = RegExp(r'src="([^"]+)"');
|
||||
|
||||
String? srcValue = exp.firstMatch(images[0])?.group(1);
|
||||
imageUrl = 'https://www.japscan.me$srcValue';
|
||||
|
||||
if (htmll.querySelectorAll('.col-7 > p').isNotEmpty) {
|
||||
final stat = htmll
|
||||
.querySelectorAll('.col-7 > p')
|
||||
.where((element) => element.innerHtml.contains('Statut:'))
|
||||
.map((e) => e.text)
|
||||
.toList();
|
||||
if (stat.isNotEmpty) {
|
||||
status = stat[0].replaceAll('Statut:', '').trim();
|
||||
}
|
||||
|
||||
final auth = htmll
|
||||
.querySelectorAll('.col-7 > p')
|
||||
.where((element) => element.innerHtml.contains('Auteur(s):'))
|
||||
.map((e) => e.text)
|
||||
.toList();
|
||||
if (auth.isNotEmpty) {
|
||||
author = auth[0].replaceAll('Auteur(s):', '').trim();
|
||||
}
|
||||
} else {
|
||||
author = "";
|
||||
status = "";
|
||||
}
|
||||
|
||||
final genres = htmll
|
||||
.querySelectorAll('.col-7 > p')
|
||||
.where((element) => element.innerHtml.contains('Genre(s):'))
|
||||
.map((e) => e.text.replaceAll('Genre(s):', '').trim())
|
||||
.toList();
|
||||
if (genres.isNotEmpty) {
|
||||
for (var ok in genres[0].split(',')) {
|
||||
genre.add(ok);
|
||||
}
|
||||
}
|
||||
|
||||
final synop = htmll
|
||||
.querySelectorAll('p.list-group-item ')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
if (synop.isNotEmpty) {
|
||||
description = synop[0];
|
||||
}
|
||||
}
|
||||
|
||||
final urls =
|
||||
htmll.querySelectorAll('.col-8 ').map((e) => e.outerHtml).toList();
|
||||
|
||||
for (var ok in urls) {
|
||||
RegExp exp = RegExp(r'href="([^"]+)"');
|
||||
|
||||
String? srcValue = exp.firstMatch(ok)?.group(1);
|
||||
chapterUrl.add('https://www.japscan.me$srcValue');
|
||||
}
|
||||
|
||||
final chapterTitlee =
|
||||
htmll.querySelectorAll('.col-8').map((e) => e.text.trim()).toList();
|
||||
|
||||
if (chapterTitlee.isNotEmpty) {
|
||||
for (var ok in chapterTitlee) {
|
||||
chapterTitle.add(ok);
|
||||
}
|
||||
}
|
||||
|
||||
final chapterDatee =
|
||||
htmll.querySelectorAll('.col-4').map((e) => e.text.trim()).toList();
|
||||
if (chapterDatee.isNotEmpty) {
|
||||
for (var ok in chapterDatee) {
|
||||
chapterDate.add(ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chapterDate.isNotEmpty &&
|
||||
chapterTitle.isNotEmpty &&
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mangayomi/models/comick/popular_manga_comick.dart';
|
||||
import 'package:mangayomi/services/cloudflare/cloudflare_bypass.dart';
|
||||
import 'package:mangayomi/services/http_res_to_dom_html.dart';
|
||||
import 'package:mangayomi/source/source_list.dart';
|
||||
import 'package:mangayomi/source/source_model.dart';
|
||||
|
|
@ -85,11 +86,12 @@ Future<GetMangaModel> getPopularManga(GetPopularMangaRef ref,
|
|||
/*mangathemesia*/
|
||||
/**************/
|
||||
if (getWpMangTypeSource(source) == TypeSource.mangathemesia) {
|
||||
final dom = await httpResToDom(
|
||||
url: '${getWpMangaUrl(source)}/manga/?title=&page=$page&order=popular',
|
||||
headers: {});
|
||||
|
||||
if (dom
|
||||
final dom = await cloudflareBypassDom(
|
||||
url: '${getWpMangaUrl(source)}/manga/?title=&page=$page&order=popular',
|
||||
bypass: true,
|
||||
source: source,
|
||||
);
|
||||
if (dom!
|
||||
.querySelectorAll(
|
||||
'.utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx')
|
||||
.isNotEmpty) {
|
||||
|
|
@ -206,6 +208,30 @@ Future<GetMangaModel> getPopularManga(GetPopularMangaRef ref,
|
|||
.map((e) => e.attributes['title'])
|
||||
.toList();
|
||||
}
|
||||
} else if (source == "japscan") {
|
||||
final htmll = await cloudflareBypassDom(
|
||||
url: "https://www.japscan.lol/",
|
||||
bypass: true,
|
||||
source: source,
|
||||
);
|
||||
if (htmll!.querySelectorAll('#top_mangas_week > ul > li ').isNotEmpty) {
|
||||
final urls = htmll
|
||||
.querySelectorAll('#top_mangas_week > ul > li > a')
|
||||
.where((e) => e.attributes['href'].toString().contains('manga'))
|
||||
.map((e) => e.attributes['href'])
|
||||
.toList();
|
||||
for (var ok in urls) {
|
||||
url.add("https://www.japscan.me$ok");
|
||||
}
|
||||
name = htmll
|
||||
.querySelectorAll(
|
||||
'#top_mangas_week > ul > li > a.text-dark.font-weight-bold')
|
||||
.map((e) => e.innerHtml)
|
||||
.toList();
|
||||
for (var ia in name) {
|
||||
image.add("");
|
||||
}
|
||||
}
|
||||
}
|
||||
return GetMangaModel(
|
||||
name: name,
|
||||
|
|
|
|||
155
lib/services/webview.dart
Normal file
155
lib/services/webview.dart
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/providers/hive_provider.dart';
|
||||
import 'package:mangayomi/services/cloudflare/cookie.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MangaWebView extends ConsumerStatefulWidget {
|
||||
final String url;
|
||||
final String source;
|
||||
const MangaWebView({super.key, required this.url, required this.source});
|
||||
|
||||
@override
|
||||
ConsumerState<MangaWebView> createState() => _MangaWebViewState();
|
||||
}
|
||||
|
||||
class _MangaWebViewState extends ConsumerState<MangaWebView> {
|
||||
final GlobalKey webViewKey = GlobalKey();
|
||||
|
||||
double progress = 0;
|
||||
|
||||
InAppWebViewController? webViewController;
|
||||
String _url = "";
|
||||
@override
|
||||
void initState() {
|
||||
setState(() {
|
||||
_url = widget.url;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
title: Text(
|
||||
_url,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
webViewController?.goBack();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
onPressed: () {
|
||||
webViewController?.goForward();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
webViewController?.reload();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await InAppBrowser.openWithSystemBrowser(
|
||||
url: WebUri.uri(Uri.parse(_url)));
|
||||
},
|
||||
icon: const Icon(Icons.read_more))
|
||||
],
|
||||
),
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
webViewController?.goBack();
|
||||
return false;
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
progress < 1.0
|
||||
? LinearProgressIndicator(value: progress)
|
||||
: Container(),
|
||||
Flexible(
|
||||
child: InAppWebView(
|
||||
key: webViewKey,
|
||||
onWebViewCreated: (controller) async {
|
||||
webViewController = controller;
|
||||
},
|
||||
onLoadStart: (controller, url) async {
|
||||
setState(() {
|
||||
_url = url.toString();
|
||||
});
|
||||
},
|
||||
onPermissionRequest: (controller, request) async {
|
||||
return PermissionResponse(
|
||||
resources: request.resources,
|
||||
action: PermissionResponseAction.GRANT);
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
var uri = navigationAction.request.url!;
|
||||
|
||||
if (![
|
||||
"http",
|
||||
"https",
|
||||
"file",
|
||||
"chrome",
|
||||
"data",
|
||||
"javascript",
|
||||
"about"
|
||||
].contains(uri.scheme)) {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
// Launch the App
|
||||
await launchUrl(
|
||||
uri,
|
||||
);
|
||||
// and cancel the request
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
}
|
||||
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
onLoadStop: (controller, url) async {
|
||||
setState(() {
|
||||
_url = url.toString();
|
||||
});
|
||||
},
|
||||
onReceivedError: (controller, request, error) {},
|
||||
onProgressChanged: (controller, progress) async {
|
||||
setState(() {
|
||||
this.progress = progress / 100;
|
||||
});
|
||||
},
|
||||
onUpdateVisitedHistory: (controller, url, isReload) async {
|
||||
await setCookie(widget.source, url.toString());
|
||||
setState(() {
|
||||
_url = url.toString();
|
||||
});
|
||||
},
|
||||
initialSettings: InAppWebViewSettings(
|
||||
userAgent: Hive.box(HiveConstant.hiveBoxAppSettings).get(
|
||||
"ua",
|
||||
defaultValue:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")),
|
||||
initialUrlRequest:
|
||||
URLRequest(url: WebUri.uri(Uri.parse(widget.url))),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -421,10 +421,11 @@ List<SourceModel> sourcesList = [
|
|||
lang: "fr",
|
||||
typeSource: TypeSource.mmrcms,
|
||||
logoUrl: ''),
|
||||
// SourceModel(
|
||||
// sourceName: "FR Scan",
|
||||
// url: "https://frscan.ws",
|
||||
// lang: "fr",
|
||||
// typeSource: TypeSource.mmrcms,
|
||||
// logoUrl: ''),
|
||||
SourceModel(
|
||||
sourceName: "Japscan",
|
||||
url: "https://japscan.lol",
|
||||
lang: "fr",
|
||||
typeSource: TypeSource.single,
|
||||
logoUrl: ''),
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import 'dart:developer';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
|
||||
Map<String, String> headers(String source) {
|
||||
source = source.toLowerCase();
|
||||
log(source);
|
||||
final cookie = Hive.box(HiveConstant.hiveBoxAppSettings)
|
||||
.get("$source-cookie", defaultValue: "");
|
||||
final userAgent = Hive.box(HiveConstant.hiveBoxAppSettings).get("ua",
|
||||
defaultValue:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0");
|
||||
return source == 'mangakawaii'
|
||||
? {
|
||||
'Referer': 'https://www.mangakawaii.io/',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/8\$userAgentRandomizer1.0.4\$userAgentRandomizer3.1\$userAgentRandomizer2 Safari/537.36',
|
||||
'User-Agent': userAgent,
|
||||
'Accept-Language': 'fr'
|
||||
}
|
||||
: source == 'mangahere'
|
||||
|
|
@ -15,8 +19,19 @@ Map<String, String> headers(String source) {
|
|||
: source == 'comick'
|
||||
? {
|
||||
'Referer': 'https://comick.app/',
|
||||
'User-Agent':
|
||||
'Tachiyomi Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/8\\\$userAgentRandomizer1.0.4\\\$userAgentRandomizer3.1\\\$userAgentRandomizer2 Safari/537.36'
|
||||
'User-Agent': 'Tachiyomi $userAgent'
|
||||
}
|
||||
: {};
|
||||
: source == "japscan"
|
||||
? {
|
||||
'User-Agent': userAgent,
|
||||
'Referer': "https://www.japscan.lol/",
|
||||
"Cookie": cookie
|
||||
}
|
||||
: source == 'sushiscan'
|
||||
? {
|
||||
'User-Agent': userAgent,
|
||||
'Referer': "https://www.sushscan.net/",
|
||||
"Cookie": cookie
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||
import 'package:mangayomi/models/manga_type.dart';
|
||||
import 'package:mangayomi/providers/hive_provider.dart';
|
||||
import 'package:mangayomi/source/source_model.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/utils/lang.dart';
|
||||
import 'package:mangayomi/views/browse/extension/refresh_filter_data.dart';
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ class SourcesScreen extends ConsumerWidget {
|
|||
child: element.logoUrl.isEmpty
|
||||
? const Icon(Icons.source_outlined)
|
||||
: CachedNetworkImage(
|
||||
httpHeaders: headers(element.sourceName),
|
||||
imageUrl: element.logoUrl,
|
||||
fit: BoxFit.contain,
|
||||
width: 37,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/providers/hive_provider.dart';
|
||||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
|
|
@ -653,7 +654,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
Map<String, String> data = {
|
||||
'url': widget.modelManga!.link!,
|
||||
'source': widget.modelManga!.source!,
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mangayomi/models/manga_type.dart';
|
||||
import 'package:mangayomi/services/get_manga_detail.dart';
|
||||
import 'package:mangayomi/services/get_popular_manga.dart';
|
||||
import 'package:mangayomi/utils/colors.dart';
|
||||
import 'package:mangayomi/views/manga/home/manga_search_screen.dart';
|
||||
import 'package:mangayomi/views/widgets/bottom_text_widget.dart';
|
||||
import 'package:mangayomi/views/widgets/cover_view_widget.dart';
|
||||
|
|
@ -32,7 +34,21 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
|||
title: Text('${widget.mangaType.source}'),
|
||||
actions: [
|
||||
MangaSearchButton(
|
||||
source: widget.mangaType.source!, lang: widget.mangaType.lang!)
|
||||
source: widget.mangaType.source!, lang: widget.mangaType.lang!),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Map<String, String> data = {
|
||||
'url': getWpMangaUrl(widget.mangaType.source!),
|
||||
'source': widget.mangaType.source!,
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.public,
|
||||
size: 22,
|
||||
color: secondaryColor(context),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: getManga.when(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
||||
|
||||
|
|
@ -44,13 +46,13 @@ class ImageViewHorizontal extends StatelessWidget {
|
|||
onDoubleTap: onDoubleTap,
|
||||
loadStateChanged: loadStateChanged,
|
||||
)
|
||||
: ExtendedImage.network(
|
||||
url,
|
||||
cache: true,
|
||||
: ExtendedImage(
|
||||
image: CachedNetworkImageProvider(url,
|
||||
cacheManager: CacheManager(
|
||||
Config(url, stalePeriod: const Duration(days: 7))),
|
||||
headers: headers(source)),
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
enableMemoryCache: false,
|
||||
cacheMaxAge: const Duration(days: 7),
|
||||
headers: headers(source),
|
||||
mode: ExtendedImageMode.gesture,
|
||||
initGestureConfigHandler: initGestureConfigHandler,
|
||||
onDoubleTap: onDoubleTap,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/utils/media_query.dart';
|
||||
|
|
@ -46,62 +47,64 @@ class ImageViewVertical extends StatelessWidget {
|
|||
clearMemoryCacheWhenDispose: true,
|
||||
enableMemoryCache: false,
|
||||
File('${path.path}${padIndex(index + 1)}.jpg'))
|
||||
: ExtendedImage.network(url,
|
||||
headers: headers(source),
|
||||
: ExtendedImage(
|
||||
image: CachedNetworkImageProvider(url,
|
||||
cacheManager: CacheManager(
|
||||
Config(url, stalePeriod: const Duration(days: 7))),
|
||||
headers: headers(source)),
|
||||
handleLoadingProgress: true,
|
||||
fit: BoxFit.contain,
|
||||
cacheMaxAge: const Duration(days: 7),
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
enableMemoryCache: false,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0;
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 0,
|
||||
end: progress,
|
||||
),
|
||||
builder: (context, value, _) => Container(
|
||||
color: Colors.black,
|
||||
height: mediaHeight(context, 0.8),
|
||||
child: Center(
|
||||
child: progress == 0
|
||||
? const CircularProgressIndicator()
|
||||
: CircularProgressIndicator(
|
||||
value: progress,
|
||||
),
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0;
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 0,
|
||||
end: progress,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state.extendedImageLoadState == LoadState.failed) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: mediaHeight(context, 0.8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
state.reLoadImage();
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.replay_outlined,
|
||||
size: 30,
|
||||
)),
|
||||
],
|
||||
));
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
builder: (context, value, _) => Container(
|
||||
color: Colors.black,
|
||||
height: mediaHeight(context, 0.8),
|
||||
child: Center(
|
||||
child: progress == 0
|
||||
? const CircularProgressIndicator()
|
||||
: CircularProgressIndicator(
|
||||
value: progress,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state.extendedImageLoadState == LoadState.failed) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: mediaHeight(context, 0.8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
state.reLoadImage();
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.replay_outlined,
|
||||
size: 30,
|
||||
)),
|
||||
],
|
||||
));
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
if (index + 1 == length)
|
||||
SizedBox(
|
||||
height: mediaHeight(context, 0.3),
|
||||
|
|
@ -132,7 +135,4 @@ class ImageViewVertical extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import flutter_inappwebview
|
||||
import flutter_js
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
|
|
@ -12,6 +13,7 @@ import sqflite
|
|||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
|
|
|
|||
18
pubspec.lock
18
pubspec.lock
|
|
@ -360,13 +360,29 @@ packages:
|
|||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
sha256: "6d6c741ddba1dba5229d63ba75767064791a7ce845196b45e31105e93d67c949"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0-beta.22"
|
||||
flutter_inappwebview_internal_annotations:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_internal_annotations
|
||||
sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_js:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/kodjodevf/background_downloader.git
|
||||
permission_handler: ^10.2.0
|
||||
flutter_inappwebview: ^6.0.0-beta.22
|
||||
flutter_cache_manager: ^3.3.0
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
|
|
|||
Loading…
Reference in a new issue