webview & cloudflare bypass

This commit is contained in:
kodjodevf 2023-04-24 13:06:07 +01:00
parent 67fd3d739c
commit aa21f07b13
18 changed files with 656 additions and 94 deletions

View file

@ -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'
}
}

View file

@ -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"]!,
),
);
},
),
];
}

View 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!;
}

View 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);
}
}

View file

@ -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++) {

View file

@ -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 &&

View file

@ -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
View 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))),
),
),
],
),
),
);
}
}

View file

@ -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: ''),
];

View file

@ -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
}
: {};
}

View file

@ -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,

View file

@ -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(

View file

@ -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(

View file

@ -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,

View file

@ -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;
}

View file

@ -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"))

View file

@ -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:

View file

@ -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.