diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 978aebb3..57495485 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -1,31 +1,33 @@ -name: 报告Bug -description: 报告APP运行时出现的问题 +name: 报告Bug/Report a bug +description: 报告APP出现的问题/Reporting problems with the APP title: "[Bug]: " labels: ["bug🐞"] body: - type: markdown attributes: value: | - 感谢报告问题, 请先补全标题后填写以下信息 + 感谢报告问题, 请先补全标题后填写以下信息. + + Thank you for reporting a problem, please complete the title and fill in the following information. - type: textarea id: what-happened attributes: - label: 描述 - description: 描述问题 + label: 描述/Description + description: 描述问题/Describe the problem validations: required: true - type: input id: version attributes: label: Version - description: 使用的APP版本 + description: 使用的APP版本/App version value: "latest" validations: required: true - type: dropdown id: platform attributes: - label: 使用的操作系统 + label: 使用的操作系统/Operating system multiple: true options: - Android @@ -37,10 +39,10 @@ body: - type: textarea id: logs attributes: - label: 日志 + label: 日志/logs description: 上传日志, 在设置-logs中, 点击右上角的菜单后, 点击导出日志; 或者将错误相关日志粘贴到这里 - type: textarea id: screenshotOrVideo attributes: - label: 截图或视频 - description: 在这里上传相关的屏幕截图或者视频 \ No newline at end of file + label: 截图或视频/Screenshot or video + description: 在这里上传相关的屏幕截图或者视频/Upload relevant screenshots or videos here \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 5eb6d314..2460e652 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -1,24 +1,26 @@ -name: 功能建议 -description: 提出改进APP的建议 +name: 功能建议/Feature Request +description: 提出改进APP的建议/Suggest improvements to the APP title: "[Enhancement]: " labels: ["enhancement🚀"] body: - type: markdown attributes: value: | - 欢迎提出功能建议, 请先补全标题后填写以下信息 + 欢迎提出功能建议, 请先补全标题后填写以下信息. + + Welcome to make a feature request, please fill in the following information after completing the title. - type: textarea id: what-happened attributes: - label: 描述 - description: 描述具体的建议 + label: 描述/Description + description: 描述具体的建议/Describe your suggestion. validations: required: true - type: dropdown id: platform attributes: - label: 操作系统 - description: 如果建议针对某个平台, 请在此选择 + label: 操作系统/Operating System + description: 如果建议针对某个平台, 请在此选择/If the feature is for a particular platform, please select here multiple: true options: - Android @@ -30,5 +32,5 @@ body: - type: textarea id: screenshotOrVideo attributes: - label: 图片 - description: 如果需要图片描述, 请在这里上传 \ No newline at end of file + label: 图片/picture + description: 如果需要图片描述, 请在这里上传/If you need a picture description, please upload it here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/other.yaml b/.github/ISSUE_TEMPLATE/other.yaml index 7056de03..b7d3072b 100644 --- a/.github/ISSUE_TEMPLATE/other.yaml +++ b/.github/ISSUE_TEMPLATE/other.yaml @@ -1,5 +1,5 @@ name: 其它/other -description: 其它内容 +description: 其它内容/Other contents body: - type: markdown attributes: @@ -8,7 +8,13 @@ body: 如果你想提出功能建议或者优化建议, 请切换到功能建议模板; - 对于其它情况, 填写并提交此处的内容 + 对于其它情况, 填写并提交此处的内容. + + If you wish to report issues occurring during the app's runtime (such as the inability to view a particular comic source, login issues, non-functional features, etc.), please switch to the Bug Report template. + + If you would like to make feature requests or optimization suggestions, please switch to the Feature Request template. + + For any other situations, please fill out and submit the content here. - type: textarea id: what-happened attributes: diff --git a/README.md b/README.md index 0d5c3a4d..44132aac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pica Comic -[![flutter](https://img.shields.io/badge/flutter-3.13.9-blue)](https://flutter.dev/) +[![flutter](https://img.shields.io/badge/flutter-3.16.0-blue)](https://flutter.dev/) [![License](https://img.shields.io/github/license/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/blob/master/LICENSE) [![Download](https://img.shields.io/github/v/release/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/releases) [![stars](https://img.shields.io/github/stars/wgh136/PicaComic)](https://github.com/wgh136/PicaComic/stargazers) diff --git a/lib/base.dart b/lib/base.dart index 032c8880..1ae22e46 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -3,6 +3,7 @@ import 'package:pica_comic/network/htmanga_network/htmanga_main_network.dart'; import 'package:pica_comic/network/jm_network/jm_main_network.dart'; import 'package:pica_comic/network/picacg_network/methods.dart'; import 'package:pica_comic/network/download.dart'; +import 'package:pica_comic/network/webdav.dart'; import 'package:pica_comic/tools/io_tools.dart'; import 'package:pica_comic/tools/notification.dart'; import 'package:pica_comic/foundation/history.dart'; @@ -86,13 +87,13 @@ class Appdata { "0", //46 webdav version "0", //47 eh warning "https://nhentai.net", //48 nhentai domain - "0", //49 阅读器中双击放缩 + "1", //49 阅读器中双击放缩 "", //50 language, empty=system "", //51 默认收藏夹 "0", //52 me page "0", //53 本地收藏添加位置(尾/首) "0", //54 阅读后移动本地收藏(否/尾/首) - "0", //55 长按缩放 + "1", //55 长按缩放 ]; ///屏蔽的关键词 @@ -207,6 +208,7 @@ class Appdata { } Future writeData() async { + Webdav.uploadData(); var s = await SharedPreferences.getInstance(); await s.setString("token", token); await s.setString("userName", user.name); diff --git a/lib/foundation/def.dart b/lib/foundation/def.dart index 18b1037a..ffff18ee 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.2.7-beta"; +const appVersion = "2.2.7"; //定义宽屏设备的临界值 const changePoint = 600; diff --git a/lib/network/eh_network/eh_main_network.dart b/lib/network/eh_network/eh_main_network.dart index dbe2eafa..163b799e 100644 --- a/lib/network/eh_network/eh_main_network.dart +++ b/lib/network/eh_network/eh_main_network.dart @@ -16,8 +16,8 @@ import 'package:pica_comic/network/cache_network.dart'; import 'package:pica_comic/network/res.dart'; import 'package:pica_comic/tools/translations.dart'; -class EhNetwork{ - factory EhNetwork() => cache==null?(cache=EhNetwork.create()):cache!; +class EhNetwork { + factory EhNetwork() => cache == null ? (cache = EhNetwork.create()) : cache!; static EhNetwork? cache; @@ -25,7 +25,7 @@ class EhNetwork{ var folderNames = List.generate(10, (index) => "Favorite $index"); - EhNetwork.create(){ + EhNetwork.create() { updateUrl(); getCookies(true); } @@ -48,189 +48,187 @@ class EhNetwork{ String cookiesStr = ""; ///更新画廊站点 - void updateUrl(){ - _ehBaseUrl = appdata.settings[20]=="0"?"https://e-hentai.org":"https://exhentai.org"; - _ehApiUrl = appdata.settings[20]=="0"?"https://api.e-hentai.org/api.php":"https://exhentai.org/api.php"; + void updateUrl() { + _ehBaseUrl = appdata.settings[20] == "0" + ? "https://e-hentai.org" + : "https://exhentai.org"; + _ehApiUrl = appdata.settings[20] == "0" + ? "https://api.e-hentai.org/api.php" + : "https://exhentai.org/api.php"; getCookies(true); } ///设置请求cookie - Future getCookies(bool setNW, [String? url]) async{ - if(appdata.ehId == ""){ + Future getCookies(bool setNW, [String? url]) async { + url ??= ehBaseUrl; + if (appdata.ehId == "") { return ""; } - var cookies = await cookieJar.loadForRequest(Uri.parse(ehBaseUrl)); - cookieJar.delete(Uri.parse(ehBaseUrl), true); + var cookies = await cookieJar.loadForRequest(Uri.parse(url)); + cookieJar.delete(Uri.parse(url), true); cookies.removeWhere((element) => ["nw", "ipb_member_id", "ipb_pass_hash"].contains(element.name)); - var igneousField = cookies.firstWhereOrNull((element) => element.name == "igneous"); - if(igneousField != null && appdata.igneous != igneousField.value && igneousField.value != "mystery"){ + var igneousField = + cookies.firstWhereOrNull((element) => element.name == "igneous"); + if (igneousField != null && + appdata.igneous != igneousField.value && + igneousField.value != "mystery") { appdata.igneous = igneousField.value; appdata.writeData(); } var shouldAdd = [ - if(setNW) - Cookie("nw", "1"), - if(appdata.ehId != "") - Cookie("ipb_member_id", appdata.ehId), - if(appdata.ehPassHash != "") - Cookie("ipb_pass_hash", appdata.ehPassHash), - if(appdata.igneous != "" && igneousField == null) + if (setNW) Cookie("nw", "1"), + if (appdata.ehId != "") Cookie("ipb_member_id", appdata.ehId), + if (appdata.ehPassHash != "") Cookie("ipb_pass_hash", appdata.ehPassHash), + if (appdata.igneous != "" && igneousField == null) Cookie("igneous", appdata.igneous), ]; cookies.addAll(shouldAdd); - await cookieJar.saveFromResponse(Uri.parse(url ?? ehBaseUrl), cookies); + await cookieJar.saveFromResponse(Uri.parse(url), cookies); var res = ""; - for(var cookie in cookies){ + for (var cookie in cookies) { res += "${cookie.name}=${cookie.value}; "; } - if(res.length < 2){ + if (res.length < 2) { return ""; } - cookiesStr = res.substring(0,res.length-2); + cookiesStr = res.substring(0, res.length - 2); return cookiesStr; } ///从url获取数据, 在请求时设置了cookie - Future> request(String url, {Map? headers, - CacheExpiredTime expiredTime=CacheExpiredTime.short, bool setNW = true}) async{ + Future> request(String url, + {Map? headers, + CacheExpiredTime expiredTime = CacheExpiredTime.short, + bool setNW = true}) async { await getCookies(setNW, url); var options = BaseOptions( connectTimeout: const Duration(seconds: 8), sendTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 8), followRedirects: true, - headers: { - "user-agent": webUA, - ...?headers - } - ); + headers: {"user-agent": webUA, ...?headers}); var dio = CachedNetwork(); try { - var data = await dio.get( - url, - options, - cookieJar: cookieJar, - expiredTime: expiredTime - ); - if(data.data.isEmpty){ + var data = await dio.get(url, options, + cookieJar: cookieJar, expiredTime: expiredTime); + if (data.data.isEmpty) { throw Exception("Empty Data"); } await getCookies(true); - if((data.data).substring(0,4) == "Your"){ - return const Res(null, errorMessage: "Your IP address has been temporarily banned"); + if ((data.data).substring(0, 4) == "Your") { + return const Res(null, + errorMessage: "Your IP address has been temporarily banned"); } return Res(data.data); - } - on DioException catch(e){ + } on DioException catch (e) { String? message; - if(e.type!=DioExceptionType.unknown){ - message = e.message??"未知".tl; - }else{ + if (e.type != DioExceptionType.unknown) { + message = e.message ?? "未知".tl; + } else { message = e.toString().split("\n").elementAtOrNull(1); } - return Res(null, errorMessage: message??"网络错误"); - } - catch(e){ + return Res(null, errorMessage: message ?? "网络错误"); + } catch (e) { String? message; - if(e.toString() != "null"){ + if (e.toString() != "null") { message = e.toString(); } - return Res(null, errorMessage: message??"网络错误"); + return Res(null, errorMessage: message ?? "网络错误"); } } ///eh APi请求 - Future> apiRequest(Map data, {Map? headers,}) async{ - await setNetworkProxy();//更新代理 + Future> apiRequest( + Map data, { + Map? headers, + }) async { + await setNetworkProxy(); //更新代理 var options = BaseOptions( connectTimeout: const Duration(seconds: 8), sendTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 8), headers: { "user-agent": webUA, - "cookie": "nw=1${appdata.ehId=="" ? "" : ";ipb_member_id=${appdata.ehId};ipb_pass_hash=${appdata.ehPassHash}"}", + "cookie": + "nw=1${appdata.ehId == "" ? "" : ";ipb_member_id=${appdata.ehId};ipb_pass_hash=${appdata.ehPassHash}"}", ...?headers - } - ); + }); - var dio = Dio(options) - ..interceptors.add(LogInterceptor()); + var dio = Dio(options)..interceptors.add(LogInterceptor()); - try{ + try { var res = await dio.post(ehApiUrl, data: data); return Res(res.data); - } - on DioException catch(e){ + } on DioException catch (e) { String? message; - if(e.type!=DioExceptionType.unknown){ - message = e.message??"未知".tl; - }else{ + if (e.type != DioExceptionType.unknown) { + message = e.message ?? "未知".tl; + } else { message = e.toString().split("\n").elementAtOrNull(1); } - return Res(null, errorMessage: message??"网络错误"); - } - catch(e){ + return Res(null, errorMessage: message ?? "网络错误"); + } catch (e) { String? message; - if(e.toString() != "null"){ + if (e.toString() != "null") { message = e.toString(); } - return Res(null, errorMessage: message??"网络错误"); + return Res(null, errorMessage: message ?? "网络错误"); } } - Future> post(String url, dynamic data, {Map? headers,}) async{ + Future> post( + String url, + dynamic data, { + Map? headers, + }) async { await getCookies(true, url); - await setNetworkProxy();//更新代理 + await setNetworkProxy(); //更新代理 var options = BaseOptions( connectTimeout: const Duration(seconds: 8), sendTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 8), receiveDataWhenStatusError: true, - validateStatus: (status)=>status==200||status==302, - headers: { - "user-agent": webUA, - ...?headers - } - ); + validateStatus: (status) => status == 200 || status == 302, + headers: {"user-agent": webUA, ...?headers}); - var dio = logDio(options) - ..interceptors.add(LogInterceptor()); + var dio = logDio(options)..interceptors.add(LogInterceptor()); dio.interceptors.add(CookieManager(cookieJar)); - try{ + try { var res = await dio.post(url, data: data); return Res(res.data ?? ""); - } - on DioException catch(e){ + } on DioException catch (e) { String? message; - if(e.type!=DioExceptionType.unknown){ - message = e.message??"未知".tl; - }else{ + if (e.type != DioExceptionType.unknown) { + message = e.message ?? "未知".tl; + } else { message = e.toString().split("\n").elementAtOrNull(1); } - return Res(null, errorMessage: message??"网络错误"); - } - catch(e, s){ + return Res(null, errorMessage: message ?? "网络错误"); + } catch (e, s) { LogManager.addLog(LogLevel.error, "Network", "$e\n$s"); String? message; - if(e.toString() != "null"){ + if (e.toString() != "null") { message = e.toString(); } - return Res(null, errorMessage: message??"网络错误"); + return Res(null, errorMessage: message ?? "网络错误"); } } ///获取用户名, 同时用于检测cookie是否有效 - Future getUserName() async{ + Future getUserName() async { try { await cookieJar.deleteAll(); cookiesStr = ""; - var res = await request("https://forums.e-hentai.org/", headers: { - "referer": "https://forums.e-hentai.org/index.php?", - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-encoding": "gzip, deflate, br", - "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7" - }, expiredTime: CacheExpiredTime.no); + var res = await request("https://forums.e-hentai.org/", + headers: { + "referer": "https://forums.e-hentai.org/index.php?", + "accept": + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-encoding": "gzip, deflate, br", + "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7" + }, + expiredTime: CacheExpiredTime.no); if (res.error) { return false; } @@ -246,48 +244,58 @@ class EhNetwork{ appdata.igneous = ""; } return name != null; - } - catch(e, s){ + } catch (e, s) { LogManager.addLog(LogLevel.error, "Network", "$e\n$s"); return false; } } ///解析星星的html元素的位置属性, 返回评分 - double getStarsFromPosition(String position){ - int i =0; - while(position[i]!=";"){ + double getStarsFromPosition(String position) { + int i = 0; + while (position[i] != ";") { i++; - if(i == position.length){ + if (i == position.length) { break; } } - switch(position.substring(0,i)){ - case "background-position:0px -1px": return 5; - case "background-position:0px -21px": return 4.5; - case "background-position:-16px -1px": return 4; - case "background-position:-16px -21px": return 3.5; - case "background-position:-32px -1px": return 3; - case "background-position:-32px -21px": return 2.5; - case "background-position:-48px -1px": return 2; - case "background-position:-48px -21px": return 1.5; - case "background-position:-64px -1px": return 1; - case "background-position:-64px -21px": return 0.5; + switch (position.substring(0, i)) { + case "background-position:0px -1px": + return 5; + case "background-position:0px -21px": + return 4.5; + case "background-position:-16px -1px": + return 4; + case "background-position:-16px -21px": + return 3.5; + case "background-position:-32px -1px": + return 3; + case "background-position:-32px -21px": + return 2.5; + case "background-position:-48px -1px": + return 2; + case "background-position:-48px -21px": + return 1.5; + case "background-position:-64px -1px": + return 1; + case "background-position:-64px -21px": + return 0.5; } return 0.5; } ///从e-hentai链接中获取当前页面的所有画廊 - Future> getGalleries(String url,{bool leaderboard = false, bool favoritePage=false}) async{ + Future> getGalleries(String url, + {bool leaderboard = false, bool favoritePage = false}) async { //从一个链接中获取所有画廊, 同时获得下一页的链接 //leaderboard比正常的表格多了第一列 bool ignoreExamination = url.contains("favorites"); int t = 0; - if(leaderboard){ + if (leaderboard) { t++; } var res = await request(url, expiredTime: CacheExpiredTime.no); - if(res.error){ + if (res.error) { return Res(null, errorMessage: res.errorMessage); } try { @@ -299,13 +307,24 @@ class EhNetwork{ try { var type = items[i].children[0 + t].children[0].text; var time = items[i].children[1 + t].children[2].children[0].text; - var stars = getStarsFromPosition( - items[i].children[1 + t].children[2].children[1].attributes["style"]!); - var cover = items[i].children[1 + t].children[1].children[0].children[0] + var stars = getStarsFromPosition(items[i] + .children[1 + t] + .children[2] + .children[1] + .attributes["style"]!); + var cover = items[i] + .children[1 + t] + .children[1] + .children[0] + .children[0] .attributes["src"]; if (cover![0] == 'd') { - cover = - items[i].children[1 + t].children[1].children[0].children[0].attributes["data-src"]; + cover = items[i] + .children[1 + t] + .children[1] + .children[0] + .children[0] + .attributes["data-src"]; } var title = items[i].children[2 + t].children[0].children[0].text; var link = items[i].children[2 + t].children[0].attributes["href"]; @@ -314,29 +333,19 @@ class EhNetwork{ try { uploader = items[i].children[3 + t].children[0].children[0].text; pages = int.parse(items[i].children[3 + t].children[1].text.nums); - } - catch (e) { + } catch (e) { //收藏夹页没有uploader } var tags = []; - for (var node in items[i].children[2 + t].children[0].children[1].children) { + for (var node + in items[i].children[2 + t].children[0].children[1].children) { tags.add(node.attributes["title"]!); } galleries.add(EhGalleryBrief( - title, - type, - time, - uploader, - cover!, - stars, - link!, - tags, - ignoreExamination: ignoreExamination, - pages: pages - )); - } - catch (e) { + title, type, time, uploader, cover!, stars, link!, tags, + ignoreExamination: ignoreExamination, pages: pages)); + } catch (e) { //表格中存在空行或者被屏蔽 continue; } @@ -351,116 +360,134 @@ class EhNetwork{ g.galleries = galleries; //获取收藏夹名称 - if(favoritePage && appdata.ehAccount != ""){ + if (favoritePage && appdata.ehAccount != "") { var names = []; try { var folderDivs = document.querySelectorAll("div.fp"); for (var folderDiv in folderDivs) { - names.add(folderDiv.children - .elementAtOrNull(2) - ?.text ?? "Favorite ${names.length}"); + names.add(folderDiv.children.elementAtOrNull(2)?.text ?? + "Favorite ${names.length}"); } - if(names.length != 10){ + if (names.length != 10) { names = names.sublist(0, 10); } folderNames = names; - } - catch(e){ + } catch (e) { //忽视 } return Res(g, subData: folderNames); } return Res(g); - } - catch(e, s){ + } catch (e, s) { LogManager.addLog(LogLevel.error, "Data Analysis", "$e\n$s"); return Res(null, errorMessage: "解析失败: $e"); } } ///获取画廊的下一页 - Future getNextPageGalleries(Galleries galleries) async{ - if(galleries.next==null) return true; + Future getNextPageGalleries(Galleries galleries) async { + if (galleries.next == null) return true; var next = await getGalleries(galleries.next!); - if(next.error) return false; + if (next.error) return false; galleries.galleries.addAll(next.data.galleries); galleries.next = next.data.next; return true; } ///从漫画详情页链接中获取漫画详细信息 - Future> getGalleryInfo(String link, [bool setNW = true]) async{ - try{ - var res = await request(link, expiredTime: CacheExpiredTime.no, setNW: setNW); - if (res.error){ + Future> getGalleryInfo(String link, [bool setNW = true]) async { + try { + var res = + await request(link, expiredTime: CacheExpiredTime.no, setNW: setNW); + if (res.error) { return Res(null, errorMessage: res.errorMessage); } - if(res.data.contains("Content Warning")){ + if (res.data.contains("Content Warning") && + res.data.contains("Never Warn Me Again")) { return const Res(null, errorMessage: "Content Warning"); } var document = parse(res.data); //tags var tags = >{}; - var tagLists = document.querySelectorAll("div#taglist > table > tbody > tr"); + var tagLists = + document.querySelectorAll("div#taglist > table > tbody > tr"); for (var tr in tagLists) { var list = []; for (var div in tr.children[1].children) { list.add(div.children[0].text); } - tags[tr.children[0].text.substring(0, tr.children[0].text.length - 1)] = list; + tags[tr.children[0].text.substring(0, tr.children[0].text.length - 1)] = + list; } String maxPage = "1"; - for(var element in document.querySelectorAll("td.gdt2")){ - if(element.text.contains("pages")){ + for (var element in document.querySelectorAll("td.gdt2")) { + if (element.text.contains("pages")) { maxPage = element.text.nums; } } bool favorite = true; - if(document.getElementById("favoritelink")?.text == " Add to Favorites"){ + if (document.getElementById("favoritelink")?.text == + " Add to Favorites") { favorite = false; } - var coverPath = document.querySelector("div#gleft > div#gd1 > div")!.attributes["style"]!; - coverPath = RegExp(r"https?://([-a-zA-Z0-9.]+(/\S*)?\.(?:jpg|jpeg|gif|png))").firstMatch(coverPath)![0]!; + var coverPath = document + .querySelector("div#gleft > div#gd1 > div")! + .attributes["style"]!; + coverPath = + RegExp(r"https?://([-a-zA-Z0-9.]+(/\S*)?\.(?:jpg|jpeg|gif|png))") + .firstMatch(coverPath)![0]!; //评论 var comments = []; - for(var c in document.getElementsByClassName("c1")){ - var name = c.getElementsByClassName("c3")[0].getElementsByTagName("a").elementAtOrNull(0)?.text??"未知"; - var time = c.getElementsByClassName("c3")[0].text.subStringOrNull(11,32) ?? "Unknown"; + for (var c in document.getElementsByClassName("c1")) { + var name = c + .getElementsByClassName("c3")[0] + .getElementsByTagName("a") + .elementAtOrNull(0) + ?.text ?? + "未知"; + var time = + c.getElementsByClassName("c3")[0].text.subStringOrNull(11, 32) ?? + "Unknown"; var content = c.getElementsByClassName("c6")[0].text; comments.add(Comment(name, content, time)); } //上传者 - var uploader = document.getElementById("gdn")!.children.elementAtOrNull(0)?.text??"未知"; + var uploader = + document.getElementById("gdn")!.children.elementAtOrNull(0)?.text ?? + "未知"; //星星 - var stars = - getStarsFromPosition(document.getElementById("rating_image")!.attributes["style"]!); + var stars = getStarsFromPosition( + document.getElementById("rating_image")!.attributes["style"]!); //平均分数 var rating = document.getElementById("rating_label")?.text; //类型 var type = document.getElementsByClassName("cs")[0].text; //时间 - var time = document.querySelector("div#gdd > table > tbody > tr > td.gdt2")!.text; + var time = document + .querySelector("div#gdd > table > tbody > tr > td.gdt2")! + .text; //身份认证数据 var auth = getVariablesFromJsCode(res.data); var thumbnailUrls = []; var imgDom = document.querySelectorAll("div.gdtl > a > img"); - for(var i in imgDom){ - if(i.attributes["src"] != null) { + for (var i in imgDom) { + if (i.attributes["src"] != null) { thumbnailUrls.add(i.attributes["src"]!); } } var title = document.querySelector("h1#gn")!.text; var subTitle = document.querySelector("h1#gj")?.text; - if(subTitle!=null && subTitle.removeAllBlank == ""){ + if (subTitle != null && subTitle.removeAllBlank == "") { subTitle = null; } - var thumbnailDiv = document.querySelectorAll("div.gdtm > div").elementAtOrNull(0); - if(thumbnailDiv != null) { + var thumbnailDiv = + document.querySelectorAll("div.gdtm > div").elementAtOrNull(0); + if (thumbnailDiv != null) { var pattern = RegExp(r"/m/(\d+)/"); var match = pattern.firstMatch(thumbnailDiv.attributes["style"] ?? ""); @@ -471,49 +498,67 @@ class EhNetwork{ } } } - return Res(Gallery(title, type, time, uploader, stars, rating, coverPath, - tags, comments, auth, favorite, link, maxPage, thumbnailUrls, subTitle)); - } - catch(e, s){ + return Res(Gallery( + title, + type, + time, + uploader, + stars, + rating, + coverPath, + tags, + comments, + auth, + favorite, + link, + maxPage, + thumbnailUrls, + subTitle)); + } catch (e, s) { LogManager.addLog(LogLevel.error, "Data Analysis", "$e\n$s"); return Res(null, errorMessage: e.toString()); } } - Future>> getComments(String url) async{ + Future>> getComments(String url) async { var res = await request("$url?hc=1", expiredTime: CacheExpiredTime.no); - if(res.error){ + if (res.error) { return Res(null, errorMessage: res.errorMessage); } - try{ + try { var document = parse(res.data); var resComments = []; var comments = document.getElementsByClassName("c1"); - for(var c in comments){ - var name = c.getElementsByClassName("c3")[0].getElementsByTagName("a").elementAtOrNull(0)?.text??"未知"; - var time = c.getElementsByClassName("c3")[0].text.substring(11,32); + for (var c in comments) { + var name = c + .getElementsByClassName("c3")[0] + .getElementsByTagName("a") + .elementAtOrNull(0) + ?.text ?? + "未知"; + var infoStr = c.getElementsByClassName("c3")[0].text; + var time = infoStr.substring(10, infoStr.indexOf(" by")); var content = c.getElementsByClassName("c6")[0].text; resComments.add(Comment(name, content, time)); } return Res(resComments); - } - catch(e, s){ + } catch (e, s) { LogManager.addLog(LogLevel.error, "Data Analysis", "$e\n$s"); return Res(null, errorMessage: e.toString()); } } /// page starts from 1 - Future>> getReaderLinks(String link, int page) async{ + Future>> getReaderLinks(String link, int page) async { String url = "$link?inline_set=ts_m"; - if(page != 1){ - url = "$url&p=${page-1}"; + if (page != 1) { + url = "$url&p=${page - 1}"; } var res = await request(url); - if(res.error){ + if (res.error) { return Res(null, errorMessage: res.errorMessage); } - try{ + try { var urls_ = []; var temp = parse(res.data); var links = temp.querySelectorAll("div#gdt > div.gdtm > div > a"); @@ -525,36 +570,35 @@ class EhNetwork{ urls_.add(link.attributes["href"]!); } return Res(urls_); - } - catch(e, s){ + } catch (e, s) { LogManager.addLog(LogLevel.error, "Data Analysis", "$e\n$s"); return Res(null, errorMessage: e.toString()); } } - Future>> getThumbnailUrls(Gallery gallery) async{ - if(gallery.auth!["thumbnailKey"] == null){ + Future>> getThumbnailUrls(Gallery gallery) async { + if (gallery.auth!["thumbnailKey"] == null) { var res = await request("${gallery.link}?inline_set=ts_m"); - if(res.error){ + if (res.error) { return Res.fromErrorRes(res); } var document = parse(res.data); var thumbnailDiv = document.querySelectorAll("div.gdtm > div")[0]; var pattern = RegExp(r"/m/(\d+)/"); - var match = pattern.firstMatch(thumbnailDiv.attributes["style"]??""); + var match = pattern.firstMatch(thumbnailDiv.attributes["style"] ?? ""); if (match != null) { var extractedValue = match.group(1); - if(extractedValue != null){ + if (extractedValue != null) { gallery.auth!["thumbnailKey"] = extractedValue; } - }else{ + } else { return const Res(null, errorMessage: "Failed to get Thumbnail"); } } return Res(List.generate(int.parse(gallery.maxPage), (index) { var page = (index ~/ 20).toString(); - if(page.length == 1){ + if (page.length == 1) { page = "0$page"; } return "https://ehgt.org/m/${gallery.auth!["thumbnailKey"]!}/${getGalleryId(gallery.link)}-$page.jpg"; @@ -562,18 +606,31 @@ class EhNetwork{ } ///搜索e-hentai - Future> search(String keyword) async{ - if(keyword!=""){ + Future> search(String keyword, + {int? fCats, int? startPages, int? endPages, int? minStars}) async { + if (keyword != "") { appdata.searchHistory.remove(keyword); appdata.searchHistory.add(keyword); appdata.writeHistory(); } - var res = await getGalleries("$ehBaseUrl/?f_search=$keyword&inline_set=dm_l"); - Future.delayed(const Duration(microseconds: 500),(){ - try{ + var requestUrl = "$ehBaseUrl/?f_search=$keyword&inline_set=dm_l"; + if (fCats != null) { + requestUrl += "&f_cats=$fCats"; + } + if (startPages != null) { + requestUrl += "&f_spf=$startPages"; + } + if (endPages != null) { + requestUrl += "&f_spt=$endPages"; + } + if (minStars != null) { + requestUrl += "&f_srdd=$minStars"; + } + var res = await getGalleries(requestUrl); + Future.delayed(const Duration(microseconds: 500), () { + try { StateController.find().update(); - } - catch(e){ + } catch (e) { //忽视 } }); @@ -581,19 +638,23 @@ class EhNetwork{ } ///获取排行榜 - Future> getLeaderboard(EhLeaderboardType type) async{ - var res = await getGalleries("https://e-hentai.org/toplist.php?tl=${type.value}",leaderboard: true); - if(res.error) return Res(null, errorMessage: res.errorMessage); + Future> getLeaderboard(EhLeaderboardType type) async { + var res = await getGalleries( + "https://e-hentai.org/toplist.php?tl=${type.value}", + leaderboard: true); + if (res.error) return Res(null, errorMessage: res.errorMessage); return Res(EhLeaderboard(type, res.data.galleries, 0)); } ///获取排行榜的下一页 - Future getLeaderboardNextPage(EhLeaderboard leaderboard) async{ - if(leaderboard.loaded == EhLeaderboard.max){ + Future getLeaderboardNextPage(EhLeaderboard leaderboard) async { + if (leaderboard.loaded == EhLeaderboard.max) { return; - }else{ - var res = await getGalleries("$ehBaseUrl/toplist.php?tl=${leaderboard.type.value}&p=${leaderboard.loaded+1}",leaderboard: true); - if(!res.error){ + } else { + var res = await getGalleries( + "$ehBaseUrl/toplist.php?tl=${leaderboard.type.value}&p=${leaderboard.loaded + 1}", + leaderboard: true); + if (!res.error) { leaderboard.galleries.addAll(res.data.galleries); } leaderboard.loaded++; @@ -601,7 +662,7 @@ class EhNetwork{ } ///评分 - Future rateGallery(Map auth, int rating) async{ + Future rateGallery(Map auth, int rating) async { var res = await apiRequest({ "method": "rategallery", "apiuid": auth["apiuid"], @@ -614,57 +675,47 @@ class EhNetwork{ } ///收藏 - Future favorite(String gid, String token, {String id = "0"}) async{ + Future favorite(String gid, String token, {String id = "0"}) async { var res = await post( "https://e-hentai.org/gallerypopups.php?gid=$gid&t=$token&act=addfav", "favcat=$id&favnote=&apply=Add+to+Favorites&update=1", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - ); - if(res.error){ + headers: {"Content-Type": "application/x-www-form-urlencoded"}); + if (res.error) { return false; } - if(res.error || res.data.isEmpty || res.data[0] != "<"){ + if (res.error || res.data.isEmpty || res.data[0] != "<") { return false; - }else { + } else { return true; } } ///取消收藏 - Future unfavorite(String gid, String token) async{ + Future unfavorite(String gid, String token) async { var res = await post( "https://e-hentai.org/gallerypopups.php?gid=$gid&t=$token&act=addfav", "favcat=favdel&favnote=&apply=Apply+Changes&update=1", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - ); - if(res.error || res.data[0] != "<"){ + headers: {"Content-Type": "application/x-www-form-urlencoded"}); + if (res.error || res.data[0] != "<") { return false; - }else { + } else { return true; } } ///发送评论 - Future> comment(String content, String link) async{ + Future> comment(String content, String link) async { var res = await post( - link, - "commenttext_new=${Uri.encodeComponent(content)}", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - ); + link, "commenttext_new=${Uri.encodeComponent(content)}", + headers: {"Content-Type": "application/x-www-form-urlencoded"}); - if(res.error){ + if (res.error) { return Res(null, errorMessage: res.errorMessage); } var document = parse(res.data); - if(document.querySelector("p.br") != null){ - return Res(null,errorMessage: document.querySelector("p.br")!.text); + if (document.querySelector("p.br") != null) { + return Res(null, errorMessage: document.querySelector("p.br")!.text); } return const Res(true); } -} \ No newline at end of file +} diff --git a/lib/tools/debug.dart b/lib/tools/debug.dart index 8a761afa..64fa02d3 100644 --- a/lib/tools/debug.dart +++ b/lib/tools/debug.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:pica_comic/views/widgets/show_message.dart'; ///用于测试函数 void debug() async{ print(await getLibraryDirectory()); diff --git a/lib/tools/notification.dart b/lib/tools/notification.dart index fb6d644d..bfbee9f0 100644 --- a/lib/tools/notification.dart +++ b/lib/tools/notification.dart @@ -2,133 +2,136 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:pica_comic/views/downloading_page.dart'; import '../foundation/app.dart'; -class Notifications{ +class Notifications { FlutterLocalNotificationsPlugin? flutterLocalNotificationsPlugin; final progressId = 72382; - Future requestPermission() async{ + Future requestPermission() async { try { - if(App.isAndroid) { - return await flutterLocalNotificationsPlugin!.resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>()!.requestPermission(); - }else if(App.isIOS) { + if (App.isAndroid) { + return await flutterLocalNotificationsPlugin! + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()! + .requestNotificationsPermission(); + } else if (App.isIOS) { return await flutterLocalNotificationsPlugin ?.resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() + IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); + alert: true, + badge: true, + sound: true, + ); } return true; - } - catch(e){ + } catch (e) { return false; } } - Future init() async{ - if(!(App.isAndroid || App.isIOS)) return; - flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + Future init() async { + if (!(App.isAndroid || App.isIOS)) return; + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@drawable/notification'); final DarwinInitializationSettings initializationSettingsDarwin = - DarwinInitializationSettings( - onDidReceiveLocalNotification: onDidReceiveLocalNotification); - InitializationSettings initializationSettings = - InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - ); - await flutterLocalNotificationsPlugin!.initialize( - initializationSettings, - onDidReceiveNotificationResponse: onDidReceiveNotificationResponse + DarwinInitializationSettings( + onDidReceiveLocalNotification: onDidReceiveLocalNotification); + InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, ); + await flutterLocalNotificationsPlugin!.initialize(initializationSettings, + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse); } - void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async { - if(!(App.isAndroid || App.isIOS)) return; + void onDidReceiveNotificationResponse( + NotificationResponse notificationResponse) async { + if (!(App.isAndroid || App.isIOS)) return; final String? payload = notificationResponse.payload; - if(payload != "item y"){ - App.globalTo(()=>const DownloadingPage()); + if (payload != "item y") { + App.globalTo(() => const DownloadingPage()); } } void onDidReceiveLocalNotification( int id, String? title, String? body, String? payload) async { - if(payload != "item y"){ - App.globalTo(()=>const DownloadingPage()); + if (payload != "item y") { + App.globalTo(() => const DownloadingPage()); } } - void sendProgressNotification(int progress, int total, String title, String content) async{ - if(!(App.isAndroid || App.isIOS)) return; + void sendProgressNotification( + int progress, int total, String title, String content) async { + if (!(App.isAndroid || App.isIOS)) return; AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails('download', '下载漫画', - channelDescription: '显示下载进度', - importance: Importance.low, - priority: Priority.low, - showProgress: true, - maxProgress: total, - progress: progress, - ongoing: true, - onlyAlertOnce: true, - autoCancel: false - ); + AndroidNotificationDetails('download', '下载漫画', + channelDescription: '显示下载进度', + importance: Importance.low, + priority: Priority.low, + showProgress: true, + maxProgress: total, + progress: progress, + ongoing: true, + onlyAlertOnce: true, + autoCancel: false); DarwinNotificationDetails ios = const DarwinNotificationDetails( - presentSound: false, - presentAlert: false, - presentBadge: false - ); + presentSound: false, presentAlert: false, presentBadge: false); NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails, iOS: ios); + NotificationDetails(android: androidNotificationDetails, iOS: ios); await flutterLocalNotificationsPlugin!.show( progressId, title, content, notificationDetails, payload: 'item x'); } - void endProgress() async{ - if(!(App.isAndroid || App.isIOS)) return; + void endProgress() async { + if (!(App.isAndroid || App.isIOS)) return; await flutterLocalNotificationsPlugin!.cancel(progressId); } - void sendNotification(String title, String content) async{ - if(!(App.isAndroid || App.isIOS)) return; + void cancelAll() async { + if (!(App.isAndroid || App.isIOS)) return; + try { + await flutterLocalNotificationsPlugin!.cancelAll(); + } catch (e) { + // ignore + } + } + + void sendNotification(String title, String content) async { + if (!(App.isAndroid || App.isIOS)) return; AndroidNotificationDetails androidNotificationDetails = - const AndroidNotificationDetails('PicaComic', '通知', - channelDescription: '通知', - importance: Importance.max, - priority: Priority.max, + const AndroidNotificationDetails( + 'PicaComic', + '通知', + channelDescription: '通知', + importance: Importance.max, + priority: Priority.max, ); DarwinNotificationDetails ios = const DarwinNotificationDetails(); NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails, iOS: ios); - await flutterLocalNotificationsPlugin!.show( - 1145140, title, content, notificationDetails, - payload: 'item x'); + NotificationDetails(android: androidNotificationDetails, iOS: ios); + await flutterLocalNotificationsPlugin! + .show(1145140, title, content, notificationDetails, payload: 'item x'); } - void sendUnimportantNotification(String title, String content) async{ - if(!(App.isAndroid || App.isIOS)) return; + void sendUnimportantNotification(String title, String content) async { + if (!(App.isAndroid || App.isIOS)) return; AndroidNotificationDetails androidNotificationDetails = - const AndroidNotificationDetails('punchIN', '打卡', + const AndroidNotificationDetails( + 'punchIN', + '打卡', channelDescription: '打卡', importance: Importance.low, priority: Priority.low, ); DarwinNotificationDetails ios = const DarwinNotificationDetails( - presentAlert: false, - presentSound: false, - presentBadge: false - ); + presentAlert: false, presentSound: false, presentBadge: false); NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails, iOS: ios); - await flutterLocalNotificationsPlugin!.show( - 51515568, title, content, notificationDetails, - payload: 'item y'); + NotificationDetails(android: androidNotificationDetails, iOS: ios); + await flutterLocalNotificationsPlugin! + .show(51515568, title, content, notificationDetails, payload: 'item y'); } -} \ No newline at end of file +} diff --git a/lib/tools/time.dart b/lib/tools/time.dart index 12cc702c..38a91e5c 100644 --- a/lib/tools/time.dart +++ b/lib/tools/time.dart @@ -1,3 +1,4 @@ +import 'package:intl/intl.dart'; import 'package:pica_comic/tools/translations.dart'; String timeToString(DateTime time){ @@ -24,4 +25,11 @@ extension TimeExtension on DateTime{ Duration operator-(DateTime other){ return Duration(microseconds: microsecondsSinceEpoch - other.microsecondsSinceEpoch); } + static DateTime parseEhTime(String dateString){ + final format = DateFormat('d MMMM yyyy, HH:mm', 'en_US'); + final dateTime = format.parse(dateString); + return dateTime; + } + + String get toCompareString => timeToString(this); } \ No newline at end of file diff --git a/lib/tools/translations.dart b/lib/tools/translations.dart index 8d2b3de6..bfdfabe3 100644 --- a/lib/tools/translations.dart +++ b/lib/tools/translations.dart @@ -420,6 +420,20 @@ extension AppTranslation on String { "在系统设置中管理APP支持的链接": "在系統設定中管理APP支援的鏈接", "网络收藏": "網路收藏", "取消收藏": "取消收藏", + //v2.2.7 + "本地收藏": "本地收藏", + "新收藏添加至": "新收藏新增至", + "阅读后移动本地收藏至": "閱讀後移動本地收藏至", + "最前": "最前", + "最后": "最後", + "无操作": "無操作", + "长按缩放": "長按縮放", + "同步数据中": "同步資料中", + "最少星星": "最少星星", + "高级选项": "進階選項", + "热搜": "熱搜", + "隐藏": "隱藏", + "显示收藏": "顯示收藏", }, 'en_US': { '有可用更新': 'Updates available', @@ -930,12 +944,27 @@ extension AppTranslation on String { "默认收藏夹": "Default Folder", "用于快速收藏": "Used to quickly add favorites", "无效的默认收藏夹": "Invalid Default Folder", - "必须设置一个有效的收藏夹才能使用快速收藏": "A valid favorite must be set up to use this feature.", + "必须设置一个有效的收藏夹才能使用快速收藏": + "A valid favorite must be set up to use this feature.", "前往设置": "settings", "应用链接": "App Links", "在系统设置中管理APP支持的链接": "Manage APP links in system settings", "网络收藏": "Network collection", - "取消收藏": "Unfavorite" + "取消收藏": "Unfavorite", + // v2.2.7 + "本地收藏": "Local Favorites", + "新收藏添加至": "Add new favorite to", + "阅读后移动本地收藏至": "After reading, move local favorite to", + "最前": "front", + "最后": "end", + "无操作": "no", + "长按缩放": "Long press to zoom", + "同步数据中": "Synchronizing data", + "最少星星": "Min Stars", + "高级选项": "Advanced Options", + "热搜": "Hot Search", + "隐藏": "Hide", + "显示收藏": "Show Favorites", } }; } diff --git a/lib/views/all_favorites_page.dart b/lib/views/all_favorites_page.dart index 9ebbe6a6..37b93050 100644 --- a/lib/views/all_favorites_page.dart +++ b/lib/views/all_favorites_page.dart @@ -16,10 +16,14 @@ class AllFavoritesPage extends StatefulWidget { State createState() => _AllFavoritesPageState(); } -class _AllFavoritesPageState extends State with SingleTickerProviderStateMixin{ +class _AllFavoritesPageState extends State + with SingleTickerProviderStateMixin { late TabController controller; - int pages = int.parse(appdata.settings[21][0]) + int.parse(appdata.settings[21][1]) + - int.parse(appdata.settings[21][2]) + int.parse(appdata.settings[21][4]) + int.parse(appdata.settings[21][5]); + int pages = int.parse(appdata.settings[21][0]) + + int.parse(appdata.settings[21][1]) + + int.parse(appdata.settings[21][2]) + + int.parse(appdata.settings[21][4]) + + int.parse(appdata.settings[21][5]); @override void initState() { @@ -38,33 +42,40 @@ class _AllFavoritesPageState extends State with SingleTickerPr TabBar( splashBorderRadius: const BorderRadius.all(Radius.circular(10)), isScrollable: MediaQuery.of(context).size.width < pages * 90, + tabAlignment: TabAlignment.center, tabs: [ - if(appdata.settings[21][0] == "1") - const Tab(text: "Picacg",), - if(appdata.settings[21][1] == "1") - const Tab(text: "EHentai",), - if(appdata.settings[21][2] == "1") - Tab(text: "禁漫天堂".tl,), - if(appdata.settings[21][4] == "1") - Tab(text: "绅士漫画".tl,), - if(appdata.settings[21][5] == "1") - const Tab(text: "Nhentai",), + if (appdata.settings[21][0] == "1") + const Tab( + text: "Picacg", + ), + if (appdata.settings[21][1] == "1") + const Tab( + text: "EHentai", + ), + if (appdata.settings[21][2] == "1") + Tab( + text: "禁漫天堂".tl, + ), + if (appdata.settings[21][4] == "1") + Tab( + text: "绅士漫画".tl, + ), + if (appdata.settings[21][5] == "1") + const Tab( + text: "Nhentai", + ), ], - controller: controller,), + controller: controller, + ), Expanded( child: TabBarView( controller: controller, children: [ - if(appdata.settings[21][0] == "1") - const FavoritesPage(), - if(appdata.settings[21][1] == "1") - const EhFavoritePage(), - if(appdata.settings[21][2] == "1") - const JmFavoritePage(), - if(appdata.settings[21][4] == "1") - const HtFavoritePage(), - if(appdata.settings[21][5] == "1") - const NhentaiFavoritePage(), + if (appdata.settings[21][0] == "1") const FavoritesPage(), + if (appdata.settings[21][1] == "1") const EhFavoritePage(), + if (appdata.settings[21][2] == "1") const JmFavoritePage(), + if (appdata.settings[21][4] == "1") const HtFavoritePage(), + if (appdata.settings[21][5] == "1") const NhentaiFavoritePage(), ], ), ) @@ -72,4 +83,4 @@ class _AllFavoritesPageState extends State with SingleTickerPr ), ); } -} \ No newline at end of file +} diff --git a/lib/views/app_views/accounts_page.dart b/lib/views/app_views/accounts_page.dart index 2c81fcc3..7dbc23bf 100644 --- a/lib/views/app_views/accounts_page.dart +++ b/lib/views/app_views/accounts_page.dart @@ -122,7 +122,7 @@ class AccountsPage extends StatelessWidget { ListTile( title: Text("登录".tl), onTap: () => App.globalTo(() => const LoginPage()) - ?.then((value) { + .then((value) { logic.update(); Webdav.uploadData(); }), @@ -139,7 +139,7 @@ class AccountsPage extends StatelessWidget { ListTile( title: Text("登录".tl), onTap: () => App.globalTo(() => const EhLoginPage()) - ?.then((v) { + .then((v) { logic.update(); Webdav.uploadData(); }), @@ -211,7 +211,7 @@ class AccountsPage extends StatelessWidget { ListTile( title: Text("登录".tl), onTap: () => App.globalTo(() => const JmLoginPage()) - ?.then((v) { + .then((v) { logic.update(); Webdav.uploadData(); }), @@ -286,7 +286,7 @@ class AccountsPage extends StatelessWidget { ListTile( title: Text("登录".tl), onTap: () => App.globalTo(() => const HtLoginPage()) - ?.then((v) { + .then((v) { logic.update(); Webdav.uploadData(); }), diff --git a/lib/views/category_page.dart b/lib/views/category_page.dart index 617e87f6..e272498c 100644 --- a/lib/views/category_page.dart +++ b/lib/views/category_page.dart @@ -32,6 +32,7 @@ class _AllCategoryPageState extends State with TickerProviderSt TabBar( splashBorderRadius: const BorderRadius.all(Radius.circular(10)), isScrollable: true, + tabAlignment: TabAlignment.center, tabs: [ if(appdata.settings[21][0] == "1") Tab(text: "Picacg".tl, key: const Key("Picacg分类"),), diff --git a/lib/views/eh_views/eh_comments_page.dart b/lib/views/eh_views/eh_comments_page.dart index a3c42d27..a3143264 100644 --- a/lib/views/eh_views/eh_comments_page.dart +++ b/lib/views/eh_views/eh_comments_page.dart @@ -1,12 +1,12 @@ 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/time.dart'; import 'package:pica_comic/tools/translations.dart'; import 'package:pica_comic/views/widgets/show_error.dart'; import 'package:pica_comic/views/widgets/show_message.dart'; import '../../base.dart'; import '../../foundation/app.dart'; -import '../widgets/selectable_text.dart'; import '../widgets/side_bar.dart'; class CommentsPageLogic extends StateController{ @@ -61,17 +61,22 @@ class CommentsPage extends StatelessWidget { (context,index){ var comment = logic.comments[index]; return Card( - margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), + margin: const EdgeInsets.fromLTRB(12, 4, 12, 4), elevation: 0, color: Theme.of(context).colorScheme.surfaceVariant, child: Padding( - padding: const EdgeInsets.fromLTRB(5, 10, 5, 10), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${uploader==comment.name?"(上传者)":""}${comment.name}",style: const TextStyle(fontSize: 16,fontWeight: FontWeight.w500),), const SizedBox(height: 2,), - CustomSelectableText(text: comment.content) + SelectableText(comment.content), + const SizedBox(height: 4,), + Align( + alignment: Alignment.centerRight, + child: Text(TimeExtension.parseEhTime(comment.time).toCompareString, style: const TextStyle(fontSize: 12),), + ) ], ), ), diff --git a/lib/views/eh_views/eh_search_page.dart b/lib/views/eh_views/eh_search_page.dart index 506480a5..7bbdcfe4 100644 --- a/lib/views/eh_views/eh_search_page.dart +++ b/lib/views/eh_views/eh_search_page.dart @@ -17,12 +17,18 @@ class SearchPageComicsList extends ComicsPage{ final String keyword; final Widget? head_; final PageData data; - const SearchPageComicsList(this.keyword, this.data, {this.head_, super.key}); + final int? fCats; + final int? startPages; + final int? endPages; + final int? minStars; + const SearchPageComicsList(this.keyword, this.data, + {this.fCats, this.startPages, this.endPages, this.minStars, this.head_, super.key}); @override Future>> getComics(int i) async{ if(data.galleries == null){ - var res = await EhNetwork().search(keyword); + var res = await EhNetwork().search(keyword, fCats: fCats, + startPages: startPages, endPages: endPages, minStars: minStars); if(res.error){ return Res(null, errorMessage: res.errorMessage); }else{ @@ -73,7 +79,12 @@ class SearchPageComicsList extends ComicsPage{ class EhSearchPage extends StatefulWidget { final String keyword; - const EhSearchPage(this.keyword, {Key? key}) : super(key: key); + final int? fCats; + final int? startPages; + final int? endPages; + final int? minStars; + const EhSearchPage(this.keyword, {this.fCats, this.startPages, this.endPages + , this.minStars, Key? key}) : super(key: key); @override State createState() => _SearchPageState(); @@ -92,6 +103,10 @@ class _SearchPageState extends State { keyword, data, key: Key(keyword), + fCats: widget.fCats, + startPages: widget.startPages, + endPages: widget.endPages, + minStars: widget.minStars, head_: SliverPersistentHeader( floating: true, delegate: _SliverAppBarDelegate( diff --git a/lib/views/explore_page.dart b/lib/views/explore_page.dart index d35fc33c..6da79117 100644 --- a/lib/views/explore_page.dart +++ b/lib/views/explore_page.dart @@ -10,7 +10,6 @@ import 'package:pica_comic/views/nhentai/nhentai_main_page.dart'; import 'package:pica_comic/views/pic_views/games_page.dart'; import 'package:pica_comic/views/pic_views/home_page.dart'; import 'package:pica_comic/tools/translations.dart'; - import '../foundation/app.dart'; import '../foundation/ui_mode.dart'; @@ -43,35 +42,36 @@ class _ExplorePageState extends State } Widget buildFAB() => Material( - color: Colors.transparent, - child: FloatingActionButton( - key: const Key("FAB"), - child: const Icon(Icons.refresh), - onPressed: () { - int page = controller.index; - var logics = [ + color: Colors.transparent, + child: FloatingActionButton( + key: const Key("FAB"), + child: const Icon(Icons.refresh), + onPressed: () { + int page = controller.index; + var logics = [ () => StateController.find().refresh_(), - if (appdata.settings[24][1] == "1") + if (appdata.settings[24][1] == "1") () => StateController.find().refresh_(), - if (appdata.settings[24][2] == "1") + if (appdata.settings[24][2] == "1") () => StateController.find().refresh_(), - if (appdata.settings[24][3] == "1") + if (appdata.settings[24][3] == "1") () => StateController.find().refresh_(), - if (appdata.settings[24][4] == "1") + if (appdata.settings[24][4] == "1") () => StateController.find().refresh_(), - if (appdata.settings[24][5] == "1") + if (appdata.settings[24][5] == "1") () => StateController.find().refresh_(), - if (appdata.settings[24][6] == "1") + if (appdata.settings[24][6] == "1") () => HitomiHomePageComics.refresh(), - if (appdata.settings[24][7] == "1") - () => StateController.find().refresh_(), - if (appdata.settings[24][9] == "1") + if (appdata.settings[24][7] == "1") + () => StateController.find() + .refresh_(), + if (appdata.settings[24][9] == "1") () => StateController.find().refresh_(), - ]; - logics[page](); - }, - ), - ); + ]; + logics[page](); + }, + ), + ); @override Widget build(BuildContext context) { @@ -80,6 +80,7 @@ class _ExplorePageState extends State Widget tabBar = TabBar( splashBorderRadius: const BorderRadius.all(Radius.circular(10)), isScrollable: true, + tabAlignment: TabAlignment.center, tabs: [ if (appdata.settings[24][0] == "1") Tab( @@ -191,14 +192,15 @@ class _ExplorePageState extends State return Stack( children: [ - Positioned.fill(child: Column( + Positioned.fill( + child: Column( children: [ tabBar, Expanded( child: NotificationListener( onNotification: (notifications) { - if(notifications.metrics.axis == Axis.horizontal){ - if(!showFB){ + if (notifications.metrics.axis == Axis.horizontal) { + if (!showFB) { setState(() { showFB = true; }); @@ -208,11 +210,11 @@ class _ExplorePageState extends State var current = notifications.metrics.pixels; - if((current > location && current != 0) && showFB){ + if ((current > location && current != 0) && showFB) { setState(() { showFB = false; }); - }else if((current < location || current == 0) && !showFB){ + } else if ((current < location || current == 0) && !showFB) { setState(() { showFB = true; }); @@ -224,24 +226,15 @@ class _ExplorePageState extends State child: TabBarView( controller: controller, children: [ - if(appdata.settings[24][0] == "1") - const HomePage(), - if(appdata.settings[24][1] == "1") - const GamesPage(), - if(appdata.settings[24][2] == "1") - const EhHomePage(), - if(appdata.settings[24][3] == "1") - const EhPopularPage(), - if(appdata.settings[24][4] == "1") - const JmHomePage(), - if(appdata.settings[24][5] == "1") - const JmLatestPage(), - if(appdata.settings[24][6] == "1") - const HitomiHomePage(), - if(appdata.settings[24][7] == "1") - const NhentaiHomePage(), - if(appdata.settings[24][9] == "1") - const HtHomePage() + if (appdata.settings[24][0] == "1") const HomePage(), + if (appdata.settings[24][1] == "1") const GamesPage(), + if (appdata.settings[24][2] == "1") const EhHomePage(), + if (appdata.settings[24][3] == "1") const EhPopularPage(), + if (appdata.settings[24][4] == "1") const JmHomePage(), + if (appdata.settings[24][5] == "1") const JmLatestPage(), + if (appdata.settings[24][6] == "1") const HitomiHomePage(), + if (appdata.settings[24][7] == "1") const NhentaiHomePage(), + if (appdata.settings[24][9] == "1") const HtHomePage() ], ), ), @@ -255,8 +248,9 @@ class _ExplorePageState extends State duration: const Duration(milliseconds: 150), reverseDuration: const Duration(milliseconds: 150), child: showFB ? buildFAB() : const SizedBox(), - transitionBuilder: (widget, animation){ - var tween = Tween(begin: const Offset(0,1), end: const Offset(0,0)); + transitionBuilder: (widget, animation) { + var tween = Tween( + begin: const Offset(0, 1), end: const Offset(0, 0)); return SlideTransition( position: tween.animate(animation), child: widget, diff --git a/lib/views/hitomi_views/hitomi_comic_page.dart b/lib/views/hitomi_views/hitomi_comic_page.dart index 1039bfe0..567af6c6 100644 --- a/lib/views/hitomi_views/hitomi_comic_page.dart +++ b/lib/views/hitomi_views/hitomi_comic_page.dart @@ -142,7 +142,6 @@ class HitomiComicPage extends ComicPage { @override void tapOnTags(String tag) { - tag = tag.replaceAll(" ", "_"); MainPage.to(() => HitomiSearchPage(tag)); } diff --git a/lib/views/main_page.dart b/lib/views/main_page.dart index 14c7be86..6d9ad136 100644 --- a/lib/views/main_page.dart +++ b/lib/views/main_page.dart @@ -59,7 +59,7 @@ class MainPage extends StatefulWidget { Navigator.of(navigatorKey?.currentContext ?? App.globalContext!).canPop(); static void back() { - if(canPop()) { + if (canPop()) { navigatorKey?.currentState?.pop(); } } @@ -100,7 +100,8 @@ class _MainPageState extends State { set i(int value) { _i = value; - Navigator.popUntil(MainPage.navigatorKey!.currentContext!, (route) => route.isFirst); + Navigator.popUntil( + MainPage.navigatorKey!.currentContext!, (route) => route.isFirst); } final pages = [ @@ -231,11 +232,7 @@ class _MainPageState extends State { appdata.writeData(); } //清除未正常退出时的下载通知 - try { - notifications.endProgress(); - } catch (e) { - //不清楚清除一个不存在的通知会不会引发错误 - } + notifications.cancelAll(); //检查是否打卡 if (appdata.user.isPunched == false && appdata.settings[6] == "1") { if (App.isMobile) { @@ -293,7 +290,8 @@ class _MainPageState extends State { Expanded( child: ClipRect( child: Navigator( - key: (MainPage.navigatorKey ?? (MainPage.navigatorKey = GlobalKey())), + key: (MainPage.navigatorKey ?? + (MainPage.navigatorKey = GlobalKey())), onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) { return Column( @@ -530,8 +528,8 @@ class _NavigateBarState extends State { "排行榜".tl, i == 3, () => setState(() => i = 3)), const Divider(), const Spacer(), - NavigatorItem(Icons.construction, Icons.construction, "工具".tl, false, - openTool), + NavigatorItem(Icons.construction, Icons.construction, "工具".tl, + false, openTool), NavigatorItem(Icons.search, Icons.search, "搜索".tl, false, () => MainPage.to(() => PreSearchPage())), NavigatorItem( @@ -561,8 +559,7 @@ class _NavigateBarState extends State { children: [ const Flexible( child: IconButton( - icon: Icon(Icons.construction), - onPressed: openTool), + icon: Icon(Icons.construction), onPressed: openTool), ), Flexible( child: IconButton( diff --git a/lib/views/page_template/comic_page.dart b/lib/views/page_template/comic_page.dart index f4802657..9c3afe7b 100644 --- a/lib/views/page_template/comic_page.dart +++ b/lib/views/page_template/comic_page.dart @@ -71,10 +71,11 @@ class ComicPageLogic extends StateController { bool favorite = false; History? history; - void get(Future> Function() loadData, Future Function(T) loadFavorite, String id) async { + void get(Future> Function() loadData, + Future Function(T) loadFavorite, String id) async { var res = await loadData(); if (res.error) { - if(res.errorMessage == "Exit"){ + if (res.errorMessage == "Exit") { return; } message = res.errorMessage; @@ -94,8 +95,8 @@ class ComicPageLogic extends StateController { update(); } - updateHistory(History? newHistory){ - if(newHistory != null) { + updateHistory(History? newHistory) { + if (newHistory != null) { history = newHistory; update(); } @@ -107,7 +108,8 @@ abstract class ComicPage extends StatelessWidget { /// and allow user to download or read comic. const ComicPage({super.key}); - ComicPageLogic get _logic => StateController.find>(tag: tag); + ComicPageLogic get _logic => + StateController.find>(tag: tag); /// title String? get title; @@ -194,11 +196,12 @@ abstract class ComicPage extends StatelessWidget { String? get url => null; /// callback when a thumbnail is tapped - void onThumbnailTapped(int index){} + void onThumbnailTapped(int index) {} ActionFunc? get searchSimilar => null; - Widget thumbnailImageBuilder(int index, String imageUrl) => _thumbnailImageBuilder(index); + Widget thumbnailImageBuilder(int index, String imageUrl) => + _thumbnailImageBuilder(index); /// The source of this comic, displayed at the beginning of the [title], /// can be translated into the user's language. @@ -209,7 +212,7 @@ abstract class ComicPage extends StatelessWidget { FavoriteItem toLocalFavoriteItem(); - void scrollListener(){ + void scrollListener() { try { var logic = _logic; bool temp = logic.showAppbarTitle; @@ -218,14 +221,13 @@ abstract class ComicPage extends StatelessWidget { } logic.showAppbarTitle = logic.controller.position.pixels > boundingTextSize(title!, const TextStyle(fontSize: 22), - maxWidth: logic.width!) - .height + + maxWidth: logic.width!) + .height + 50; if (temp != logic.showAppbarTitle) { logic.update(); } - } - catch(e){ + } catch (e) { return; } } @@ -234,7 +236,7 @@ abstract class ComicPage extends StatelessWidget { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints){ + return LayoutBuilder(builder: (context, constraints) { return Scaffold( body: StateBuilder>( tag: tag, @@ -242,7 +244,7 @@ abstract class ComicPage extends StatelessWidget { initState: (logic) { tagsStack.push(_logic); }, - dispose: (logic){ + dispose: (logic) { tagsStack.pop(); }, builder: (logic) { @@ -284,25 +286,32 @@ abstract class ComicPage extends StatelessWidget { message: "更多".tl, child: IconButton( icon: const Icon(Icons.more_horiz), - onPressed: (){ + onPressed: () { showMenu( context: context, - position: RelativeRect.fromLTRB( - MediaQuery.of(context).size.width, 0, - MediaQuery.of(context).size.width, 0), + position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, + 0, MediaQuery.of(context).size.width, 0), items: [ - PopupMenuItem(child: Text("分享".tl), - onTap: ()=>Share.share(title! + (url??"")),), - PopupMenuItem(child: Text("复制标题".tl), - onTap: ()=>Clipboard.setData(ClipboardData(text: title!)),), - if(url != null) - PopupMenuItem(child: Text("复制链接".tl), - onTap: ()=>Clipboard.setData(ClipboardData(text: url!)),), - if(url != null) - PopupMenuItem(child: Text("在浏览器中打开".tl), - onTap: ()=>launchUrlString(url!)), - if(searchSimilar != null) - PopupMenuItem(onTap: searchSimilar!,child: Text("搜索相似画廊".tl)), + PopupMenuItem( + child: Text("分享".tl), + onTap: () => Share.share(title! + (url ?? "")), + ), + PopupMenuItem( + child: Text("复制标题".tl), + onTap: () => Clipboard.setData(ClipboardData(text: title!)), + ), + if (url != null) + PopupMenuItem( + child: Text("复制链接".tl), + onTap: () => Clipboard.setData(ClipboardData(text: url!)), + ), + if (url != null) + PopupMenuItem( + child: Text("在浏览器中打开".tl), + onTap: () => launchUrlString(url!)), + if (searchSimilar != null) + PopupMenuItem( + onTap: searchSimilar!, child: Text("搜索相似画廊".tl)), ]); }, ), @@ -312,25 +321,32 @@ abstract class ComicPage extends StatelessWidget { message: "收藏".tl, child: IconButton( icon: const Icon(Icons.book_outlined), - onPressed: () async{ - if(LocalFavoritesManager().folderNames == null){ + onPressed: () async { + if (LocalFavoritesManager().folderNames == null) { await LocalFavoritesManager().readData(); } - if(!LocalFavoritesManager().folderNames!.contains(appdata.settings[51])){ - showDialog(context: App.globalContext!, builder: (context) => AlertDialog( - title: Text("无效的默认收藏夹".tl), - content: Text("必须设置一个有效的收藏夹才能使用快速收藏".tl), - actions: [ - TextButton(onPressed: (){ - App.globalBack(); - NewSettingsPage.open(0); - }, child: Text("前往设置".tl)) - ], - )); + if (!LocalFavoritesManager() + .folderNames! + .contains(appdata.settings[51])) { + showDialog( + context: App.globalContext!, + builder: (context) => AlertDialog( + title: Text("无效的默认收藏夹".tl), + content: Text("必须设置一个有效的收藏夹才能使用快速收藏".tl), + actions: [ + TextButton( + onPressed: () { + App.globalBack(); + NewSettingsPage.open(0); + }, + child: Text("前往设置".tl)) + ], + )); } else { - LocalFavoritesManager().addComic(appdata.settings[51], toLocalFavoriteItem()); + LocalFavoritesManager() + .addComic(appdata.settings[51], toLocalFavoriteItem()); showMessage(App.globalContext!, "成功添加到默认收藏夹".tl); - if(!_logic.favorite) { + if (!_logic.favorite) { _logic.favorite = true; logic.update(); } @@ -372,12 +388,23 @@ abstract class ComicPage extends StatelessWidget { } Widget buildSubTitle(BuildContext context) { - if(subTitle == null || subTitle == ""){ - return const SliverToBoxAdapter(child: SizedBox(height: 0,),); + if (subTitle == null || subTitle == "") { + return const SliverToBoxAdapter( + child: SizedBox( + height: 0, + ), + ); } return SliverPadding( - padding: UiMode.m1(context) ? const EdgeInsets.fromLTRB(10, 0, 10, 8) : const EdgeInsets.fromLTRB(20, 0, 20, 8), - sliver: SliverToBoxAdapter(child: SelectableText(subTitle!, style: const TextStyle(fontSize: 18),),), + padding: UiMode.m1(context) + ? const EdgeInsets.fromLTRB(10, 0, 10, 8) + : const EdgeInsets.fromLTRB(20, 0, 20, 8), + sliver: SliverToBoxAdapter( + child: SelectableText( + subTitle!, + style: const TextStyle(fontSize: 18), + ), + ), ); } @@ -389,8 +416,7 @@ abstract class ComicPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildCover( - context, logic, 350, _logic.width!), + buildCover(context, logic, 350, _logic.width!), const SizedBox( height: 20, ), @@ -405,8 +431,7 @@ abstract class ComicPage extends StatelessWidget { width: _logic.width!, child: Row( children: [ - buildCover( - context, logic, 550, _logic.width! / 2), + buildCover(context, logic, 550, _logic.width! / 2), SizedBox( width: _logic.width! / 2, child: Column( @@ -427,12 +452,11 @@ abstract class ComicPage extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16), child: SizedBox( - width: width-32, + width: width - 32, height: height - 32, - child: RoundedImage(image: CachedNetworkImageProvider( - cover, - headers: headers - ),), + child: RoundedImage( + image: CachedNetworkImageProvider(cover, headers: headers), + ), ), ), onTap: () => App.globalTo(() => ShowImagePage(cover)), @@ -455,7 +479,7 @@ abstract class ComicPage extends StatelessWidget { text = "未知".tl; } - List> buildPopMenus(){ + List> buildPopMenus() { return [ PopupMenuItem( child: Text("复制".tl), @@ -477,13 +501,13 @@ abstract class ComicPage extends StatelessWidget { child: Text("收藏".tl), onTap: () { var res = source.tlEN; - if(source == "EHentai"){ + if (source == "EHentai") { res += ":$key"; } - if(source == "Nhentai" && key == "Artists"){ + if (source == "Nhentai" && key == "Artists") { res += ":Artist"; } - if(text.contains(" ")){ + if (text.contains(" ")) { res += ":\"$text\""; } else { res += ":$text"; @@ -512,7 +536,7 @@ abstract class ComicPage extends StatelessWidget { ? colorScheme.primaryContainer : colorScheme.surfaceVariant, borderRadius: const BorderRadius.all(Radius.circular(12))), - margin: EdgeInsets.fromLTRB(3*size, 3*size, 3*size, 3*size), + margin: EdgeInsets.fromLTRB(3 * size, 3 * size, 3 * size, 3 * size), child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(12)), onTap: title ? null : () => tapOnTags(text), @@ -532,7 +556,8 @@ abstract class ComicPage extends StatelessWidget { child: enableTranslationToCN ? (title ? Text(text.translateTagsCategoryToCN) - : Text(TagsTranslation.translationTagWithNamespace(text, key))) + : Text( + TagsTranslation.translationTagWithNamespace(text, key))) : Text(text), ), ), @@ -575,27 +600,36 @@ abstract class ComicPage extends StatelessWidget { ), )); - if(logic.history != null && logic.history!.ep != 0) { + if (logic.history != null && logic.history!.ep != 0) { res2.add(Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 8), height: 38, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.tertiaryContainer, - borderRadius: const BorderRadius.all(Radius.circular(8)) - ), + color: Theme.of(context).colorScheme.tertiaryContainer, + borderRadius: const BorderRadius.all(Radius.circular(8))), child: Row( children: [ - const SizedBox(width: 8,), - const Icon(Icons.history, size: 24,), - const SizedBox(width: 4,), + const SizedBox( + width: 8, + ), + const Icon( + Icons.history, + size: 24, + ), + const SizedBox( + width: 4, + ), Text("上次阅读到第 @ep 章第 @page 页".tlParams({ "ep": logic.history!.ep.toString(), "page": logic.history!.page.toString() })), const Spacer(), - TextButton(onPressed: () => continueRead(logic.history!), + TextButton( + onPressed: () => continueRead(logic.history!), child: Text("继续阅读".tl)), - const SizedBox(width: 8,) + const SizedBox( + width: 8, + ) ], ), )); @@ -664,16 +698,20 @@ abstract class ComicPage extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(16)), child: Card( elevation: 1, - color: (_logic.history?.readEpisode ?? const {}).contains(i+1) ? - colorScheme.secondaryContainer.withOpacity(0.8) - : colorScheme.secondaryContainer, + color: + (_logic.history?.readEpisode ?? const {}).contains(i + 1) + ? colorScheme.secondaryContainer.withOpacity(0.8) + : colorScheme.secondaryContainer, margin: EdgeInsets.zero, child: Center( - child: Text(eps!.eps[i], - style: TextStyle(color: - (_logic.history?.readEpisode ?? const {}).contains(i+1) ? - colorScheme.outline - : null),), + child: Text( + eps!.eps[i], + style: TextStyle( + color: (_logic.history?.readEpisode ?? const {}) + .contains(i + 1) + ? colorScheme.outline + : null), + ), ), ), onTap: () => eps!.onTap(i), @@ -728,20 +766,23 @@ abstract class ComicPage extends StatelessWidget { ]; } - Widget _thumbnailImageBuilder(int index){ + Widget _thumbnailImageBuilder(int index) { return CachedNetworkImage( imageUrl: thumbnails!.thumbnails[index], httpHeaders: headers, fit: BoxFit.contain, - placeholder: (context, s) => ColoredBox( - color: Theme.of(context).colorScheme.surfaceVariant), + placeholder: (context, s) => + ColoredBox(color: Theme.of(context).colorScheme.surfaceVariant), errorWidget: (context, s, d) => const Icon(Icons.error), ); } List buildThumbnails(BuildContext context) { - if (thumbnails == null || (thumbnails!.thumbnails.isEmpty && !tag.contains("Hitomi") && !tag.contains("Eh"))) return []; - if(thumbnails!.thumbnails.isEmpty){ + if (thumbnails == null || + (thumbnails!.thumbnails.isEmpty && + !tag.contains("Hitomi") && + !tag.contains("Eh"))) return []; + if (thumbnails!.thumbnails.isEmpty) { thumbnails!.get(update); } return [ @@ -762,9 +803,10 @@ abstract class ComicPage extends StatelessWidget { const SizedBox( width: 20, ), - Text( + Text( "预览".tl, - style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + style: const TextStyle( + fontWeight: FontWeight.w500, fontSize: 16), ) ], )), @@ -785,12 +827,14 @@ abstract class ComicPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Expanded(child: InkWell( + Expanded( + child: InkWell( onTap: () => onThumbnailTapped(index), borderRadius: const BorderRadius.all(Radius.circular(16)), child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16)), + borderRadius: + const BorderRadius.all(Radius.circular(16)), border: Border.all( color: Theme.of(context).colorScheme.outline, ), @@ -798,13 +842,17 @@ abstract class ComicPage extends StatelessWidget { width: double.infinity, height: double.infinity, child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: thumbnailImageBuilder(index, thumbnails!.thumbnails[index]), + borderRadius: + const BorderRadius.all(Radius.circular(16)), + child: thumbnailImageBuilder( + index, thumbnails!.thumbnails[index]), ), ), )), - const SizedBox(height: 4,), - Text((index+1).toString()), + const SizedBox( + height: 4, + ), + Text((index + 1).toString()), ], ), ); @@ -869,12 +917,9 @@ abstract class ComicPage extends StatelessWidget { } void favoriteComic(FavoriteComicWidget widget) { - if(UiMode.m1(context)){ - showModalBottomSheet( - context: context, - builder: (context) => widget - ); - }else{ + if (UiMode.m1(context)) { + showModalBottomSheet(context: context, builder: (context) => widget); + } else { showSideBar(context, widget, title: "收藏漫画".tl, useSurfaceTintColor: true); } } @@ -889,7 +934,7 @@ class FavoriteComicWidget extends StatefulWidget { this.foldersLoader, this.selectFolderCallback, this.initialFolder, - this.favoriteOnPlatform=false, + this.favoriteOnPlatform = false, this.cancelPlatformFavorite, required this.setFavorite, super.key}); @@ -939,11 +984,11 @@ class _FavoriteComicWidgetState extends State { @override void initState() { - LocalFavoritesManager().find(widget.target).then((folder){ - Future.microtask(() => setState(()=>addedFolders = folder)); + LocalFavoritesManager().find(widget.target).then((folder) { + Future.microtask(() => setState(() => addedFolders = folder)); }); selectID = widget.initialFolder; - if(!widget.havePlatformFavorite){ + if (!widget.havePlatformFavorite) { page = 1; } folders = widget.folders; @@ -952,41 +997,48 @@ class _FavoriteComicWidgetState extends State { @override Widget build(BuildContext context) { - assert(widget.havePlatformFavorite || page!=0); + assert(widget.havePlatformFavorite || page != 0); - Widget buildFolder(String name, String id, int p){ + Widget buildFolder(String name, String id, int p) { return InkWell( onTap: () => setState(() { - selectID=id; - page=p; + selectID = id; + page = p; }), child: SizedBox( - height: 80, + height: 56, width: double.infinity, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Row( children: [ - Icon(Icons.folder, size: 30, color: Theme.of(context).colorScheme.secondary,), - const SizedBox(width: 12,), + Icon( + Icons.folder, + size: 30, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox( + width: 12, + ), Text(name), - if(addedFolders.contains(name) && p == 1) - const SizedBox(width: 12,), - if(addedFolders.contains(name) && p == 1) - Container( + if (addedFolders.contains(name) && p == 1) + const SizedBox( + width: 12, + ), + if (addedFolders.contains(name) && p == 1) + Container( width: 60, height: 30, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.tertiaryContainer, - borderRadius: const BorderRadius.all(Radius.circular(8)) - ), + color: Theme.of(context).colorScheme.tertiaryContainer, + borderRadius: + const BorderRadius.all(Radius.circular(8))), child: Center( child: Text("已收藏".tl), ), ), const Spacer(), - if(selectID == id) - const AnimatedCheckIcon() + if (selectID == id) const AnimatedCheckIcon() ], ), ), @@ -999,9 +1051,9 @@ class _FavoriteComicWidgetState extends State { width: 120, child: FilledButton( child: Text("收藏".tl), - onPressed: (){ + onPressed: () { hideMessage(context); - if(selectID != null){ + if (selectID != null) { widget.setFavorite(true); App.globalBack(); widget.selectFolderCallback?.call(selectID!, page); @@ -1012,21 +1064,25 @@ class _FavoriteComicWidgetState extends State { Widget platform = SingleChildScrollView( child: Column( - children: List.generate(folders.length, (index) => - buildFolder(folders.values.elementAt(index), folders.keys.elementAt(index), 0)), + children: List.generate( + folders.length, + (index) => buildFolder(folders.values.elementAt(index), + folders.keys.elementAt(index), 0)), ), ); - if(widget.favoriteOnPlatform){ - platform = Center(child: Text("已收藏".tl),); - if(page == 0) { + if (widget.favoriteOnPlatform) { + platform = Center( + child: Text("已收藏".tl), + ); + if (page == 0) { button = SizedBox( height: 35, width: 120, child: FilledButton( - onPressed: (){ + onPressed: () { hideMessage(context); - if(addedFolders.isEmpty){ + if (addedFolders.isEmpty) { widget.setFavorite(false); } App.globalBack(); @@ -1038,36 +1094,37 @@ class _FavoriteComicWidgetState extends State { } } - if(page == 1 && addedFolders.contains(selectID)){ + if (page == 1 && addedFolders.contains(selectID)) { button = SizedBox( height: 35, width: 120, child: FilledButton( - onPressed: (){ + onPressed: () { hideMessage(context); App.globalBack(); - if(addedFolders.length == 1 && !widget.favoriteOnPlatform){ + if (addedFolders.length == 1 && !widget.favoriteOnPlatform) { widget.setFavorite(false); } - LocalFavoritesManager().deleteComicWithTarget(selectID!, widget.target); + LocalFavoritesManager() + .deleteComicWithTarget(selectID!, widget.target); }, child: const Text("取消收藏"), ), ); - } - - else if(widget.havePlatformFavorite && widget.needLoadFolderData && !loadedData){ - widget.foldersLoader!.call().then((res){ - if(res.error){ + } else if (widget.havePlatformFavorite && + widget.needLoadFolderData && + !loadedData) { + widget.foldersLoader!.call().then((res) { + if (res.error) { showMessage(App.globalContext, res.errorMessageWithoutNull); - }else{ + } else { setState(() { loadedData = true; folders = res.data; }); } }); - platform = const Center( + platform = const Center( child: CircularProgressIndicator(), ); } @@ -1076,12 +1133,12 @@ class _FavoriteComicWidgetState extends State { var localFolders = LocalFavoritesManager().folderNames; - if(localFolders == null){ - LocalFavoritesManager().readData().then((value) => setState(()=>{})); + if (localFolders == null) { + LocalFavoritesManager().readData().then((value) => setState(() => {})); local = const SizedBox(); - }else{ - var children = List.generate(localFolders.length, (index) => - buildFolder(localFolders[index], localFolders[index], 1)); + } else { + var children = List.generate(localFolders.length, + (index) => buildFolder(localFolders[index], localFolders[index], 1)); children.add(SizedBox( height: 56, width: double.infinity, @@ -1091,18 +1148,24 @@ class _FavoriteComicWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ Text("新建".tl), - const SizedBox(width: 4,), + const SizedBox( + width: 4, + ), const Icon(Icons.add), ], ), - onPressed: () => showDialog(context: App.globalContext!, - builder: (_) => const CreateFolderDialog()).then((value) => setState((){})), + onPressed: () => showDialog( + context: App.globalContext!, + builder: (_) => const CreateFolderDialog()) + .then((value) => setState(() {})), ), ), )); - local = SingleChildScrollView(child: Column( - children: children, - ),); + local = SingleChildScrollView( + child: Column( + children: children, + ), + ); } return DefaultTabController( @@ -1110,34 +1173,32 @@ class _FavoriteComicWidgetState extends State { child: Column( children: [ TabBar( - onTap: (i){ - setState(() { - if(i == 0){ - selectID = widget.initialFolder; - }else{ - selectID = null; - } - page = i; - if(!widget.havePlatformFavorite){ - page = 1; - } - }); - }, - tabs: [ - if (widget.havePlatformFavorite) + onTap: (i) { + setState(() { + if (i == 0) { + selectID = widget.initialFolder; + } else { + selectID = null; + } + page = i; + if (!widget.havePlatformFavorite) { + page = 1; + } + }); + }, + tabs: [ + if (widget.havePlatformFavorite) + Tab( + text: "网络".tl, + ), Tab( - text: "网络".tl, + text: "本地".tl, ), - Tab( - text: "本地".tl, - ), - - ]), + ]), Expanded( child: TabBarView( children: [ - if(widget.havePlatformFavorite) - platform, + if (widget.havePlatformFavorite) platform, local, ], ), @@ -1158,7 +1219,7 @@ class _FavoriteComicWidgetState extends State { class RoundedImage extends StatefulWidget { const RoundedImage({required this.image, super.key}); - + final ImageProvider image; @override @@ -1178,19 +1239,19 @@ class _RoundedImageState extends State { @override Widget build(BuildContext context) { - if(failed){ + if (failed) { return const Center( child: Icon(Icons.error), ); } - if(image == null){ + if (image == null) { return const SizedBox( child: Center( child: CircularProgressIndicator(), ), ); - }else{ + } else { return CustomPaint( painter: _RoundedImagePainter(image: image!, borderRadius: 16), child: const SizedBox( @@ -1200,18 +1261,18 @@ class _RoundedImageState extends State { ); } } - - void _loadImage() async{ + + void _loadImage() async { final imageStream = widget.image.resolve(ImageConfiguration.empty); var listener = ImageStreamListener((imageInfo, _) { - if(mounted) { + if (mounted) { setState(() { image = imageInfo.image; }); } - }, onError: (error, stack){ - if(kDebugMode){ + }, onError: (error, stack) { + if (kDebugMode) { print("$error\n$stack"); } setState(() { @@ -1223,7 +1284,6 @@ class _RoundedImageState extends State { } } - class _RoundedImagePainter extends CustomPainter { final ui.Image image; final double borderRadius; @@ -1253,19 +1313,22 @@ class _RoundedImagePainter extends CustomPainter { Rect drawRect = Offset(xOffset, yOffset) & Size(drawWidth, drawHeight); // Create a rounded rectangle path - RRect roundedRect = RRect.fromRectAndRadius(drawRect, Radius.circular(borderRadius)); + RRect roundedRect = + RRect.fromRectAndRadius(drawRect, Radius.circular(borderRadius)); Path clipPath = Path()..addRRect(roundedRect); // Clip the canvas with the rounded rectangle path canvas.clipPath(clipPath); // Draw the image within the clipped area - Rect srcRect = Rect.fromLTRB(0, 0, image.width.toDouble(), image.height.toDouble()); + Rect srcRect = + Rect.fromLTRB(0, 0, image.width.toDouble(), image.height.toDouble()); canvas.drawImageRect(image, srcRect, drawRect, Paint()); } @override bool shouldRepaint(_RoundedImagePainter oldDelegate) { - return image != oldDelegate.image || borderRadius != oldDelegate.borderRadius; + return image != oldDelegate.image || + borderRadius != oldDelegate.borderRadius; } -} \ No newline at end of file +} diff --git a/lib/views/pre_search_page.dart b/lib/views/pre_search_page.dart index d0fee7b7..cb19282f 100644 --- a/lib/views/pre_search_page.dart +++ b/lib/views/pre_search_page.dart @@ -13,6 +13,7 @@ import 'package:pica_comic/views/jm_views/jm_search_page.dart'; import 'package:pica_comic/views/nhentai/search_page.dart'; import 'package:pica_comic/views/pic_views/search_page.dart'; import 'package:pica_comic/views/widgets/custom_chips.dart'; +import 'package:pica_comic/views/widgets/select.dart'; import 'package:pica_comic/views/widgets/show_error.dart'; import 'package:pica_comic/views/widgets/show_message.dart'; import '../base.dart'; @@ -32,7 +33,8 @@ class _FloatingSearchBar extends StatelessWidget { required this.f, required this.controller, this.onChanged, - this.focusNode}) + this.focusNode, + required this.showMenu}) : super(key: key); final double height = 56; @@ -45,6 +47,7 @@ class _FloatingSearchBar extends StatelessWidget { final TextEditingController controller; final void Function(String)? onChanged; final FocusNode? focusNode; + final void Function() showMenu; @override Widget build(BuildContext context) { @@ -70,29 +73,33 @@ class _FloatingSearchBar extends StatelessWidget { ), ), Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: TextField( - cursorColor: colorScheme.primary, - style: textTheme.bodyLarge, - textAlignVertical: TextAlignVertical.center, - controller: controller, - onChanged: onChanged, - focusNode: focusNode, - decoration: InputDecoration( - isCollapsed: true, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - hintText: supportingText, - hintStyle: textTheme.bodyLarge?.apply( - color: colorScheme.onSurfaceVariant, - ), + child: TextField( + cursorColor: colorScheme.primary, + style: textTheme.bodyLarge, + textAlignVertical: TextAlignVertical.center, + controller: controller, + onChanged: onChanged, + focusNode: focusNode, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: supportingText, + hintStyle: textTheme.bodyLarge?.apply( + color: colorScheme.onSurfaceVariant, ), - textInputAction: TextInputAction.search, - onSubmitted: f, ), + textInputAction: TextInputAction.search, + onSubmitted: f, ), ), + if(MediaQuery.of(context).size.width <= 950) + Tooltip( + message: "menu", + child: IconButton( + icon: const Icon(Icons.menu), + onPressed: showMenu, + ), + ) ]), ), ), @@ -108,6 +115,14 @@ class PreSearchController extends StateController { var suggestions = >[]; + // eh advanced options + int ehFCats = 0; + int? ehStartPage; + int? ehEndPage; + int? ehMinStars; + + String? language; + void updateTarget(int i) { target = i; update(); @@ -138,13 +153,18 @@ class PreSearchPage extends StatelessWidget { final FocusNode _focusNode = FocusNode(); void search([String? s, int? type]) { - final keyword = (s ?? controller.text).trim(); + var keyword = (s ?? controller.text).trim(); + if(searchController.language != null && [1,5].contains(searchController.target)){ + keyword += " language:${searchController.language}"; + } switch (type ?? searchController.target) { case 0: MainPage.to(() => SearchPage(keyword)); break; case 1: - MainPage.to(() => EhSearchPage(keyword)); + MainPage.to(() => EhSearchPage(keyword, fCats: searchController.ehFCats, + startPages: searchController.ehStartPage, endPages: searchController.ehEndPage, + minStars: searchController.ehMinStars,)); break; case 2: MainPage.to(() => JmSearchPage(keyword)); @@ -205,13 +225,24 @@ class PreSearchPage extends StatelessWidget { find(TagsTranslation.cosplayerTags, TranslationType.cosplayer); } + void showMenu(){ + scaffoldKey.currentState!.openEndDrawer(); + } + + final scaffoldKey = GlobalKey(); + @override Widget build(BuildContext context) { return Scaffold( + key: scaffoldKey, + endDrawerEnableOpenDragGesture: false, floatingActionButton: FloatingActionButton( onPressed: search, child: const Icon(Icons.search), ), + endDrawer: Drawer( + child: buildDrawer(), + ), body: Column( children: [ if (UiMode.m1(context)) @@ -230,6 +261,7 @@ class PreSearchPage extends StatelessWidget { searchController.update([1, 100]); }, focusNode: _focusNode, + showMenu: showMenu, ), const SizedBox( height: 8, @@ -240,6 +272,26 @@ class PreSearchPage extends StatelessWidget { ); } + Widget buildDrawer(){ + return DefaultTabController(length: 2, child: Column( + children: [ + SizedBox( + height: MediaQuery.of(App.globalContext!).padding.top, + ), + const TabBar(tabs: [ + Tab(icon: Icon(Icons.favorite), key: Key("1"),), + Tab(icon: Icon(Icons.history), key: Key("1"),), + ]), + Expanded( + child: TabBarView(children: [ + buildFavoriteSideBar(), + buildHistorySideBar() + ]), + ), + ], + )); + } + Widget buildBody(BuildContext context) { var widget = StateBuilder( id: 100, @@ -259,8 +311,8 @@ class PreSearchPage extends StatelessWidget { } Widget buildMainView(BuildContext context) { - final showSideBar = MediaQuery.of(context).size.width > 900; - var addWidth = (MediaQuery.of(context).size.width - 900) * 0.25; + final showSideBar = MediaQuery.of(context).size.width > 950; + var addWidth = (MediaQuery.of(context).size.width - 950) * 0.25; addWidth = addWidth.clamp(0, 50); return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -269,7 +321,7 @@ class PreSearchPage extends StatelessWidget { SizedBox( width: 250 + addWidth, height: double.infinity, - child: buildHistorySideBar(context), + child: buildHistorySideBar(), ), if (showSideBar) const VerticalDivider(), Expanded( @@ -287,10 +339,8 @@ class PreSearchPage extends StatelessWidget { title: Text("搜索选项".tl), ), buildTargetSelector(context), - buildModeSelector(context), + buildSearchOptions(context), buildHotSearch(context), - if (!showSideBar) buildFavorite(context), - if (!showSideBar) buildHistory(context), SizedBox(height: MediaQuery.of(context).padding.bottom,), ], ), @@ -324,7 +374,7 @@ class PreSearchPage extends StatelessWidget { return StateBuilder( builder: (logic) { - void onSelected(String text, TranslationType? type, [bool? male]) { + void onSelected(String text, TranslationType? type) { var words = controller.text.split(" "); if (words.length >= 2 && check("${words[words.length - 2]} ${words[words.length - 1]}", @@ -336,8 +386,9 @@ class PreSearchPage extends StatelessWidget { controller.text.replaceLast(words[words.length - 1], ""); } if (text.contains(" ")) { - if (logic.target == 3) { + if (logic.target == 3 && ["male", "female", "language"].contains(type?.name)) { text = text.replaceAll(" ", '_'); + text = "${type?.name}:$text"; } else { text = "\"$text\""; } @@ -517,7 +568,7 @@ class PreSearchPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text("目标".tl), + Text("目标".tl, style: Theme.of(context).textTheme.bodyLarge,), Wrap( children: [ buildItem(logic, 0, "Picacg"), @@ -533,7 +584,8 @@ class PreSearchPage extends StatelessWidget { if (appdata.settings[21][5] == "1") buildItem(logic, 5, "Nhentai"), ], - ) + ), + const SizedBox(height: 8,) ], ), ); @@ -541,7 +593,7 @@ class PreSearchPage extends StatelessWidget { ); } - Widget buildModeSelector(BuildContext context) { + Widget buildSearchOptions(BuildContext context) { List buildPicacg(PreSearchController logic) { Widget buildItem(String text, int index) => Padding( padding: const EdgeInsets.all(5), @@ -606,19 +658,142 @@ class PreSearchPage extends StatelessWidget { ]; } + Widget buildLangSelector(){ + const languages = ["chinese", "japanese", "english"]; + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + const SizedBox(width: 8,), + Text("语言".tl), + const SizedBox(width: 16,), + Select( + initialValue: languages.indexOf(searchController.language ?? ""), + whenChange: (i) => searchController.language = languages[i], + values: languages, + outline: true, + ), + ], + ), + ); + } + + Widget buildEH(){ + Widget buildCategoryItem(String title, int value, double width){ + bool disabled = searchController.ehFCats & (1 << value) == 1 << value; + return AnimatedContainer( + duration: const Duration(milliseconds: 120), + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), + width: width, + height: 38, + decoration: BoxDecoration( + color: !disabled ? App.colors(context).tertiaryContainer : App.colors(context).tertiaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(8) + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: (){ + disabled ? + searchController.ehFCats -= (1 << value) : + searchController.ehFCats += (1 << value); + searchController.update(["mode"]); + }, + child: Center( + child: Text(title, style: const TextStyle(fontSize: 14),), + ), + ), + ); + } + + const categories = ["Misc", "Doujinshi", "Manga", "Artist CG", "Game CG", + "Image Set", "Cosplay", "Asian Porn", "Non-H", "Western"]; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("高级选项".tl, style: Theme.of(context).textTheme.bodyLarge,), + LayoutBuilder(builder: (context, constrains) => Wrap( + children: List.generate(categories.length, (index){ + const minWidth = 86; + var items = constrains.maxWidth ~/ minWidth; + return buildCategoryItem(categories[index], index, constrains.maxWidth/items-items); + }), + )), + const SizedBox(height: 8,), + Row( + children: [ + const SizedBox(width: 8,), + const Text("Pages From"), + const SizedBox(width: 16,), + SizedBox( + width: 84, + //height: 38, + child: TextField( + onChanged: (s) => searchController.ehStartPage = int.tryParse(s), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9]")) + ], + ), + ), + const SizedBox(width: 16,), + const Text("To"), + const SizedBox(width: 16,), + SizedBox( + width: 84, + //height: 38, + child: TextField( + onChanged: (s) => searchController.ehEndPage = int.tryParse(s), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9]")) + ], + ), + ), + ], + ), + const SizedBox(height: 12,), + Row( + children: [ + const SizedBox(width: 8,), + Text("最少星星".tl), + const SizedBox(width: 16,), + Select( + initialValue: searchController.ehMinStars, + whenChange: (i) => searchController.ehMinStars = i, + values: const ["0", "1", "2", "3", "4", "5"], + outline: true, + ), + ], + ), + buildLangSelector(), + const SizedBox(height: 8,) + ], + ), + ); + } + return StateBuilder( + id: "mode", builder: (logic) { - if (![0, 2, 5].contains(searchController.target)) { + if (![0, 1, 2, 5].contains(searchController.target)) { return const SizedBox(); } + if(searchController.target == 1){ + return buildEH(); + } + return Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text("漫画排序模式".tl), + Text("漫画排序模式".tl, style: Theme.of(context).textTheme.bodyLarge,), Wrap( children: switch (logic.target) { 0 => buildPicacg(logic), @@ -626,7 +801,10 @@ class PreSearchPage extends StatelessWidget { 5 => buildNhentai(logic), _ => throw UnimplementedError() }, - ) + ), + if(logic.target == 5) + buildLangSelector(), + const SizedBox(height: 8,) ], ), ); @@ -703,167 +881,7 @@ class PreSearchPage extends StatelessWidget { ]; } - Widget buildHistory(BuildContext context) { - buildClearButton() { - if (appdata.searchHistory.isNotEmpty) { - return Row(children: [ - Padding( - padding: const EdgeInsets.only(top: 8, left: 4), - child: InkWell( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - onTap: () { - appdata.searchHistory.clear(); - appdata.writeHistory(); - searchController.update(); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - color: Theme.of(context).colorScheme.secondaryContainer), - width: 125, - height: 26, - child: Row( - children: [ - const SizedBox( - width: 5, - ), - const Icon( - Icons.clear_all, - color: Colors.indigo, - ), - Text("清除历史记录".tl) - ], - ), - ), - ), - ) - ]); - } else { - return const SizedBox(); - } - } - - return StateBuilder( - builder: (controller) { - return Card( - elevation: 0, - margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("历史搜索".tl), - Wrap( - children: [ - for (var s in appdata.searchHistory.reversed) - Container( - margin: const EdgeInsets.fromLTRB(4, 4, 4, 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - borderRadius: const BorderRadius.all(Radius.circular(8)) - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(8)), - onTap: () => search(s), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Text(s), - ), - ), - ), - ), - ], - ), - buildClearButton(), - ], - ), - ); - }, - ); - } - - Widget buildFavorite(BuildContext context) { - if (appdata.favoriteTags.isEmpty) { - return const SizedBox(); - } - - return StateBuilder( - builder: (controller) { - var items = []; - for (var s in appdata.favoriteTags){ - var display = s.replaceAll(":", " : "); - items.add(Container( - margin: const EdgeInsets.fromLTRB(4, 4, 4, 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - borderRadius: const BorderRadius.all(Radius.circular(8)) - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - onTap: () { - int type = switch(s.split(':').first){ - "Picacg" => 0, - "EHentai" => 1, - "JMComic" => 2, - "hitomi" => 3, - "HtComic" => 4, - "Nhentai" => 5, - _ => 0 - }; - final keyword = s.substring(s.indexOf(':')+1); - search(keyword, type); - }, - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(display), - const SizedBox(width: 4,), - InkWell( - onTap: (){ - appdata.favoriteTags.remove(s); - searchController.update(); - appdata.writeHistory(); - }, - borderRadius: BorderRadius.circular(12), - child: const Padding( - padding: EdgeInsets.all(3), - child: Icon(Icons.close, size: 16,), - ), - ) - ], - ), - ), - ), - ), - )); - } - - return Card( - elevation: 0, - margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("收藏".tl), - Wrap( - children: items, - ), - ], - ), - ); - }, - ); - } - - Widget buildHistorySideBar(BuildContext context) { + Widget buildHistorySideBar() { return StateBuilder( builder: (logic) => ListView.builder( padding: EdgeInsets.zero, @@ -871,7 +889,7 @@ class PreSearchPage extends StatelessWidget { itemBuilder: (context, index) { if (index == 0) { return ListTile( - leading: const Icon(Icons.history_toggle_off), + leading: const Icon(Icons.history), title: Text("历史搜索".tl), trailing: TextButton( onPressed: () { @@ -959,7 +977,7 @@ class PreSearchPage extends StatelessWidget { const SizedBox( width: 8, ), - Text("热搜".tl), + Text("热搜".tl, style: Theme.of(context).textTheme.bodyLarge,), const Spacer(), const Icon(Icons.arrow_right), const SizedBox( diff --git a/lib/views/search_page.dart b/lib/views/search_page.dart index f1bcef19..020732bc 100644 --- a/lib/views/search_page.dart +++ b/lib/views/search_page.dart @@ -106,7 +106,7 @@ class _SearchPageState extends State { final comicType = widget.type; - void onSelected(String text, TranslationType? type, [bool? male]) { + void onSelected(String text, TranslationType? type) { var words = controller.text.split(" "); if (words.length >= 2 && check("${words[words.length - 2]} ${words[words.length - 1]}", text, @@ -118,8 +118,9 @@ class _SearchPageState extends State { controller.text.replaceLast(words[words.length - 1], ""); } if (text.contains(" ")) { - if (comicType == ComicType.hitomi) { + if (comicType == ComicType.hitomi && ["male", "female", "language"].contains(type?.name)) { text = text.replaceAll(" ", '_'); + text = "${type?.name}:$text"; } else { text = "\"$text\""; } diff --git a/lib/views/settings/local_favorite_settings.dart b/lib/views/settings/local_favorite_settings.dart index 3dc15257..d864ad03 100644 --- a/lib/views/settings/local_favorite_settings.dart +++ b/lib/views/settings/local_favorite_settings.dart @@ -16,33 +16,38 @@ class _LocalFavoritesSettingsState extends State { Widget build(BuildContext context) { return Column( children: [ - FutureBuilder(future: LocalFavoritesManager().readData(), builder: (context, data){ - if(LocalFavoritesManager().folderNames == null){ - return const SizedBox(); - } else { - return ListTile( - leading: const Icon(Icons.book), - title: Text("默认收藏夹".tl), - subtitle: Text("用于快速收藏".tl), - trailing: Select( - initialValue: LocalFavoritesManager().folderNames!.indexOf(appdata.settings[51]), - whenChange: (i) { - appdata.settings[51] = LocalFavoritesManager().folderNames![i]; - appdata.updateSettings(); - }, - values: LocalFavoritesManager().folderNames!, - inPopUpWidget: false, - ), - ); - } - }), + FutureBuilder( + future: LocalFavoritesManager().readData(), + builder: (context, data) { + if (LocalFavoritesManager().folderNames == null) { + return const SizedBox(); + } else { + return ListTile( + leading: const Icon(Icons.book), + title: Text("默认收藏夹".tl), + subtitle: Text("用于快速收藏".tl), + trailing: Select( + initialValue: LocalFavoritesManager() + .folderNames! + .indexOf(appdata.settings[51]), + whenChange: (i) { + appdata.settings[51] = + LocalFavoritesManager().folderNames![i]; + appdata.updateSettings(); + }, + values: LocalFavoritesManager().folderNames!, + inPopUpWidget: false, + ), + ); + } + }), ListTile( leading: const Icon(Icons.bookmark_add), title: Text("新收藏添加至".tl), trailing: Select( - values: ["末尾".tl, "前部".tl], + values: ["最后".tl, "最前".tl], initialValue: int.parse(appdata.settings[53]), - whenChange: (i){ + whenChange: (i) { appdata.settings[53] = i.toString(); appdata.updateSettings(); }, @@ -52,9 +57,9 @@ class _LocalFavoritesSettingsState extends State { leading: const Icon(Icons.move_up), title: Text("阅读后移动本地收藏至".tl), trailing: Select( - values: ["无操作".tl, "末尾".tl, "前部".tl], + values: ["无操作".tl, "最后".tl, "最前".tl], initialValue: int.parse(appdata.settings[54]), - whenChange: (i){ + whenChange: (i) { appdata.settings[54] = i.toString(); appdata.updateSettings(); }, diff --git a/lib/views/show_image_page.dart b/lib/views/show_image_page.dart index 9ce169b0..63ef1896 100644 --- a/lib/views/show_image_page.dart +++ b/lib/views/show_image_page.dart @@ -21,7 +21,6 @@ class ShowImagePage extends StatelessWidget { child: IconButton( icon: const Icon( Icons.download, - color: Colors.white, ), onPressed: () async { saveImage(getImageUrl(url), ""); @@ -31,7 +30,7 @@ class ShowImagePage extends StatelessWidget { Tooltip( message: "分享".tl, child: IconButton( - icon: const Icon(Icons.share, color: Colors.white), + icon: const Icon(Icons.share), onPressed: () async { shareImageFromCache(url, ""); }, diff --git a/lib/views/widgets/comment.dart b/lib/views/widgets/comment.dart index acb87f7e..dd95d9af 100644 --- a/lib/views/widgets/comment.dart +++ b/lib/views/widgets/comment.dart @@ -38,14 +38,14 @@ class CommentTile extends StatelessWidget { return InkWell( borderRadius: const BorderRadius.all(Radius.circular(16)), child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 8, 16), + padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), child: SizedBox( width: double.infinity, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Avatar( - size: 50, + size: 58, avatarUrl: avatarUrl, frame: frameUrl, slogan: slogan, @@ -57,7 +57,6 @@ class CommentTile extends StatelessWidget { width: 8, ), Expanded( - flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/views/widgets/select.dart b/lib/views/widgets/select.dart index 3524a366..1e203dcc 100644 --- a/lib/views/widgets/select.dart +++ b/lib/views/widgets/select.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../foundation/app.dart'; + class Select extends StatefulWidget { const Select({ required this.initialValue, @@ -7,8 +9,9 @@ class Select extends StatefulWidget { required this.whenChange, Key? key, required this.values, - this.inPopUpWidget=false, - this.disabledValues=const [] + bool? inPopUpWidget, + this.disabledValues=const [], + this.outline = false, }) : super(key: key); ///初始值, 提供values的下标 final int? initialValue; @@ -18,8 +21,8 @@ class Select extends StatefulWidget { final double width; ///发生改变时的回调 final void Function(int) whenChange; - final bool inPopUpWidget; final List disabledValues; + final bool outline; @override State { } final renderBox = context.findRenderObject() as RenderBox; var offset = renderBox.localToGlobal(Offset.zero); - var size = widget.inPopUpWidget?Size(550, MediaQuery.of(context).size.height*0.9):MediaQuery.of(context).size; - if(widget.inPopUpWidget){ - offset = Offset( - offset.dx - (MediaQuery.of(context).size.width-size.width)/2, - offset.dy - MediaQuery.of(context).size.height*0.05 - ); - } + var size = MediaQuery.of(context).size; showMenu( - context: context, + context: App.globalContext!, initialValue: value, - position: RelativeRect.fromLTRB(offset.dx+widget.width, offset.dy+20, size.width-offset.dx-widget.width, size.height-offset.dy-20), + position: RelativeRect.fromLTRB(offset.dx, offset.dy+20, offset.dx+widget.width, size.height-offset.dy-20), constraints: BoxConstraints( maxWidth: widget.width, minWidth: widget.width, @@ -70,20 +67,26 @@ class _SelectState extends State