From c28f4d40c2ed75652754b9e63be605cbf2722c97 Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:30:30 +0800 Subject: [PATCH 01/38] Add selection in history page and refresh home page after history changed (#199) --- lib/pages/history_page.dart | 254 ++++++++++++++++++++++++++---------- lib/pages/home_page.dart | 10 +- 2 files changed, 193 insertions(+), 71 deletions(-) diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index b4ea318..f813d1a 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -33,82 +33,202 @@ class _HistoryPageState extends State { } var comics = HistoryManager().getAll(); - var controller = FlyoutController(); + + bool multiSelectMode = false; + Map selectedComics = {}; + + void selectAll() { + setState(() { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + }); + } + + void deSelect() { + setState(() { + selectedComics.clear(); + }); + } + + void invertSelection() { + setState(() { + comics.asMap().forEach((k, v) { + selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + }); + selectedComics.removeWhere((k, v) => !v); + }); + } @override Widget build(BuildContext context) { - return Scaffold( - body: SmoothCustomScrollView( - slivers: [ - SliverAppbar( - title: Text('History'.tl), - actions: [ - Tooltip( - message: 'Clear History'.tl, - child: Flyout( - controller: controller, - flyoutBuilder: (context) { - return FlyoutContent( - title: 'Clear History'.tl, - content: Text( - 'Are you sure you want to clear your history?'.tl), - actions: [ - Button.filled( - color: context.colorScheme.error, - onPressed: () { - HistoryManager().clearHistory(); - context.pop(); - }, - child: Text('Clear'.tl), - ), - ], - ); - }, - child: IconButton( - icon: const Icon(Icons.clear_all), - onPressed: () { - controller.show(); - }, - ), - ), - ) - ], - ), - SliverGridComics( - comics: comics, - badgeBuilder: (c) { - return ComicSource.find(c.sourceKey)?.name; - }, - menuBuilder: (c) { - return [ - MenuEntry( - icon: Icons.remove, - text: 'Remove'.tl, + List selectActions = [ + IconButton( + icon: const Icon(Icons.select_all), + tooltip: "Select All".tl, + onPressed: selectAll + ), + IconButton( + icon: const Icon(Icons.deselect), + tooltip: "Deselect".tl, + onPressed: deSelect + ), + IconButton( + icon: const Icon(Icons.flip), + tooltip: "Invert Selection".tl, + onPressed: invertSelection + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: "Delete".tl, + onPressed: selectedComics.isEmpty ? null : () { + for (final comic in selectedComics.keys) { + if (comic.sourceKey.startsWith("Unknown")) { + HistoryManager().remove( + comic.id, + ComicType(int.parse(comic.sourceKey.split(':')[1])), + ); + } else if (comic.sourceKey == 'local') { + HistoryManager().remove( + comic.id, + ComicType.local, + ); + } else { + HistoryManager().remove( + comic.id, + ComicType(comic.sourceKey.hashCode), + ); + } + } + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + }, + ), + ]; + + List normalActions = [ + Tooltip( + message: 'Clear History'.tl, + child: Flyout( + controller: controller, + flyoutBuilder: (context) { + return FlyoutContent( + title: 'Clear History'.tl, + content: Text('Are you sure you want to clear your history?'.tl), + actions: [ + Button.filled( color: context.colorScheme.error, - onClick: () { - if (c.sourceKey.startsWith("Unknown")) { - HistoryManager().remove( - c.id, - ComicType(int.parse(c.sourceKey.split(':')[1])), - ); - } else if (c.sourceKey == 'local') { - HistoryManager().remove( - c.id, - ComicType.local, - ); + onPressed: () { + HistoryManager().clearHistory(); + context.pop(); + }, + child: Text('Clear'.tl), + ), + ], + ); + }, + child: IconButton( + icon: const Icon(Icons.clear_all), + onPressed: () { + controller.show(); + }, + ), + ), + ) + ]; + + return PopScope( + canPop: !multiSelectMode, + onPopInvokedWithResult: (didPop, result) { + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + } + }, + child: Scaffold( + body: SmoothCustomScrollView( + slivers: [ + SliverAppbar( + leading: Tooltip( + message: multiSelectMode ? "Cancel".tl : "Back".tl, + child: IconButton( + onPressed: () { + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); } else { - HistoryManager().remove( - c.id, - ComicType(c.sourceKey.hashCode), - ); + context.pop(); } }, + icon: multiSelectMode + ? const Icon(Icons.close) + : const Icon(Icons.arrow_back), ), - ]; - }, - ), - ], + ), + title: multiSelectMode + ? Text(selectedComics.length.toString()) + : Text('History'.tl), + actions: multiSelectMode ? selectActions : normalActions, + ), + SliverGridComics( + comics: comics, + selections: selectedComics, + onLongPressed: (c) { + setState(() { + multiSelectMode = true; + selectedComics[c as History] = true; + }); + }, + onTap: multiSelectMode ? (c) { + setState(() { + if (selectedComics.containsKey(c as History)) { + selectedComics.remove(c); + } else { + selectedComics[c] = true; + } + if (selectedComics.isEmpty) { + multiSelectMode = false; + } + }); + } : null, + badgeBuilder: (c) { + return ComicSource.find(c.sourceKey)?.name; + }, + menuBuilder: (c) { + return [ + MenuEntry( + icon: Icons.remove, + text: 'Remove'.tl, + color: context.colorScheme.error, + onClick: () { + if (c.sourceKey.startsWith("Unknown")) { + HistoryManager().remove( + c.id, + ComicType(int.parse(c.sourceKey.split(':')[1])), + ); + } else if (c.sourceKey == 'local') { + HistoryManager().remove( + c.id, + ComicType.local, + ); + } else { + HistoryManager().remove( + c.id, + ComicType(c.sourceKey.hashCode), + ); + } + }, + ), + ]; + }, + ), + ], + ), ), ); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f11e6d2..ddb9584 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -197,10 +197,12 @@ class _HistoryState extends State<_History> { late int count; void onHistoryChange() { - setState(() { - history = HistoryManager().getRecent(); - count = HistoryManager().count(); - }); + if (mounted) { + setState(() { + history = HistoryManager().getRecent(); + count = HistoryManager().count(); + }); + } } @override From bd5341696873845c7eb5c8fea01eea0fb27ea979 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 18 Feb 2025 18:37:57 +0800 Subject: [PATCH 02/38] Improve webdav settings --- assets/translation.json | 12 ++++++++-- lib/pages/settings/app.dart | 46 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index fa5b53d..44f5018 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -353,7 +353,11 @@ "No updates found": "未找到更新", "All Comics": "全部漫画", "The comic will be marked as no updates as soon as you read it.": "漫画将在您阅读后立即标记为无更新", - "Disable": "禁用" + "Disable": "禁用", + "Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据", + "Webdav is enabled." : "已启用Webdav", + "Cache cleared": "缓存已清除", + "Disabled": "已禁用" }, "zh_TW": { "Home": "首頁", @@ -709,6 +713,10 @@ "No updates found": "未找到更新", "All Comics": "全部漫畫", "The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新", - "Disable": "禁用" + "Disable": "禁用", + "Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據", + "Webdav is enabled." : "已啟用Webdav", + "Cache cleared": "緩存已清除", + "Disabled": "已禁用" } } \ No newline at end of file diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index c324af9..3415aea 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -332,8 +332,8 @@ class _WebdavSettingState extends State<_WebdavSetting> { String pass = ""; bool isTesting = false; - bool upload = true; + bool isEnabled = false; @override void initState() { @@ -348,6 +348,7 @@ class _WebdavSettingState extends State<_WebdavSetting> { url = configs[0]; user = configs[1]; pass = configs[2]; + isEnabled = true; } @override @@ -411,6 +412,49 @@ class _WebdavSettingState extends State<_WebdavSetting> { ], ), const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.info_outline, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text("Once the operation is successful, app will automatically sync data with the server.".tl), + ), + ], + ), + ), + if (isEnabled) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.sync, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text("Webdav is enabled.".tl), + ), + Button.text( + onPressed: () { + appdata.settings['webdav'] = []; + appdata.saveData(); + context.showMessage(message: "Disabled".tl); + App.rootPop(); + }, + child: Text("Disable".tl), + ) + ], + ), + ).paddingTop(16), + const SizedBox(height: 16), Center( child: Button.filled( isLoading: isTesting, From d73e152cec11c3fd0310b204696ce9b3261d4edd Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 18 Feb 2025 18:49:25 +0800 Subject: [PATCH 03/38] Fixed an issue where clicking on local comics on the home page would cause the history to be lost. Close #196 --- lib/foundation/history.dart | 2 +- lib/pages/comic_details_page/comic_page.dart | 2 ++ lib/pages/reader/images.dart | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index bd21d1d..a8a0e01 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -319,7 +319,7 @@ class HistoryManager with ChangeNotifier { for (var element in res) { _cachedHistoryIds![element["id"] as String] = true; } - for (var key in cachedHistories.keys) { + for (var key in cachedHistories.keys.toList()) { if (!_cachedHistoryIds!.containsKey(key)) { cachedHistories.remove(key); } diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 6cf8913..f0b34a9 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -165,6 +165,8 @@ class _ComicPageState extends LoadingState cid: widget.id, name: localComic.title, chapters: localComic.chapters, + initialPage: history?.page, + initialChapter: history?.ep, history: history ?? History.fromModel( model: localComic, diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 7b8eee0..0091f01 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -406,6 +406,9 @@ class _ContinuousModeState extends State<_ContinuousMode> } void onPositionChanged() { + if (itemPositionsListener.itemPositions.value.isEmpty) { + return; + } var page = itemPositionsListener.itemPositions.value.first.index; page = page.clamp(1, reader.maxPage); if (page != reader.page) { From 5fb0d2327d979ae0695e0f4f90d2c292ab0b0095 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 18 Feb 2025 19:39:15 +0800 Subject: [PATCH 04/38] Improve history display. Part of #200 --- assets/translation.json | 8 ++++---- lib/pages/comic_details_page/comic_page.dart | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 44f5018..6cbaeaf 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -336,7 +336,6 @@ "Number of images preloaded": "预加载图片数量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Chapter @ep Page @page": "上次阅读: 第 @ep 章 第 @page 页", "Last Reading: Page @page": "上次阅读: 第 @page 页", "Replies": "回复", "Follow Updates": "追更", @@ -357,7 +356,8 @@ "Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据", "Webdav is enabled." : "已启用Webdav", "Cache cleared": "缓存已清除", - "Disabled": "已禁用" + "Disabled": "已禁用", + "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页" }, "zh_TW": { "Home": "首頁", @@ -696,7 +696,6 @@ "Number of images preloaded": "預加載圖片數量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Chapter @ep Page @page": "上次閱讀: 第 @ep 章 第 @page 頁", "Last Reading: Page @page": "上次閱讀: 第 @page 頁", "Replies": "回覆", "Follow Updates": "追更", @@ -717,6 +716,7 @@ "Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據", "Webdav is enabled." : "已啟用Webdav", "Cache cleared": "緩存已清除", - "Disabled": "已禁用" + "Disabled": "已禁用", + "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁" } } \ No newline at end of file diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index f0b34a9..609246c 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -385,8 +385,9 @@ class _ComicPageState extends LoadingState var ep = history!.ep; String text; if (haveChapter) { - text = "Last Reading: Chapter @ep Page @page".tlParams({ - 'ep': ep, + text = "Last Reading: @epName Page @page".tlParams({ + 'epName': comic.chapters!.values.elementAt( + math.min(ep - 1, comic.chapters!.length - 1)), 'page': page, }); } else { From 0c54a9be1186447b8ee553d8e564d06d779b0fb7 Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:22:09 +0800 Subject: [PATCH 05/38] Improve WebDAV: add auto sync option and improve settings UI (#203) --- assets/translation.json | 8 ++--- lib/pages/settings/app.dart | 61 +++++++++++++++++++++---------------- lib/utils/data_sync.dart | 10 ++++-- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 6cbaeaf..93356c5 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -354,10 +354,10 @@ "The comic will be marked as no updates as soon as you read it.": "漫画将在您阅读后立即标记为无更新", "Disable": "禁用", "Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据", - "Webdav is enabled." : "已启用Webdav", "Cache cleared": "缓存已清除", "Disabled": "已禁用", - "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页" + "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页", + "WebDAV Auto Sync": "WebDAV 自动同步" }, "zh_TW": { "Home": "首頁", @@ -714,9 +714,9 @@ "The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新", "Disable": "禁用", "Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據", - "Webdav is enabled." : "已啟用Webdav", "Cache cleared": "緩存已清除", "Disabled": "已禁用", - "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁" + "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁", + "WebDAV Auto Sync": "WebDAV 自動同步" } } \ No newline at end of file diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 3415aea..9dbd3ee 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -330,6 +330,7 @@ class _WebdavSettingState extends State<_WebdavSetting> { String url = ""; String user = ""; String pass = ""; + bool autoSync = false; bool isTesting = false; bool upload = true; @@ -349,6 +350,7 @@ class _WebdavSettingState extends State<_WebdavSetting> { user = configs[1]; pass = configs[2]; isEnabled = true; + autoSync = appdata.settings['webdavAutoSync'] ?? false; } @override @@ -358,6 +360,18 @@ class _WebdavSettingState extends State<_WebdavSetting> { body: SingleChildScrollView( child: Column( children: [ + const SizedBox(height: 12), + SwitchListTile( + title: Text("WebDAV Auto Sync".tl), + value: autoSync, + onChanged: (value) { + setState(() { + autoSync = value; + appdata.settings['webdavAutoSync'] = value; + appdata.saveData(); + }); + }, + ), const SizedBox(height: 12), TextField( decoration: const InputDecoration( @@ -428,39 +442,33 @@ class _WebdavSettingState extends State<_WebdavSetting> { ], ), ), - if (isEnabled) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon(Icons.sync, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text("Webdav is enabled.".tl), - ), - Button.text( - onPressed: () { - appdata.settings['webdav'] = []; - appdata.saveData(); - context.showMessage(message: "Disabled".tl); - App.rootPop(); - }, - child: Text("Disable".tl), - ) - ], - ), - ).paddingTop(16), const SizedBox(height: 16), Center( child: Button.filled( isLoading: isTesting, onPressed: () async { var oldConfig = appdata.settings['webdav']; + var oldAutoSync = appdata.settings['webdavAutoSync']; + + if (url.trim().isEmpty && user.trim().isEmpty && pass.trim().isEmpty) { + appdata.settings['webdav'] = []; + appdata.settings['webdavAutoSync'] = false; + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); + return; + } + appdata.settings['webdav'] = [url, user, pass]; + appdata.settings['webdavAutoSync'] = autoSync; + + if (!autoSync) { + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); + return; + } + setState(() { isTesting = true; }); @@ -472,6 +480,7 @@ class _WebdavSettingState extends State<_WebdavSetting> { isTesting = false; }); appdata.settings['webdav'] = oldConfig; + appdata.settings['webdavAutoSync'] = oldAutoSync; context.showMessage(message: testResult.errorMessage!); return; } diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 4ee6435..3cd9937 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -40,15 +40,19 @@ class DataSync with ChangeNotifier { bool get isEnabled { var config = appdata.settings['webdav']; - return config is List && config.isNotEmpty; + var autoSync = appdata.settings['webdavAutoSync'] ?? false; + return autoSync && config is List && config.isNotEmpty; } List? _validateConfig() { var config = appdata.settings['webdav']; - if (config is! List || (config.isNotEmpty && config.length != 3)) { + if (config is! List) { return null; } - if (config.whereType().length != 3) { + if (config.isEmpty) { + return []; + } + if (config.length != 3 || config.whereType().length != 3) { return null; } return List.from(config); From 90f0c9dab38372c3758546df1a79e5554863be2f Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Tue, 18 Feb 2025 23:11:51 +0800 Subject: [PATCH 06/38] Fix comic menu cannot work in history_page when use mobile device (#204) --- lib/foundation/history.dart | 1 - lib/pages/history_page.dart | 141 +++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index a8a0e01..87edb15 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -7,7 +7,6 @@ import 'dart:ffi' as ffi; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart' show ChangeNotifier; -import 'package:sqlite3/common.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index f813d1a..049021e 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -29,12 +29,18 @@ class _HistoryPageState extends State { void onUpdate() { setState(() { comics = HistoryManager().getAll(); + if (multiSelectMode) { + selectedComics.removeWhere((comic, _) => !comics.contains(comic)); + if (selectedComics.isEmpty) { + multiSelectMode = false; + } + } }); } var comics = HistoryManager().getAll(); var controller = FlyoutController(); - + bool multiSelectMode = false; Map selectedComics = {}; @@ -59,55 +65,72 @@ class _HistoryPageState extends State { }); } + void _removeHistory(History comic) { + if (comic.sourceKey.startsWith("Unknown")) { + HistoryManager().remove( + comic.id, + ComicType(int.parse(comic.sourceKey.split(':')[1])), + ); + } else if (comic.sourceKey == 'local') { + HistoryManager().remove( + comic.id, + ComicType.local, + ); + } else { + HistoryManager().remove( + comic.id, + ComicType(comic.sourceKey.hashCode), + ); + } + } + @override Widget build(BuildContext context) { List selectActions = [ IconButton( - icon: const Icon(Icons.select_all), - tooltip: "Select All".tl, - onPressed: selectAll + icon: const Icon(Icons.select_all), + tooltip: "Select All".tl, + onPressed: selectAll ), IconButton( - icon: const Icon(Icons.deselect), - tooltip: "Deselect".tl, - onPressed: deSelect + icon: const Icon(Icons.deselect), + tooltip: "Deselect".tl, + onPressed: deSelect ), IconButton( - icon: const Icon(Icons.flip), - tooltip: "Invert Selection".tl, - onPressed: invertSelection + icon: const Icon(Icons.flip), + tooltip: "Invert Selection".tl, + onPressed: invertSelection ), IconButton( icon: const Icon(Icons.delete), tooltip: "Delete".tl, - onPressed: selectedComics.isEmpty ? null : () { - for (final comic in selectedComics.keys) { - if (comic.sourceKey.startsWith("Unknown")) { - HistoryManager().remove( - comic.id, - ComicType(int.parse(comic.sourceKey.split(':')[1])), - ); - } else if (comic.sourceKey == 'local') { - HistoryManager().remove( - comic.id, - ComicType.local, - ); - } else { - HistoryManager().remove( - comic.id, - ComicType(comic.sourceKey.hashCode), - ); - } - } - setState(() { - multiSelectMode = false; - selectedComics.clear(); - }); - }, + onPressed: selectedComics.isEmpty + ? null + : () { + final comicsToDelete = List.from(selectedComics.keys); + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + + for (final comic in comicsToDelete) { + _removeHistory(comic); + } + }, ), ]; List normalActions = [ + IconButton( + icon: const Icon(Icons.checklist), + tooltip: multiSelectMode ? "Exit Multi-Select".tl : "Multi-Select".tl, + onPressed: () { + setState(() { + multiSelectMode = !multiSelectMode; + }); + }, + ), Tooltip( message: 'Clear History'.tl, child: Flyout( @@ -178,24 +201,21 @@ class _HistoryPageState extends State { SliverGridComics( comics: comics, selections: selectedComics, - onLongPressed: (c) { - setState(() { - multiSelectMode = true; - selectedComics[c as History] = true; - }); - }, - onTap: multiSelectMode ? (c) { - setState(() { - if (selectedComics.containsKey(c as History)) { - selectedComics.remove(c); - } else { - selectedComics[c] = true; - } - if (selectedComics.isEmpty) { - multiSelectMode = false; - } - }); - } : null, + onLongPressed: null, + onTap: multiSelectMode + ? (c) { + setState(() { + if (selectedComics.containsKey(c as History)) { + selectedComics.remove(c); + } else { + selectedComics[c] = true; + } + if (selectedComics.isEmpty) { + multiSelectMode = false; + } + }); + } + : null, badgeBuilder: (c) { return ComicSource.find(c.sourceKey)?.name; }, @@ -206,22 +226,7 @@ class _HistoryPageState extends State { text: 'Remove'.tl, color: context.colorScheme.error, onClick: () { - if (c.sourceKey.startsWith("Unknown")) { - HistoryManager().remove( - c.id, - ComicType(int.parse(c.sourceKey.split(':')[1])), - ); - } else if (c.sourceKey == 'local') { - HistoryManager().remove( - c.id, - ComicType.local, - ); - } else { - HistoryManager().remove( - c.id, - ComicType(c.sourceKey.hashCode), - ); - } + _removeHistory(c as History); }, ), ]; From d0d27206cd3ea32fcac49bbbcee46ffe79faf60b Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:09:18 +0800 Subject: [PATCH 07/38] Fix thumbnail tap functionality to navigate to the correct reader page (#205) --- lib/pages/reader/reader.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index ed1cb71..609c9e0 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -156,6 +156,9 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { if (chapter < 1) { chapter = 1; } + if (widget.initialPage != null) { + page = (widget.initialPage! / imagesPerPage).ceil(); + } mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; Future.microtask(() { From 26a1d68913d0191c5e663732ad69842f7aa33a0a Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 19 Feb 2025 16:45:46 +0800 Subject: [PATCH 08/38] Fix invalid template. --- lib/foundation/appdata.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index e6c1dcc..a44c052 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -188,9 +188,9 @@ const defaultCustomImageProcessing = ''' * @returns {Promise | {image: Promise, onCancel: () => void}} - The processed image */ function processImage(image, cid, eid, page, sourceKey) { - let image = new Promise((resolve, reject) => { + let futureImage = new Promise((resolve, reject) => { resolve(image); }); - return image; + return futureImage; } '''; From a471e79ef2fcbb45dd3226f41c5400ba8c1b42ad Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 19 Feb 2025 17:32:05 +0800 Subject: [PATCH 09/38] Improve init --- lib/foundation/app.dart | 15 +++++++++++++++ lib/foundation/appdata.dart | 10 +++++----- lib/init.dart | 37 ++++++++++++++++++------------------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 91b6509..d575d26 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -3,8 +3,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:venera/foundation/history.dart'; import 'appdata.dart'; +import 'favorites.dart'; +import 'local.dart'; export "widget_utils.dart"; export "context.dart"; @@ -51,6 +54,14 @@ class _App { BuildContext get rootContext => rootNavigatorKey.currentContext!; + final Appdata data = appdata; + + final HistoryManager history = HistoryManager(); + + final LocalFavoritesManager favorites = LocalFavoritesManager(); + + final LocalManager local = LocalManager(); + void rootPop() { rootNavigatorKey.currentState?.maybePop(); } @@ -66,6 +77,10 @@ class _App { Future init() async { cachePath = (await getApplicationCacheDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path; + await data.init(); + await history.init(); + await favorites.init(); + await local.init(); } Function? _forceRebuildHandler; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index a44c052..4b93e29 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -6,8 +6,8 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/io.dart'; -class _Appdata { - final _Settings settings = _Settings(); +class Appdata { + final Settings settings = Settings(); var searchHistory = []; @@ -110,10 +110,10 @@ class _Appdata { } } -final appdata = _Appdata(); +final appdata = Appdata(); -class _Settings with ChangeNotifier { - _Settings(); +class Settings with ChangeNotifier { + Settings(); final _data = { 'comicDisplayMode': 'detailed', // detailed, brief diff --git a/lib/init.dart b/lib/init.dart index 972d857..6f8bcda 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -4,10 +4,7 @@ import 'package:rhttp/rhttp.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; -import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/js_engine.dart'; -import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:venera/pages/comic_source_page.dart'; @@ -32,25 +29,18 @@ extension _FutureInit on Future { } Future init() async { - await Rhttp.init(); - await SAFTaskWorker().init().wait(); - await AppTranslation.init().wait(); - await appdata.init().wait(); await App.init().wait(); - await HistoryManager().init().wait(); - await TagsTranslation.readData().wait(); - await LocalFavoritesManager().init().wait(); SingleInstanceCookieJar("${App.dataPath}/cookie.db"); - await JsEngine().init().wait(); - await ComicSource.init().wait(); - await LocalManager().init().wait(); + var futures = [ + Rhttp.init(), + SAFTaskWorker().init().wait(), + AppTranslation.init().wait(), + TagsTranslation.readData().wait(), + JsEngine().init().then((_) => ComicSource.init()).wait(), + ]; + await Future.wait(futures); CacheManager().setLimitSize(appdata.settings['cacheSize']); - if (appdata.settings['searchSources'] == null) { - appdata.settings['searchSources'] = ComicSource.all() - .where((e) => e.searchPageData != null) - .map((e) => e.key) - .toList(); - } + _checkOldConfigs(); if (App.isAndroid) { handleLinks(); } @@ -59,6 +49,15 @@ Future init() async { }; } +void _checkOldConfigs() { + if (appdata.settings['searchSources'] == null) { + appdata.settings['searchSources'] = ComicSource.all() + .where((e) => e.searchPageData != null) + .map((e) => e.key) + .toList(); + } +} + Future _checkAppUpdates() async { var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0; var now = DateTime.now().millisecondsSinceEpoch; From ee0da9a26aaa1491d99192a5f5c82ea353765155 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 19 Feb 2025 22:38:18 +0800 Subject: [PATCH 10/38] Fix the wrong sorting of follow_updates_page. Close #206 --- lib/pages/follow_updates_page.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index 45fa8a5..67fec8f 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -133,7 +133,19 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } else if (b.updateTime == null) { return 1; } - return b.updateTime!.compareTo(a.updateTime!); + try { + var aNums = a.updateTime!.split('-').map(int.parse).toList(); + var bNums = b.updateTime!.split('-').map(int.parse).toList(); + for (int i = 0; i < aNums.length; i++) { + if (aNums[i] != bNums[i]) { + return bNums[i] - aNums[i]; + } + } + return 0; + } + catch(_) { + return 0; + } }); } From a630771f0bdf611deea69b29e54c86559ad560be Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:43:23 +0800 Subject: [PATCH 11/38] Improve WebDAV data sync version handling and force sync (#207) * Fix WebDAV auto sync default setting initialization * Improve WebDAV data sync version handling and force sync --- assets/translation.json | 2 ++ lib/init.dart | 12 ++++++++ lib/pages/settings/app.dart | 13 ++++---- lib/utils/data_sync.dart | 60 ++++++++++++++++++++++--------------- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 93356c5..8aad7cd 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -189,6 +189,7 @@ "Operation": "操作", "Upload": "上传", "Saved": "已保存", + "Saved Failed": "保存失败", "Sync Data": "同步数据", "Syncing Data": "正在同步数据", "Data Sync": "数据同步", @@ -549,6 +550,7 @@ "Operation": "操作", "Upload": "上傳", "Saved": "已保存", + "Saved Failed": "保存失敗", "Sync Data": "同步數據", "Syncing Data": "正在同步數據", "Data Sync": "數據同步", diff --git a/lib/init.dart b/lib/init.dart index 6f8bcda..ad18abd 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -56,6 +56,18 @@ void _checkOldConfigs() { .map((e) => e.key) .toList(); } + + if (appdata.settings['webdavAutoSync'] == null) { + var webdavConfig = appdata.settings['webdav']; + if (webdavConfig is List && + webdavConfig.length == 3 && + webdavConfig.whereType().length == 3) { + appdata.settings['webdavAutoSync'] = true; + } else { + appdata.settings['webdavAutoSync'] = false; + } + appdata.saveData(); + } } Future _checkAppUpdates() async { diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 9dbd3ee..cc7202d 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -473,8 +473,8 @@ class _WebdavSettingState extends State<_WebdavSetting> { isTesting = true; }); var testResult = upload - ? await DataSync().uploadData() - : await DataSync().downloadData(); + ? await DataSync().uploadData(forceSync: true) + : await DataSync().downloadData(forceSync: true); if (testResult.error) { setState(() { isTesting = false; @@ -482,11 +482,12 @@ class _WebdavSettingState extends State<_WebdavSetting> { appdata.settings['webdav'] = oldConfig; appdata.settings['webdavAutoSync'] = oldAutoSync; context.showMessage(message: testResult.errorMessage!); - return; + context.showMessage(message: "Saved Failed".tl); + } else { + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); } - appdata.saveData(); - context.showMessage(message: "Saved".tl); - App.rootPop(); }, child: Text("Continue".tl), ), diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 3cd9937..f644385 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -58,7 +58,7 @@ class DataSync with ChangeNotifier { return List.from(config); } - Future> uploadData() async { + Future> uploadData({bool forceSync = false}) async { if (isDownloading) return const Res(true); if (haveWaitingTask) return const Res(true); while (isUploading) { @@ -102,20 +102,32 @@ class DataSync with ChangeNotifier { } try { - appdata.settings['dataVersion']++; - await appdata.saveData(false); - var data = await exportAppData(); - var time = - (DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString(); - var filename = time; - filename += '-'; - filename += appdata.settings['dataVersion'].toString(); - filename += '.venera'; var files = await client.readDir('/'); files = files.where((e) => e.name!.endsWith('.venera')).toList(); - var old = files.firstWhereOrNull((e) => e.name!.startsWith("$time-")); - if (old != null) { - await client.remove(old.name!); + files.sort((a, b) => b.name!.compareTo(a.name!)); + var remoteFile = files.firstWhereOrNull((e) => e.name!.endsWith('.venera')); + var remoteVersion = 0; + if (remoteFile != null) { + remoteVersion = int.tryParse(remoteFile.name!.split('-').elementAtOrNull(1)?.split('.').first ?? '0') ?? 0; + } + var localVersion = appdata.settings['dataVersion'] ?? 0; + + if (!forceSync && remoteVersion >= localVersion) { + Log.info("Data Sync", 'Local: $localVersion Remote: $remoteVersion Skip upload ForceSync: $forceSync'); + return const Res(true); + } + + appdata.settings['dataVersion'] = forceSync ? remoteVersion + 1 : localVersion + 1; + await appdata.saveData(false); + var data = await exportAppData(); + var time = (DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString(); + var filename = '$time-${appdata.settings['dataVersion']}.venera'; + + if (!forceSync) { + var old = files.firstWhereOrNull((e) => e.name!.startsWith("$time-")); + if (old != null) { + await client.remove(old.name!); + } } if (files.length >= 10) { files.sort((a, b) => a.name!.compareTo(b.name!)); @@ -123,7 +135,7 @@ class DataSync with ChangeNotifier { } await client.write(filename, await data.readAsBytes()); data.deleteIgnoreError(); - Log.info("Upload Data", "Data uploaded successfully"); + Log.info("Data Sync", "Local: ${appdata.settings['dataVersion']} Remote: $remoteVersion Data uploaded successfully ForceSync: $forceSync"); return const Res(true); } catch (e, s) { Log.error("Upload Data", e, s); @@ -135,7 +147,7 @@ class DataSync with ChangeNotifier { } } - Future> downloadData() async { + Future> downloadData({bool forceSync = false}) async { if (haveWaitingTask) return const Res(true); while (isDownloading || isUploading) { haveWaitingTask = true; @@ -184,16 +196,16 @@ class DataSync with ChangeNotifier { if (file == null) { throw 'No data file found'; } - var version = - file.name!.split('-').elementAtOrNull(1)?.split('.').first; - if (version != null && int.tryParse(version) != null) { - var currentVersion = appdata.settings['dataVersion']; - if (currentVersion != null && int.parse(version) <= currentVersion) { - Log.info("Data Sync", 'No new data to download'); - return const Res(true); - } + var version = file.name!.split('-').elementAtOrNull(1)?.split('.').first; + var remoteVersion = int.tryParse(version ?? '') ?? 0; + var localVersion = appdata.settings['dataVersion'] ?? 0; + + if (!forceSync && remoteVersion <= localVersion) { + Log.info("Data Sync", 'Local: $localVersion Remote: $remoteVersion Skip download ForceSync: $forceSync'); + return const Res(true); } - Log.info("Data Sync", "Downloading data from WebDAV server"); + + Log.info("Data Sync", "Local: $localVersion Remote: $remoteVersion Downloading data ForceSync: $forceSync"); var localFile = File(FilePath.join(App.cachePath, file.name!)); await client.read2File(file.name!, localFile.path); await importAppData(localFile, true); From 2b3c7a85649dfa9d027d5a30917a507c698f8db5 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 19 Feb 2025 22:51:02 +0800 Subject: [PATCH 12/38] Add a button to mark all comics as read. --- assets/translation.json | 8 ++++++-- lib/pages/follow_updates_page.dart | 24 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 8aad7cd..e24350b 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -358,7 +358,9 @@ "Cache cleared": "缓存已清除", "Disabled": "已禁用", "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页", - "WebDAV Auto Sync": "WebDAV 自动同步" + "WebDAV Auto Sync": "WebDAV 自动同步", + "Mark all as read": "全部标记为已读", + "Do you want to mark all as read?" : "您要全部标记为已读吗?" }, "zh_TW": { "Home": "首頁", @@ -719,6 +721,8 @@ "Cache cleared": "緩存已清除", "Disabled": "已禁用", "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁", - "WebDAV Auto Sync": "WebDAV 自動同步" + "WebDAV Auto Sync": "WebDAV 自動同步", + "Mark all as read": "全部標記為已讀", + "Do you want to mark all as read?" : "您要全部標記為已讀嗎?" } } \ No newline at end of file diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index 67fec8f..cf3dfa9 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -142,8 +142,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } } return 0; - } - catch(_) { + } catch (_) { return 0; } }); @@ -282,6 +281,27 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { "Updates".tl, style: ts.s18, ), + const Spacer(), + if (updatedComics.isNotEmpty) + IconButton( + icon: Icon(Icons.clear_all), + onPressed: () { + showConfirmDialog( + context: App.rootContext, + title: "Mark all as read".tl, + content: "Do you want to mark all as read?".tl, + onConfirm: () { + for (var comic in updatedComics) { + LocalFavoritesManager().markAsRead( + comic.id, + comic.type, + ); + } + updateFollowUpdatesUI(); + }, + ); + }, + ), ], ), ), From bd5d10e91989cfc790e05ea89ac783142b847924 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 13:08:55 +0800 Subject: [PATCH 13/38] Improve comic chapters. --- lib/components/appbar.dart | 1 + lib/foundation/comic_source/models.dart | 160 ++++++++++++---- .../image_favorites_provider.dart | 2 +- lib/foundation/local.dart | 10 +- lib/network/download.dart | 5 +- lib/pages/comic_details_page/actions.dart | 24 +-- lib/pages/comic_details_page/chapters.dart | 18 +- lib/pages/comic_details_page/comic_page.dart | 4 +- lib/pages/reader/images.dart | 2 +- lib/pages/reader/loading.dart | 2 +- lib/pages/reader/reader.dart | 6 +- lib/pages/reader/scaffold.dart | 179 ++++++++++++++++-- lib/utils/cbz.dart | 3 +- lib/utils/import_comic.dart | 5 +- 14 files changed, 324 insertions(+), 97 deletions(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index c29d696..5be9e44 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -632,6 +632,7 @@ class _TabViewBodyState extends State { void didChangeDependencies() { super.didChangeDependencies(); _controller = widget.controller ?? DefaultTabController.of(context); + _currentIndex = _controller.index; _controller.addListener(updateIndex); } diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 8e39b46..9d439bb 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -128,12 +128,7 @@ class ComicDetails with HistoryMixin { final Map> tags; /// id-name - final Map? chapters; - - /// key is group name. - /// When this field is not null, [chapters] will be a merged map of all groups. - /// Only available in some sources. - final Map>? groupedChapters; + final ComicChapters? chapters; final List? thumbnails; @@ -176,45 +171,13 @@ class ComicDetails with HistoryMixin { return res; } - static Map? _getChapters(dynamic chapters) { - if (chapters == null) return null; - var result = {}; - if (chapters is Map) { - for (var entry in chapters.entries) { - var value = entry.value; - if (value is Map) { - result.addAll(Map.from(value)); - } else { - result[entry.key.toString()] = value.toString(); - } - } - } - return result; - } - - static Map>? _getGroupedChapters(dynamic chapters) { - if (chapters == null) return null; - var result = >{}; - if (chapters is Map) { - for (var entry in chapters.entries) { - var value = entry.value; - if (value is Map) { - result[entry.key.toString()] = Map.from(value); - } - } - } - if (result.isEmpty) return null; - return result; - } - ComicDetails.fromJson(Map json) : title = json["title"], subTitle = json["subtitle"], cover = json["cover"], description = json["description"], tags = _generateMap(json["tags"]), - chapters = _getChapters(json["chapters"]), - groupedChapters = _getGroupedChapters(json["chapters"]), + chapters = ComicChapters.fromJsonOrNull(json["chapters"]), sourceKey = json["sourceKey"], comicId = json["comicId"], thumbnails = ListOrNull.from(json["thumbnails"]), @@ -342,3 +305,122 @@ class ArchiveInfo { description = json["description"], id = json["id"]; } + +class ComicChapters { + final Map? _chapters; + + final Map>? _groupedChapters; + + /// Create a ComicChapters object with a flat map + const ComicChapters(Map this._chapters) + : _groupedChapters = null; + + /// Create a ComicChapters object with a grouped map + const ComicChapters.grouped( + Map> this._groupedChapters) + : _chapters = null; + + factory ComicChapters.fromJson(dynamic json) { + if (json is! Map) throw ArgumentError("Invalid json type"); + var chapters = {}; + var groupedChapters = >{}; + for (var entry in json.entries) { + var key = entry.key; + var value = entry.value; + if (key is! String) throw ArgumentError("Invalid key type"); + if (value is Map) { + groupedChapters[key] = Map.from(value); + } else { + chapters[key] = value.toString(); + } + } + if (chapters.isNotEmpty) { + return ComicChapters(chapters); + } else { + return ComicChapters.grouped(groupedChapters); + } + } + + static fromJsonOrNull(dynamic json) { + if (json == null) return null; + return ComicChapters.fromJson(json); + } + + Map toJson() { + if (_chapters != null) { + return _chapters; + } else { + return _groupedChapters!; + } + } + + /// Whether the chapters are grouped + bool get isGrouped => _groupedChapters != null; + + /// All group names + Iterable get groups => _groupedChapters?.keys ?? []; + + /// All chapters. + /// If the chapters are grouped, all groups will be merged. + Map get allChapters { + if (_chapters != null) return _chapters; + var res = {}; + for (var entry in _groupedChapters!.values) { + res.addAll(entry); + } + return res; + } + + /// Get a group of chapters by name + Map getGroup(String group) { + return _groupedChapters![group] ?? {}; + } + + /// Get a group of chapters by index + Map getGroupByIndex(int index) { + return _groupedChapters!.values.elementAt(index); + } + + /// Get a chapter by index + int get length { + return isGrouped + ? _groupedChapters!.values.map((e) => e.length).reduce((a, b) => a + b) + : _chapters!.length; + } + + /// Get the number of groups + int get groupCount => _groupedChapters?.length ?? 0; + + /// Iterate all chapter ids + Iterable get ids sync* { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + yield* entry.keys; + } + } else { + yield* _chapters!.keys; + } + } + + /// Iterate all chapter titles + Iterable get titles sync* { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + yield* entry.values; + } + } else { + yield* _chapters!.values; + } + } + + String? operator [](String key) { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + if (entry.containsKey(key)) return entry[key]; + } + return null; + } else { + return _chapters![key]; + } + } +} diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index 0ccea9d..b983c93 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -97,7 +97,7 @@ class ImageFavoritesProvider if (localComic == null) { return null; } - var epIndex = localComic.chapters?.keys.toList().indexOf(eid) ?? -1; + var epIndex = localComic.chapters?.ids.toList().indexOf(eid) ?? -1; if (epIndex == -1 && localComic.hasChapters) { return null; } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 8cafdfc..4aa516c 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/reader/reader.dart'; -import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'app.dart'; @@ -34,7 +33,7 @@ class LocalComic with HistoryMixin implements Comic { /// key: chapter id, value: chapter title /// /// chapter id is the name of the directory in `LocalManager.path/$directory` - final Map? chapters; + final ComicChapters? chapters; bool get hasChapters => chapters != null; @@ -67,7 +66,7 @@ class LocalComic with HistoryMixin implements Comic { subtitle = row[2] as String, tags = List.from(jsonDecode(row[3] as String)), directory = row[4] as String, - chapters = MapOrNull.from(jsonDecode(row[5] as String)), + chapters = ComicChapters.fromJsonOrNull(jsonDecode(row[5] as String)), cover = row[6] as String, comicType = ComicType(row[7] as int), downloadedChapters = List.from(jsonDecode(row[8] as String)), @@ -99,6 +98,7 @@ class LocalComic with HistoryMixin implements Comic { "tags": tags, "description": description, "sourceKey": sourceKey, + "chapters": chapters?.toJson(), }; } @@ -391,7 +391,7 @@ class LocalManager with ChangeNotifier { var directory = Directory(comic.baseDir); if (comic.hasChapters) { var cid = - ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); + ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String); directory = Directory(FilePath.join(directory.path, cid)); } var files = []; @@ -425,7 +425,7 @@ class LocalManager with ChangeNotifier { if (comic == null) return false; if (comic.chapters == null || ep == null) return true; return comic.downloadedChapters - .contains(comic.chapters!.keys.elementAt(ep - 1)); + .contains(comic.chapters!.ids.elementAt(ep - 1)); } List downloadingTasks = []; diff --git a/lib/network/download.dart b/lib/network/download.dart index 058b93e..76a69f9 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -328,8 +328,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _images = {}; _totalCount = 0; int cpCount = 0; - int totalCpCount = chapters?.length ?? comic!.chapters!.length; - for (var i in comic!.chapters!.keys) { + int totalCpCount = + chapters?.length ?? comic!.chapters!.allChapters.length; + for (var i in comic!.chapters!.allChapters.keys) { if (chapters != null && !chapters!.contains(i)) { continue; } diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index 7636426..da10543 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -98,7 +98,7 @@ abstract mixin class _ComicPageActions { void read([int? ep, int? page]) { App.rootContext .to( - () => Reader( + () => Reader( type: comic.comicType, cid: comic.id, name: comic.title, @@ -219,7 +219,7 @@ abstract mixin class _ComicPageActions { isGettingLink = true; }); var res = - await comicSource.archiveDownloader!.getDownloadUrl( + await comicSource.archiveDownloader!.getDownloadUrl( comic.id, archives![selected].id, ); @@ -262,7 +262,7 @@ abstract mixin class _ComicPageActions { if (localComic != null) { for (int i = 0; i < comic.chapters!.length; i++) { if (localComic.downloadedChapters - .contains(comic.chapters!.keys.elementAt(i))) { + .contains(comic.chapters!.ids.elementAt(i))) { downloaded.add(i); } } @@ -270,8 +270,8 @@ abstract mixin class _ComicPageActions { await showSideBar( App.rootContext, _SelectDownloadChapter( - comic.chapters!.values.toList(), - (v) => selected = v, + comic.chapters!.titles.toList(), + (v) => selected = v, downloaded, ), ); @@ -281,7 +281,7 @@ abstract mixin class _ComicPageActions { comicId: comic.id, comic: comic, chapters: selected!.map((i) { - return comic.chapters!.keys.elementAt(i); + return comic.chapters!.ids.elementAt(i); }).toList(), )); } @@ -298,13 +298,13 @@ abstract mixin class _ComicPageActions { var context = App.mainNavigatorKey!.currentContext!; if (config['action'] == 'search') { context.to(() => SearchResultPage( - text: config['keyword'] ?? '', - sourceKey: comicSource.key, - options: const [], - )); + text: config['keyword'] ?? '', + sourceKey: comicSource.key, + options: const [], + )); } else if (config['action'] == 'category') { context.to( - () => CategoryComicsPage( + () => CategoryComicsPage( category: config['keyword'] ?? '', categoryKey: comicSource.categoryData!.key, param: config['param'], @@ -432,4 +432,4 @@ abstract mixin class _ComicPageActions { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/comic_details_page/chapters.dart b/lib/pages/comic_details_page/chapters.dart index 364871c..2f13089 100644 --- a/lib/pages/comic_details_page/chapters.dart +++ b/lib/pages/comic_details_page/chapters.dart @@ -33,7 +33,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { late History? history; - late Map chapters; + late ComicChapters chapters; @override void initState() { @@ -101,7 +101,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { if (reverse) { i = chapters.length - i - 1; } - var key = chapters.keys.elementAt(i); + var key = chapters.ids.elementAt(i); var value = chapters[key]!; bool visited = (history?.readEpisode ?? {}).contains(i + 1); return Padding( @@ -182,7 +182,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> late History? history; - late Map> chapters; + late ComicChapters chapters; late TabController tabController; @@ -197,9 +197,9 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> @override void didChangeDependencies() { state = context.findAncestorStateOfType<_ComicPageState>()!; - chapters = state.comic.groupedChapters!; + chapters = state.comic.chapters!; tabController = TabController( - length: chapters.keys.length, + length: chapters.ids.length, vsync: this, ); tabController.addListener(onTabChange); @@ -226,7 +226,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> Widget build(BuildContext context) { return SliverLayoutBuilder( builder: (context, constrains) { - var group = chapters.values.elementAt(index); + var group = chapters.getGroupByIndex(index); int length = group.length; bool canShowAll = showAll; if (!showAll) { @@ -265,7 +265,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> child: AppTabBar( withUnderLine: false, controller: tabController, - tabs: chapters.keys.map((e) => Tab(text: e)).toList(), + tabs: chapters.groups.map((e) => Tab(text: e)).toList(), ), ), SliverPadding(padding: const EdgeInsets.only(top: 8)), @@ -279,12 +279,12 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> var key = group.keys.elementAt(i); var value = group[key]!; var chapterIndex = 0; - for (var j = 0; j < chapters.length; j++) { + for (var j = 0; j < chapters.groupCount; j++) { if (j == index) { chapterIndex += i; break; } - chapterIndex += chapters.values.elementAt(j).length; + chapterIndex += chapters.getGroupByIndex(j).length; } bool visited = (history?.readEpisode ?? {}).contains(chapterIndex + 1); diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 609246c..de3a662 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -386,7 +386,7 @@ class _ComicPageState extends LoadingState String text; if (haveChapter) { text = "Last Reading: @epName Page @page".tlParams({ - 'epName': comic.chapters!.values.elementAt( + 'epName': comic.chapters!.titles.elementAt( math.min(ep - 1, comic.chapters!.length - 1)), 'page': page, }); @@ -610,7 +610,7 @@ class _ComicPageState extends LoadingState } return _ComicChapters( history: history, - groupedMode: comic.groupedChapters != null, + groupedMode: comic.chapters!.isGrouped, ); } diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 0091f01..a90382b 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -45,7 +45,7 @@ class _ReaderImagesState extends State<_ReaderImages> { } else { var res = await reader.type.comicSource!.loadComicPages!( reader.widget.cid, - reader.widget.chapters?.keys.elementAt(reader.chapter - 1), + reader.widget.chapters?.ids.elementAt(reader.chapter - 1), ); if (res.error) { setState(() { diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index 38bfd57..e8a09d4 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -101,7 +101,7 @@ class ReaderProps { final String name; - final Map? chapters; + final ComicChapters? chapters; final History history; diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 609c9e0..4e3d139 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -76,9 +76,7 @@ class Reader extends StatefulWidget { final String name; - /// key: Chapter ID, value: Chapter Name - /// null if the comic is a gallery - final Map? chapters; + final ComicChapters? chapters; /// Starts from 1, invalid values equal to 1 final int? initialPage; @@ -105,7 +103,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { String get cid => widget.cid; - String get eid => widget.chapters?.keys.elementAt(chapter - 1) ?? '0'; + String get eid => widget.chapters?.ids.elementAt(chapter - 1) ?? '0'; List? images; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 453081b..a60cdb3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -279,7 +279,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { List tags = context.reader.widget.tags; String author = context.reader.widget.author; - var epName = context.reader.widget.chapters?.values + var epName = context.reader.widget.chapters?.titles .elementAtOrNull(context.reader.chapter - 1) ?? "E${context.reader.chapter}"; var translatedTags = tags.map((e) => e.translateTagsToCN).toList(); @@ -561,7 +561,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } Widget buildPageInfoText() { - var epName = context.reader.widget.chapters?.values + var epName = context.reader.widget.chapters?.titles .elementAtOrNull(context.reader.chapter - 1) ?? "E${context.reader.chapter}"; if (epName.length > 8) { @@ -614,7 +614,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void openChapterDrawer() { showSideBar( context, - _ChaptersView(context.reader), + context.reader.widget.chapters!.isGrouped + ? _GroupedChaptersView(context.reader) + : _ChaptersView(context.reader), width: 400, ); } @@ -1030,14 +1032,27 @@ class _ChaptersView extends StatefulWidget { class _ChaptersViewState extends State<_ChaptersView> { bool desc = false; + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + int epIndex = widget.reader.chapter - 2; + _scrollController = ScrollController( + initialScrollOffset: (epIndex * 48.0 + 52).clamp(0, double.infinity), + ); + } + @override Widget build(BuildContext context) { var chapters = widget.reader.widget.chapters!; var current = widget.reader.chapter - 1; return Scaffold( body: SmoothCustomScrollView( + controller: _scrollController, slivers: [ SliverAppbar( + style: AppbarStyle.shadow, title: Text("Chapters".tl), actions: [ Tooltip( @@ -1063,26 +1078,35 @@ class _ChaptersViewState extends State<_ChaptersView> { if (desc) { index = chapters.length - 1 - index; } - var chapter = chapters.values.elementAt(index); - return ListTile( - shape: Border( - left: BorderSide( - color: current == index - ? context.colorScheme.primary - : Colors.transparent, - width: 4, - ), - ), - title: Text( - chapter, - style: current == index - ? ts.withColor(context.colorScheme.primary).bold - : null, - ), + var chapter = chapters.titles.elementAt(index); + return InkWell( onTap: () { widget.reader.toChapter(index + 1); Navigator.of(context).pop(); }, + child: Container( + height: 48, + padding: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: current == index + ? context.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + chapter, + style: current == index + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + ), + ), ); }, childCount: chapters.length, @@ -1093,3 +1117,120 @@ class _ChaptersViewState extends State<_ChaptersView> { ); } } + +class _GroupedChaptersView extends StatefulWidget { + const _GroupedChaptersView(this.reader); + + final _ReaderState reader; + + @override + State<_GroupedChaptersView> createState() => _GroupedChaptersViewState(); +} + +class _GroupedChaptersViewState extends State<_GroupedChaptersView> + with SingleTickerProviderStateMixin { + ComicChapters get chapters => widget.reader.widget.chapters!; + + late final TabController tabController; + + late final ScrollController _scrollController; + + late final initialGroupName; + + @override + void initState() { + super.initState(); + int index = 0; + int epIndex = widget.reader.chapter - 1; + while (epIndex >= 0) { + epIndex -= chapters.getGroupByIndex(index).length; + index++; + } + tabController = TabController( + length: chapters.groups.length, + vsync: this, + initialIndex: index - 1, + ); + initialGroupName = chapters.groups.elementAt(index - 1); + var epIndexAtGroup = widget.reader.chapter - 1; + for (var i = 0; i < index-1; i++) { + epIndexAtGroup -= chapters.getGroupByIndex(i).length; + } + _scrollController = ScrollController( + initialScrollOffset: (epIndexAtGroup * 48.0).clamp(0, double.infinity), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Appbar(title: Text("Chapters".tl)), + AppTabBar( + controller: tabController, + tabs: chapters.groups.map((e) => Tab(text: e)).toList(), + ), + Expanded( + child: TabViewBody( + controller: tabController, + children: chapters.groups.map(buildGroup).toList(), + ), + ), + ], + ); + } + + Widget buildGroup(String groupName) { + var group = chapters.getGroup(groupName); + return SmoothCustomScrollView( + controller: initialGroupName == groupName ? _scrollController : null, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + var name = group.values.elementAt(index); + var i = 0; + for (var g in chapters.groups) { + if (g == groupName) { + break; + } + i += chapters.getGroup(g).length; + } + i += index + 1; + return InkWell( + onTap: () { + widget.reader.toChapter(i); + context.pop(); + }, + child: Container( + height: 48, + padding: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: widget.reader.chapter == i + ? context.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: widget.reader.chapter == i + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + ), + ), + ); + }, + childCount: group.length, + ), + ), + ], + ); + } +} diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 0bd00d2..dcd68d0 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter_7zip/flutter_7zip.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/utils/ext.dart'; @@ -176,7 +177,7 @@ abstract class CBZ { tags: metaData.tags, comicType: ComicType.local, directory: dest.name, - chapters: cpMap, + chapters: ComicChapters.fromJson(cpMap), downloadedChapters: cpMap?.keys.toList() ?? [], cover: 'cover.${coverFile.extension}', createdAt: DateTime.now(), diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index 7473c06..ecb186b 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/local.dart'; @@ -262,7 +263,9 @@ class ImportComic { subtitle: subtitle ?? '', tags: tags ?? [], directory: directory.path, - chapters: hasChapters ? Map.fromIterables(chapters, chapters) : null, + chapters: hasChapters + ? ComicChapters(Map.fromIterables(chapters, chapters)) + : null, cover: coverPath, comicType: ComicType.local, downloadedChapters: chapters, From edc2cb066ba2f1dd5d8f09caff3676f6d8225acd Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 13:16:09 +0800 Subject: [PATCH 14/38] Fixed download speed display. --- lib/foundation/local.dart | 2 +- lib/network/images.dart | 12 +++++------- lib/pages/downloading_page.dart | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 4aa516c..2d19c34 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -509,7 +509,7 @@ class LocalManager with ChangeNotifier { var dir = Directory(FilePath.join(path, c.directory)); dir.deleteIgnoreError(recursive: true); } - // Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. + // Deleting a local comic means that it's no longer available, thus both favorite and history should be deleted. if (c.comicType == ComicType.local) { if (HistoryManager().find(c.id, c.comicType) != null) { HistoryManager().remove(c.id, c.comicType); diff --git a/lib/network/images.dart b/lib/network/images.dart index 6a5c9a1..f16e0ea 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -139,12 +139,10 @@ class ImageDownloader { var buffer = []; await for (var data in stream) { buffer.addAll(data); - if (expectedBytes != null) { - yield ImageDownloadProgress( - currentBytes: buffer.length, - totalBytes: expectedBytes, - ); - } + yield ImageDownloadProgress( + currentBytes: buffer.length, + totalBytes: expectedBytes, + ); } if (configs['onResponse'] is JSInvokable) { @@ -194,7 +192,7 @@ class ImageDownloader { class ImageDownloadProgress { final int currentBytes; - final int totalBytes; + final int? totalBytes; final Uint8List? imageBytes; diff --git a/lib/pages/downloading_page.dart b/lib/pages/downloading_page.dart index 795d6e0..1ec15c6 100644 --- a/lib/pages/downloading_page.dart +++ b/lib/pages/downloading_page.dart @@ -15,6 +15,15 @@ class DownloadingPage extends StatefulWidget { } class _DownloadingPageState extends State { + DownloadTask? firstTask; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + firstTask = LocalManager().downloadingTasks.firstOrNull; + firstTask?.addListener(update); + } + @override void initState() { LocalManager().addListener(update); @@ -24,10 +33,17 @@ class _DownloadingPageState extends State { @override void dispose() { LocalManager().removeListener(update); + firstTask?.removeListener(update); super.dispose(); } void update() { + var currentFirstTask = LocalManager().downloadingTasks.firstOrNull; + if (currentFirstTask != firstTask) { + firstTask?.removeListener(update); + firstTask = currentFirstTask; + firstTask?.addListener(update); + } if(mounted) { setState(() {}); } From 40b9b5b329a001323be7fa80af8a78cc2a5c2db8 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 13:25:56 +0800 Subject: [PATCH 15/38] Fixed downloading cover. Close #208 --- 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 76a69f9..39d77c0 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -423,7 +423,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { "comic": comic?.toJson(), "chapters": chapters, "path": path, - "cover": cover, + "cover": _cover, "images": _images, "downloadedCount": _downloadedCount, "totalCount": _totalCount, From 011619340fa58b31afb196b9f5d181f35cf1d517 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 16:21:39 +0800 Subject: [PATCH 16/38] Fixed the update time was not updated after checking. --- lib/foundation/favorites.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 422ddad..d4fee6f 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -185,6 +185,18 @@ class FavoriteItemWithUpdateInfo extends FavoriteItem { var sourceName = type.comicSource?.name ?? "Unknown"; return "$updateTime | $sourceName"; } + + @override + operator ==(Object other) { + return other is FavoriteItemWithUpdateInfo && + other.updateTime == updateTime && + other.hasNewUpdate == hasNewUpdate && + super == other; + } + + @override + int get hashCode => + super.hashCode ^ updateTime.hashCode ^ hasNewUpdate.hashCode; } class LocalFavoritesManager with ChangeNotifier { From a63d458707950406624ba485e7261eae4a8ae074 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 19:16:26 +0800 Subject: [PATCH 17/38] Improve history with grouped chapters. --- lib/foundation/comic_source/models.dart | 2 +- lib/foundation/history.dart | 64 +++++++++++--------- lib/foundation/local.dart | 1 + lib/pages/comic_details_page/chapters.dart | 9 ++- lib/pages/comic_details_page/comic_page.dart | 13 +++- lib/pages/reader/loading.dart | 1 + lib/pages/reader/reader.dart | 27 ++++++++- 7 files changed, 79 insertions(+), 38 deletions(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 9d439bb..4be9310 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -376,7 +376,7 @@ class ComicChapters { return _groupedChapters![group] ?? {}; } - /// Get a group of chapters by index + /// Get a group of chapters by index(0-based) Map getGroupByIndex(int index) { return _groupedChapters!.values.elementAt(index); } diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 87edb15..0c017cf 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -50,17 +50,24 @@ class History implements Comic { @override String cover; + /// index of chapters. 1-based. int ep; + /// index of pages. 1-based. int page; + /// index of chapter groups. 1-based. + /// If [group] is not null, [ep] is the index of chapter in the group. + int? group; + @override String id; /// readEpisode is a set of episode numbers that have been read. - /// - /// The number of episodes is 1-based. - Set readEpisode; + /// For normal chapters, it is a set of chapter numbers. + /// For grouped chapters, it is a set of strings in the format of "group_number-chapter_number". + /// 1-based. + Set readEpisode; @override int? maxPage; @@ -69,29 +76,17 @@ class History implements Comic { {required HistoryMixin model, required this.ep, required this.page, - Set? readChapters, + this.group, + Set? readChapters, DateTime? time}) : type = model.historyType, title = model.title, subtitle = model.subTitle ?? '', cover = model.cover, id = model.id, - readEpisode = readChapters ?? {}, + readEpisode = readChapters ?? {}, time = time ?? DateTime.now(); - Map toMap() => { - "type": type.value, - "time": time.millisecondsSinceEpoch, - "title": title, - "subtitle": subtitle, - "cover": cover, - "ep": ep, - "page": page, - "id": id, - "readEpisode": readEpisode.toList(), - "max_page": maxPage - }; - History.fromMap(Map map) : type = HistoryType(map["type"]), time = DateTime.fromMillisecondsSinceEpoch(map["time"]), @@ -101,8 +96,9 @@ class History implements Comic { ep = map["ep"], page = map["page"], id = map["id"], - readEpisode = Set.from( - (map["readEpisode"] as List?)?.toSet() ?? const {}), + readEpisode = Set.from( + (map["readEpisode"] as List?)?.toSet() ?? + const {}), maxPage = map["max_page"]; @override @@ -119,11 +115,11 @@ class History implements Comic { ep = row["ep"], page = row["page"], id = row["id"], - readEpisode = Set.from((row["readEpisode"] as String) + readEpisode = Set.from((row["readEpisode"] as String) .split(',') - .where((element) => element != "") - .map((e) => int.parse(e))), - maxPage = row["max_page"]; + .where((element) => element != "")), + maxPage = row["max_page"], + group = row["chapter_group"]; @override bool operator ==(Object other) { @@ -212,18 +208,24 @@ class HistoryManager with ChangeNotifier { ep int, page int, readEpisode text, - max_page int + max_page int, + chapter_group int ); """); + var columns = _db.select("PRAGMA table_info(history);"); + if (!columns.any((element) => element["name"] == "chapter_group")) { + _db.execute("alter table history add column chapter_group int;"); + } + notifyListeners(); ImageFavoriteManager().init(); isInitialized = true; } - static const _insertHistorySql = """ - insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + static const _insertHistorySql = """ + insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page, chapter_group) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """; static Future _addHistoryAsync(int dbAddr, History newItem) { @@ -239,7 +241,8 @@ class HistoryManager with ChangeNotifier { newItem.ep, newItem.page, newItem.readEpisode.join(','), - newItem.maxPage + newItem.maxPage, + newItem.group ]); }); } @@ -281,7 +284,8 @@ class HistoryManager with ChangeNotifier { newItem.ep, newItem.page, newItem.readEpisode.join(','), - newItem.maxPage + newItem.maxPage, + newItem.group ]); if (_cachedHistoryIds == null) { updateCache(); diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 2d19c34..c2b462c 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -115,6 +115,7 @@ class LocalComic with HistoryMixin implements Comic { chapters: chapters, initialChapter: history?.ep, initialPage: history?.page, + initialChapterGroup: history?.group, history: history ?? History.fromModel( model: this, diff --git a/lib/pages/comic_details_page/chapters.dart b/lib/pages/comic_details_page/chapters.dart index 2f13089..0a9586f 100644 --- a/lib/pages/comic_details_page/chapters.dart +++ b/lib/pages/comic_details_page/chapters.dart @@ -286,8 +286,13 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> } chapterIndex += chapters.getGroupByIndex(j).length; } - bool visited = - (history?.readEpisode ?? {}).contains(chapterIndex + 1); + String rawIndex = (chapterIndex + 1).toString(); + String groupedIndex = "${index + 1}-${i + 1}"; + bool visited = false; + if (history != null) { + visited = history!.readEpisode.contains(groupedIndex) || + history!.readEpisode.contains(rawIndex); + } return Padding( padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), child: Material( diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index de3a662..fef8424 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -167,6 +167,7 @@ class _ComicPageState extends LoadingState chapters: localComic.chapters, initialPage: history?.page, initialChapter: history?.ep, + initialChapterGroup: history?.group, history: history ?? History.fromModel( model: localComic, @@ -383,11 +384,19 @@ class _ComicPageState extends LoadingState bool haveChapter = comic.chapters != null; var page = history!.page; var ep = history!.ep; + 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); text = "Last Reading: @epName Page @page".tlParams({ - 'epName': comic.chapters!.titles.elementAt( - math.min(ep - 1, comic.chapters!.length - 1)), + 'epName': epName, 'page': page, }); } else { diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index e8a09d4..d8fb732 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -33,6 +33,7 @@ class _ReaderWithLoadingState history: data.history, initialChapter: widget.initialEp ?? data.history.ep, initialPage: widget.initialPage ?? data.history.page, + initialChapterGroup: data.history.group, author: data.author, tags: data.tags, ); diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 4e3d139..8e1412f 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -62,6 +62,7 @@ class Reader extends StatefulWidget { required this.history, this.initialPage, this.initialChapter, + this.initialChapterGroup, required this.author, required this.tags, }); @@ -84,6 +85,9 @@ class Reader extends StatefulWidget { /// Starts from 1, invalid values equal to 1 final int? initialChapter; + /// Starts from 1, invalid values equal to 1 + final int? initialChapterGroup; + final History history; @override @@ -147,13 +151,18 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { @override void initState() { page = widget.initialPage ?? 1; - chapter = widget.initialChapter ?? 1; if (page < 1) { page = 1; } + chapter = widget.initialChapter ?? 1; if (chapter < 1) { chapter = 1; } + if (widget.initialChapterGroup != null) { + for (int i=0; i<(widget.initialChapterGroup!-1); i++) { + chapter += widget.chapters!.getGroupByIndex(i).length; + } + } if (widget.initialPage != null) { page = (widget.initialPage! / imagesPerPage).ceil(); } @@ -238,11 +247,23 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { void updateHistory() { if (history != null) { history!.page = page; - history!.ep = chapter; if (maxPage > 1) { history!.maxPage = maxPage; } - history!.readEpisode.add(chapter); + if (widget.chapters?.isGrouped ?? false) { + int g = 0; + int c = chapter; + while (c > widget.chapters!.getGroupByIndex(g).length) { + c -= widget.chapters!.getGroupByIndex(g).length; + g++; + } + history!.readEpisode.add('${g + 1}-$c'); + history!.ep = c; + history!.group = g+1; + } else { + history!.readEpisode.add(chapter.toString()); + history!.ep = chapter; + } history!.time = DateTime.now(); _updateHistoryTimer?.cancel(); _updateHistoryTimer = Timer(const Duration(seconds: 1), () { From 36ab104c818caf2ad7035635984036687f3da61a Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 19:33:47 +0800 Subject: [PATCH 18/38] 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 d575d26..7415bbc 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.0"; + final version = "1.3.1"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index 97295c1..10e6e78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.0+130 +version: 1.3.1+131 environment: sdk: '>=3.6.0 <4.0.0' From bbf31a4bbed3afc257bb779b33dc41d0e2827f4e Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:59:08 +0800 Subject: [PATCH 19/38] Add AppImage build support (#210) --- .github/workflows/main.yml | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7900e6b..b7752a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,6 +148,45 @@ jobs: sudo rm -rf build/linux/arch/pkg sudo rm -rf build/linux/arch/src sudo rm -rf build/linux/arch/PKGBUILD + - name: Build AppImage + run: | + sudo apt-get install -y libfuse2 + wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x appimagetool + + mkdir -p Venera.AppDir + cp -r build/linux/x64/release/bundle/* Venera.AppDir/ + + cat > Venera.AppDir/venera.desktop << EOF + [Desktop Entry] + Name=Venera + Exec=venera + Icon=venera + Type=Application + Categories=Utility; + EOF + + cp assets/app_icon.png Venera.AppDir/venera.png + + cat > Venera.AppDir/AppRun << EOF + #!/bin/sh + HERE=\$(dirname \$(readlink -f "\${0}")) + export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH} + export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH} + export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS} + exec "\${HERE}"/venera "\$@" + EOF + chmod +x Venera.AppDir/AppRun + + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + ./appimagetool Venera.AppDir Venera-${APP_VERSION}-x86_64.AppImage + + mkdir -p build/linux/appimage + mv Venera-${APP_VERSION}-x86_64.AppImage build/linux/appimage/ + - uses: actions/upload-artifact@v4 + with: + name: appimage_build + path: build/linux/appimage - uses: actions/upload-artifact@v4 with: name: deb_build @@ -170,6 +209,45 @@ jobs: sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 dart pub global activate flutter_to_debian - run: python3 debian/build.py arm64 + - name: Build AppImage + run: | + sudo apt-get install -y libfuse2 + wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage" + chmod +x appimagetool + + mkdir -p Venera.AppDir + cp -r build/linux/arm64/release/bundle/* Venera.AppDir/ + + cat > Venera.AppDir/venera.desktop << EOF + [Desktop Entry] + Name=Venera + Exec=venera + Icon=venera + Type=Application + Categories=Utility; + EOF + + cp assets/app_icon.png Venera.AppDir/venera.png + + cat > Venera.AppDir/AppRun << EOF + #!/bin/sh + HERE=\$(dirname \$(readlink -f "\${0}")) + export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH} + export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH} + export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS} + exec "\${HERE}"/venera "\$@" + EOF + chmod +x Venera.AppDir/AppRun + + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + ./appimagetool Venera.AppDir Venera-${APP_VERSION}-aarch64.AppImage + + mkdir -p build/linux/appimage + mv Venera-${APP_VERSION}-aarch64.AppImage build/linux/appimage/ + - uses: actions/upload-artifact@v4 + with: + name: appimage_arm64_build + path: build/linux/appimage - uses: actions/upload-artifact@v4 with: name: deb_arm64_build @@ -208,6 +286,14 @@ jobs: with: name: deb_arm64_build path: outputs + - uses: actions/download-artifact@v4 + with: + name: appimage_build + path: outputs + - uses: actions/download-artifact@v4 + with: + name: appimage_arm64_build + path: outputs - uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} @@ -219,5 +305,6 @@ jobs: outputs/*.exe outputs/*.deb outputs/*.zst + outputs/*.AppImage env: GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }} From 6db00eaf718d8424fa935bda444fcd31723318cc Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 23:03:16 +0800 Subject: [PATCH 20/38] Fix variable type --- lib/pages/reader/scaffold.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index a60cdb3..214dc10 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -1135,7 +1135,7 @@ class _GroupedChaptersViewState extends State<_GroupedChaptersView> late final ScrollController _scrollController; - late final initialGroupName; + late final String initialGroupName; @override void initState() { From 26adfc6c4ff439f4f2953079a1aab7e3386ba7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=92=E7=A0=82=E7=B3=96?= <90336521+lings03@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:09:01 +0800 Subject: [PATCH 21/38] Fix missing chapterGroup when continueRead (#213) --- lib/pages/comic_details_page/actions.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index da10543..90f0a35 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -95,7 +95,9 @@ abstract mixin class _ComicPageActions { /// [ep] the episode number, start from 1 /// /// [page] the page number, start from 1 - void read([int? ep, int? page]) { + /// + /// [group] the chapter group number, start from 1 + void read([int? ep, int? page, int? group]) { App.rootContext .to( () => Reader( @@ -105,6 +107,7 @@ abstract mixin class _ComicPageActions { chapters: comic.chapters, initialChapter: ep, initialPage: page, + initialChapterGroup: group, history: history ?? History.fromModel(model: comic, ep: 0, page: 0), author: comic.findAuthor() ?? '', tags: comic.plainTags, @@ -118,7 +121,8 @@ abstract mixin class _ComicPageActions { void continueRead() { var ep = history?.ep ?? 1; var page = history?.page ?? 1; - read(ep, page); + var group = history?.group ?? 1; + read(ep, page, group); } void onReadEnd(); From 24b7319bb5c35e0531fbac516ac4de642ea61b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=92=E7=A0=82=E7=B3=96?= <90336521+lings03@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:12:01 +0800 Subject: [PATCH 22/38] Add option to differentiate images per page for landscape and portrait orientations (#214) --- assets/translation.json | 6 ++++-- lib/foundation/appdata.dart | 3 ++- lib/pages/reader/reader.dart | 16 ++++++++++++-- lib/pages/settings/reader.dart | 39 +++++++++++++++++++++++++++++----- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index e24350b..8d4f849 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -106,7 +106,8 @@ "Continuous (Right to Left)": "连续(从右到左)", "Continuous (Top to Bottom)": "连续(从上到下)", "Auto page turning interval": "自动翻页间隔", - "The number of pic in screen (Only Gallery Mode)": "同屏幕图片数量(仅画廊模式)", + "The number of pic in screen for landscape (Only Gallery Mode)": "横屏同屏幕图片数量(仅画廊模式)", + "The number of pic in screen for portrait (Only Gallery Mode)": "竖屏同屏幕图片数量(仅画廊模式)", "Theme Mode": "主题模式", "System": "系统", "Light": "浅色", @@ -468,7 +469,8 @@ "Continuous (Right to Left)": "連續(從右到左)", "Continuous (Top to Bottom)": "連續(從上到下)", "Auto page turning interval": "自動翻頁間隔", - "The number of pic in screen (Only Gallery Mode)": "同螢幕圖片數量(僅畫廊模式)", + "The number of pic in screen for landscape (Only Gallery Mode)": "橫向同螢幕圖片數量(僅畫廊模式)", + "The number of pic in screen for portrait (Only Gallery Mode)": "直向同螢幕圖片數量(僅畫廊模式)", "Theme Mode": "主題模式", "System": "系統", "Light": "浅色", diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 4b93e29..567cdda 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -133,7 +133,8 @@ class Settings with ChangeNotifier { 'defaultSearchTarget': null, 'autoPageTurningInterval': 5, // in seconds 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] - 'readerScreenPicNumber': 1, // 1 - 5 + 'readerScreenPicNumberForLandscape': 1, // 1 - 5 + 'readerScreenPicNumberForPortrait': 1, // 1 - 5 'enableTapToTurnPages': true, 'reverseTapToTurnPages': false, 'enablePageAnimation': true, diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 8e1412f..9cf2bc5 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -113,9 +113,18 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { late ReaderMode mode; - int get imagesPerPage => appdata.settings['readerScreenPicNumber'] ?? 1; + int get imagesPerPage { + if (isPortrait) { + return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; + } else { + return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; + } + } - int _lastImagesPerPage = appdata.settings['readerScreenPicNumber'] ?? 1; + late int _lastImagesPerPage; + + bool get isPortrait => + MediaQuery.of(context).orientation == Orientation.portrait; @override void didChangeDependencies() { @@ -168,6 +177,9 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; + _lastImagesPerPage = isPortrait + ? appdata.settings['readerScreenPicNumberForPortrait'] ?? 1 + : appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; Future.microtask(() { updateHistory(); }); diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 7efb632..97e19fd 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -50,8 +50,10 @@ class _ReaderSettingsState extends State { onChanged: () { var readerMode = appdata.settings['readerMode']; if (readerMode?.toLowerCase().startsWith('continuous') ?? false) { - appdata.settings['readerScreenPicNumber'] = 1; - widget.onChanged?.call('readerScreenPicNumber'); + appdata.settings['readerScreenPicNumberForLandscape'] = 1; + widget.onChanged?.call('readerScreenPicNumberForLandscape'); + appdata.settings['readerScreenPicNumberForPortrait'] = 1; + widget.onChanged?.call('readerScreenPicNumberForPortrait'); } widget.onChanged?.call("readerMode"); }, @@ -81,13 +83,40 @@ class _ReaderSettingsState extends State { : 1.0, duration: Duration(milliseconds: 300), child: _SliderSetting( - title: "The number of pic in screen (Only Gallery Mode)".tl, - settingsIndex: "readerScreenPicNumber", + 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("readerScreenPicNumber"); + 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"); }, ), ), From 7b601058ebc0f1900b4ac35f71f564a681db7131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=92=E7=A0=82=E7=B3=96?= <90336521+lings03@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:12:53 +0800 Subject: [PATCH 23/38] Change history of page and maxPage (#216) --- lib/pages/reader/reader.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 9cf2bc5..17d68e3 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -258,9 +258,15 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { void updateHistory() { if (history != null) { - history!.page = page; + if(page == maxPage){ + /// Record the last image of chapter + history!.page = images?.length ?? 1; + } else { + /// Record the first image of the page + history!.page = (page-1) * imagesPerPage + 1; + } if (maxPage > 1) { - history!.maxPage = maxPage; + history!.maxPage = images?.length ?? 1; } if (widget.chapters?.isGrouped ?? false) { int g = 0; From e829f567e574ad375eccfab9fe71bc0e6f91c19e Mon Sep 17 00:00:00 2001 From: nyne <67669799+wgh136@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:25:06 +0800 Subject: [PATCH 24/38] Revert "Improve WebDAV data sync version handling and force sync (#207)" (#218) This reverts commit a630771f0bdf611deea69b29e54c86559ad560be. --- assets/translation.json | 2 -- lib/init.dart | 12 -------- lib/pages/settings/app.dart | 13 ++++---- lib/utils/data_sync.dart | 60 +++++++++++++++---------------------- 4 files changed, 30 insertions(+), 57 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 8d4f849..7f27d3d 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -190,7 +190,6 @@ "Operation": "操作", "Upload": "上传", "Saved": "已保存", - "Saved Failed": "保存失败", "Sync Data": "同步数据", "Syncing Data": "正在同步数据", "Data Sync": "数据同步", @@ -554,7 +553,6 @@ "Operation": "操作", "Upload": "上傳", "Saved": "已保存", - "Saved Failed": "保存失敗", "Sync Data": "同步數據", "Syncing Data": "正在同步數據", "Data Sync": "數據同步", diff --git a/lib/init.dart b/lib/init.dart index ad18abd..6f8bcda 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -56,18 +56,6 @@ void _checkOldConfigs() { .map((e) => e.key) .toList(); } - - if (appdata.settings['webdavAutoSync'] == null) { - var webdavConfig = appdata.settings['webdav']; - if (webdavConfig is List && - webdavConfig.length == 3 && - webdavConfig.whereType().length == 3) { - appdata.settings['webdavAutoSync'] = true; - } else { - appdata.settings['webdavAutoSync'] = false; - } - appdata.saveData(); - } } Future _checkAppUpdates() async { diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index cc7202d..9dbd3ee 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -473,8 +473,8 @@ class _WebdavSettingState extends State<_WebdavSetting> { isTesting = true; }); var testResult = upload - ? await DataSync().uploadData(forceSync: true) - : await DataSync().downloadData(forceSync: true); + ? await DataSync().uploadData() + : await DataSync().downloadData(); if (testResult.error) { setState(() { isTesting = false; @@ -482,12 +482,11 @@ class _WebdavSettingState extends State<_WebdavSetting> { appdata.settings['webdav'] = oldConfig; appdata.settings['webdavAutoSync'] = oldAutoSync; context.showMessage(message: testResult.errorMessage!); - context.showMessage(message: "Saved Failed".tl); - } else { - appdata.saveData(); - context.showMessage(message: "Saved".tl); - App.rootPop(); + return; } + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); }, child: Text("Continue".tl), ), diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index f644385..3cd9937 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -58,7 +58,7 @@ class DataSync with ChangeNotifier { return List.from(config); } - Future> uploadData({bool forceSync = false}) async { + Future> uploadData() async { if (isDownloading) return const Res(true); if (haveWaitingTask) return const Res(true); while (isUploading) { @@ -102,32 +102,20 @@ class DataSync with ChangeNotifier { } try { - var files = await client.readDir('/'); - files = files.where((e) => e.name!.endsWith('.venera')).toList(); - files.sort((a, b) => b.name!.compareTo(a.name!)); - var remoteFile = files.firstWhereOrNull((e) => e.name!.endsWith('.venera')); - var remoteVersion = 0; - if (remoteFile != null) { - remoteVersion = int.tryParse(remoteFile.name!.split('-').elementAtOrNull(1)?.split('.').first ?? '0') ?? 0; - } - var localVersion = appdata.settings['dataVersion'] ?? 0; - - if (!forceSync && remoteVersion >= localVersion) { - Log.info("Data Sync", 'Local: $localVersion Remote: $remoteVersion Skip upload ForceSync: $forceSync'); - return const Res(true); - } - - appdata.settings['dataVersion'] = forceSync ? remoteVersion + 1 : localVersion + 1; + appdata.settings['dataVersion']++; await appdata.saveData(false); var data = await exportAppData(); - var time = (DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString(); - var filename = '$time-${appdata.settings['dataVersion']}.venera'; - - if (!forceSync) { - var old = files.firstWhereOrNull((e) => e.name!.startsWith("$time-")); - if (old != null) { - await client.remove(old.name!); - } + var time = + (DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString(); + var filename = time; + filename += '-'; + filename += appdata.settings['dataVersion'].toString(); + filename += '.venera'; + var files = await client.readDir('/'); + files = files.where((e) => e.name!.endsWith('.venera')).toList(); + var old = files.firstWhereOrNull((e) => e.name!.startsWith("$time-")); + if (old != null) { + await client.remove(old.name!); } if (files.length >= 10) { files.sort((a, b) => a.name!.compareTo(b.name!)); @@ -135,7 +123,7 @@ class DataSync with ChangeNotifier { } await client.write(filename, await data.readAsBytes()); data.deleteIgnoreError(); - Log.info("Data Sync", "Local: ${appdata.settings['dataVersion']} Remote: $remoteVersion Data uploaded successfully ForceSync: $forceSync"); + Log.info("Upload Data", "Data uploaded successfully"); return const Res(true); } catch (e, s) { Log.error("Upload Data", e, s); @@ -147,7 +135,7 @@ class DataSync with ChangeNotifier { } } - Future> downloadData({bool forceSync = false}) async { + Future> downloadData() async { if (haveWaitingTask) return const Res(true); while (isDownloading || isUploading) { haveWaitingTask = true; @@ -196,16 +184,16 @@ class DataSync with ChangeNotifier { if (file == null) { throw 'No data file found'; } - var version = file.name!.split('-').elementAtOrNull(1)?.split('.').first; - var remoteVersion = int.tryParse(version ?? '') ?? 0; - var localVersion = appdata.settings['dataVersion'] ?? 0; - - if (!forceSync && remoteVersion <= localVersion) { - Log.info("Data Sync", 'Local: $localVersion Remote: $remoteVersion Skip download ForceSync: $forceSync'); - return const Res(true); + var version = + file.name!.split('-').elementAtOrNull(1)?.split('.').first; + if (version != null && int.tryParse(version) != null) { + var currentVersion = appdata.settings['dataVersion']; + if (currentVersion != null && int.parse(version) <= currentVersion) { + Log.info("Data Sync", 'No new data to download'); + return const Res(true); + } } - - Log.info("Data Sync", "Local: $localVersion Remote: $remoteVersion Downloading data ForceSync: $forceSync"); + Log.info("Data Sync", "Downloading data from WebDAV server"); var localFile = File(FilePath.join(App.cachePath, file.name!)); await client.read2File(file.name!, localFile.path); await importAppData(localFile, true); From 8adf61b54fdf11e1e93b177e5385999939d26516 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 13:00:15 +0800 Subject: [PATCH 25/38] Fix multi-image mode --- lib/pages/reader/images.dart | 2 +- lib/pages/reader/reader.dart | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index a90382b..dba0e70 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -232,7 +232,7 @@ class _GalleryModeState extends State<_GalleryMode> ImageProvider imageProvider = _createImageProviderFromKey(imageKey, context); return Expanded( - child: Image( + child: ComicImage( image: imageProvider, fit: BoxFit.contain, ), diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 17d68e3..892b2e7 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -114,6 +114,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { late ReaderMode mode; int get imagesPerPage { + if (mode.isContinuous) return 1; if (isPortrait) { return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; } else { @@ -126,12 +127,6 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _checkImagesPerPageChange(); - } - void _checkImagesPerPageChange() { int currentImagesPerPage = imagesPerPage; if (_lastImagesPerPage != currentImagesPerPage) { @@ -173,13 +168,10 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } } if (widget.initialPage != null) { - page = (widget.initialPage! / imagesPerPage).ceil(); + page = widget.initialPage!; } mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; - _lastImagesPerPage = isPortrait - ? appdata.settings['readerScreenPicNumberForPortrait'] ?? 1 - : appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; Future.microtask(() { updateHistory(); }); @@ -194,6 +186,15 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _lastImagesPerPage = imagesPerPage; + if (imagesPerPage != 1) { + page = (widget.initialPage! / imagesPerPage).ceil(); + } + } + void setImageCacheSize() async { var availableRAM = await MemoryInfo.getFreePhysicalMemorySize(); if (availableRAM == null) return; From fba49233c82e26e500ee46cfc36266f41fad049d Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 13:14:28 +0800 Subject: [PATCH 26/38] Refactor --- lib/pages/reader/reader.dart | 117 +++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 892b2e7..2df6cd9 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -40,9 +40,13 @@ import 'package:window_manager/window_manager.dart'; import 'package:battery_plus/battery_plus.dart'; part 'scaffold.dart'; + part 'images.dart'; + part 'gesture.dart'; + part 'comic_image.dart'; + part 'loading.dart'; extension _ReaderContext on BuildContext { @@ -94,7 +98,8 @@ class Reader extends StatefulWidget { State createState() => _ReaderState(); } -class _ReaderState extends State with _ReaderLocation, _ReaderWindow { +class _ReaderState extends State + with _ReaderLocation, _ReaderWindow, _VolumeListener, _ImagePerPageHandler { @override void update() { setState(() {}); @@ -111,37 +116,11 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { List? images; + @override late ReaderMode mode; - int get imagesPerPage { - if (mode.isContinuous) return 1; - if (isPortrait) { - return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; - } else { - return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; - } - } - - late int _lastImagesPerPage; - - bool get isPortrait => - MediaQuery.of(context).orientation == Orientation.portrait; - - void _checkImagesPerPageChange() { - int currentImagesPerPage = imagesPerPage; - if (_lastImagesPerPage != currentImagesPerPage) { - _adjustPageForImagesPerPageChange( - _lastImagesPerPage, currentImagesPerPage); - _lastImagesPerPage = currentImagesPerPage; - } - } - - void _adjustPageForImagesPerPageChange( - int oldImagesPerPage, int newImagesPerPage) { - int previousImageIndex = (page - 1) * oldImagesPerPage; - int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; - page = newPage; - } + @override + bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait; History? history; @@ -150,8 +129,6 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { var focusNode = FocusNode(); - VolumeListener? volumeListener; - @override void initState() { page = widget.initialPage ?? 1; @@ -163,7 +140,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { chapter = 1; } if (widget.initialChapterGroup != null) { - for (int i=0; i<(widget.initialChapterGroup!-1); i++) { + for (int i = 0; i < (widget.initialChapterGroup! - 1); i++) { chapter += widget.chapters!.getGroupByIndex(i).length; } } @@ -189,10 +166,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { @override void didChangeDependencies() { super.didChangeDependencies(); - _lastImagesPerPage = imagesPerPage; - if (imagesPerPage != 1) { - page = (widget.initialPage! / imagesPerPage).ceil(); - } + initImagesPerPage(widget.initialPage ?? 1); } void setImageCacheSize() async { @@ -259,12 +233,12 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { void updateHistory() { if (history != null) { - if(page == maxPage){ + if (page == maxPage) { /// Record the last image of chapter history!.page = images?.length ?? 1; } else { /// Record the first image of the page - history!.page = (page-1) * imagesPerPage + 1; + history!.page = (page - 1) * imagesPerPage + 1; } if (maxPage > 1) { history!.maxPage = images?.length ?? 1; @@ -278,7 +252,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } history!.readEpisode.add('${g + 1}-$c'); history!.ep = c; - history!.group = g+1; + history!.group = g + 1; } else { history!.readEpisode.add(chapter.toString()); history!.ep = chapter; @@ -291,6 +265,61 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { }); } } +} + +abstract mixin class _ImagePerPageHandler { + late int _lastImagesPerPage; + + bool get isPortrait; + + int get page; + + set page(int value); + + ReaderMode get mode; + + void initImagesPerPage(int initialPage) { + _lastImagesPerPage = imagesPerPage; + if (imagesPerPage != 1) { + page = (initialPage / imagesPerPage).ceil(); + } + } + + /// The number of images displayed on one screen + int get imagesPerPage { + if (mode.isContinuous) return 1; + if (isPortrait) { + return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; + } else { + return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; + } + } + + /// Check if the number of images per page has changed + void _checkImagesPerPageChange() { + int currentImagesPerPage = imagesPerPage; + if (_lastImagesPerPage != currentImagesPerPage) { + _adjustPageForImagesPerPageChange( + _lastImagesPerPage, currentImagesPerPage); + _lastImagesPerPage = currentImagesPerPage; + } + } + + /// Adjust the page number when the number of images per page changes + void _adjustPageForImagesPerPageChange( + int oldImagesPerPage, int newImagesPerPage) { + int previousImageIndex = (page - 1) * oldImagesPerPage; + int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; + page = newPage; + } +} + +abstract mixin class _VolumeListener { + bool toNextPage(); + + bool toPrevPage(); + + VolumeListener? volumeListener; void handleVolumeEvent() { if (!App.isAndroid) { @@ -301,12 +330,8 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { volumeListener?.cancel(); } volumeListener = VolumeListener( - onDown: () { - toNextPage(); - }, - onUp: () { - toPrevPage(); - }, + onDown: toNextPage, + onUp: toPrevPage, )..listen(); } From 3f10473fb6db982d08708f29b2bf494f65b927d8 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 13:21:03 +0800 Subject: [PATCH 27/38] Fix invalid number of available source updates. --- lib/pages/home_page.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ddb9584..c715e22 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -605,6 +605,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { super.dispose(); } + int get _availableUpdates { + int c = 0; + ComicSource.availableUpdates.forEach((key, version) { + var source = ComicSource.find(key); + if (source != null) { + if (compareSemVer(version, source.version)) { + c++; + } + } + }); + return c; + } + @override Widget build(BuildContext context) { return SliverToBoxAdapter( @@ -668,7 +681,7 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { }).toList(), ).paddingHorizontal(16).paddingBottom(16), ), - if (ComicSource.availableUpdates.isNotEmpty) + if (_availableUpdates > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -687,7 +700,7 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { Icon(Icons.update, color: context.colorScheme.primary, size: 20,), const SizedBox(width: 8), Text("@c updates".tlParams({ - 'c': ComicSource.availableUpdates.length, + 'c': _availableUpdates, }), style: ts.withColor(context.colorScheme.primary),), ], ), From bf51cd5cee4cae32b3f592c63fe68be2c8b470bf Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 13:36:14 +0800 Subject: [PATCH 28/38] Improve checking follow updates. --- lib/pages/follow_updates_page.dart | 38 ++++++++++++++++++++---------- lib/utils/data_sync.dart | 16 ++++++++----- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index cf3dfa9..23eb0dd 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -6,6 +6,7 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; +import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/translations.dart'; import '../foundation/global_state.dart'; @@ -440,7 +441,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } void setFolder(String folder) async { - FollowUpdatesService.cancelChecking?.call(); + FollowUpdatesService._cancelChecking?.call(); LocalFavoritesManager().prepareTableForFollowUpdates(folder); var count = LocalFavoritesManager().count(folder); @@ -479,7 +480,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } void checkNow() async { - FollowUpdatesService.cancelChecking?.call(); + FollowUpdatesService._cancelChecking?.call(); bool isCanceled = false; void onCancel() { @@ -649,12 +650,14 @@ Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) { /// Background service for checking updates abstract class FollowUpdatesService { - static bool isChecking = false; + static bool _isChecking = false; - static void Function()? cancelChecking; + static void Function()? _cancelChecking; - static void check() async { - if (isChecking) { + static bool _isInitialized = false; + + static void _check() async { + if (_isChecking) { return; } var folder = appdata.settings["followUpdatesFolder"]; @@ -662,11 +665,16 @@ abstract class FollowUpdatesService { return; } bool isCanceled = false; - cancelChecking = () { + _cancelChecking = () { isCanceled = true; }; - isChecking = true; + _isChecking = true; + + while (DataSync().isDownloading) { + await Future.delayed(const Duration(milliseconds: 100)); + } + int updated = 0; try { await for (var progress in _updateFolder(folder, false)) { @@ -676,21 +684,27 @@ abstract class FollowUpdatesService { updated = progress.updated; } } finally { - cancelChecking = null; - isChecking = false; + _cancelChecking = null; + _isChecking = false; if (updated > 0) { updateFollowUpdatesUI(); } } } + /// Initialize the checker. static void initChecker() { - Timer.periodic(const Duration(hours: 1), (timer) { - check(); + if (_isInitialized) return; + _isInitialized = true; + _check(); + // A short interval will not affect the performance since every comic has a check time. + Timer.periodic(const Duration(minutes: 5), (timer) { + _check(); }); } } +/// Update the UI of follow updates. void updateFollowUpdatesUI() { GlobalState.findOrNull<_FollowUpdatesWidgetState>()?.updateCount(); GlobalState.findOrNull<_FollowUpdatesPageState>()?.updateComics(); diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 3cd9937..9b6839a 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -32,9 +32,13 @@ class DataSync with ChangeNotifier { factory DataSync() => instance ?? (instance = DataSync._()); - bool isDownloading = false; + bool _isDownloading = false; - bool isUploading = false; + bool get isDownloading => _isDownloading; + + bool _isUploading = false; + + bool get isUploading => _isUploading; bool haveWaitingTask = false; @@ -66,7 +70,7 @@ class DataSync with ChangeNotifier { await Future.delayed(const Duration(milliseconds: 100)); } haveWaitingTask = false; - isUploading = true; + _isUploading = true; notifyListeners(); try { var config = _validateConfig(); @@ -130,7 +134,7 @@ class DataSync with ChangeNotifier { return Res.error(e.toString()); } } finally { - isUploading = false; + _isUploading = false; notifyListeners(); } } @@ -142,7 +146,7 @@ class DataSync with ChangeNotifier { await Future.delayed(const Duration(milliseconds: 100)); } haveWaitingTask = false; - isDownloading = true; + _isDownloading = true; notifyListeners(); try { var config = _validateConfig(); @@ -205,7 +209,7 @@ class DataSync with ChangeNotifier { return Res.error(e.toString()); } } finally { - isDownloading = false; + _isDownloading = false; notifyListeners(); } } From a014587a94b34d51aeb297737c305d552fb34c12 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 14:13:05 +0800 Subject: [PATCH 29/38] Do not switch chapters if the current chapter is the first or last chapter in the chapter group. --- lib/foundation/comic_source/models.dart | 2 +- lib/pages/reader/gesture.dart | 4 +-- lib/pages/reader/images.dart | 9 +++---- lib/pages/reader/reader.dart | 34 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 4be9310..c80fea0 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -381,7 +381,7 @@ class ComicChapters { return _groupedChapters!.values.elementAt(index); } - /// Get a chapter by index + /// Get total number of chapters int get length { return isGrouped ? _groupedChapters!.values.map((e) => e.length).reduce((a, b) => a + b) diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 3122339..0b3928c 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -131,11 +131,11 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet } if (context.reader.mode.key.startsWith('gallery')) { if (forward) { - if (!context.reader.toNextPage()) { + if (!context.reader.toNextPage() && !context.reader.isLastChapterOfGroup) { context.reader.toNextChapter(); } } else { - if (!context.reader.toPrevPage()) { + if (!context.reader.toPrevPage() && !context.reader.isFirstChapterOfGroup) { context.reader.toPrevChapter(); } } diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index dba0e70..5ce0e56 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -206,11 +206,11 @@ class _GalleryModeState extends State<_GalleryMode> ), onPageChanged: (i) { if (i == 0) { - if (!reader.toPrevChapter()) { + if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) { reader.toPage(1); } } else if (i == totalPages + 1) { - if (!reader.toNextChapter()) { + if (reader.isLastChapterOfGroup || !reader.toNextChapter()) { reader.toPage(totalPages); } } else { @@ -575,15 +575,14 @@ class _ContinuousModeState extends State<_ContinuousMode> } if (notification is ScrollUpdateNotification) { - var length = reader.maxChapter; if (!scrollController.hasClients) return false; if (scrollController.position.pixels <= scrollController.position.minScrollExtent && - reader.chapter != 1) { + !reader.isFirstChapterOfGroup) { context.readerScaffold.setFloatingButton(-1); } else if (scrollController.position.pixels >= scrollController.position.maxScrollExtent && - reader.chapter < length) { + !reader.isLastChapterOfGroup) { context.readerScaffold.setFloatingButton(1); } else { context.readerScaffold.setFloatingButton(0); diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 2df6cd9..27d82da 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -265,6 +265,40 @@ class _ReaderState extends State }); } } + + bool get isFirstChapterOfGroup { + if (widget.chapters?.isGrouped ?? false) { + int c = chapter - 1; + int g = 1; + while (c > 0) { + c -= widget.chapters!.getGroupByIndex(g - 1).length; + g++; + } + if (c == 0) { + return true; + } else { + return false; + } + } + return chapter == 1; + } + + bool get isLastChapterOfGroup { + if (widget.chapters?.isGrouped ?? false) { + int c = chapter; + int g = 1; + while (c > 0) { + c -= widget.chapters!.getGroupByIndex(g - 1).length; + g++; + } + if (c == 0) { + return true; + } else { + return false; + } + } + return chapter == maxChapter; + } } abstract mixin class _ImagePerPageHandler { From f3c191f7f357cb18b569b03c5f085e8eac3e8a7a Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 14:24:36 +0800 Subject: [PATCH 30/38] update dependencies. --- lib/network/cloudflare.dart | 5 +- pubspec.lock | 98 +++++++++++++++++-------------------- pubspec.yaml | 14 +++--- 3 files changed, 56 insertions(+), 61 deletions(-) diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index 4c078e2..8b51fe8 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -14,7 +14,7 @@ import 'cookie_jar.dart'; class CloudflareException implements DioException { final String url; - const CloudflareException(this.url); + CloudflareException(this.url); @override String toString() { @@ -55,6 +55,9 @@ class CloudflareException implements DioException { @override DioExceptionType get type => DioExceptionType.badResponse; + + @override + DioExceptionReadableStringBuilder? stringBuilder; } class CloudflareInterceptor extends Interceptor { diff --git a/pubspec.lock b/pubspec.lock index d35693e..89b4555 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.4.0" app_links_linux: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" desktop_webview_window: dependency: "direct main" description: @@ -158,18 +158,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" dynamic_color: dependency: "direct main" description: @@ -190,10 +190,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -262,10 +262,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -400,10 +400,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.24" flutter_qjs: dependency: "direct main" description: @@ -417,18 +417,18 @@ packages: dependency: "direct main" description: name: flutter_reorderable_grid_view - sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915" + sha256: a7e0f9d5ba12fd232eb07fbb7f570ae35491045a6bba1858f6eb50c675526dfe url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" flutter_rust_bridge: dependency: transitive description: name: flutter_rust_bridge - sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" + sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" flutter_saf: dependency: "direct main" description: @@ -492,18 +492,18 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_parser: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" http_profile: dependency: transitive description: @@ -528,14 +528,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" json_annotation: dependency: transitive description: @@ -572,10 +564,10 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" local_auth: dependency: "direct main" description: @@ -596,10 +588,10 @@ packages: dependency: transitive description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.3" local_auth_platform_interface: dependency: transitive description: @@ -725,10 +717,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" photo_view: dependency: "direct main" description: @@ -758,18 +750,18 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" rhttp: dependency: "direct main" description: name: rhttp - sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e" + sha256: "3deabc6c3384b4efa252dfb4a5059acc6530117fdc1b10f5f67ff9768c9af75a" url: "https://pub.dev" source: hosted - version: "0.9.8" + version: "0.10.0" screen_retriever: dependency: transitive description: @@ -823,10 +815,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "10.1.3" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: @@ -876,18 +868,18 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 + sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.4" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" + sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233" url: "https://pub.dev" source: hosted - version: "0.5.28" + version: "0.5.30" stack_trace: dependency: transitive description: @@ -1004,18 +996,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1061,10 +1053,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.11.0" window_manager: dependency: "direct main" description: @@ -1106,5 +1098,5 @@ packages: source: hosted version: "0.0.10" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 10e6e78..557f1d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,16 +14,16 @@ dependencies: path_provider: any intl: any window_manager: ^0.4.3 - sqlite3: ^2.4.7 - sqlite3_flutter_libs: ^0.5.28 + sqlite3: ^2.7.4 + sqlite3_flutter_libs: ^0.5.30 flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs ref: 5978d0c7784fbbefcacc573547f0ab01ba59b7b3 crypto: ^3.0.6 - dio: ^5.7.0 + dio: ^5.8.0+1 html: ^0.15.5 - pointycastle: ^3.9.1 + pointycastle: ^4.0.0 url_launcher: ^6.3.0 path: ^1.9.0 photo_view: @@ -31,7 +31,7 @@ dependencies: url: https://github.com/wgh136/photo_view ref: 94724a0b mime: ^2.0.0 - share_plus: ^10.1.3 + share_plus: ^10.1.4 scrollable_positioned_list: git: url: https://github.com/venera-app/flutter.widgets @@ -48,7 +48,7 @@ dependencies: url: https://github.com/pichillilorenzo/flutter_inappwebview path: flutter_inappwebview ref: 0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676 - app_links: ^6.3.3 + app_links: ^6.4.0 sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 @@ -57,7 +57,7 @@ dependencies: git: url: https://github.com/venera-app/lodepng_flutter ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00 - rhttp: 0.9.8 + rhttp: 0.10.0 webdav_client: git: url: https://github.com/wgh136/webdav_client From 4eff50dbedffb05b8ee23c3a89d4975550589b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=92=E7=A0=82=E7=B3=96?= <90336521+lings03@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:25:38 +0800 Subject: [PATCH 31/38] Fix history of maxPage when maxPage in reader is 1 (#220) Due to the change of page and maxPage before, the history of maxPage should be real maxPage. If not, when maxPage in reader is 1, the maxPage in history will be none or the last ep's real maxPage. --- lib/pages/reader/reader.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 27d82da..ef0cbbe 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -240,9 +240,7 @@ class _ReaderState extends State /// Record the first image of the page history!.page = (page - 1) * imagesPerPage + 1; } - if (maxPage > 1) { - history!.maxPage = images?.length ?? 1; - } + history!.maxPage = images?.length ?? 1; if (widget.chapters?.isGrouped ?? false) { int g = 0; int c = chapter; From 3efc4794d0a3c64a71b346b69380e9ceb1da6bdb Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:46:22 +0800 Subject: [PATCH 32/38] Fix webdav prevent immediate upload when webdavAutoSync toggle (#221) --- assets/translation.json | 2 ++ lib/init.dart | 12 ++++++++++ lib/pages/settings/app.dart | 45 ++++++++++++++++++++++--------------- lib/utils/data_sync.dart | 2 +- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 7f27d3d..8d4f849 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -190,6 +190,7 @@ "Operation": "操作", "Upload": "上传", "Saved": "已保存", + "Saved Failed": "保存失败", "Sync Data": "同步数据", "Syncing Data": "正在同步数据", "Data Sync": "数据同步", @@ -553,6 +554,7 @@ "Operation": "操作", "Upload": "上傳", "Saved": "已保存", + "Saved Failed": "保存失敗", "Sync Data": "同步數據", "Syncing Data": "正在同步數據", "Data Sync": "數據同步", diff --git a/lib/init.dart b/lib/init.dart index 6f8bcda..8435771 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -56,6 +56,18 @@ void _checkOldConfigs() { .map((e) => e.key) .toList(); } + + if (appdata.implicitData['webdavAutoSync'] == null) { + var webdavConfig = appdata.settings['webdav']; + if (webdavConfig is List && + webdavConfig.length == 3 && + webdavConfig.whereType().length == 3) { + appdata.implicitData['webdavAutoSync'] = true; + } else { + appdata.implicitData['webdavAutoSync'] = false; + } + appdata.writeImplicitData(); + } } Future _checkAppUpdates() async { diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 9dbd3ee..4bb032f 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -350,7 +350,15 @@ class _WebdavSettingState extends State<_WebdavSetting> { user = configs[1]; pass = configs[2]; isEnabled = true; - autoSync = appdata.settings['webdavAutoSync'] ?? false; + autoSync = appdata.implicitData['webdavAutoSync'] ?? false; + } + + void onAutoSyncChanged(bool value) { + setState(() { + autoSync = value; + appdata.implicitData['webdavAutoSync'] = value; + appdata.writeImplicitData(); + }); } @override @@ -364,13 +372,7 @@ class _WebdavSettingState extends State<_WebdavSetting> { SwitchListTile( title: Text("WebDAV Auto Sync".tl), value: autoSync, - onChanged: (value) { - setState(() { - autoSync = value; - appdata.settings['webdavAutoSync'] = value; - appdata.saveData(); - }); - }, + onChanged: onAutoSyncChanged, ), const SizedBox(height: 12), TextField( @@ -448,11 +450,14 @@ class _WebdavSettingState extends State<_WebdavSetting> { isLoading: isTesting, onPressed: () async { var oldConfig = appdata.settings['webdav']; - var oldAutoSync = appdata.settings['webdavAutoSync']; - - if (url.trim().isEmpty && user.trim().isEmpty && pass.trim().isEmpty) { + var oldAutoSync = appdata.implicitData['webdavAutoSync']; + + if (url.trim().isEmpty && + user.trim().isEmpty && + pass.trim().isEmpty) { appdata.settings['webdav'] = []; - appdata.settings['webdavAutoSync'] = false; + appdata.implicitData['webdavAutoSync'] = false; + appdata.writeImplicitData(); appdata.saveData(); context.showMessage(message: "Saved".tl); App.rootPop(); @@ -460,7 +465,8 @@ class _WebdavSettingState extends State<_WebdavSetting> { } appdata.settings['webdav'] = [url, user, pass]; - appdata.settings['webdavAutoSync'] = autoSync; + appdata.implicitData['webdavAutoSync'] = autoSync; + appdata.writeImplicitData(); if (!autoSync) { appdata.saveData(); @@ -480,13 +486,16 @@ class _WebdavSettingState extends State<_WebdavSetting> { isTesting = false; }); appdata.settings['webdav'] = oldConfig; - appdata.settings['webdavAutoSync'] = oldAutoSync; + appdata.implicitData['webdavAutoSync'] = oldAutoSync; + appdata.writeImplicitData(); + appdata.saveData(); context.showMessage(message: testResult.errorMessage!); - return; + context.showMessage(message: "Saved Failed".tl); + } else { + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); } - appdata.saveData(); - context.showMessage(message: "Saved".tl); - App.rootPop(); }, child: Text("Continue".tl), ), diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 9b6839a..e73cd9f 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -44,7 +44,7 @@ class DataSync with ChangeNotifier { bool get isEnabled { var config = appdata.settings['webdav']; - var autoSync = appdata.settings['webdavAutoSync'] ?? false; + var autoSync = appdata.implicitData['webdavAutoSync'] ?? false; return autoSync && config is List && config.isNotEmpty; } From be0daddd82abde1bc4f073568581b6a9a293a959 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 17:01:12 +0800 Subject: [PATCH 33/38] Notify changes after the updating is completed. --- lib/foundation/favorites.dart | 22 ++++++++++++++++++++-- lib/pages/follow_updates_page.dart | 8 +++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index d4fee6f..f7ac7a4 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -797,7 +797,7 @@ class LocalFavoritesManager with ChangeNotifier { } } - void updateInfo(String folder, FavoriteItem comic) { + void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) { _db.execute(""" update "$folder" set name = ?, author = ?, cover_path = ?, tags = ? @@ -810,7 +810,9 @@ class LocalFavoritesManager with ChangeNotifier { comic.id, comic.type.value ]); - notifyListeners(); + if (notify) { + notifyListeners(); + } } String folderToJson(String folder) { @@ -900,6 +902,18 @@ class LocalFavoritesManager with ChangeNotifier { ]); } + void updateCheckTime( + String folder, + String id, + ComicType type, + ) { + _db.execute(""" + update "$folder" + set last_check_time = ? + where id == ? and type == ?; + """, [DateTime.now().millisecondsSinceEpoch, id, type.value]); + } + int countUpdates(String folder) { return _db.select(""" select count(*) as c from "$folder" @@ -961,4 +975,8 @@ class LocalFavoritesManager with ChangeNotifier { void close() { _db.dispose(); } + + void notifyChanges() { + notifyListeners(); + } } diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index 23eb0dd..7b56868 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -603,7 +603,7 @@ void _updateFolderBase( tags: newTags, ); - LocalFavoritesManager().updateInfo(folder, item); + LocalFavoritesManager().updateInfo(folder, item, false); var updateTime = newInfo.findUpdateTime(); if (updateTime != null && updateTime != c.updateTime) { @@ -613,6 +613,8 @@ void _updateFolderBase( c.type, updateTime, ); + } else { + LocalFavoritesManager().updateCheckTime(folder, c.id, c.type); } updated++; return; @@ -639,6 +641,10 @@ void _updateFolderBase( await Future.wait(futures); + if (updated > 0) { + LocalFavoritesManager().notifyChanges(); + } + stream.close(); } From 7fe81ae418e5bf3545d30e26d6c9715014fb98cf Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 21 Feb 2025 22:53:01 +0800 Subject: [PATCH 34/38] Improve switch pages gesture with gallery mode. --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 89b4555..3a12883 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -725,8 +725,8 @@ packages: dependency: "direct main" description: path: "." - ref: "94724a0b" - resolved-ref: "94724a0b7f94167fd1ae061f84e14ae04cae5c39" + ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 + resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 url: "https://github.com/wgh136/photo_view" source: git version: "0.14.0" diff --git a/pubspec.yaml b/pubspec.yaml index 557f1d2..f5e3026 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,7 @@ dependencies: photo_view: git: url: https://github.com/wgh136/photo_view - ref: 94724a0b + ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 mime: ^2.0.0 share_plus: ^10.1.4 scrollable_positioned_list: From 5645d805f56e1f9f4646ef6c41096feb6dab3d87 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 22 Feb 2025 10:41:56 +0800 Subject: [PATCH 35/38] Improve changing chapter gesture with continuous mode. --- assets/translation.json | 8 +- lib/pages/reader/images.dart | 238 ++++++++++++++++++++++++++++++--- lib/pages/reader/scaffold.dart | 129 ++++-------------- 3 files changed, 254 insertions(+), 121 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 8d4f849..801d496 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -361,7 +361,9 @@ "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页", "WebDAV Auto Sync": "WebDAV 自动同步", "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": "向上滑动查看下一章" }, "zh_TW": { "Home": "首頁", @@ -725,6 +727,8 @@ "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁", "WebDAV Auto Sync": "WebDAV 自動同步", "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": "向上滑動查看下一章" } } \ No newline at end of file diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 5ce0e56..24b1ac8 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -154,7 +154,6 @@ class _GalleryModeState extends State<_GalleryMode> builder: (BuildContext context, int index) { if (index == 0 || index == totalPages + 1) { return PhotoViewGalleryPageOptions.customChild( - scaleStateController: PhotoViewScaleStateController(), child: const SizedBox(), ); } else { @@ -168,7 +167,7 @@ class _GalleryModeState extends State<_GalleryMode> cached[index] = true; cache(index); - photoViewControllers[index] = PhotoViewController(); + photoViewControllers[index] ??= PhotoViewController(); if (reader.imagesPerPage == 1) { return PhotoViewGalleryPageOptions( @@ -350,6 +349,8 @@ const Set _kTouchLikeDeviceTypes = { PointerDeviceKind.unknown }; +const double _kChangeChapterOffset = 200; + class _ContinuousMode extends StatefulWidget { const _ContinuousMode({super.key}); @@ -364,7 +365,9 @@ class _ContinuousModeState extends State<_ContinuousMode> var itemScrollController = ItemScrollController(); var itemPositionsListener = ItemPositionsListener.create(); var photoViewController = PhotoViewController(); - late ScrollController scrollController; + ScrollController? _scrollController; + + ScrollController get scrollController => _scrollController!; var isCTRLPressed = false; static var _isMouseScrolling = false; @@ -372,6 +375,7 @@ class _ContinuousModeState extends State<_ContinuousMode> bool disableScroll = false; late List cached; + int get preCacheCount => appdata.settings["preloadImageCount"]; /// Whether the user was scrolling the page. @@ -386,6 +390,11 @@ class _ContinuousModeState extends State<_ContinuousMode> ); } + bool prepareToPrevChapter = false; + bool prepareToNextChapter = false; + bool jumpToNextChapter = false; + bool jumpToPrevChapter = false; + @override void initState() { reader = context.reader; @@ -464,6 +473,18 @@ class _ContinuousModeState extends State<_ContinuousMode> } } + void onScroll() { + if (prepareToPrevChapter) { + jumpToNextChapter = false; + jumpToPrevChapter = scrollController.offset < + scrollController.position.minScrollExtent - _kChangeChapterOffset; + } else if (prepareToNextChapter) { + jumpToNextChapter = scrollController.offset > + scrollController.position.maxScrollExtent + _kChangeChapterOffset; + jumpToPrevChapter = false; + } + } + @override Widget build(BuildContext context) { Widget widget = ScrollablePositionedList.builder( @@ -471,7 +492,11 @@ class _ContinuousModeState extends State<_ContinuousMode> itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener, scrollControllerCallback: (scrollController) { - this.scrollController = scrollController; + if (_scrollController != null) { + _scrollController!.removeListener(onScroll); + } + _scrollController = scrollController; + _scrollController!.addListener(onScroll); }, itemCount: reader.maxPage + 2, addSemanticIndexes: false, @@ -481,7 +506,7 @@ class _ContinuousModeState extends State<_ContinuousMode> reverse: reader.mode == ReaderMode.continuousRightToLeft, physics: isCTRLPressed || _isMouseScrolling || disableScroll ? const NeverScrollableScrollPhysics() - : const ClampingScrollPhysics(), + : const BouncingScrollPhysics(), itemBuilder: (context, index) { if (index == 0 || index == reader.maxPage + 1) { return const SizedBox(); @@ -496,18 +521,28 @@ class _ContinuousModeState extends State<_ContinuousMode> ImageProvider image = _createImageProvider(index, context); - return ComicImage( - filterQuality: FilterQuality.medium, - image: image, - width: width, - height: height, - fit: BoxFit.contain, + return ColoredBox( + color: context.colorScheme.surface, + child: ComicImage( + filterQuality: FilterQuality.medium, + image: image, + width: width, + height: height, + fit: BoxFit.contain, + ), ); }, scrollBehavior: const MaterialScrollBehavior() .copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes), ); + widget = Stack( + children: [ + Positioned.fill(child: buildBackground(context)), + Positioned.fill(child: widget), + ], + ); + widget = Listener( onPointerDown: (event) { fingers++; @@ -530,6 +565,13 @@ class _ContinuousModeState extends State<_ContinuousMode> disableScroll = false; }); } + if (fingers == 0) { + if (jumpToPrevChapter) { + reader.toPrevChapter(); + } else if (jumpToNextChapter) { + reader.toNextChapter(); + } + } }, onPointerCancel: (event) { fingers--; @@ -577,15 +619,37 @@ class _ContinuousModeState extends State<_ContinuousMode> if (notification is ScrollUpdateNotification) { if (!scrollController.hasClients) return false; if (scrollController.position.pixels <= - scrollController.position.minScrollExtent && + scrollController.position.minScrollExtent && !reader.isFirstChapterOfGroup) { - context.readerScaffold.setFloatingButton(-1); + if (!prepareToPrevChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + context.readerScaffold.setFloatingButton(-1); + setState(() { + prepareToPrevChapter = true; + }); + } } else if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent && + scrollController.position.maxScrollExtent && !reader.isLastChapterOfGroup) { - context.readerScaffold.setFloatingButton(1); + if (!prepareToNextChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + context.readerScaffold.setFloatingButton(1); + setState(() { + prepareToNextChapter = true; + }); + } } else { context.readerScaffold.setFloatingButton(0); + if (prepareToPrevChapter || prepareToNextChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + setState(() { + prepareToPrevChapter = false; + prepareToNextChapter = false; + }); + } } } @@ -618,6 +682,26 @@ class _ContinuousModeState extends State<_ContinuousMode> ); } + Widget buildBackground(BuildContext context) { + return Column( + children: [ + SizedBox(height: context.padding.top + 16), + if (prepareToPrevChapter) + _SwipeChangeChapterProgress( + controller: scrollController, + isPrev: true, + ), + const Spacer(), + if (prepareToNextChapter) + _SwipeChangeChapterProgress( + controller: scrollController, + isPrev: false, + ), + SizedBox(height: context.padding.bottom + 16), + ], + ); + } + @override Future animateToPage(int page) { return itemScrollController.scrollTo( @@ -758,3 +842,127 @@ void _precacheImage(int page, BuildContext context) { context, ); } + +class _SwipeChangeChapterProgress extends StatefulWidget { + const _SwipeChangeChapterProgress({ + this.controller, + required this.isPrev, + }); + + final ScrollController? controller; + + final bool isPrev; + + @override + State<_SwipeChangeChapterProgress> createState() => + _SwipeChangeChapterProgressState(); +} + +class _SwipeChangeChapterProgressState + extends State<_SwipeChangeChapterProgress> { + double value = 0; + + late final isPrev = widget.isPrev; + + ScrollController? controller; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + controller = widget.controller; + controller!.addListener(onScroll); + } + } + + @override + void didUpdateWidget(covariant _SwipeChangeChapterProgress oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + controller?.removeListener(onScroll); + controller = widget.controller; + controller?.addListener(onScroll); + if (value != 0) { + setState(() { + value = 0; + }); + } + } + } + + @override + void dispose() { + super.dispose(); + controller?.removeListener(onScroll); + } + + void onScroll() { + var position = controller!.position.pixels; + var offset = isPrev + ? controller!.position.minScrollExtent - position + : position - controller!.position.maxScrollExtent; + var newValue = offset / _kChangeChapterOffset; + newValue = newValue.clamp(0.0, 1.0); + if (newValue != value) { + setState(() { + value = newValue; + }); + } + } + + @override + Widget build(BuildContext context) { + final msg = widget.isPrev + ? "Swipe down for previous chapter".tl + : "Swipe up for next chapter".tl; + + return CustomPaint( + painter: _ProgressPainter( + value: value, + backgroundColor: context.colorScheme.surfaceContainer, + color: context.colorScheme.primaryContainer, + ), + child: Text(msg).paddingVertical(4).paddingHorizontal(16), + ); + } +} + +class _ProgressPainter extends CustomPainter { + final double value; + + final Color backgroundColor; + + final Color color; + + const _ProgressPainter({ + required this.value, + required this.backgroundColor, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; + canvas.drawRRect( + RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(16)), + paint, + ); + + paint.color = color; + canvas.drawRRect( + RRect.fromLTRBR( + 0, 0, size.width * value, size.height, Radius.circular(16)), + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return oldDelegate is! _ProgressPainter || + oldDelegate.value != value || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.color != color; + } +} diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 214dc10..a468bff 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -26,73 +26,21 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var lastValue = 0; - var fABValue = ValueNotifier(0); - _ReaderGestureDetectorState? _gestureDetectorState; - _DragListener? _floatingButtonDragListener; - void setFloatingButton(int value) { lastValue = showFloatingButtonValue; if (value == 0) { if (showFloatingButtonValue != 0) { showFloatingButtonValue = 0; - fABValue.value = 0; update(); } - if (_floatingButtonDragListener != null) { - _gestureDetectorState!.removeDragListener(_floatingButtonDragListener!); - _floatingButtonDragListener = null; - } } - var readerMode = context.reader.mode; if (value == 1 && showFloatingButtonValue == 0) { showFloatingButtonValue = 1; - _floatingButtonDragListener = _DragListener( - onMove: (offset) { - if (readerMode == ReaderMode.continuousTopToBottom) { - fABValue.value -= offset.dy; - } else if (readerMode == ReaderMode.continuousLeftToRight) { - fABValue.value -= offset.dx; - } else if (readerMode == ReaderMode.continuousRightToLeft) { - fABValue.value += offset.dx; - } - }, - onEnd: () { - if (fABValue.value.abs() > 58 * 3) { - setState(() { - showFloatingButtonValue = 0; - }); - context.reader.toNextChapter(); - } - fABValue.value = 0; - }, - ); - _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } else if (value == -1 && showFloatingButtonValue == 0) { showFloatingButtonValue = -1; - _floatingButtonDragListener = _DragListener( - onMove: (offset) { - if (readerMode == ReaderMode.continuousTopToBottom) { - fABValue.value += offset.dy; - } else if (readerMode == ReaderMode.continuousLeftToRight) { - fABValue.value += offset.dx; - } else if (readerMode == ReaderMode.continuousRightToLeft) { - fABValue.value -= offset.dx; - } - }, - onEnd: () { - if (fABValue.value.abs() > 58 * 3) { - setState(() { - showFloatingButtonValue = 0; - }); - context.reader.toPrevChapter(); - } - fABValue.value = 0; - }, - ); - _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } } @@ -778,62 +726,35 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); case -1: case 1: - return Container( + return SizedBox( width: 58, height: 58, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( + child: Material( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(16), - ), - child: ValueListenableBuilder( - valueListenable: fABValue, - builder: (context, value, child) { - return Stack( - children: [ - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - if (showFloatingButtonValue == 1) { - context.reader.toNextChapter(); - } else if (showFloatingButtonValue == -1) { - context.reader.toPrevChapter(); - } - setFloatingButton(0); - }, - borderRadius: BorderRadius.circular(16), - child: Center( - child: Icon( - showFloatingButtonValue == 1 - ? Icons.arrow_forward_ios - : Icons.arrow_back_ios_outlined, - size: 24, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ), - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - height: value.clamp(0, 58 * 3) / 3, - child: ColoredBox( - color: Theme.of(context) - .colorScheme - .surfaceTint - .toOpacity(0.2), - child: const SizedBox.expand(), - ), - ), - ], - ); - }, + elevation: 2, + child: InkWell( + onTap: () { + if (showFloatingButtonValue == 1) { + context.reader.toNextChapter(); + } else if (showFloatingButtonValue == -1) { + context.reader.toPrevChapter(); + } + setFloatingButton(0); + }, + borderRadius: BorderRadius.circular(16), + child: Center( + child: Icon( + showFloatingButtonValue == 1 + ? Icons.arrow_forward_ios + : Icons.arrow_back_ios_outlined, + size: 24, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), ), ); } From 1464b7d5e5c55385612085cc915530f8c70799ed Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 22 Feb 2025 11:33:58 +0800 Subject: [PATCH 36/38] Improve changing chapter gesture with continuous mode. --- lib/pages/reader/images.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 24b1ac8..609890a 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -349,7 +349,7 @@ const Set _kTouchLikeDeviceTypes = { PointerDeviceKind.unknown }; -const double _kChangeChapterOffset = 200; +const double _kChangeChapterOffset = 160; class _ContinuousMode extends StatefulWidget { const _ContinuousMode({super.key}); @@ -567,8 +567,10 @@ class _ContinuousModeState extends State<_ContinuousMode> } if (fingers == 0) { if (jumpToPrevChapter) { + context.readerScaffold.setFloatingButton(0); reader.toPrevChapter(); } else if (jumpToNextChapter) { + context.readerScaffold.setFloatingButton(0); reader.toNextChapter(); } } @@ -697,7 +699,7 @@ class _ContinuousModeState extends State<_ContinuousMode> controller: scrollController, isPrev: false, ), - SizedBox(height: context.padding.bottom + 16), + SizedBox(height: 36), ], ); } From 5a886f75044b6775e677329aa3f2312e1aab43cb Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 22 Feb 2025 19:31:23 +0800 Subject: [PATCH 37/38] Improve ui --- assets/translation.json | 6 ++---- lib/pages/comic_details_page/comic_page.dart | 11 +++-------- lib/pages/reader/images.dart | 17 ++++++++++++++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 801d496..4e5dcce 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -338,7 +338,7 @@ "Number of images preloaded": "预加载图片数量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Page @page": "上次阅读: 第 @page 页", + "Last Reading": "上次阅读", "Replies": "回复", "Follow Updates": "追更", "Not Configured": "未配置", @@ -358,7 +358,6 @@ "Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据", "Cache cleared": "缓存已清除", "Disabled": "已禁用", - "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页", "WebDAV Auto Sync": "WebDAV 自动同步", "Mark all as read": "全部标记为已读", "Do you want to mark all as read?" : "您要全部标记为已读吗?", @@ -704,7 +703,7 @@ "Number of images preloaded": "預加載圖片數量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Page @page": "上次閱讀: 第 @page 頁", + "Last Reading": "上次閱讀", "Replies": "回覆", "Follow Updates": "追更", "Not Configured": "未配置", @@ -724,7 +723,6 @@ "Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據", "Cache cleared": "緩存已清除", "Disabled": "已禁用", - "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁", "WebDAV Auto Sync": "WebDAV 自動同步", "Mark all as read": "全部標記為已讀", "Do you want to mark all as read?" : "您要全部標記為已讀嗎?", diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index fef8424..5447a31 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -372,7 +372,7 @@ class _ComicPageState extends LoadingState padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: context.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(24), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -395,14 +395,9 @@ class _ComicPageState extends LoadingState .getGroupByIndex(group - 1) .values .elementAt(ep - 1); - text = "Last Reading: @epName Page @page".tlParams({ - 'epName': epName, - 'page': page, - }); + text = "${"Last Reading".tl}: $epName P$page"; } else { - text = "Last Reading: Page @page".tlParams({ - 'page': page, - }); + text = "${"Last Reading".tl}: P$page"; } return Text(text); }, diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 609890a..43932c4 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -921,10 +921,21 @@ class _SwipeChangeChapterProgressState return CustomPaint( painter: _ProgressPainter( value: value, - backgroundColor: context.colorScheme.surfaceContainer, - color: context.colorScheme.primaryContainer, + backgroundColor: context.colorScheme.surfaceContainerLow, + color: context.colorScheme.surfaceContainerHighest, ), - child: Text(msg).paddingVertical(4).paddingHorizontal(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + widget.isPrev ? Icons.arrow_downward : Icons.arrow_upward, + color: context.colorScheme.onSurface, + size: 16, + ), + const SizedBox(width: 4), + Text(msg), + ], + ).paddingVertical(6).paddingHorizontal(16), ); } } From 38d5563534c824c7bab4471749287bfa6be7f6d7 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 22 Feb 2025 21:08:30 +0800 Subject: [PATCH 38/38] Show download status on reader chapter view. --- lib/pages/reader/chapters.dart | 242 +++++++++++++++++++++++++++++++++ lib/pages/reader/reader.dart | 2 + lib/pages/reader/scaffold.dart | 215 ----------------------------- 3 files changed, 244 insertions(+), 215 deletions(-) create mode 100644 lib/pages/reader/chapters.dart diff --git a/lib/pages/reader/chapters.dart b/lib/pages/reader/chapters.dart new file mode 100644 index 0000000..2b9a629 --- /dev/null +++ b/lib/pages/reader/chapters.dart @@ -0,0 +1,242 @@ +part of 'reader.dart'; + +class _ChaptersView extends StatefulWidget { + const _ChaptersView(this.reader); + + final _ReaderState reader; + + @override + State<_ChaptersView> createState() => _ChaptersViewState(); +} + +class _ChaptersViewState extends State<_ChaptersView> { + bool desc = false; + + late final ScrollController _scrollController; + + var downloaded = []; + + @override + void initState() { + super.initState(); + int epIndex = widget.reader.chapter - 2; + _scrollController = ScrollController( + initialScrollOffset: (epIndex * 48.0 + 52).clamp(0, double.infinity), + ); + var local = LocalManager().find(widget.reader.cid, widget.reader.type); + if (local != null) { + downloaded = local.downloadedChapters; + } + } + + @override + Widget build(BuildContext context) { + var chapters = widget.reader.widget.chapters!; + var current = widget.reader.chapter - 1; + return Scaffold( + body: SmoothCustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppbar( + style: AppbarStyle.shadow, + title: Text("Chapters".tl), + actions: [ + Tooltip( + message: "Click to change the order".tl, + child: TextButton.icon( + icon: Icon( + !desc ? Icons.arrow_upward : Icons.arrow_downward, + size: 18, + ), + label: Text(!desc ? "Ascending".tl : "Descending".tl), + onPressed: () { + setState(() { + desc = !desc; + }); + }, + ), + ), + ], + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (desc) { + index = chapters.length - 1 - index; + } + var chapter = chapters.titles.elementAt(index); + return _ChapterListTile( + onTap: () { + widget.reader.toChapter(index + 1); + Navigator.of(context).pop(); + }, + title: chapter, + isActive: current == index, + isDownloaded: + downloaded.contains(chapters.ids.elementAt(index)), + ); + }, + childCount: chapters.length, + ), + ), + ], + ), + ); + } +} + +class _GroupedChaptersView extends StatefulWidget { + const _GroupedChaptersView(this.reader); + + final _ReaderState reader; + + @override + State<_GroupedChaptersView> createState() => _GroupedChaptersViewState(); +} + +class _GroupedChaptersViewState extends State<_GroupedChaptersView> + with SingleTickerProviderStateMixin { + ComicChapters get chapters => widget.reader.widget.chapters!; + + late final TabController tabController; + + late final ScrollController _scrollController; + + late final String initialGroupName; + + var downloaded = []; + + @override + void initState() { + super.initState(); + int index = 0; + int epIndex = widget.reader.chapter - 1; + while (epIndex >= 0) { + epIndex -= chapters.getGroupByIndex(index).length; + index++; + } + tabController = TabController( + length: chapters.groups.length, + vsync: this, + initialIndex: index - 1, + ); + initialGroupName = chapters.groups.elementAt(index - 1); + var epIndexAtGroup = widget.reader.chapter - 1; + for (var i = 0; i < index - 1; i++) { + epIndexAtGroup -= chapters.getGroupByIndex(i).length; + } + _scrollController = ScrollController( + initialScrollOffset: (epIndexAtGroup * 48.0).clamp(0, double.infinity), + ); + var local = LocalManager().find(widget.reader.cid, widget.reader.type); + if (local != null) { + downloaded = local.downloadedChapters; + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Appbar(title: Text("Chapters".tl)), + AppTabBar( + controller: tabController, + tabs: chapters.groups.map((e) => Tab(text: e)).toList(), + ), + Expanded( + child: TabViewBody( + controller: tabController, + children: chapters.groups.map(buildGroup).toList(), + ), + ), + ], + ); + } + + Widget buildGroup(String groupName) { + var group = chapters.getGroup(groupName); + return SmoothCustomScrollView( + controller: initialGroupName == groupName ? _scrollController : null, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + var name = group.values.elementAt(index); + var i = 0; + for (var g in chapters.groups) { + if (g == groupName) { + break; + } + i += chapters.getGroup(g).length; + } + i += index + 1; + return _ChapterListTile( + onTap: () { + widget.reader.toChapter(i); + context.pop(); + }, + title: name, + isActive: widget.reader.chapter == i, + isDownloaded: downloaded.contains(group.keys.elementAt(index)), + ); + }, + childCount: group.length, + ), + ), + ], + ); + } +} + +class _ChapterListTile extends StatelessWidget { + const _ChapterListTile({ + required this.title, + required this.isActive, + required this.isDownloaded, + required this.onTap, + }); + + final String title; + + final bool isActive; + + final bool isDownloaded; + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: + isActive ? context.colorScheme.primary : Colors.transparent, + width: 4, + ), + ), + ), + child: Row( + children: [ + Text( + title, + style: isActive + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + const Spacer(), + if (isDownloaded) + Icon( + Icons.download_done_rounded, + color: context.colorScheme.secondary, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index ef0cbbe..d1d0cea 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -49,6 +49,8 @@ part 'comic_image.dart'; part 'loading.dart'; +part 'chapters.dart'; + extension _ReaderContext on BuildContext { _ReaderState get reader => findAncestorStateOfType<_ReaderState>()!; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index a468bff..41ef5e6 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -940,218 +940,3 @@ class _ClockWidgetState extends State<_ClockWidget> { ); } } - -class _ChaptersView extends StatefulWidget { - const _ChaptersView(this.reader); - - final _ReaderState reader; - - @override - State<_ChaptersView> createState() => _ChaptersViewState(); -} - -class _ChaptersViewState extends State<_ChaptersView> { - bool desc = false; - - late final ScrollController _scrollController; - - @override - void initState() { - super.initState(); - int epIndex = widget.reader.chapter - 2; - _scrollController = ScrollController( - initialScrollOffset: (epIndex * 48.0 + 52).clamp(0, double.infinity), - ); - } - - @override - Widget build(BuildContext context) { - var chapters = widget.reader.widget.chapters!; - var current = widget.reader.chapter - 1; - return Scaffold( - body: SmoothCustomScrollView( - controller: _scrollController, - slivers: [ - SliverAppbar( - style: AppbarStyle.shadow, - title: Text("Chapters".tl), - actions: [ - Tooltip( - message: "Click to change the order".tl, - child: TextButton.icon( - icon: Icon( - !desc ? Icons.arrow_upward : Icons.arrow_downward, - size: 18, - ), - label: Text(!desc ? "Ascending".tl : "Descending".tl), - onPressed: () { - setState(() { - desc = !desc; - }); - }, - ), - ), - ], - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (desc) { - index = chapters.length - 1 - index; - } - var chapter = chapters.titles.elementAt(index); - return InkWell( - onTap: () { - widget.reader.toChapter(index + 1); - Navigator.of(context).pop(); - }, - child: Container( - height: 48, - padding: const EdgeInsets.only(left: 16), - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: current == index - ? context.colorScheme.primary - : Colors.transparent, - width: 2, - ), - ), - ), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - chapter, - style: current == index - ? ts.withColor(context.colorScheme.primary).bold.s16 - : ts.s16, - ), - ), - ), - ); - }, - childCount: chapters.length, - ), - ), - ], - ), - ); - } -} - -class _GroupedChaptersView extends StatefulWidget { - const _GroupedChaptersView(this.reader); - - final _ReaderState reader; - - @override - State<_GroupedChaptersView> createState() => _GroupedChaptersViewState(); -} - -class _GroupedChaptersViewState extends State<_GroupedChaptersView> - with SingleTickerProviderStateMixin { - ComicChapters get chapters => widget.reader.widget.chapters!; - - late final TabController tabController; - - late final ScrollController _scrollController; - - late final String initialGroupName; - - @override - void initState() { - super.initState(); - int index = 0; - int epIndex = widget.reader.chapter - 1; - while (epIndex >= 0) { - epIndex -= chapters.getGroupByIndex(index).length; - index++; - } - tabController = TabController( - length: chapters.groups.length, - vsync: this, - initialIndex: index - 1, - ); - initialGroupName = chapters.groups.elementAt(index - 1); - var epIndexAtGroup = widget.reader.chapter - 1; - for (var i = 0; i < index-1; i++) { - epIndexAtGroup -= chapters.getGroupByIndex(i).length; - } - _scrollController = ScrollController( - initialScrollOffset: (epIndexAtGroup * 48.0).clamp(0, double.infinity), - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Appbar(title: Text("Chapters".tl)), - AppTabBar( - controller: tabController, - tabs: chapters.groups.map((e) => Tab(text: e)).toList(), - ), - Expanded( - child: TabViewBody( - controller: tabController, - children: chapters.groups.map(buildGroup).toList(), - ), - ), - ], - ); - } - - Widget buildGroup(String groupName) { - var group = chapters.getGroup(groupName); - return SmoothCustomScrollView( - controller: initialGroupName == groupName ? _scrollController : null, - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - var name = group.values.elementAt(index); - var i = 0; - for (var g in chapters.groups) { - if (g == groupName) { - break; - } - i += chapters.getGroup(g).length; - } - i += index + 1; - return InkWell( - onTap: () { - widget.reader.toChapter(i); - context.pop(); - }, - child: Container( - height: 48, - padding: const EdgeInsets.only(left: 16), - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: widget.reader.chapter == i - ? context.colorScheme.primary - : Colors.transparent, - width: 2, - ), - ), - ), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - name, - style: widget.reader.chapter == i - ? ts.withColor(context.colorScheme.primary).bold.s16 - : ts.s16, - ), - ), - ), - ); - }, - childCount: group.length, - ), - ), - ], - ); - } -}