diff --git a/lib/foundation/def.dart b/lib/foundation/def.dart index 90b72dc6..9fb50dc4 100644 --- a/lib/foundation/def.dart +++ b/lib/foundation/def.dart @@ -16,7 +16,7 @@ const String webUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"; //App版本 -const appVersion = "2.3.0"; +const appVersion = "2.3.0-hotfix"; //定义宽屏设备的临界值 const changePoint = 600; diff --git a/lib/foundation/image_manager.dart b/lib/foundation/image_manager.dart index bdc795b2..3c9636a3 100644 --- a/lib/foundation/image_manager.dart +++ b/lib/foundation/image_manager.dart @@ -7,6 +7,7 @@ import 'package:html/parser.dart'; import 'package:path_provider/path_provider.dart'; import 'package:dio/dio.dart'; import 'package:pica_comic/foundation/log.dart'; +import 'package:pica_comic/network/app_dio.dart'; import 'package:pica_comic/network/eh_network/eh_models.dart'; import 'package:pica_comic/network/eh_network/get_gallery_id.dart'; import 'package:pica_comic/network/hitomi_network/hitomi_models.dart'; @@ -116,7 +117,7 @@ class ImageManager { } /// 获取图片, 适用于没有任何限制的图片链接 - Stream getImage(String url) async* { + Stream getImage(String url, [Map? headers]) async* { while (loadingItems[url] != null) { var progress = loadingItems[url]!; yield progress; @@ -141,7 +142,7 @@ class ImageManager { sendTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 8), followRedirects: true, - headers: { + headers: headers ?? { "user-agent": webUA, }); @@ -154,7 +155,7 @@ class ImageManager { final savePath = "${(await getTemporaryDirectory()).path}${pathSep}imageCache$pathSep$fileName"; yield DownloadProgress(0, 100, url, savePath); - var dio = Dio(options); + var dio = logDio(options); if (url.contains("nhentai")) { dio.interceptors.add(CookieManager(NhentaiNetwork().cookieJar!)); } @@ -225,7 +226,7 @@ class ImageManager { followRedirects: true, headers: {"user-agent": webUA, "cookie": EhNetwork().cookiesStr}); - var dio = Dio(options); + var dio = logDio(options); // Get imgKey const urlsOnePage = 40; @@ -276,12 +277,24 @@ class ImageManager { "showkey": gallery.auth!["showKey"] }); - var image = const JsonDecoder().convert(apiRes.data)["i3"] as String; + var apiJson = const JsonDecoder().convert(apiRes.data); + + var i6 = apiJson["i6"] as String; + + var originImage = i6.split("").first; + + var image = apiJson["i3"] as String; + image = image.substring( + image.indexOf("src=\"") + 5, image.indexOf("\" style") - 1); + if (image.contains("/img/509.gif")) { throw ImageExceedError(); } - image = image.substring( - image.indexOf("src=\"") + 5, image.indexOf("\" style") - 1); + + if(appdata.settings[29] == "1"){ + image = originImage; + } + var res = await dio.get(image, options: Options(responseType: ResponseType.stream)); @@ -364,7 +377,7 @@ class ImageManager { var fileName = image.hash + url.substring(l); final savePath = "${(await getTemporaryDirectory()).path}${pathSep}imageCache$pathSep$fileName"; - var dio = Dio(); + var dio = logDio(); dio.options.headers = { "User-Agent": webUA, "Referer": "https://hitomi.la/reader/$galleryId.html" @@ -461,7 +474,7 @@ class ImageManager { final savePath = "${(await getTemporaryDirectory()).path}${pathSep}imageCache$pathSep$fileName"; - var dio = Dio(); + var dio = logDio(); yield DownloadProgress(0, 1, url, savePath); var bytes = []; diff --git a/lib/foundation/local_favorites.dart b/lib/foundation/local_favorites.dart index fe939b1e..85c842eb 100644 --- a/lib/foundation/local_favorites.dart +++ b/lib/foundation/local_favorites.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; @@ -7,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:pica_comic/base.dart'; import 'package:pica_comic/foundation/app.dart'; import 'package:pica_comic/foundation/log.dart'; +import 'package:pica_comic/network/app_dio.dart'; import 'package:pica_comic/network/eh_network/eh_main_network.dart'; import 'package:pica_comic/network/eh_network/eh_models.dart'; import 'package:pica_comic/network/eh_network/get_gallery_id.dart'; @@ -193,7 +195,7 @@ class LocalFavoritesManager { await clearAll(); for (var folder in allComics.keys) { - createFolder(folder); + createFolder(folder, true); var comics = allComics[folder]!; for (int i = 0; i < comics.length; i++) { addComic(folder, comics[i]); @@ -238,12 +240,28 @@ class LocalFavoritesManager { } /// create a folder - void createFolder(String name) { + void createFolder(String name, [bool renameWhenInvalidName = false]) { if(name.isEmpty){ - throw "name is empty!"; + if(renameWhenInvalidName) { + int i = 0; + while (folderNames.contains(i.toString())) { + i++; + } + name = i.toString(); + } else { + throw "name is empty!"; + } } if (folderNames.contains(name)) { - throw Exception("Folder is existing"); + if(renameWhenInvalidName) { + int i = 0; + while (folderNames.contains(i.toString())) { + i++; + } + name = i.toString(); + } else { + throw Exception("Folder is existing"); + } } _db.execute(""" create table "$name"( @@ -319,16 +337,25 @@ class LocalFavoritesManager { } } + int _loading = 0; + /// get comic cover Future getCover(FavoriteItem item) async { - var path = "${appdataPath!}${pathSep}favoritesCover"; + var path = "${appdataPath!}/favoritesCover"; var hash = - md5.convert(const Utf8Encoder().convert(item.coverPath)).toString(); - var file = File("$path$pathSep$hash.jpg"); + md5.convert(const Utf8Encoder().convert(item.coverPath)).toString(); + var file = File("$path/$hash.jpg"); if (file.existsSync()) { return file; - } else { - var dio = Dio(BaseOptions(headers: { + } + if(item.type == ComicType.ehentai) { + while (_loading > 2) { + await Future.delayed(const Duration(milliseconds: 200)); + } + _loading++; + } + try { + var dio = logDio(BaseOptions(headers: { if (item.type == ComicType.ehentai) "cookie": EhNetwork().cookiesStr, if (item.type == ComicType.ehentai || item.type == ComicType.hitomi) "User-Agent": webUA, @@ -337,8 +364,19 @@ class LocalFavoritesManager { var res = await dio.get(item.coverPath); file.createSync(recursive: true); file.writeAsBytesSync(res.data!); + var awaitTime = Random().nextInt(500) + 500; + await Future.delayed(Duration(milliseconds: awaitTime)); return file; } + catch(e){ + await Future.delayed(const Duration(seconds: 5)); + rethrow; + } + finally{ + if(item.type == ComicType.ehentai) { + _loading--; + } + } } /// delete a folder diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 9ba08503..788a9902 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -34,7 +34,17 @@ class MyLogInterceptor implements Interceptor { } class AppHttpAdapter implements HttpClientAdapter{ - final adapter = IOHttpClientAdapter(); + final HttpClientAdapter adapter; + + AppHttpAdapter(bool http2): + adapter = http2 ? Http2Adapter(ConnectionManager( + idleTimeout: const Duration(seconds: 10), + onClientCreate: (_, config) { + if (proxyHttpOverrides?.proxyStr != null) { + config.proxy = Uri.parse('http://${proxyHttpOverrides?.proxyStr}'); + } + }, + ),) : IOHttpClientAdapter(); @override void close({bool force = false}) { @@ -63,6 +73,20 @@ class AppHttpAdapter implements HttpClientAdapter{ } } + void changeHost(RequestOptions options){ + var config = const JsonDecoder().convert(File("${App.dataPath}/hosts.json").readAsStringSync()); + if(config["https"][options.uri.host] != null){ + LogManager.addLog(LogLevel.info, "Network", + "Change host from ${options.uri.host} to ${config["https"][options.uri.host]}"); + options.path = options.path.replaceFirst(options.uri.host, config["https"][options.uri.host]!); + } else if(config["http"][options.uri.host] != null){ + LogManager.addLog(LogLevel.info, "Network", + "Change host from ${options.uri.host} to ${config["http"][options.uri.host]}"); + options.path = options.path.replaceFirst(options.uri.host, config["http"][options.uri.host]!); + options.path = options.path.replaceFirst("https://", "http://"); + } + } + @override Future fetch(RequestOptions o, Stream? requestStream, Future? cancelFuture) async{ var options = o.copyWith(); @@ -72,28 +96,34 @@ class AppHttpAdapter implements HttpClientAdapter{ return await adapter.fetch(options, requestStream, cancelFuture); } createConfigFile(); - var config = const JsonDecoder().convert(File("${App.dataPath}/hosts.json").readAsStringSync()); if(options.headers["host"] == null && options.headers["Host"] == null){ options.headers["host"] = options.uri.host; } - if(config["https"][options.uri.host] != null){ - LogManager.addLog(LogLevel.info, "Network", - "Change host from ${options.uri.host} to ${config["https"][options.uri.host]}"); - options.path = options.path.replaceFirst(options.uri.host, config["https"][options.uri.host]!); - } else if(config["http"][options.uri.host] != null){ - LogManager.addLog(LogLevel.info, "Network", - "Change host from ${options.uri.host} to ${config["http"][options.uri.host]}"); - options.path = options.path.replaceFirst(options.uri.host, config["http"][options.uri.host]!); - options.path = options.path.replaceFirst("https://", "http://"); - } + changeHost(options); options.followRedirects = false; var res = await adapter.fetch(options, requestStream, cancelFuture); - if(res.statusCode == 302){ + while(res.statusCode == 302){ var location = res.headers["location"]!.first; - options.path = options.path.contains("https://") - ? "https://${options.uri.host}$location" - : "http://${options.uri.host}$location"; - return await adapter.fetch(options, requestStream, cancelFuture); + if(location.contains("http") && Uri.tryParse(location) != null){ + if(Uri.parse(location).host != o.uri.host){ + options.path = location; + changeHost(options); + res = await adapter.fetch(options, requestStream, cancelFuture); + } else { + location = Uri + .parse(location) + .path; + options.path = options.path.contains("https://") + ? "https://${options.uri.host}$location" + : "http://${options.uri.host}$location"; + res = await adapter.fetch(options, requestStream, cancelFuture); + } + } else { + options.path = options.path.contains("https://") + ? "https://${options.uri.host}$location" + : "http://${options.uri.host}$location"; + res = await adapter.fetch(options, requestStream, cancelFuture); + } } return res; } @@ -102,20 +132,7 @@ class AppHttpAdapter implements HttpClientAdapter{ Dio logDio([BaseOptions? options, bool http2 = false]) { var dio = Dio(options)..interceptors.add(MyLogInterceptor()); - if (http2) { - dio.httpClientAdapter = Http2Adapter( - ConnectionManager( - idleTimeout: const Duration(seconds: 10), - onClientCreate: (_, config) { - if (proxyHttpOverrides?.proxyStr != null) { - config.proxy = Uri.parse('http://${proxyHttpOverrides?.proxyStr}'); - } - }, - ), - ); - } else { - dio.httpClientAdapter = AppHttpAdapter(); - } + dio.httpClientAdapter = AppHttpAdapter(http2); return dio; } diff --git a/lib/network/download.dart b/lib/network/download.dart index cb6345c9..66965c0c 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -381,7 +381,7 @@ class DownloadManager{ comic = DownloadedHitomiComic.fromMap(jsonDecode(json)); } else if (id.startsWith("nhentai")) { comic = NhentaiDownloadedComic.fromJson(jsonDecode(json)); - } else if (id.startsWith("ht")) { + } else if (id.startsWith("Ht")) { comic = DownloadedHtComic.fromJson(jsonDecode(json)); } else if (id.isNum) { comic = DownloadedGallery.fromJson(jsonDecode(json)); diff --git a/lib/network/eh_network/eh_main_network.dart b/lib/network/eh_network/eh_main_network.dart index d26b339d..050d702e 100644 --- a/lib/network/eh_network/eh_main_network.dart +++ b/lib/network/eh_network/eh_main_network.dart @@ -155,7 +155,7 @@ class EhNetwork { ...?headers }); - var dio = Dio(options)..interceptors.add(LogInterceptor()); + var dio = logDio(options); try { var res = await dio.post(ehApiUrl, data: data); @@ -548,13 +548,20 @@ class EhNetwork { } } + Set loadingReaderLinks = {}; + /// page starts from 1 Future>> getReaderLinks(String link, int page) async { String url = "$link?inline_set=ts_m"; + while(loadingReaderLinks.contains(url)){ + await Future.delayed(const Duration(milliseconds: 200)); + } + loadingReaderLinks.add(url); if (page != 1) { url = "$url&p=${page - 1}"; } var res = await request(url); + loadingReaderLinks.remove(url); if (res.error) { return Res(null, errorMessage: res.errorMessage); } diff --git a/lib/network/picacg_network/methods.dart b/lib/network/picacg_network/methods.dart index 5c2bd687..4a7e985c 100644 --- a/lib/network/picacg_network/methods.dart +++ b/lib/network/picacg_network/methods.dart @@ -81,9 +81,7 @@ class PicacgNetwork { Future>> post( String url, Map? data) async { - var api = appdata.settings[3] == "1" - ? "$serverDomain/picaapi" - : "https://picaapi.picacomic.com"; + var api = "https://picaapi.picacomic.com"; if (token == "" && url != '$api/auth/sign-in' && url != "https://picaapi.picacomic.com/auth/register") { @@ -142,9 +140,10 @@ class PicacgNetwork { ///登录 Future> login(String email, String password) async { - var api = appdata.settings[3] == "1" - ? "$serverDomain/picaapi" - : "https://picaapi.picacomic.com"; + if(token.isNotEmpty){ + token = ""; + } + var api = "https://picaapi.picacomic.com"; var response = await post('$api/auth/sign-in', { "email": email, "password": password, @@ -157,7 +156,7 @@ class PicacgNetwork { try { token = res["data"]["token"]; } catch (e) { - return const Res(null, errorMessage: "Failed to get token"); + return const Res(null, errorMessage: "Failed to get token\n"); } if (kDebugMode) { print("Logging successfully"); diff --git a/lib/views/eh_views/cover_image.dart b/lib/views/eh_views/cover_image.dart new file mode 100644 index 00000000..f5d19b2a --- /dev/null +++ b/lib/views/eh_views/cover_image.dart @@ -0,0 +1,106 @@ +import 'dart:math'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:pica_comic/foundation/image_manager.dart'; + +class EhCoverImage extends StatefulWidget { + const EhCoverImage({required this.url, this.headers = const {}, super.key}); + + final String url; + + final Map headers; + + @override + State createState() => _EhCoverImageState(); +} + +class _EhCoverImageState extends State { + bool error = false; + + static Map cache = {}; + + Uint8List? get data => cache[widget.url]; + + set data(Uint8List? list){ + if(list != null){ + int totalSize = 0; + cache.forEach((key, value) => totalSize += value.length); + while(totalSize > 10 * 1024 * 1024){ + cache.remove(cache.keys.first); + } + cache[widget.url] = list; + } + } + + @override + void didChangeDependencies() { + if(error){ + setState(() { + error = false; + }); + load(); + } + super.didChangeDependencies(); + } + + static int loading = 0; + + void load() async{ + while(loading > 1){ + await Future.delayed(const Duration(milliseconds: 200)); + } + loading++; + try { + await for(var progress in ImageManager().getImage(widget.url, widget.headers)){ + if(progress.finished){ + data = progress.getFile().readAsBytesSync(); + if(mounted){ + setState(() {}); + } + } + } + var awaitTime = Random().nextInt(500) + 500; + await Future.delayed(Duration(milliseconds: awaitTime)); + } + catch(e){ + await Future.delayed(const Duration(seconds: 5)); + if(mounted){ + setState(() { + error = true; + }); + } + } + finally{ + loading--; + } + } + + @override + void initState() { + if(data == null) { + load(); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + if(error){ + return const Center( + child: Icon(Icons.error), + ); + } else if(data == null){ + return const Center( + child: SizedBox(width: 0, height: 0,), + ); + } else { + return Image.memory( + data!, + fit: BoxFit.cover, + errorBuilder: (context, url, error) => const Icon(Icons.error), + height: double.infinity, + filterQuality: FilterQuality.medium, + ); + } + } +} diff --git a/lib/views/eh_views/eh_widgets/eh_gallery_tile.dart b/lib/views/eh_views/eh_widgets/eh_gallery_tile.dart index f89f38ba..821a43e1 100644 --- a/lib/views/eh_views/eh_widgets/eh_gallery_tile.dart +++ b/lib/views/eh_views/eh_widgets/eh_gallery_tile.dart @@ -1,9 +1,9 @@ import 'package:pica_comic/foundation/app.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:pica_comic/network/eh_network/eh_main_network.dart'; import 'package:pica_comic/network/eh_network/eh_models.dart'; import 'package:pica_comic/tools/tags_translation.dart'; +import 'package:pica_comic/views/eh_views/cover_image.dart'; import 'package:pica_comic/views/eh_views/eh_gallery_page.dart'; import 'package:pica_comic/views/reader/goto_reader.dart'; import 'package:pica_comic/views/widgets/comic_tile.dart'; @@ -86,32 +86,13 @@ class EhGalleryTile extends ComicTile{ }.call(); @override - Widget get image => cached?CachedNetworkImage( - useOldImageOnUrlChange: true, - imageUrl: gallery.coverPath, - fit: BoxFit.cover, - errorWidget: (context, url, error) => const Icon(Icons.error), - height: double.infinity, - filterQuality: FilterQuality.medium, - placeholder: (context, s) => ColoredBox(color: Theme.of(context).colorScheme.surfaceVariant), - httpHeaders: { - "Cookie": EhNetwork().cookiesStr, - "User-Agent": webUA, - "Referer": EhNetwork().ehBaseUrl, - "host": Uri.parse(gallery.coverPath).host - }, - ):Image.network( - gallery.coverPath, - fit: BoxFit.cover, - errorBuilder: (context, url, error) => const Icon(Icons.error), - height: double.infinity, - frameBuilder: (BuildContext context, Widget child, int? frame, bool? wasSynchronouslyLoaded) { - return ColoredBox(color: Theme.of(context).colorScheme.surfaceVariant, child: child,); - }, + Widget get image => EhCoverImage( + url: gallery.coverPath, headers: { "Cookie": EhNetwork().cookiesStr, "User-Agent": webUA, "Referer": EhNetwork().ehBaseUrl, + "host": Uri.parse(gallery.coverPath).host }, ); diff --git a/pubspec.yaml b/pubspec.yaml index d17b375c..da7c5719 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.3.0+230 +version: 2.3.0-hotfix+230 environment: sdk: '>=3.0.0' @@ -153,9 +153,9 @@ flutter: fonts: # 这是在windows端使用的字体, 由于系统字体(微软雅黑)在Flutter App上显示效果极差, 因此使用此字体 # 仅在打包windows时取消注释 - - family: font - fonts: - - asset: fonts/NotoSansSC-Regular.otf + #- family: font + # fonts: + # - asset: fonts/NotoSansSC-Regular.otf # 这是一个英文字体, 文件很小, 无需担心体积, 用于显示Pica Comic - family: font2