From cd1cc1229e7b38d8a31ee88e5c2b94c3a6884a6b Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 30 Mar 2025 15:42:43 +0800 Subject: [PATCH 01/29] Remove native linux window decoration. --- lib/components/window_frame.dart | 27 +++++++++++++-------------- linux/my_application.cc | 1 + 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/components/window_frame.dart b/lib/components/window_frame.dart index 75c6f5b..cc2c131 100644 --- a/lib/components/window_frame.dart +++ b/lib/components/window_frame.dart @@ -564,20 +564,19 @@ class _VirtualWindowFrameState extends State Widget _buildVirtualWindowFrame(BuildContext context) { return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8), - color: Colors.transparent, - boxShadow: [ - BoxShadow( - color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2), - offset: Offset(0.0, 2), - blurRadius: 4, - ) - ], - ), - clipBehavior: Clip.antiAlias, - child: widget.child, - ); + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8), + color: Colors.transparent, + boxShadow: [ + BoxShadow( + color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2), + blurRadius: 4, + ) + ], + ), + clipBehavior: Clip.antiAlias, + child: widget.child, + ); } @override diff --git a/linux/my_application.cc b/linux/my_application.cc index 274826b..8d3f6d5 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -80,6 +80,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_default_size(window, 1280, 720); GdkVisual* visual; gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + gtk_window_set_decorated(window, FALSE); visual = gdk_screen_get_rgba_visual(screen); if (visual != NULL && gdk_screen_is_composited(screen)) { gtk_widget_set_visual(GTK_WIDGET(window), visual); From ec48dbef5776f7bc80a17a118eb736becd365a95 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 30 Mar 2025 18:23:43 +0800 Subject: [PATCH 02/29] Update linux icon --- debian/gui/venera.png | Bin 2265 -> 65254 bytes pubspec.lock | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/debian/gui/venera.png b/debian/gui/venera.png index ffd7fef165278aeb37cd8dd80cad0e18086422cc..474dba40e2b41ce337ccb8ac1292fd7ccb04708d 100644 GIT binary patch literal 65254 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 Xs=smO2u5M4l69}Ur(2OLDeZp%NeFki literal 2265 zcmV;~2qyQ5P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2yRJ4K~#8N?V5jZ z6y+JmpLg%}ekELz3lf42p^YMx0VBrN>Wrf}*l~m)VdUq4162}8JJXqVrtKdc%1qmt z&h*DfAu(geYLQ5_3jU~AAw8({*{e4Doz5UW$+XZjhB>XsIVXw$9!{>p>1-mvNa=gwCdT-_r>(L zMkQSV*y^8j7Pp@B)}rwRi~#iC^0k4om#Xvj;^BxQj3xkE{P?-sPJbFYI1powFg{7Z$NQ&G>Aq*z zQ_~vp&`<R(4A3Q8IHJS=*NyD91&*r z_!n-(uA!YH2yl}aj`%C}1IN-6@qeFe(GRb}NC5Wj((T33n5$4fb}X@kyRc-zvvaVU zGT#{r5RQfJ_=?VUrl2XiKb~8HT~h)GvMd(qea9oq9im^7};Vj2XF=fz5hP|qgud}0H}aqICVG<&d3&X9-we;>2mhE!obiLLl3wX z3i270%of1!(+7~L`2tLzX8vd9+#*0;?$Axkn!w=BVi!yrHkLM=)`_XHRDd6?^}-8( z2*59YhzoR`+TZ!$^_l?OG&TL7x_hn*c5e&7lbeT>tp0@$YJcm8wPn^urlkTrvn#H) zrT2+YI7Jiv=t%|K=z!~&rGMcYR%quQKdo)>+&)WyJ8yM?B%z?MCDbu9;QcqW(^3JB zH7oG$zhrQs!4=Cr>R8QR2?z%X1Tr^*w zDqOWQ<&Rq`0P)A?w2Ll#K!h9f7<_a_fddT^h;D$#myYfQ<;%5mUa3pKCzm+Po-V+> z-*SVD&QSfl_T5%uxd87Um*H@etQuUtOuKVcwc3R*dr*W>ma!KUO=a-iyG2mY*qWVR zy!)@$$D!@K+O-tR1t7NkMZ5S%(BQm@3_duaKm!Wn_c2(v#MleUF;571MSI?nptXa; z%pwN!?{bq1e=efc*dhRVK*NVJc+u8!-R-yPp|N_A2oo}{_JS#s80HC&x_YbDjorAy zqi%G*d2kTUe5?*xjx7QZPi)tWSpJX)Cgw0TIUYMDtH!Tdi1G1XJobpFhRy!N2{_rR zKv4mM2k#M8LAKRmMzjgr1bDAeM%S0sJ^N(xtJ?bRLESzPD8u%`R*efZV?EqznN+gSD9a$eokPZ2eLXxbW8lSnRD?q`AYk&tqRy zi>J4bB(NA#hit=E0f^)sT!YCkmkYN!e!J$S4lOe$3 z`Cm+&c4E5#^+#m9i(2w~bg@UB(JOt6ynu73EDqZ;G0D*tQG}0fz@LD5k+0;QSCr%)jwl5VQXmI@{ku0lou#JP~}Lq1Jw{mu$CAL#zcVO$?tnhJZj?GwO4`*og7-)6?p4i339U; ztawP98SKvd!x<9+h&_7p3!$MaaayF+-mhw6@arcB)l!!VA)l9$p{&jxW5t*WK*_IK zU*J53Qy>*WZyb=}?FLz`5vW8al2f6jFlLxd$4mg?m3BOTYT2DuepLHYZ9tVEL5Y#_h2?NCw8c;W#<}GJ5P$ne z0xoxREO-Pcp6P<`6uZ?Lf${_82|K_DAh${>t)he7HomqznN=0Td;6MejQwIeHoYHz$Cc2%ppYjz^LE+ORPv zKs<7y$>1{%gF`&hL=R1-Ofp8Z01A_tJMG>xP0W>9d4;AkYfXlIgzVkKfB3E8&#zVvB+D8(AkYl|EuL$0P7H`Ik z2bdr_LIf^H2l_7Ttn6v|X&oL$@MTzKjV1uOO(NR0rvsylBN!~(_Yy<&3*};PH+1{*@*=}!sf#Ax_Wr8OY1@SUW z7951pJnPy}SEzCV4Ibo5Jfg^vE-8MoeX#fa6MY>u$1&&k;#p)kW&N}^7R>rGolnNm zUoz-97-}gm&=}1q$yvqD7@@GFn5GvQNgE8bjYNr#TEnGpD!$Bx(r3wFYLnk%r{Tj% n8c1zYkhF~+XPj}y(E#`lC=d8aAheqL00000NkvXXu0mjfOzlx6 diff --git a/pubspec.lock b/pubspec.lock index ecbe6fc..5a9ac62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" battery_plus: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" io: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: From 6c179ceb95d94ddc5d00ee5b0cbae02dd6119abf Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 30 Mar 2025 18:27:52 +0800 Subject: [PATCH 03/29] Add UA to WebDav requests. Close #308 --- lib/utils/data_sync.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 04cf28b..781149e 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -100,6 +100,7 @@ class DataSync with ChangeNotifier { rhttp.ClientSettings( proxySettings: proxy == null ? null : rhttp.ProxySettings.proxy(proxy), + userAgent: "venera v${App.version}", ), ), ); @@ -172,6 +173,7 @@ class DataSync with ChangeNotifier { rhttp.ClientSettings( proxySettings: proxy == null ? null : rhttp.ProxySettings.proxy(proxy), + userAgent: "venera v${App.version}", ), ), ); From cd9b07bb3eeeb45a58fa1a4d2fc904ed4bcbceb4 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 31 Mar 2025 12:26:32 +0800 Subject: [PATCH 04/29] Fix restoring window placement on linux --- lib/main.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 463a3b0..6da09f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,8 +35,14 @@ void main(List args) { } await windowManager.setMinimumSize(const Size(500, 600)); var placement = await WindowPlacement.loadFromFile(); - await placement.applyToWindow(); - await windowManager.show(); + if (App.isLinux) { + await windowManager.show(); + await placement.applyToWindow(); + } else { + await placement.applyToWindow(); + await windowManager.show(); + } + WindowPlacement.loop(); }); } From 7631fab86b13cb07141474d98497b5ffdc2a8a3e Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 31 Mar 2025 15:46:41 +0800 Subject: [PATCH 05/29] Prevent window from closing while uploading data --- assets/translation.json | 70 +++++++++++++++++--------------- lib/components/window_frame.dart | 5 +-- lib/utils/data_sync.dart | 29 +++++++++++++ 3 files changed, 67 insertions(+), 37 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index add7a5a..2f12282 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -140,18 +140,18 @@ "Block": "屏蔽", "Add new favorite to": "添加新收藏到", "Move favorite after reading": "阅读后移动收藏", - "Delete folder?" : "删除文件夹?", - "Delete folder '@f' ?" : "删除文件夹 '@f' ?", + "Delete folder?": "删除文件夹?", + "Delete folder '@f' ?": "删除文件夹 '@f' ?", "Import from file": "从文件导入", "Failed to import": "导入失败", "Cache Limit": "缓存限制", "Set Cache Limit": "设置缓存限制", "Size in MB": "大小(MB)", - "Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录", + "Select a directory which contains the comic directories.": "选择一个包含漫画文件夹的目录", "Help": "帮助", "Export as cbz": "导出为cbz", - "Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)", - "An archive file" : "一个归档文件", + "Select an archive file (cbz, zip, 7z, cb7)": "选择一个归档文件 (cbz, zip, 7z, cb7)", + "An archive file": "一个归档文件", "Fullscreen": "全屏", "Exit": "退出", "View more": "查看更多", @@ -198,9 +198,9 @@ "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Added": "已添加", "Turn page by volume keys": "使用音量键翻页", - "Display time & battery info in reader":"在阅读器中显示时间和电量信息", - "EhViewer downloads":"EhViewer下载", - "Select an EhViewer database and a download folder.":"选择EhViewer的下载数据(导出的db文件)与存放下载内容的目录", + "Display time & battery info in reader": "在阅读器中显示时间和电量信息", + "EhViewer downloads": "EhViewer下载", + "Select an EhViewer database and a download folder.": "选择EhViewer的下载数据(导出的db文件)与存放下载内容的目录", "(EhViewer)Default": "(EhViewer)默认", "If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若通过EhViewer数据库导入漫画,程序将会按其中的下载标签自动创建收藏文件夹。", "Multi-Select": "进入多选模式", @@ -241,7 +241,7 @@ "Delete all unavailable local favorite items": "删除所有无效的本地收藏", "Deleted @a favorite items.": "已删除 @a 条无效收藏", "New version available": "有新版本可用", - "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", + "A new version is available. Do you want to update now?": "有新版本可用。您要现在更新吗?", "No new version available": "没有新版本可用", "Export as pdf": "导出为pdf", "Export as epub": "导出为epub", @@ -288,15 +288,15 @@ "Copy the title successfully": "复制标题成功", "The comic is invalid, please long press to delete, you can double click the title to copy": "该漫画已失效, 请长按删除, 可以双击标题进行复制", "No search results found": "未找到搜索结果", - "Added @c comics to download queue." : "已添加 @c 本漫画到下载队列", + "Added @c comics to download queue.": "已添加 @c 本漫画到下载队列", "Download started": "下载已开始", "Click favorite": "点击收藏", "End": "末尾", "None": "无", "View Detail": "查看详情", - "Select a directory which contains multiple archive files." : "选择一个包含多个归档文件的目录", - "Multiple archive files" : "多个归档文件", - "No valid comics found" : "未找到有效的漫画", + "Select a directory which contains multiple archive files.": "选择一个包含多个归档文件的目录", + "Multiple archive files": "多个归档文件", + "No valid comics found": "未找到有效的漫画", "Enable DNS Overrides": "启用DNS覆写", "DNS Overrides": "DNS覆写", "Custom Image Processing": "自定义图片处理", @@ -342,12 +342,12 @@ "Replies": "回复", "Follow Updates": "追更", "Not Configured": "未配置", - "Choose a folder to follow updates." : "选择一个文件夹以追更", + "Choose a folder to follow updates.": "选择一个文件夹以追更", "Choose Folder": "选择文件夹", "No folders available": "没有可用的文件夹", "Updating comics...": "更新漫画中...", - "Automatic update checking enabled." : "已启用自动更新检查", - "The app will check for updates at most once a day." : "APP将每天最多检查一次更新", + "Automatic update checking enabled.": "已启用自动更新检查", + "The app will check for updates at most once a day.": "APP将每天最多检查一次更新", "Change Folder": "更改文件夹", "Check Now": "立即检查", "Updates": "更新", @@ -360,7 +360,7 @@ "Disabled": "已禁用", "Auto Sync Data": "自动同步数据", "Mark all as read": "全部标记为已读", - "Do you want to mark all as read?" : "您要全部标记为已读吗?", + "Do you want to mark all as read?": "您要全部标记为已读吗?", "Swipe down for previous chapter": "向下滑动查看上一章", "Swipe up for next chapter": "向上滑动查看下一章", "Initial Page": "初始页面", @@ -378,7 +378,9 @@ "Page": "页面", "Jump": "跳转", "Copy Image": "复制图片", - "A valid WebDav directory URL": "有效的WebDav目录URL" + "A valid WebDav directory URL": "有效的WebDav目录URL", + "Shut Down": "关闭", + "Uploading data...": "正在上传数据..." }, "zh_TW": { "Home": "首頁", @@ -520,18 +522,18 @@ "Block": "封鎖", "Add new favorite to": "添加新收藏到", "Move favorite after reading": "閱讀後移動收藏", - "Delete folder?" : "刪除資料夾?", - "Delete folder '@f' ?" : "刪除資料夾 '@f' ?", + "Delete folder?": "刪除資料夾?", + "Delete folder '@f' ?": "刪除資料夾 '@f' ?", "Import from file": "從文件匯入", "Failed to import": "匯入失敗", "Cache Limit": "快取限制", "Set Cache Limit": "設定快取限制", "Size in MB": "大小(MB)", - "Select a directory which contains the comic directories." : "選擇一個包含漫畫資料夾的目錄", + "Select a directory which contains the comic directories.": "選擇一個包含漫畫資料夾的目錄", "Help": "幫助", "Export as cbz": "匯出為cbz", - "Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)", - "An archive file" : "一個歸檔文件", + "Select an archive file (cbz, zip, 7z, cb7)": "選擇一個歸檔文件 (cbz, zip, 7z, cb7)", + "An archive file": "一個歸檔文件", "Fullscreen": "全螢幕", "Exit": "退出", "View more": "查看更多", @@ -622,13 +624,13 @@ "Delete all unavailable local favorite items": "刪除所有無效的本機收藏", "Deleted @a favorite items.": "已刪除 @a 條無效收藏", "New version available": "有新版本可用", - "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", + "A new version is available. Do you want to update now?": "有新版本可用。您要現在更新嗎?", "No new version available": "沒有新版本可用", "Export as pdf": "匯出為pdf", "Export as epub": "匯出為epub", "Aggregated Search": "聚合搜尋", "No search results found": "未找到搜尋結果", - "Added @c comics to download queue." : "已添加 @c 本漫畫到下載佇列", + "Added @c comics to download queue.": "已添加 @c 本漫畫到下載佇列", "Download started": "下載已開始", "Click favorite": "點擊收藏", "Local comic collection is not supported at present": "本機收藏暫不支援", @@ -675,9 +677,9 @@ "End": "末尾", "None": "無", "View Detail": "查看詳情", - "Select a directory which contains multiple archive files." : "選擇一個包含多個歸檔文件的目錄", - "Multiple archive files" : "多個歸檔文件", - "No valid comics found" : "未找到有效的漫畫", + "Select a directory which contains multiple archive files.": "選擇一個包含多個歸檔文件的目錄", + "Multiple archive files": "多個歸檔文件", + "No valid comics found": "未找到有效的漫畫", "Enable DNS Overrides": "啟用DNS覆寫", "DNS Overrides": "DNS覆寫", "Custom Image Processing": "自訂圖片處理", @@ -723,12 +725,12 @@ "Replies": "回覆", "Follow Updates": "追更", "Not Configured": "未配置", - "Choose a folder to follow updates." : "選擇一個資料夾以追更", + "Choose a folder to follow updates.": "選擇一個資料夾以追更", "Choose Folder": "選擇資料夾", "No folders available": "沒有可用的資料夾", "Updating comics...": "更新漫畫中...", - "Automatic update checking enabled." : "已啟用自動更新檢查", - "The app will check for updates at most once a day." : "APP將每天最多檢查一次更新", + "Automatic update checking enabled.": "已啟用自動更新檢查", + "The app will check for updates at most once a day.": "APP將每天最多檢查一次更新", "Change Folder": "更改資料夾", "Check Now": "立即檢查", "Updates": "更新", @@ -741,7 +743,7 @@ "Disabled": "已停用", "Auto Sync Data": "自動同步資料", "Mark all as read": "全部標記為已讀", - "Do you want to mark all as read?" : "您要全部標記為已讀嗎?", + "Do you want to mark all as read?": "您要全部標記為已讀嗎?", "Swipe down for previous chapter": "向下滑動查看上一章", "Swipe up for next chapter": "向上滑動查看下一章", "Initial Page": "初始頁面", @@ -759,6 +761,8 @@ "Page": "頁面", "Jump": "跳轉", "Copy Image": "複製圖片", - "A valid WebDav directory URL": "有效的WebDav目錄URL" + "A valid WebDav directory URL": "有效的WebDav目錄URL", + "Shut Down": "關閉", + "Uploading data...": "正在上傳數據..." } } diff --git a/lib/components/window_frame.dart b/lib/components/window_frame.dart index cc2c131..11cf0b3 100644 --- a/lib/components/window_frame.dart +++ b/lib/components/window_frame.dart @@ -82,10 +82,7 @@ class _WindowFrameState extends State { return; } } - windowManager.close().then((_) { - // Make sure the app exits when the window is closed. - exit(0); - }); + exit(0); } @override diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 781149e..12bd7cc 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -1,4 +1,6 @@ import 'package:flutter/foundation.dart'; +import 'package:venera/components/components.dart'; +import 'package:venera/components/window_frame.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; @@ -10,6 +12,7 @@ import 'package:venera/utils/data.dart'; import 'package:venera/utils/ext.dart'; import 'package:webdav_client/webdav_client.dart' hide File; import 'package:rhttp/rhttp.dart' as rhttp; +import 'package:venera/utils/translations.dart'; import 'io.dart'; @@ -20,6 +23,10 @@ class DataSync with ChangeNotifier { } LocalFavoritesManager().addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged); + Future.delayed(const Duration(seconds: 1), () { + var controller = WindowFrame.of(App.rootContext); + controller.addCloseListener(_handleWindowClose); + }); } void onDataChanged() { @@ -28,6 +35,28 @@ class DataSync with ChangeNotifier { } } + bool _handleWindowClose() { + if (_isUploading) { + _showWindowCloseDialog(); + return false; + } + return true; + } + + void _showWindowCloseDialog() async { + showLoadingDialog( + App.rootContext, + cancelButtonText: "Shut Down".tl, + onCancel: () => exit(0), + barrierDismissible: false, + message: "Uploading data...".tl, + ); + while (_isUploading) { + await Future.delayed(const Duration(milliseconds: 50)); + } + exit(0); + } + static DataSync? instance; factory DataSync() => instance ?? (instance = DataSync._()); From 90441af9892cfdc93817c1bf549636db87f2dc9e Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 31 Mar 2025 16:10:14 +0800 Subject: [PATCH 06/29] Fix the issue where local comics page can not been opened when there is a comic with empty chapter list. Close #309 --- lib/foundation/comic_source/models.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index f17c76d..47b20ca 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -342,7 +342,8 @@ class ComicChapters { } else if (groupedChapters.isNotEmpty) { return ComicChapters.grouped(groupedChapters); } else { - throw ArgumentError("Empty chapter list"); + // return a empty list. + return ComicChapters(chapters); } } From 8665994572d9a8f5a89d579653dc5f796bd93c67 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 1 Apr 2025 14:57:11 +0800 Subject: [PATCH 07/29] Write logs to file. --- lib/foundation/app.dart | 4 ++++ lib/foundation/log.dart | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index d48286e..05b0744 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -47,6 +47,7 @@ class _App { late String dataPath; late String cachePath; + String? externalStoragePath; final rootNavigatorKey = GlobalKey(); @@ -77,6 +78,9 @@ class _App { Future init() async { cachePath = (await getApplicationCacheDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path; + if (isAndroid) { + externalStoragePath = (await getExternalStorageDirectory())!.path; + } } Future initComponents() async { diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart index 54b47ed..77601bf 100644 --- a/lib/foundation/log.dart +++ b/lib/foundation/log.dart @@ -1,7 +1,9 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:venera/foundation/app.dart'; import 'package:venera/utils/ext.dart'; +import 'package:venera/utils/io.dart'; class LogItem { final LogLevel level; @@ -28,9 +30,6 @@ class Log { static bool ignoreLimitation = false; - /// only for debug - static const String? logFile = null; - static void printWarning(String text) { debugPrint('\x1B[33m$text\x1B[0m'); } @@ -39,7 +38,32 @@ class Log { debugPrint('\x1B[31m$text\x1B[0m'); } + static IOSink? _file; + static bool _isFlushing = false; + + static void _tryFlush() async { + if (_file != null) { + while (_isFlushing) { + await Future.delayed(const Duration(milliseconds: 20)); + } + _isFlushing = true; + await _file!.flush(); + _isFlushing = false; + } + } + static void addLog(LogLevel level, String title, String content) { + if (_file == null) { + Directory dir; + if (App.isAndroid) { + dir = Directory(App.externalStoragePath!); + } else { + dir = Directory(App.dataPath); + } + var file = dir.joinFile("logs.txt"); + _file = file.openWrite(); + } + if (!ignoreLimitation && content.length > maxLogLength) { content = "${content.substring(0, maxLogLength)}..."; } @@ -62,8 +86,9 @@ class Log { } _logs.add(newLog); - if(logFile != null) { - File(logFile!).writeAsString(newLog.toString(), mode: FileMode.append); + if(_file != null) { + _file!.write(newLog.toString()); + _tryFlush(); } if (_logs.length > maxLogNumber) { var res = _logs.remove( From daa6e8ce18e86812a8356093db849af24a4490f2 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 1 Apr 2025 15:13:09 +0800 Subject: [PATCH 08/29] Show comic pages in details page. --- assets/translation.json | 6 ++++-- lib/pages/comic_details_page/comic_page.dart | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 2f12282..df33077 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -380,7 +380,8 @@ "Copy Image": "复制图片", "A valid WebDav directory URL": "有效的WebDav目录URL", "Shut Down": "关闭", - "Uploading data...": "正在上传数据..." + "Uploading data...": "正在上传数据...", + "Pages": "页数" }, "zh_TW": { "Home": "首頁", @@ -763,6 +764,7 @@ "Copy Image": "複製圖片", "A valid WebDav directory URL": "有效的WebDav目錄URL", "Shut Down": "關閉", - "Uploading data...": "正在上傳數據..." + "Uploading data...": "正在上傳數據...", + "Pages": "頁數" } } diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 99745a8..7c4ff48 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -461,7 +461,8 @@ class _ComicPageState extends LoadingState if (comic.tags.isEmpty && comic.uploader == null && comic.uploadTime == null && - comic.uploadTime == null) { + comic.uploadTime == null && + comic.maxPage == null) { return const SliverPadding(padding: EdgeInsets.zero); } @@ -625,6 +626,13 @@ class _ComicPageState extends LoadingState buildTag(text: formatTime(comic.updateTime!)), ], ), + if (comic.maxPage != null) + buildWrap( + children: [ + buildTag(text: 'Pages'.tl, isTitle: true), + buildTag(text: comic.maxPage.toString()), + ], + ), const SizedBox(height: 12), const Divider(), ], From 05fcb23a4d70d24eefa2ddf264830f1fba6f3fd3 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 1 Apr 2025 15:49:22 +0800 Subject: [PATCH 09/29] Limit download directory length. Close #311 --- lib/foundation/local.dart | 4 ++++ lib/pages/home_page.dart | 2 +- lib/utils/io.dart | 24 +++++++++++++----------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index c5f00b6..b52524f 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -461,6 +461,10 @@ class LocalManager with ChangeNotifier { if (comic != null) { return Directory(FilePath.join(path, comic.directory)); } + const comicDirectoryMaxLength = 128; + if (name.length > comicDirectoryMaxLength) { + name = name.substring(0, comicDirectoryMaxLength); + } var dir = findValidDirectoryName(path, name); return Directory(FilePath.join(path, dir)).create().then((value) => value); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 579e162..81da9a3 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -52,7 +52,7 @@ class _SearchBar extends StatelessWidget { Widget build(BuildContext context) { return SliverToBoxAdapter( child: Container( - height: 52, + height: App.isMobile ? 52 : 46, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Material( diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 98b39dc..288b5a1 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; @@ -132,25 +131,28 @@ extension DirectoryExtension on Directory { } /// Sanitize the file name. Remove invalid characters and trim the file name. -String sanitizeFileName(String fileName) { +String sanitizeFileName(String fileName, {String? dir, int? maxLength}) { if (fileName.endsWith('.')) { fileName = fileName.substring(0, fileName.length - 1); } - const maxLength = 255; + var maxLength = 255; + if (dir != null) { + if (!dir.endsWith('/') && !dir.endsWith('\\')) { + dir = "$dir/"; + } + maxLength -= dir.length; + } final invalidChars = RegExp(r'[<>:"/\\|?*]'); final sanitizedFileName = fileName.replaceAll(invalidChars, ' '); var trimmedFileName = sanitizedFileName.trim(); if (trimmedFileName.isEmpty) { throw Exception('Invalid File Name: Empty length.'); } - while (true) { - final bytes = utf8.encode(trimmedFileName); - if (bytes.length > maxLength) { - trimmedFileName = - trimmedFileName.substring(0, trimmedFileName.length - 1); - } else { - break; - } + if (maxLength <= 0) { + throw Exception('Invalid File Name: Max length is less than 0.'); + } + if (trimmedFileName.length > maxLength) { + trimmedFileName = trimmedFileName.substring(0, maxLength); } return trimmedFileName; } From a116b5b615757d228eb09ed2303941c5a0c438aa Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 1 Apr 2025 20:36:24 +0800 Subject: [PATCH 10/29] Update AGP to 8.9.0 --- android/app/build.gradle | 3 +-- android/gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9a8ad45..f296899 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -67,7 +67,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.github.wgh136.venera" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. @@ -125,6 +124,6 @@ flutter { } dependencies { - implementation "androidx.activity:activity-ktx:1.9.2" + implementation "androidx.activity:activity-ktx:1.10.1" implementation 'androidx.documentfile:documentfile:1.0.1' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 5e6b542..efdcc4a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 3a1cb3b..3d1ac22 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.3.2' apply false + id "com.android.application" version '8.9.0' apply false id "org.jetbrains.kotlin.android" version "1.8.10" apply false } diff --git a/pubspec.lock b/pubspec.lock index 5a9ac62..3507919 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -433,8 +433,8 @@ packages: dependency: "direct main" description: path: "." - ref: "690a03a954f1603e0149cfd479c8961b88f21336" - resolved-ref: "690a03a954f1603e0149cfd479c8961b88f21336" + ref: "34e157e4c2b248d5463231b28b1f07927591705d" + resolved-ref: "34e157e4c2b248d5463231b28b1f07927591705d" url: "https://github.com/venera-app/flutter_saf" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index d7ee5af..c7719dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,7 +72,7 @@ dependencies: flutter_saf: git: url: https://github.com/venera-app/flutter_saf - ref: 690a03a954f1603e0149cfd479c8961b88f21336 + ref: 34e157e4c2b248d5463231b28b1f07927591705d dynamic_color: ^1.7.0 shimmer_animation: ^2.1.0 flutter_memory_info: ^0.0.1 From dcc94c5b3d549c63b0abf12d65746d043aafedd8 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 1 Apr 2025 21:07:29 +0800 Subject: [PATCH 11/29] Fix crash on Android. --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 3507919..94085e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -433,8 +433,8 @@ packages: dependency: "direct main" description: path: "." - ref: "34e157e4c2b248d5463231b28b1f07927591705d" - resolved-ref: "34e157e4c2b248d5463231b28b1f07927591705d" + ref: fe182cdf40e5fa6230f451bc1d643b860f610d13 + resolved-ref: fe182cdf40e5fa6230f451bc1d643b860f610d13 url: "https://github.com/venera-app/flutter_saf" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index c7719dd..45166d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,7 +72,7 @@ dependencies: flutter_saf: git: url: https://github.com/venera-app/flutter_saf - ref: 34e157e4c2b248d5463231b28b1f07927591705d + ref: fe182cdf40e5fa6230f451bc1d643b860f610d13 dynamic_color: ^1.7.0 shimmer_animation: ^2.1.0 flutter_memory_info: ^0.0.1 From d3c115ee0c293a7c8fd57f883a2eed969de0e4ec Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 2 Apr 2025 09:41:10 +0800 Subject: [PATCH 12/29] fix log --- lib/foundation/log.dart | 13 ------------- pubspec.lock | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart index 77601bf..aefdf8c 100644 --- a/lib/foundation/log.dart +++ b/lib/foundation/log.dart @@ -39,18 +39,6 @@ class Log { } static IOSink? _file; - static bool _isFlushing = false; - - static void _tryFlush() async { - if (_file != null) { - while (_isFlushing) { - await Future.delayed(const Duration(milliseconds: 20)); - } - _isFlushing = true; - await _file!.flush(); - _isFlushing = false; - } - } static void addLog(LogLevel level, String title, String content) { if (_file == null) { @@ -88,7 +76,6 @@ class Log { _logs.add(newLog); if(_file != null) { _file!.write(newLog.toString()); - _tryFlush(); } if (_logs.length > maxLogNumber) { var res = _logs.remove( diff --git a/pubspec.lock b/pubspec.lock index 94085e8..8fa2170 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" battery_plus: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: From 3da00595b731ce05fc854468c99b43260762ce81 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 2 Apr 2025 16:23:51 +0800 Subject: [PATCH 13/29] Add a setting for long press position. Close #287 --- assets/translation.json | 10 +++- lib/components/layout.dart | 26 ++++++++++ lib/foundation/appdata.dart | 1 + lib/pages/reader/images.dart | 43 +++++++++++++--- lib/pages/reader/reader.dart | 7 +++ lib/pages/settings/reader.dart | 89 +++++++++++++++------------------- 6 files changed, 117 insertions(+), 59 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index df33077..4b42aa2 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -381,7 +381,10 @@ "A valid WebDav directory URL": "有效的WebDav目录URL", "Shut Down": "关闭", "Uploading data...": "正在上传数据...", - "Pages": "页数" + "Pages": "页数", + "Long press zoom position": "长按缩放位置", + "Press position": "按压位置", + "Screen center": "屏幕中心" }, "zh_TW": { "Home": "首頁", @@ -765,6 +768,9 @@ "A valid WebDav directory URL": "有效的WebDav目錄URL", "Shut Down": "關閉", "Uploading data...": "正在上傳數據...", - "Pages": "頁數" + "Pages": "頁數", + "Long press zoom position": "長按縮放位置", + "Press position": "按壓位置", + "Screen center": "螢幕中心" } } diff --git a/lib/components/layout.dart b/lib/components/layout.dart index 0238863..e4bf0c6 100644 --- a/lib/components/layout.dart +++ b/lib/components/layout.dart @@ -163,3 +163,29 @@ class SliverLazyToBoxAdapter extends StatelessWidget { ]); } } + +class SliverAnimatedVisibility extends StatelessWidget { + const SliverAnimatedVisibility({ + super.key, + required this.visible, + required this.child, + }); + + final bool visible; + + final Widget child; + + @override + Widget build(BuildContext context) { + var child = visible ? this.child : const SizedBox.shrink(); + + return SliverToBoxAdapter( + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: child, + ), + ); + } +} diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index b4e4073..ecb64cc 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -161,6 +161,7 @@ class Settings with ChangeNotifier { 'cacheSize': 2048, // in MB 'downloadThreads': 5, 'enableLongPressToZoom': true, + 'longPressZoomPosition': "press", // press, center 'checkUpdateOnStart': false, 'limitImageWidth': true, 'webdav': [], // empty means not configured diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index e2693e4..456ef64 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -343,10 +343,19 @@ class _GalleryModeState extends State<_GalleryMode> } var photoViewController = photoViewControllers[reader.page]!; double target = photoViewController.getInitialScale!.call()! * 1.75; - var size = MediaQuery.of(context).size; + var size = reader.size; + Offset zoomPosition; + if (appdata.settings['longPressZoomPosition'] != 'center') { + zoomPosition = Offset( + size.width / 2 - location.dx, + size.height / 2 - location.dy, + ); + } else { + zoomPosition = Offset(0, 0); + } photoViewController.animateScale?.call( target, - Offset(size.width / 2 - location.dx, size.height / 2 - location.dy), + zoomPosition, ); isLongPressing = true; } @@ -608,6 +617,13 @@ class _ContinuousModeState extends State<_ContinuousMode> } bool onScaleUpdate([double? scale]) { + if (prepareToNextChapter || prepareToPrevChapter) { + setState(() { + prepareToPrevChapter = false; + prepareToNextChapter = false; + }); + context.readerScaffold.setFloatingButton(0); + } var isZoomedIn = (scale ?? photoViewController.scale) != 1.0; if (isZoomedIn != this.isZoomedIn) { setState(() { @@ -731,7 +747,7 @@ class _ContinuousModeState extends State<_ContinuousMode> } Offset offset; var sp = scrollController.position; - if (sp.pixels < sp.minScrollExtent || sp.pixels > sp.maxScrollExtent) { + if (sp.pixels <= sp.minScrollExtent || sp.pixels >= sp.maxScrollExtent) { offset = Offset(value.dx, value.dy); } else { if (reader.mode == ReaderMode.continuousTopToBottom) { @@ -759,7 +775,10 @@ class _ContinuousModeState extends State<_ContinuousMode> delayedSetIsScrolling(false); } - if (notification is ScrollUpdateNotification) { + var scale = photoViewController.scale ?? 1.0; + + if (notification is ScrollUpdateNotification && + (scale - 1).abs() < 0.05) { if (!scrollController.hasClients) return false; if (scrollController.position.pixels <= scrollController.position.minScrollExtent && @@ -800,8 +819,8 @@ class _ContinuousModeState extends State<_ContinuousMode> }, child: widget, ); - var width = MediaQuery.of(context).size.width; - var height = MediaQuery.of(context).size.height; + var width = reader.size.width; + var height = reader.size.height; if (appdata.settings['limitImageWidth'] && width / height > 0.7 && reader.mode == ReaderMode.continuousTopToBottom) { @@ -882,9 +901,19 @@ class _ContinuousModeState extends State<_ContinuousMode> return; } double target = photoViewController.getInitialScale!.call()! * 1.75; + var size = reader.size; + Offset zoomPosition; + if (appdata.settings['longPressZoomPosition'] != 'center') { + zoomPosition = Offset( + size.width / 2 - location.dx, + size.height / 2 - location.dy, + ); + } else { + zoomPosition = Offset(0, 0); + } photoViewController.animateScale?.call( target, - Offset(0, 0), + zoomPosition, ); onScaleUpdate(target); isLongPressing = true; diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 5321f10..5bf73c8 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -309,6 +309,13 @@ class _ReaderState extends State } return chapter == maxChapter; } + + /// Get the size of the reader. + /// The size is not always the same as the size of the screen. + Size get size { + var renderBox = context.findRenderObject() as RenderBox; + return renderBox.size; + } } abstract mixin class _ImagePerPageHandler { diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index fc282ed..ea04a78 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -48,6 +48,7 @@ class _ReaderSettingsState extends State { "continuousTopToBottom": "Continuous (Top to Bottom)".tl, }, onChanged: () { + setState(() {}); var readerMode = appdata.settings['readerMode']; if (readerMode?.toLowerCase().startsWith('continuous') ?? false) { appdata.settings['readerScreenPicNumberForLandscape'] = 1; @@ -68,67 +69,55 @@ class _ReaderSettingsState extends State { widget.onChanged?.call("autoPageTurningInterval"); }, ).toSliver(), - SliverToBoxAdapter( - child: AbsorbPointer( - absorbing: (appdata.settings['readerMode'] - ?.toLowerCase() - .startsWith('continuous') ?? - false), - child: AnimatedOpacity( - opacity: (appdata.settings['readerMode'] - ?.toLowerCase() - .startsWith('continuous') ?? - false) - ? 0.5 - : 1.0, - duration: Duration(milliseconds: 300), - child: _SliderSetting( - title: "The number of pic in screen for landscape (Only Gallery Mode)".tl, - settingsIndex: "readerScreenPicNumberForLandscape", - interval: 1, - min: 1, - max: 5, - onChanged: () { - widget.onChanged?.call("readerScreenPicNumberForLandscape"); - }, - ), - ), + SliverAnimatedVisibility( + visible: appdata.settings['readerMode']!.startsWith('gallery'), + child: _SliderSetting( + title: + "The number of pic in screen for landscape (Only Gallery Mode)" + .tl, + settingsIndex: "readerScreenPicNumberForLandscape", + interval: 1, + min: 1, + max: 5, + onChanged: () { + widget.onChanged?.call("readerScreenPicNumberForLandscape"); + }, ), ), - SliverToBoxAdapter( - child: AbsorbPointer( - absorbing: (appdata.settings['readerMode'] - ?.toLowerCase() - .startsWith('continuous') ?? - false), - child: AnimatedOpacity( - opacity: (appdata.settings['readerMode'] - ?.toLowerCase() - .startsWith('continuous') ?? - false) - ? 0.5 - : 1.0, - duration: Duration(milliseconds: 300), - child: _SliderSetting( - title: "The number of pic in screen for portrait (Only Gallery Mode)".tl, - settingsIndex: "readerScreenPicNumberForPortrait", - interval: 1, - min: 1, - max: 5, - onChanged: () { - widget.onChanged?.call("readerScreenPicNumberForPortrait"); - }, - ), - ), + SliverAnimatedVisibility( + visible: appdata.settings['readerMode']!.startsWith('gallery'), + child: _SliderSetting( + title: + "The number of pic in screen for portrait (Only Gallery Mode)" + .tl, + settingsIndex: "readerScreenPicNumberForPortrait", + interval: 1, + min: 1, + max: 5, + onChanged: () { + widget.onChanged?.call("readerScreenPicNumberForPortrait"); + }, ), ), _SwitchSetting( title: 'Long press to zoom'.tl, settingKey: 'enableLongPressToZoom', onChanged: () { + setState(() {}); widget.onChanged?.call('enableLongPressToZoom'); }, ).toSliver(), + SliverAnimatedVisibility( + visible: appdata.settings['enableLongPressToZoom'] == true, + child: SelectSetting( + title: "Long press zoom position".tl, + settingKey: "longPressZoomPosition", + optionTranslation: { + "press": "Press position".tl, + "center": "Screen center".tl, + }, + ), + ), _SwitchSetting( title: 'Limit image width'.tl, subtitle: 'When using Continuous(Top to Bottom) mode'.tl, From 276e23354df0172c3a060ef1f979f11fdbcb50d1 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 3 Apr 2025 11:53:43 +0800 Subject: [PATCH 14/29] Smooth scroll for comments page. --- .../comic_details_page/comments_page.dart | 110 +++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/lib/pages/comic_details_page/comments_page.dart b/lib/pages/comic_details_page/comments_page.dart index 0240c46..5772929 100644 --- a/lib/pages/comic_details_page/comments_page.dart +++ b/lib/pages/comic_details_page/comments_page.dart @@ -99,61 +99,67 @@ class _CommentsPageState extends State { return Column( children: [ Expanded( - child: ListView.builder( - primary: false, - padding: EdgeInsets.zero, - itemCount: _comments!.length + 2, - itemBuilder: (context, index) { - if (index == 0) { - if (widget.replyComment != null) { - return Column( - children: [ - _CommentTile( - comment: widget.replyComment!, - source: widget.source, - comic: widget.data, - showAvatar: showAvatar, - showActions: false, - ), - const SizedBox(height: 8), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: context.colorScheme.outlineVariant, - width: 0.6, + child: SmoothScrollProvider( + builder: (context, controller, physics) { + return ListView.builder( + controller: controller, + physics: physics, + primary: false, + padding: EdgeInsets.zero, + itemCount: _comments!.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + if (widget.replyComment != null) { + return Column( + children: [ + _CommentTile( + comment: widget.replyComment!, + source: widget.source, + comic: widget.data, + showAvatar: showAvatar, + showActions: false, + ), + const SizedBox(height: 8), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: context.colorScheme.outlineVariant, + width: 0.6, + ), + ), + ), + child: Text( + "Replies".tl, + style: ts.s18, ), ), - ), - child: Text( - "Replies".tl, - style: ts.s18, - ), - ), - ], + ], + ); + } else { + return const SizedBox(); + } + } + index--; + + if (index == _comments!.length) { + if (_page < (maxPage ?? _page + 1)) { + loadMore(); + return const ListLoadingIndicator(); + } else { + return const SizedBox(); + } + } + + return _CommentTile( + comment: _comments![index], + source: widget.source, + comic: widget.data, + showAvatar: showAvatar, ); - } else { - return const SizedBox(); - } - } - index--; - - if (index == _comments!.length) { - if (_page < (maxPage ?? _page + 1)) { - loadMore(); - return const ListLoadingIndicator(); - } else { - return const SizedBox(); - } - } - - return _CommentTile( - comment: _comments![index], - source: widget.source, - comic: widget.data, - showAvatar: showAvatar, + }, ); }, ), From 37af7e266a063915717cfdc189db5156a9a2c9b6 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 3 Apr 2025 13:03:39 +0800 Subject: [PATCH 15/29] Allow changing chapter by volume key. Close #250 --- lib/pages/reader/reader.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 5bf73c8..9810285 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -370,8 +370,24 @@ abstract mixin class _VolumeListener { bool toPrevPage(); + bool toNextChapter(); + + bool toPrevChapter(); + VolumeListener? volumeListener; + void onDown() { + if (!toNextPage()) { + toNextChapter(); + } + } + + void onUp() { + if (!toPrevPage()) { + toPrevChapter(); + } + } + void handleVolumeEvent() { if (!App.isAndroid) { // Currently only support Android @@ -381,8 +397,8 @@ abstract mixin class _VolumeListener { volumeListener?.cancel(); } volumeListener = VolumeListener( - onDown: toNextPage, - onUp: toPrevPage, + onDown: onDown, + onUp: onUp, )..listen(); } From 971fc1da92dfbca9013dd9d287382157d7efdd1b Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 3 Apr 2025 13:04:25 +0800 Subject: [PATCH 16/29] Update version code. --- lib/foundation/app.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 05b0744..ff6b6e0 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.3.4"; + final version = "1.3.5"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index 45166d7..0c7022f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.4+134 +version: 1.3.5+135 environment: sdk: '>=3.6.0 <4.0.0' From 463ad5b5bca47cb62fdda09ffca581d71dcce933 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 4 Apr 2025 22:47:43 +0800 Subject: [PATCH 17/29] [Comic Source] New model `PageJumpTarget`. All page jump operations now use `PageJumpTarget`. --- lib/foundation/app.dart | 2 +- lib/foundation/comic_source/category.dart | 93 ++++----------- lib/foundation/comic_source/comic_source.dart | 4 +- lib/foundation/comic_source/models.dart | 107 ++++++++++++++++++ lib/foundation/comic_source/parser.dart | 95 ++++++++++++---- lib/pages/categories_page.dart | 99 +++------------- lib/pages/category_comics_page.dart | 19 +++- lib/pages/comic_details_page/actions.dart | 17 +-- lib/pages/comic_source_page.dart | 6 + lib/pages/explore_page.dart | 28 +---- pubspec.lock | 20 ++-- pubspec.yaml | 2 +- 12 files changed, 266 insertions(+), 226 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index ff6b6e0..384b0ce 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.3.5"; + final version = "1.4.0"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index 7aae515..1ade4e0 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -34,24 +34,28 @@ class CategoryButtonData { }); } +class CategoryItem { + final String label; + + final PageJumpTarget target; + + const CategoryItem(this.label, this.target); +} + abstract class BaseCategoryPart { String get title; - List get categories; - - List? get categoryParams => null; + List get categories; bool get enableRandom; - String get categoryType; - /// Data class for building a part of category page. const BaseCategoryPart(); } class FixedCategoryPart extends BaseCategoryPart { @override - final List categories; + final List categories; @override bool get enableRandom => false; @@ -59,19 +63,12 @@ class FixedCategoryPart extends BaseCategoryPart { @override final String title; - @override - final String categoryType; - - @override - final List? categoryParams; - /// A [BaseCategoryPart] that show fixed tags on category page. - const FixedCategoryPart(this.title, this.categories, this.categoryType, - [this.categoryParams]); + const FixedCategoryPart(this.title, this.categories); } class RandomCategoryPart extends BaseCategoryPart { - final List tags; + final List all; final int randomNumber; @@ -81,67 +78,23 @@ class RandomCategoryPart extends BaseCategoryPart { @override bool get enableRandom => true; - @override - final String categoryType; - - List _categories() { - if (randomNumber >= tags.length) { - return tags; + List _categories() { + if (randomNumber >= all.length) { + return all; } - var start = math.Random().nextInt(tags.length - randomNumber); - return tags.sublist(start, start + randomNumber); + var start = math.Random().nextInt(all.length - randomNumber); + return all.sublist(start, start + randomNumber); } @override - List get categories => _categories(); + List get categories => _categories(); - /// A [BaseCategoryPart] that show random tags on category page. + /// A [BaseCategoryPart] that show a part of random tags on category page. const RandomCategoryPart( - this.title, this.tags, this.randomNumber, this.categoryType); -} - -class RandomCategoryPartWithRuntimeData extends BaseCategoryPart { - final Iterable Function() loadTags; - - final int randomNumber; - - @override - final String title; - - @override - bool get enableRandom => true; - - @override - final String categoryType; - - static final random = math.Random(); - - List _categories() { - var tags = loadTags(); - if (randomNumber >= tags.length) { - return tags.toList(); - } - final start = random.nextInt(tags.length - randomNumber); - var res = List.filled(randomNumber, ''); - int index = -1; - for (var s in tags) { - index++; - if (start > index) { - continue; - } else if (index == start + randomNumber) { - break; - } - res[index - start] = s; - } - return res; - } - - @override - List get categories => _categories(); - - /// A [BaseCategoryPart] that show random tags on category page. - RandomCategoryPartWithRuntimeData( - this.title, this.loadTags, this.randomNumber, this.categoryType); + this.title, + this.all, + this.randomNumber, + ); } CategoryData getCategoryDataWithKey(String key) { diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index 1ef748c..3e4b524 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -11,6 +11,8 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/res.dart'; +import 'package:venera/pages/category_comics_page.dart'; +import 'package:venera/pages/search_result_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/init.dart'; @@ -349,7 +351,7 @@ class ExplorePagePart { /// - category:categoryName /// /// End with `@`+`param` if the category has a parameter. - final String? viewMore; + final PageJumpTarget? viewMore; const ExplorePagePart(this.title, this.comics, this.viewMore); } diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 47b20ca..9989097 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -430,3 +430,110 @@ class ComicChapters { } } } + +class PageJumpTarget { + final String sourceKey; + + final String page; + + final Map? attributes; + + const PageJumpTarget(this.sourceKey, this.page, this.attributes); + + static PageJumpTarget parse(String sourceKey, dynamic value) { + if (value is Map) { + if (value['page'] != null) { + return PageJumpTarget( + sourceKey, + value["page"] ?? "search", + value["attributes"], + ); + } else if (value["action"] != null) { + // old version `onClickTag` + var page = value["action"]; + if (page == "search") { + return PageJumpTarget( + sourceKey, + "search", + { + "text": value["keyword"], + }, + ); + } else if (page == "category") { + return PageJumpTarget( + sourceKey, + "category", + { + "category": value["keyword"], + "param": value["param"], + }, + ); + } else { + return PageJumpTarget(sourceKey, page, null); + } + } + } else if (value is String) { + // old version string encoding. search: `search:keyword`, category: `category:keyword` or `category:keyword@param` + var segments = value.split(":"); + var page = segments[0]; + if (page == "search") { + return PageJumpTarget( + sourceKey, + "search", + { + "text": segments[1], + }, + ); + } else if (page == "category") { + var c = segments[1]; + if (c.contains('@')) { + var parts = c.split('@'); + return PageJumpTarget( + sourceKey, + "category", + { + "category": parts[0], + "param": parts[1], + }, + ); + } else { + return PageJumpTarget( + sourceKey, + "category", + { + "category": c, + }, + ); + } + } else { + return PageJumpTarget(sourceKey, page, null); + } + } + return PageJumpTarget(sourceKey, "Invalid Data", null); + } + + void jump(BuildContext context) { + if (page == "search") { + context.to( + () => SearchResultPage( + text: attributes?["text"] ?? attributes?["keyword"] ?? "", + sourceKey: sourceKey, + options: List.from(attributes?["options"] ?? []), + ), + ); + } else if (page == "category") { + var key = ComicSource.find(sourceKey)!.categoryData!.key; + context.to( + () => CategoryComicsPage( + categoryKey: key, + category: attributes?["category"] ?? + (throw ArgumentError("Category name is required")), + options: List.from(attributes?["options"] ?? []), + param: attributes?["param"], + ), + ); + } else { + Log.error("Page Jump", "Unknown page: $page"); + } + } +} diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index e9ed69a..b27e9fa 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -80,9 +80,8 @@ class ComicSourceParser { Future parse(String js, String filePath) async { js = js.replaceAll("\r\n", "\n"); - var line1 = js - .split('\n') - .firstWhereOrNull((e) => e.trim().startsWith("class ")); + var line1 = + js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class ")); if (line1 == null || !line1.startsWith("class ") || !line1.contains("extends ComicSource")) { @@ -336,7 +335,7 @@ class ComicSourceParser { (e['comics'] as List).map((e) { return Comic.fromJson(e, _key!); }).toList(), - e['viewMore'], + PageJumpTarget.parse(_key!, e['viewMore']), ); }), ), @@ -404,21 +403,78 @@ class ComicSourceParser { var categoryParts = []; for (var c in doc["parts"]) { - final String name = c["name"]; - final String type = c["type"]; - final List tags = List.from(c["categories"]); - final String itemType = c["itemType"]; - List? categoryParams = ListOrNull.from(c["categoryParams"]); - final String? groupParam = c["groupParam"]; - if (groupParam != null) { - categoryParams = List.filled(tags.length, groupParam); + if (c["categories"] is! List || c["categories"].isEmpty) { + continue; } - if (type == "fixed") { - categoryParts - .add(FixedCategoryPart(name, tags, itemType, categoryParams)); - } else if (type == "random") { - categoryParts.add( - RandomCategoryPart(name, tags, c["randomNumber"] ?? 1, itemType)); + List categories = c["categories"]; + if (categories[0] is Map) { + // new format + final String name = c["name"]; + final String type = c["type"]; + final cs = categories + .map( + (e) => CategoryItem( + e['label'], + PageJumpTarget.parse(_key!, e['target']), + ), + ) + .toList(); + if (type == "fixed") { + categoryParts.add(FixedCategoryPart(name, cs)); + } else if (type == "random") { + categoryParts + .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + } + } else { + // old format + final String name = c["name"]; + final String type = c["type"]; + final List tags = List.from(c["categories"]); + final String itemType = c["itemType"]; + List? categoryParams = ListOrNull.from(c["categoryParams"]); + final String? groupParam = c["groupParam"]; + if (groupParam != null) { + categoryParams = List.filled(tags.length, groupParam); + } + var cs = []; + for (int i = 0; i < tags.length; i++) { + PageJumpTarget target; + if (itemType == 'category') { + target = PageJumpTarget( + _key!, + 'category', + { + "category": tags[i], + "param": categoryParams?.elementAtOrNull(i), + }, + ); + } else if (itemType == 'search') { + target = PageJumpTarget( + _key!, + 'search', + { + "keyword": tags[i], + }, + ); + } else if (itemType == 'search_with_namespace') { + target = PageJumpTarget( + _key!, + 'search', + { + "keyword": "$name:$tags[i]", + }, + ); + } else { + target = PageJumpTarget(_key!, itemType, null); + } + cs.add(CategoryItem(tags[i], target)); + } + if (type == "fixed") { + categoryParts.add(FixedCategoryPart(name, cs)); + } else if (type == "random") { + categoryParts + .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + } } } @@ -620,7 +676,8 @@ class ComicSourceParser { final bool multiFolder = _getValue("favorites.multiFolder"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); - final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic"); + final bool? singleFolderForSingleComic = + _getValue("favorites.singleFolderForSingleComic"); Future> retryZone(Future> Function() func) async { if (!ComicSource.find(_key!)!.isLogged) { diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index f46d975..15ed457 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -4,12 +4,10 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/pages/ranking_page.dart'; -import 'package:venera/pages/search_result_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; -import 'category_comics_page.dart'; import 'comic_source_page.dart'; class CategoriesPage extends StatefulWidget { @@ -147,43 +145,6 @@ class _CategoryPage extends StatelessWidget { return ""; } - void handleClick( - String tag, - String? param, - String type, - String namespace, - String categoryKey, - ) { - if (type == 'search') { - App.mainNavigatorKey?.currentContext?.to( - () => SearchResultPage( - text: tag, - options: const [], - sourceKey: findComicSourceKey(), - ), - ); - } else if (type == "search_with_namespace") { - if (tag.contains(" ")) { - tag = '"$tag"'; - } - App.mainNavigatorKey?.currentContext?.to( - () => SearchResultPage( - text: "$namespace:$tag", - options: const [], - sourceKey: findComicSourceKey(), - ), - ); - } else if (type == "category") { - App.mainNavigatorKey!.currentContext!.to( - () => CategoryComicsPage( - category: tag, - categoryKey: categoryKey, - param: param, - ), - ); - } - } - @override Widget build(BuildContext context) { var children = []; @@ -194,11 +155,11 @@ class _CategoryPage extends StatelessWidget { child: Wrap( children: [ if (data.enableRankingPage) - buildTag("Ranking".tl, (p0, p1) { + buildTag("Ranking".tl, () { context.to(() => RankingPage(categoryKey: data.key)); }), for (var buttonData in data.buttons) - buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap()) + buildTag(buttonData.label.tl, buttonData.onTap) ], ), )); @@ -212,36 +173,14 @@ class _CategoryPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ buildTitleWithRefresh(part.title, () => updater(() {})), - buildTagsWithParams( - part.categories, - part.categoryParams, - part.title, - (key, param) => handleClick( - key, - param, - part.categoryType, - part.title, - category, - ), - ) + buildTags(part.categories) ], ); })); } else { children.add(buildTitle(part.title)); children.add( - buildTagsWithParams( - part.categories, - part.categoryParams, - part.title, - (tag, param) => handleClick( - tag, - param, - part.categoryType, - part.title, - data.key, - ), - ), + buildTags(part.categories), ); } } @@ -280,30 +219,28 @@ class _CategoryPage extends StatelessWidget { ); } - Widget buildTagsWithParams( - List tags, - List? params, - String? namespace, - ClickTagCallback onClick, + Widget buildTags( + List categories, ) { return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), child: Wrap( children: List.generate( - tags.length, - (index) => buildTag( - tags[index], - onClick, - namespace, - params?.elementAtOrNull(index), - ), + categories.length, + (index) => buildCategory(categories[index]), ), ), ); } - Widget buildTag(String tag, ClickTagCallback onClick, - [String? namespace, String? param]) { + Widget buildCategory(CategoryItem c) { + return buildTag(c.label, () { + var context = App.mainNavigatorKey!.currentContext!; + c.target.jump(context); + }); + } + + Widget buildTag(String label, VoidCallback onClick) { return Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), child: Builder( @@ -313,10 +250,10 @@ class _CategoryPage extends StatelessWidget { color: context.colorScheme.primaryContainer.toOpacity(0.72), child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(8)), - onTap: () => onClick(tag, param), + onTap: onClick, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text(tag), + child: Text(label), ), ), ); diff --git a/lib/pages/category_comics_page.dart b/lib/pages/category_comics_page.dart index 33b12cf..a8840d6 100644 --- a/lib/pages/category_comics_page.dart +++ b/lib/pages/category_comics_page.dart @@ -9,6 +9,7 @@ class CategoryComicsPage extends StatefulWidget { required this.category, this.param, required this.categoryKey, + this.options, super.key, }); @@ -18,6 +19,8 @@ class CategoryComicsPage extends StatefulWidget { final String categoryKey; + final List? options; + @override State createState() => _CategoryComicsPageState(); } @@ -40,7 +43,16 @@ class _CategoryComicsPageState extends State { } return true; }).toList(); - optionsValue = options.map((e) => e.options.keys.first).toList(); + var defaultOptionsValue = + options.map((e) => e.options.keys.first).toList(); + if (optionsValue.length != options.length) { + var newOptionsValue = List.filled(options.length, ""); + for (var i = 0; i < options.length; i++) { + newOptionsValue[i] = + optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i]; + } + optionsValue = newOptionsValue; + } sourceKey = source.key; return; } @@ -50,6 +62,11 @@ class _CategoryComicsPageState extends State { @override void initState() { + if (widget.options != null) { + optionsValue = widget.options!; + } else { + optionsValue = []; + } findData(); super.initState(); } diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index 90f0a35..aaa68bc 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -300,21 +300,8 @@ abstract mixin class _ComicPageActions { 'keyword': tag, }; var context = App.mainNavigatorKey!.currentContext!; - if (config['action'] == 'search') { - context.to(() => SearchResultPage( - text: config['keyword'] ?? '', - sourceKey: comicSource.key, - options: const [], - )); - } else if (config['action'] == 'category') { - context.to( - () => CategoryComicsPage( - category: config['keyword'] ?? '', - categoryKey: comicSource.categoryData!.key, - param: config['param'], - ), - ); - } + var target = PageJumpTarget.parse(comicSource.key, config); + target.jump(context); } void showMoreActions() { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 96fd888..b72af0e 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -461,6 +461,7 @@ void _addAllPagesWithComicSource(ComicSource source) { var explorePages = appdata.settings['explore_pages']; var categoryPages = appdata.settings['categories']; var networkFavorites = appdata.settings['favorites']; + var searchPages = appdata.settings['searchSources']; if (source.explorePages.isNotEmpty) { for (var page in source.explorePages) { @@ -477,10 +478,15 @@ void _addAllPagesWithComicSource(ComicSource source) { !networkFavorites.contains(source.favoriteData!.key)) { networkFavorites.add(source.favoriteData!.key); } + if (source.searchPageData != null && + !searchPages.contains(source.key)) { + searchPages.add(source.key); + } appdata.settings['explore_pages'] = explorePages.toSet().toList(); appdata.settings['categories'] = categoryPages.toSet().toList(); appdata.settings['favorites'] = networkFavorites.toSet().toList(); + appdata.settings['searchSources'] = searchPages.toSet().toList(); appdata.saveData(); } diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 415d7d0..d8dba9b 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -6,13 +6,10 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/global_state.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_source_page.dart'; -import 'package:venera/pages/search_result_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; -import 'category_comics_page.dart'; - class ExplorePage extends StatefulWidget { const ExplorePage({super.key}); @@ -445,30 +442,7 @@ Iterable _buildExplorePagePart( TextButton( onPressed: () { var context = App.mainNavigatorKey!.currentContext!; - if (part.viewMore!.startsWith("search:")) { - context.to( - () => SearchResultPage( - text: part.viewMore!.replaceFirst("search:", ""), - options: const [], - sourceKey: sourceKey, - ), - ); - } else if (part.viewMore!.startsWith("category:")) { - var cp = part.viewMore!.replaceFirst("category:", ""); - var c = cp.split('@').first; - String? p = cp.split('@').last; - if (p == c) { - p = null; - } - context.to( - () => CategoryComicsPage( - category: c, - categoryKey: - ComicSource.find(sourceKey)!.categoryData!.key, - param: p, - ), - ); - } + part.viewMore!.jump(context); }, child: Text("View more".tl), ) diff --git a/pubspec.lock b/pubspec.lock index 8fa2170..94085e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" battery_plus: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" io: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c7022f..9d9ff98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.5+135 +version: 1.4.0+140 environment: sdk: '>=3.6.0 <4.0.0' From d91bca6913540bbfe2a0efb588faa387a7ac62a4 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 17:18:53 +0800 Subject: [PATCH 18/29] [Comic Source] Improve data conversion --- lib/foundation/comic_source/models.dart | 4 +++- lib/foundation/comic_source/parser.dart | 7 +++++-- lib/foundation/comic_source/types.dart | 2 +- lib/pages/comic_details_page/actions.dart | 9 ++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 9989097..5e2bc62 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -169,7 +169,9 @@ class ComicDetails with HistoryMixin { static Map> _generateMap(Map map) { var res = >{}; map.forEach((key, value) { - res[key] = List.from(value); + if (value is List) { + res[key] = List.from(value); + } }); return res; } diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index b27e9fa..6a2e590 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -1035,9 +1035,12 @@ class ComicSourceParser { var res = JsEngine().runCode(""" ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)}) """); - var r = Map.from(res); + if (res is! Map) { + return null; + } + var r = Map.from(res); r.removeWhere((key, value) => value == null); - return Map.from(r); + return PageJumpTarget.parse(_key!, r); }; } diff --git a/lib/foundation/comic_source/types.dart b/lib/foundation/comic_source/types.dart index fbacc8f..d75c3eb 100644 --- a/lib/foundation/comic_source/types.dart +++ b/lib/foundation/comic_source/types.dart @@ -41,7 +41,7 @@ typedef LikeCommentFunc = Future> Function( typedef VoteCommentFunc = Future> Function( String comicId, String? subId, String commentId, bool isUp, bool isCancel); -typedef HandleClickTagEvent = Map Function( +typedef HandleClickTagEvent = PageJumpTarget? Function( String namespace, String tag); /// [rating] is the rating value, 0-10. 1 represents 0.5 star. diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index aaa68bc..b71a0bf 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -294,14 +294,9 @@ abstract mixin class _ComicPageActions { } void onTapTag(String tag, String namespace) { - var config = comicSource.handleClickTagEvent?.call(namespace, tag) ?? - { - 'action': 'search', - 'keyword': tag, - }; + var target = comicSource.handleClickTagEvent?.call(namespace, tag); var context = App.mainNavigatorKey!.currentContext!; - var target = PageJumpTarget.parse(comicSource.key, config); - target.jump(context); + target?.jump(context); } void showMoreActions() { From 118941f239ecc3206ec3f9d6f641f87af1f04a61 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 17:45:29 +0800 Subject: [PATCH 19/29] Fix the mouse scrolling issue when multiple scroll lists are nested. --- lib/components/scroll.dart | 74 ++++++++++++++++++++++++++++++++------ lib/pages/home_page.dart | 2 +- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/lib/components/scroll.dart b/lib/components/scroll.dart index 3988630..741de7d 100644 --- a/lib/components/scroll.dart +++ b/lib/components/scroll.dart @@ -51,10 +51,32 @@ class _SmoothScrollProviderState extends State { static bool _isMouseScroll = App.isDesktop; + late int id; + + static int _id = 0; + + var activeChildren = {}; + + ScrollState? parent; + @override void initState() { _controller = widget.controller ?? ScrollController(); super.initState(); + id = _id; + _id++; + } + + @override + void didChangeDependencies() { + parent = ScrollState.maybeOf(context); + super.didChangeDependencies(); + } + + @override + void dispose() { + parent?.onChildInactive(id); + super.dispose(); } @override @@ -66,8 +88,7 @@ class _SmoothScrollProviderState extends State { const BouncingScrollPhysics(), ); } - return Listener( - behavior: HitTestBehavior.translucent, + var child = Listener( onPointerDown: (event) { _futurePosition = null; if (_isMouseScroll) { @@ -77,6 +98,9 @@ class _SmoothScrollProviderState extends State { } }, onPointerSignal: (pointerSignal) { + if (activeChildren.isNotEmpty) { + return; + } if (pointerSignal is PointerScrollEvent) { if (HardwareKeyboard.instance.isShiftPressed) { return; @@ -113,8 +137,14 @@ class _SmoothScrollProviderState extends State { }); } }, - child: ScrollControllerProvider._( + child: ScrollState._( controller: _controller, + onChildActive: (id) { + activeChildren.add(id); + }, + onChildInactive: (id) { + activeChildren.remove(id); + }, child: widget.builder( context, _controller, @@ -124,25 +154,49 @@ class _SmoothScrollProviderState extends State { ), ), ); + + if (parent != null) { + return MouseRegion( + onEnter: (_) { + parent!.onChildActive(id); + }, + onExit: (_) { + parent!.onChildInactive(id); + }, + child: child, + ); + } + + return child; } } -class ScrollControllerProvider extends InheritedWidget { - const ScrollControllerProvider._({ +class ScrollState extends InheritedWidget { + const ScrollState._({ required this.controller, required super.child, + required this.onChildActive, + required this.onChildInactive, }); final ScrollController controller; - static ScrollController of(BuildContext context) { - final ScrollControllerProvider? provider = - context.dependOnInheritedWidgetOfExactType(); - return provider!.controller; + final void Function(int id) onChildActive; + + final void Function(int id) onChildInactive; + + static ScrollState of(BuildContext context) { + final ScrollState? provider = + context.dependOnInheritedWidgetOfExactType(); + return provider!; + } + + static ScrollState? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); } @override - bool updateShouldNotify(ScrollControllerProvider oldWidget) { + bool updateShouldNotify(ScrollState oldWidget) { return oldWidget.controller != controller; } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 81da9a3..4b0b2d7 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -942,7 +942,7 @@ class _ImageFavoritesState extends State { displayType = type; }); await Future.delayed(const Duration(milliseconds: 20)); - var scrollController = ScrollControllerProvider.of(context); + var scrollController = ScrollState.of(context).controller; scrollController.animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), From 6ff30f8ac3eef0fcc06ec90e7b1ff638c118016e Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 17:48:49 +0800 Subject: [PATCH 20/29] Improve chapter display. --- lib/pages/comic_details_page/chapters.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/pages/comic_details_page/chapters.dart b/lib/pages/comic_details_page/chapters.dart index 10ece3d..7d86757 100644 --- a/lib/pages/comic_details_page/chapters.dart +++ b/lib/pages/comic_details_page/chapters.dart @@ -105,7 +105,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { var value = chapters[key]!; bool visited = (history?.readEpisode ?? {}).contains(i + 1); return Padding( - padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), child: Material( color: context.colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(16), @@ -113,7 +113,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { onTap: () => state.read(i + 1), borderRadius: BorderRadius.circular(16), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Center( child: Text( value, @@ -134,7 +134,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { }, ), gridDelegate: const SliverGridDelegateWithFixedHeight( - maxCrossAxisExtent: 200, + maxCrossAxisExtent: 250, itemHeight: 48, ), ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)), @@ -300,15 +300,15 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> history!.readEpisode.contains(rawIndex); } return Padding( - padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), child: Material( - color: context.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(16), + color: context.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), child: InkWell( onTap: () => state.read(chapterIndex + 1), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Center( child: Text( value, @@ -329,7 +329,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> }, ), gridDelegate: const SliverGridDelegateWithFixedHeight( - maxCrossAxisExtent: 200, + maxCrossAxisExtent: 250, itemHeight: 48, ), ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)), From f87afbe397ddf5d2b4e0749ab691d94dad96371e Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 18:00:55 +0800 Subject: [PATCH 21/29] Fix issues with empty chapter list. --- lib/pages/comic_details_page/comic_page.dart | 24 ++++++++++++-------- lib/pages/reader/images.dart | 6 +++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 7c4ff48..aa05355 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -17,10 +17,8 @@ import 'package:venera/foundation/image_provider/cached_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/network/download.dart'; -import 'package:venera/pages/category_comics_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/pages/reader/reader.dart'; -import 'package:venera/pages/search_result_page.dart'; import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; @@ -411,14 +409,20 @@ class _ComicPageState extends LoadingState var group = history!.group; String text; if (haveChapter) { - var epName = group == null - ? comic.chapters!.titles.elementAt( - math.min(ep - 1, comic.chapters!.length - 1), - ) - : comic.chapters! - .getGroupByIndex(group - 1) - .values - .elementAt(ep - 1); + var epName = "E$ep"; + try { + epName = group == null + ? comic.chapters!.titles.elementAt( + math.min(ep - 1, comic.chapters!.length - 1), + ) + : comic.chapters! + .getGroupByIndex(group - 1) + .values + .elementAt(ep - 1); + } + catch(e) { + // ignore + } text = "${"Last Reading".tl}: $epName P$page"; } else { text = "${"Last Reading".tl}: P$page"; diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 456ef64..a33aeac 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -43,9 +43,10 @@ class _ReaderImagesState extends State<_ReaderImages> { }); } } else { + var cp = reader.widget.chapters?.ids.elementAtOrNull(reader.chapter - 1); var res = await reader.type.comicSource!.loadComicPages!( reader.widget.cid, - reader.widget.chapters?.ids.elementAt(reader.chapter - 1), + cp, ); if (res.error) { setState(() { @@ -747,7 +748,8 @@ class _ContinuousModeState extends State<_ContinuousMode> } Offset offset; var sp = scrollController.position; - if (sp.pixels <= sp.minScrollExtent || sp.pixels >= sp.maxScrollExtent) { + if (sp.pixels <= sp.minScrollExtent || + sp.pixels >= sp.maxScrollExtent) { offset = Offset(value.dx, value.dy); } else { if (reader.mode == ReaderMode.continuousTopToBottom) { From 554b9f2a77658eaf7fc29e0421e8a06e7d8687d1 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 19:31:41 +0800 Subject: [PATCH 22/29] Fix search sources in search results page. --- assets/translation.json | 6 ++++-- lib/pages/search_result_page.dart | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 4b42aa2..51e8819 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -384,7 +384,8 @@ "Pages": "页数", "Long press zoom position": "长按缩放位置", "Press position": "按压位置", - "Screen center": "屏幕中心" + "Screen center": "屏幕中心", + "Suggestions": "建议" }, "zh_TW": { "Home": "首頁", @@ -771,6 +772,7 @@ "Pages": "頁數", "Long press zoom position": "長按縮放位置", "Press position": "按壓位置", - "Screen center": "螢幕中心" + "Screen center": "螢幕中心", + "Suggestions": "建議" } } diff --git a/lib/pages/search_result_page.dart b/lib/pages/search_result_page.dart index 94f700d..c0abf46 100644 --- a/lib/pages/search_result_page.dart +++ b/lib/pages/search_result_page.dart @@ -441,6 +441,11 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> { @override Widget build(BuildContext context) { + var sources = ComicSource.all(); + var enabled = appdata.settings['searchSources'] as List; + sources.removeWhere((e) { + return !enabled.contains(e.key); + }); return ContentDialog( title: "Settings".tl, content: Column( @@ -452,7 +457,7 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> { Wrap( spacing: 8, runSpacing: 8, - children: ComicSource.all().map((e) { + children: sources.map((e) { return OptionChip( text: e.name.tl, isSelected: searchTarget == e.key, From c096f5a2d813d4fdf85fdc6ef8d38b0bdcdd5585 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 20:11:05 +0800 Subject: [PATCH 23/29] Add dynamic category part. --- lib/foundation/comic_source/category.dart | 37 +++++++++++++++++++++++ lib/foundation/comic_source/parser.dart | 25 +++++++++++---- lib/pages/category_comics_page.dart | 3 ++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index 1ade4e0..a27e247 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -97,6 +97,43 @@ class RandomCategoryPart extends BaseCategoryPart { ); } +class DynamicCategoryPart extends BaseCategoryPart { + final JSAutoFreeFunction loader; + + final String sourceKey; + + @override + List get categories { + var data = loader([]); + print(data); + if (data is! List) { + throw "DynamicCategoryPart loader must return a List"; + } + var res = []; + for (var item in data) { + if (item is! Map) { + throw "DynamicCategoryPart loader must return a List of Map"; + } + var label = item['label']; + var target = PageJumpTarget.parse(sourceKey, item['target']); + if (label is! String) { + throw "Category label must be a String"; + } + res.add(CategoryItem(label, target)); + } + return res; + } + + @override + bool get enableRandom => false; + + @override + final String title; + + /// A [BaseCategoryPart] that show dynamic tags on category page. + const DynamicCategoryPart(this.title, this.loader, this.sourceKey); +} + CategoryData getCategoryDataWithKey(String key) { for (var source in ComicSource.all()) { if (source.categoryData?.key == key) { diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index 6a2e590..0d92978 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -403,27 +403,40 @@ class ComicSourceParser { var categoryParts = []; for (var c in doc["parts"]) { - if (c["categories"] is! List || c["categories"].isEmpty) { + if (c["categories"] != null && c["categories"] is! List) { continue; } - List categories = c["categories"]; - if (categories[0] is Map) { + List? categories = c["categories"]; + if (categories == null || categories[0] is Map) { // new format final String name = c["name"]; final String type = c["type"]; final cs = categories - .map( + ?.map( (e) => CategoryItem( e['label'], PageJumpTarget.parse(_key!, e['target']), ), ) .toList(); + if (type != "dynamic" && (cs == null || cs.isEmpty)) { + continue; + } if (type == "fixed") { - categoryParts.add(FixedCategoryPart(name, cs)); + categoryParts.add(FixedCategoryPart(name, cs!)); } else if (type == "random") { categoryParts - .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); + .add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1)); + } else if (type == "dynamic" && categories == null) { + var loader = c["loader"]; + if (loader is! JSInvokable) { + throw "DynamicCategoryPart loader must be a function"; + } + categoryParts.add(DynamicCategoryPart( + name, + JSAutoFreeFunction(loader), + _key!, + )); } } else { // old format diff --git a/lib/pages/category_comics_page.dart b/lib/pages/category_comics_page.dart index a8840d6..f29aaaf 100644 --- a/lib/pages/category_comics_page.dart +++ b/lib/pages/category_comics_page.dart @@ -34,6 +34,9 @@ class _CategoryComicsPageState extends State { void findData() { for (final source in ComicSource.all()) { if (source.categoryData?.key == widget.categoryKey) { + if (source.categoryComicsData == null) { + throw "The comic source ${source.name} does not support category comics"; + } data = source.categoryComicsData!; options = data.options.where((element) { if (element.notShowWhen.contains(widget.category)) { From 6eb0060dd641cb75340c52c44aac7e1e3dfc129c Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 20:29:30 +0800 Subject: [PATCH 24/29] Add debug page. --- lib/pages/settings/app.dart | 11 ---- lib/pages/settings/debug.dart | 95 +++++++++++++++++++++++++++ lib/pages/settings/settings_page.dart | 6 +- 3 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 lib/pages/settings/debug.dart diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index e190d14..a6f91f8 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -140,17 +140,6 @@ class _AppSettingsState extends State { }, actionTitle: 'Set'.tl, ).toSliver(), - _SettingPartTitle( - title: "Log".tl, - icon: Icons.error_outline, - ), - _CallbackSetting( - title: "Open Log".tl, - callback: () { - context.to(() => const LogsPage()); - }, - actionTitle: 'Open'.tl, - ).toSliver(), _SettingPartTitle( title: "User".tl, icon: Icons.person_outline, diff --git a/lib/pages/settings/debug.dart b/lib/pages/settings/debug.dart new file mode 100644 index 0000000..232861b --- /dev/null +++ b/lib/pages/settings/debug.dart @@ -0,0 +1,95 @@ +part of 'settings_page.dart'; + +class DebugPage extends StatefulWidget { + const DebugPage({super.key}); + + @override + State createState() => DebugPageState(); +} + +class DebugPageState extends State { + final controller = TextEditingController(); + + var result = ""; + + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("Debug".tl)), + _CallbackSetting( + title: "Reload Configs", + actionTitle: "Reload", + callback: () { + ComicSourceManager().reload(); + }, + ).toSliver(), + _CallbackSetting( + title: "Open Log".tl, + callback: () { + context.to(() => const LogsPage()); + }, + actionTitle: 'Open'.tl, + ).toSliver(), + SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 8), + const Text( + "JS Evaluator", + style: TextStyle(fontSize: 16), + ).toAlign(Alignment.centerLeft).paddingLeft(16), + Container( + width: double.infinity, + height: 200, + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: TextField( + controller: controller, + maxLines: null, + expands: true, + textAlign: TextAlign.start, + textAlignVertical: TextAlignVertical.top, + decoration: InputDecoration( + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(8), + ), + ), + ), + TextButton( + onPressed: () { + try { + var res = JsEngine().runCode(controller.text); + setState(() { + result = res.toString(); + }); + } catch (e) { + setState(() { + result = e.toString(); + }); + } + }, + child: const Text("Run"), + ).toAlign(Alignment.centerRight).paddingRight(16), + const Text( + "Result", + style: TextStyle(fontSize: 16), + ).toAlign(Alignment.centerLeft).paddingLeft(16), + Container( + width: double.infinity, + height: 200, + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outline), + borderRadius: BorderRadius.circular(4), + ), + child: SingleChildScrollView( + child: Text(result).paddingAll(4), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index cb79750..2bfc02f 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -30,6 +30,7 @@ part 'local_favorites.dart'; part 'app.dart'; part 'about.dart'; part 'network.dart'; +part 'debug.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({this.initialPage = -1, super.key}); @@ -55,6 +56,7 @@ class _SettingsPageState extends State implements PopEntry { "APP", "Network", "About", + "Debug" ]; final icons = [ @@ -64,7 +66,8 @@ class _SettingsPageState extends State implements PopEntry { Icons.collections_bookmark_rounded, Icons.apps, Icons.public, - Icons.info + Icons.info, + Icons.bug_report, ]; double offset = 0; @@ -350,6 +353,7 @@ class _SettingsPageState extends State implements PopEntry { 4 => const AppSettings(), 5 => const NetworkSettings(), 6 => const AboutSettings(), + 7 => const DebugPage(), _ => throw UnimplementedError() }; } From aa8eec5792d4dbac8ff3d93ae02aee0ca2103aed Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 20:48:04 +0800 Subject: [PATCH 25/29] Improve UI. --- lib/components/comic.dart | 7 ++++++- lib/pages/settings/settings_page.dart | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 76aaebd..3ad1f2b 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -334,7 +334,12 @@ class ComicTile extends StatelessWidget { } var children = []; - for (var line in text.split('\n')) { + var lines = text.split('\n'); + lines.removeWhere((e) => e.trim().isEmpty); + if (lines.length > 3) { + lines = lines.sublist(0, 3); + } + for (var line in lines) { children.add(Container( margin: const EdgeInsets.fromLTRB(2, 0, 2, 2), padding: constraints.maxWidth < 80 diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 2bfc02f..6adbfca 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -249,6 +249,9 @@ class _SettingsPageState extends State implements PopEntry { } void handlePointerDown(PointerDownEvent event) { + if (!App.isIOS) { + return; + } if (event.position.dx < 20) { gestureRecognizer.addPointer(event); } From fcf0334d55781852424deff8582539f162a7c4a9 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 20:58:06 +0800 Subject: [PATCH 26/29] Fix the issue that the downloaded chapters was not saved when download a comic without select chapters. Close #305 --- lib/network/download.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/network/download.dart b/lib/network/download.dart index d21017c..bb8c6d3 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -482,7 +482,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { chapters: comic!.chapters, cover: File(_cover!.split("file://").last).name, comicType: ComicType(source.key.hashCode), - downloadedChapters: chapters ?? [], + downloadedChapters: chapters ?? comic?.chapters?.ids.toList() ?? [], createdAt: DateTime.now(), ); } From 211850d73e48600d8b7cc0e44626e1b7b29cadfb Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 21:22:00 +0800 Subject: [PATCH 27/29] Improve comic source importing UI --- assets/translation.json | 8 +++++-- lib/pages/comic_source_page.dart | 36 ++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 51e8819..9749fc6 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -385,7 +385,9 @@ "Long press zoom position": "长按缩放位置", "Press position": "按压位置", "Screen center": "屏幕中心", - "Suggestions": "建议" + "Suggestions": "建议", + "Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题", + "Click the setting icon to change the source list url.": "点击设置图标更改源列表URL" }, "zh_TW": { "Home": "首頁", @@ -773,6 +775,8 @@ "Long press zoom position": "長按縮放位置", "Press position": "按壓位置", "Screen center": "螢幕中心", - "Suggestions": "建議" + "Suggestions": "建議", + "Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題", + "Click the setting icon to change the source list url.": "點擊設定圖示更改源列表URL" } } diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index b72af0e..49dd89f 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -374,8 +374,35 @@ class _ComicSourceListState extends State<_ComicSourceList> { } else { var currentKey = ComicSource.all().map((e) => e.key).toList(); return ListView.builder( - itemCount: json!.length, + itemCount: json!.length + 1, itemBuilder: (context, index) { + if (index == 0) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: context.colorScheme.primaryContainer, + ), + child: Row( + children: [ + const Icon(Icons.info_outline), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Do not report any issues related to sources to App repo.".tl), + Text("Click the setting icon to change the source list url.".tl), + ], + ), + ), + ], + ), + ); + } + index--; + var key = json![index]["key"]; var action = currentKey.contains(key) ? const Icon(Icons.check, size: 20).paddingRight(8) @@ -403,9 +430,14 @@ class _ComicSourceListState extends State<_ComicSourceList> { }, ).fixHeight(32); + var description = json![index]["version"]; + if (json![index]["description"] != null) { + description = "$description\n${json![index]["description"]}"; + } + return ListTile( title: Text(json![index]["name"]), - subtitle: Text(json![index]["version"]), + subtitle: Text(description), trailing: action, ); }, From 49481bfa6a08cafe6605acd771137aa8fa72e3d4 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 21:32:31 +0800 Subject: [PATCH 28/29] Fix windows arm64 build script --- pubspec.lock | 20 ++++++++++---------- windows/build_arm64.iss | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 94085e8..8fa2170 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" battery_plus: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: diff --git a/windows/build_arm64.iss b/windows/build_arm64.iss index db4fc70..4e1d1dc 100644 --- a/windows/build_arm64.iss +++ b/windows/build_arm64.iss @@ -2,11 +2,11 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Venera" -#define MyAppVersion "1.3.4" +#define MyAppVersion "{{version}}" #define MyAppPublisher "nyne" #define MyAppURL "https://github.com/venera-app/venera" #define MyAppExeName "venera.exe" -#define RootPath "D:\code\venera" +#define RootPath "{{root_path}}" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. From 2481780ab330980a90d7c3c4918fff4ca3dff0b1 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 5 Apr 2025 22:03:54 +0800 Subject: [PATCH 29/29] fix issues reported by analyzer. --- lib/foundation/comic_source/category.dart | 1 - lib/foundation/log.dart | 2 -- 2 files changed, 3 deletions(-) diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index a27e247..383a7ad 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -105,7 +105,6 @@ class DynamicCategoryPart extends BaseCategoryPart { @override List get categories { var data = loader([]); - print(data); if (data is! List) { throw "DynamicCategoryPart loader must return a List"; } diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart index aefdf8c..fba4caf 100644 --- a/lib/foundation/log.dart +++ b/lib/foundation/log.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/utils/ext.dart';