52 Commits

Author SHA1 Message Date
9d8ade6fe0 Add log export functionality. 2025-07-23 14:35:27 +08:00
6245399810 Improve UI of comic source page. 2025-07-23 14:28:40 +08:00
c074e7f9d1 Add default source list url. 2025-07-23 14:16:53 +08:00
f822e198ea Update version code. 2025-07-22 17:55:51 +08:00
7035f11eb5 Add optional image parameter to showInputDialog for captcha support. Close #422 2025-07-22 17:51:40 +08:00
f2f5a4f573 Convert between Simplified Chinese and Traditional Chinese when searching favorites. Close #438 2025-07-20 18:52:05 +08:00
2acf234f7d Fix response handling for unhandled method calls in flutter_window.cpp 2025-07-20 18:47:49 +08:00
9ed8f351c7 Add heartbeat monitoring in release builds. 2025-07-20 18:45:42 +08:00
7c35dc7cf7 Update doc. 2025-07-20 16:39:21 +08:00
ccb03343f4 Fix the issue where the toolbar can not be open when chapter data loading failed. Close #415 2025-07-13 20:22:56 +08:00
角砂糖
b9817ec030 Fix page calculation logic && trigger recalculation on orientation change (#428) 2025-06-26 19:55:21 +08:00
角砂糖
5ebb554e54 Add an option to filter logs by level (#427) 2025-06-26 19:55:07 +08:00
23ee79fe9d Set high refresh rate on Android. 2025-06-23 19:39:47 +08:00
nyne
85baac657a Merge pull request #421 from lings03/local
Allow user to keep favorite and history when delete local comic. Close #420
2025-06-23 19:06:34 +08:00
nyne
cceca6b96f Merge branch 'master' into local 2025-06-23 19:04:20 +08:00
角砂糖
b5b0dc85e3 Show group in last read and history when group existing. (#419) 2025-06-23 19:03:24 +08:00
nyne
50044c4372 Merge pull request #418 from lings03/reverse
Add a option to reverse the default chapter order. Close #414
2025-06-23 19:02:52 +08:00
nyne
5fd7f1b880 Merge branch 'master' into reverse 2025-06-23 19:00:48 +08:00
角砂糖
058fde3f5a Add a button to show system status bar (#417) 2025-06-23 19:00:04 +08:00
角砂糖
a2d46123dd Add missing translation
debug时发现之前少了这一句翻译,也不至于提个pr,顺便加在这里吧
2025-06-22 20:07:15 +08:00
角砂糖
01acc4f9de Allow user to keep favorite and history when delete local comic. Close #420 2025-06-22 19:50:38 +08:00
角砂糖
856aae0769 Add a option to reverse the default chapter order. Close #414 2025-06-22 00:29:49 +08:00
nyne
8eda8adcc8 Merge pull request #410 from venera-app/v1.4.5-dev
V1.4.5
2025-06-18 16:52:59 +08:00
defd4b8624 Update version code. 2025-06-18 16:39:02 +08:00
b2a164e066 Remove the config file repository url from app. 2025-06-18 16:34:49 +08:00
a46ceebf19 Fixed the issue where the update dialog was not showed on startup. 2025-06-18 16:07:36 +08:00
cc08445f13 Set initial chapter to first downloaded chapter if there is no history when starting to read a local comic. Close #405 2025-06-17 17:36:13 +08:00
93f7f72d07 Fixed some issues when using custom download path on Android. Close #400 2025-06-17 17:15:35 +08:00
20f7ab4866 Clear folder value if it does not exist in local favorites. Close #389 2025-06-15 15:02:45 +08:00
54363919cd Fixed RangeError when translating tags. Close #356 2025-06-15 14:58:15 +08:00
182a821fc5 Fixed the issue where the download task would stop after exiting the reader. Close #387 2025-06-15 14:58:15 +08:00
8868c6edb3 Update Flutter SDK version to 3.32.4 2025-06-15 14:58:15 +08:00
角砂糖
fffbb4ed23 Only add closeListener when app is desktop (#397) 2025-06-04 12:11:45 +08:00
角砂糖
b057be0311 Fix abnormal history recording when not flipping pages. Close #392 (#395) 2025-06-03 17:36:20 +08:00
角砂糖
fc5fed1707 Fix history of page when show single image on first page (#393) 2025-06-03 17:35:45 +08:00
角砂糖
8525f5318f Fix page calculate when showSingleImageOnFirstPage is enabled (#391) 2025-06-03 17:35:17 +08:00
角砂糖
d58cafc4a0 Fix abnormal single image height when imagesPerPage > 1. Close #379 2025-05-31 10:50:17 +08:00
23afafd1d6 Update rhttp 2025-05-26 19:05:15 +08:00
nyne
3b6e0adbbb Merge pull request #377 from venera-app/v1.4.4
V1.4.4
2025-05-26 18:18:43 +08:00
20a57c7a36 Update version code 2025-05-26 18:10:07 +08:00
665f50ed2a Fixed an issue where comic counts would become invalid after renaming a favorite folder. Close #357 2025-05-26 16:42:05 +08:00
55733ef505 Update selectAll method to handle search mode for selecting comics. Close #359 2025-05-26 16:09:23 +08:00
0c46214619 Reduce maximum length for comic directory names to improve consistency. Close #362 2025-05-26 15:35:24 +08:00
749a1a47fb Fix dialog content overflow. Close #363 2025-05-25 20:33:31 +08:00
76e9ef87d4 Add functionality to delete specific comic chapters. Close #368 2025-05-25 20:26:35 +08:00
dcd6466547 Improve performance of deleting favorites, coping favorites, moving favorites and deleting downloads. Close #365 2025-05-24 16:24:53 +08:00
ed70fdba93 Improve reordering local comics. Close #374 2025-05-22 20:51:47 +08:00
ded0068ea6 Improve performance for clearing history. 2025-05-22 20:37:25 +08:00
nyne
7dc6be622a fix clearing history. 2025-05-22 20:01:07 +08:00
nyne
88f093f7e5 Add clear unfavorited history functionality. Close #372 2025-05-22 19:59:42 +08:00
8f357b3e6c Merge branch 'master' into v1.4.4 2025-05-20 15:51:28 +08:00
9ee82975e8 Handle invalid appdata file. 2025-05-20 15:40:30 +08:00
40 changed files with 5186 additions and 422 deletions

View File

@@ -1322,13 +1322,15 @@ let UI = {
* Show an input dialog * Show an input dialog
* @param title {string} * @param title {string}
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message. * @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha.
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null. * @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
*/ */
showInputDialog: (title, validator) => { showInputDialog: (title, validator, image) => {
return sendMessage({ return sendMessage({
method: 'UI', method: 'UI',
function: 'showInputDialog', function: 'showInputDialog',
title: title, title: title,
image: image,
validator: validator validator: validator
}) })
}, },

3982
assets/opencc.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -234,8 +234,10 @@
"Please add some sources": "请添加一些源", "Please add some sources": "请添加一些源",
"Please check your settings": "请检查您的设置", "Please check your settings": "请检查您的设置",
"No Category Pages": "没有分类页面", "No Category Pages": "没有分类页面",
"Group @group": "第 @group 组",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
"Page @page": "第 @page 页", "Page @page": "第 @page 页",
"Remove local favorite and history": "删除本地收藏和历史记录",
"Also remove files on disk": "同时删除磁盘上的文件", "Also remove files on disk": "同时删除磁盘上的文件",
"Copy to app local path": "将漫画复制到本地存储目录中", "Copy to app local path": "将漫画复制到本地存储目录中",
"Delete all unavailable local favorite items": "删除所有无效的本地收藏", "Delete all unavailable local favorite items": "删除所有无效的本地收藏",
@@ -388,10 +390,22 @@
"Suggestions": "建议", "Suggestions": "建议",
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题", "Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
"Show single image on first page": "在首页显示单张图片", "Show single image on first page": "在首页显示单张图片",
"Show system status bar": "显示系统状态栏",
"Click to select an image": "点击选择一张图片", "Click to select an image": "点击选择一张图片",
"Source URL": "地址", "Repo URL": "仓库地址",
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
"Double tap to zoom": "双击缩放" "Double tap to zoom": "双击缩放",
"Clear Unfavorited": "清除未收藏",
"Reverse": "反转",
"Delete Chapters": "删除章节",
"Path copied to clipboard": "路径已复制到剪贴板",
"Reverse default chapter order": "反转默认章节顺序",
"Reload Configs": "重新加载配置文件",
"Reload": "重载",
"Disable Length Limitation": "禁用长度限制",
"Only valid for this run": "仅对本次运行有效",
"Logs": "日志",
"Export logs": "导出日志"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -628,8 +642,10 @@
"Please add some sources": "請添加一些源", "Please add some sources": "請添加一些源",
"Please check your settings": "請檢查您的設定", "Please check your settings": "請檢查您的設定",
"No Category Pages": "沒有分類頁面", "No Category Pages": "沒有分類頁面",
"Group @group": "第 @group 組",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
"Page @page": "第 @page 頁", "Page @page": "第 @page 頁",
"Remove local favorite and history": "刪除本機收藏和歷史記錄",
"Also remove files on disk": "同時刪除磁碟上的文件", "Also remove files on disk": "同時刪除磁碟上的文件",
"Copy to app local path": "將漫畫複製到本機儲存目錄中", "Copy to app local path": "將漫畫複製到本機儲存目錄中",
"Delete all unavailable local favorite items": "刪除所有無效的本機收藏", "Delete all unavailable local favorite items": "刪除所有無效的本機收藏",
@@ -782,9 +798,21 @@
"Suggestions": "建議", "Suggestions": "建議",
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題", "Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
"Show single image on first page": "在首頁顯示單張圖片", "Show single image on first page": "在首頁顯示單張圖片",
"Show system status bar": "顯示系統狀態欄",
"Click to select an image": "點擊選擇一張圖片", "Click to select an image": "點擊選擇一張圖片",
"Source URL": "地址", "Repo URL": "倉庫地址",
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
"Double tap to zoom": "雙擊縮放" "Double tap to zoom": "雙擊縮放",
"Clear Unfavorited": "清除未收藏",
"Reverse": "反轉",
"Delete Chapters": "刪除章節",
"Path copied to clipboard": "路徑已複製到剪貼簿",
"Reverse default chapter order": "反轉預設章節順序",
"Reload Configs": "重新載入設定檔",
"Reload": "重載",
"Disable Length Limitation": "禁用長度限制",
"Only valid for this run": "僅對本次運行有效",
"Logs": "日誌",
"Export logs": "匯出日誌"
} }
} }

View File

@@ -9,13 +9,47 @@ Venera uses [flutter_qjs](https://github.com/wgh136/flutter_qjs) as js engine wh
This document will describe how to write a comic source for Venera. This document will describe how to write a comic source for Venera.
## Preparation ## Comic Source List
Venera can display a list of comic sources in the app.
You can use the following repo url:
```
https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json
```
The repo is maintained by the Venera team.
> The link is a mirror of the original repo. To contribute your comic source, please visit the [original repo](https://github.com/venera-app/venera-configs)
You should provide a repository url to let the app load the comic source list.
The url should point to a JSON file that contains the list of comic sources.
The JSON file should have the following format:
```json
[
{
"name": "Source Name",
"url": "https://example.com/source.js",
"filename": "Relative path to the source file",
"version": "1.0.0",
"description": "A brief description of the source"
}
]
```
Only one of `url` and `filename` should be provided.
The description field is optional.
## Create a Comic Source
### Preparation
- Install Venera. Using flutter to run the project is recommended since it's easier to debug. - Install Venera. Using flutter to run the project is recommended since it's easier to debug.
- An editor that supports javascript. - An editor that supports javascript.
- Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs). - Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs).
## Start Writing ### Start Writing
The template contains detailed comments and examples. You can refer to it when writing your own comic source. The template contains detailed comments and examples. You can refer to it when writing your own comic source.
@@ -23,7 +57,7 @@ Here is a brief introduction to the template:
> Note: Javascript api document is [here](js_api.md). > Note: Javascript api document is [here](js_api.md).
### Write basic information #### Write basic information
```javascript ```javascript
class NewComicSource extends ComicSource { class NewComicSource extends ComicSource {
@@ -49,7 +83,7 @@ In this part, you need to do the following:
- Change the class name to your source name. - Change the class name to your source name.
- Fill in the name, key, version, minAppVersion, and url fields. - Fill in the name, key, version, minAppVersion, and url fields.
### init function #### init function
```javascript ```javascript
/** /**
@@ -64,7 +98,7 @@ The function will be called when the source is initialized. You can do some init
Remove this function if not used. Remove this function if not used.
### Account #### Account
```javascript ```javascript
// [Optional] account related // [Optional] account related
@@ -140,7 +174,7 @@ In this part, you can implement login, logout, and register functions.
Remove this part if not used. Remove this part if not used.
### Explore page #### Explore page
```javascript ```javascript
// explore page list // explore page list
@@ -185,7 +219,7 @@ There are three types of explore pages:
- multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page. - multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page.
- mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button. - mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button.
### Category Page #### Category Page
```javascript ```javascript
// categories // categories
@@ -227,7 +261,7 @@ Category page is a static page that contains multiple parts, each part contains
A comic source can only have one category page. A comic source can only have one category page.
### Category Comics Page #### Category Comics Page
```javascript ```javascript
/// category comic loading related /// category comic loading related
@@ -280,7 +314,7 @@ When user clicks on a category, the category comics page will be displayed.
This part is used to load comics of a category. This part is used to load comics of a category.
### Search #### Search
```javascript ```javascript
/// search related /// search related
@@ -339,7 +373,7 @@ This part is used to load search results.
`load` and `loadNext` functions are used to load search results. `load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored. If `load` function is implemented, `loadNext` function will be ignored.
### Favorites #### Favorites
```javascript ```javascript
// favorite related // favorite related
@@ -411,7 +445,7 @@ This part is used to manage network favorites of the source.
`load` and `loadNext` functions are used to load search results. `load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored. If `load` function is implemented, `loadNext` function will be ignored.
### Comic Details #### Comic Details
```javascript ```javascript
/// single comic related /// single comic related
@@ -576,7 +610,7 @@ If `load` function is implemented, `loadNext` function will be ignored.
This part is used to load comic details. This part is used to load comic details.
### Settings #### Settings
```javascript ```javascript
/* /*
@@ -635,7 +669,7 @@ This part is used to load comic details.
This part is used to provide settings for the source. This part is used to provide settings for the source.
### Translations #### Translations
```javascript ```javascript
// [Optional] translations for the strings in this config // [Optional] translations for the strings in this config

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -21,11 +22,13 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
import 'package:venera/foundation/image_provider/history_image_provider.dart'; import 'package:venera/foundation/image_provider/history_image_provider.dart';
import 'package:venera/foundation/image_provider/local_comic_image.dart'; import 'package:venera/foundation/image_provider/local_comic_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/network/cloudflare.dart'; import 'package:venera/network/cloudflare.dart';
import 'package:venera/pages/comic_details_page/comic_page.dart'; import 'package:venera/pages/comic_details_page/comic_page.dart';
import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';

View File

@@ -37,9 +37,11 @@ mixin class JsUiApi {
case 'showInputDialog': case 'showInputDialog':
var title = message['title']; var title = message['title'];
var validator = message['validator']; var validator = message['validator'];
var image = message['image'];
if (title is! String) return; if (title is! String) return;
if (validator != null && validator is! JSInvokable) return; if (validator != null && validator is! JSInvokable) return;
return _showInputDialog(title, validator); if (image != null && image is! String) return;
return _showInputDialog(title, validator, image);
case 'showSelectDialog': case 'showSelectDialog':
var title = message['title']; var title = message['title'];
var options = message['options']; var options = message['options'];
@@ -124,12 +126,13 @@ mixin class JsUiApi {
controller?.close(); controller?.close();
} }
Future<String?> _showInputDialog(String title, JSInvokable? validator) async { Future<String?> _showInputDialog(String title, JSInvokable? validator, String? image) async {
String? result; String? result;
var func = validator == null ? null : JSAutoFreeFunction(validator); var func = validator == null ? null : JSAutoFreeFunction(validator);
await showInputDialog( await showInputDialog(
context: App.rootContext, context: App.rootContext,
title: title, title: title,
image: image,
onConfirm: (v) { onConfirm: (v) {
if (func != null) { if (func != null) {
var res = func.call([v]); var res = func.call([v]);

View File

@@ -41,18 +41,22 @@ class NetworkError extends StatelessWidget {
], ],
), ),
), ),
const SizedBox( const SizedBox(height: 8),
height: 8,
),
Text( Text(
cfe == null ? message : "Cloudflare verification required".tl, cfe == null ? message : "Cloudflare verification required".tl,
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 3, maxLines: 3,
), ),
if (retry != null) TextButton(
const SizedBox( onPressed: () {
height: 12, saveFile(
data: utf8.encode(Log().toString()),
filename: 'log.txt',
);
},
child: Text("Export logs".tl),
), ),
const SizedBox(height: 8),
if (retry != null) if (retry != null)
if (cfe != null) if (cfe != null)
FilledButton( FilledButton(
@@ -74,15 +78,11 @@ class NetworkError extends StatelessWidget {
body = Column( body = Column(
children: [ children: [
const Appbar(title: Text("")), const Appbar(title: Text("")),
Expanded( Expanded(child: body),
child: body,
)
], ],
); );
} }
return Material( return Material(child: body);
child: body,
);
} }
} }
@@ -94,9 +94,7 @@ class ListLoadingIndicator extends StatelessWidget {
return const SizedBox( return const SizedBox(
width: double.infinity, width: double.infinity,
height: 80, height: 80,
child: Center( child: Center(child: FiveDotLoadingAnimation()),
child: FiveDotLoadingAnimation(),
),
); );
} }
} }
@@ -108,10 +106,9 @@ class SliverListLoadingIndicator extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// SliverToBoxAdapter can not been lazy loaded. // SliverToBoxAdapter can not been lazy loaded.
// Use SliverList to make sure the animation can be lazy loaded. // Use SliverList to make sure the animation can be lazy loaded.
return SliverList.list(children: const [ return SliverList.list(
SizedBox(), children: const [SizedBox(), ListLoadingIndicator()],
ListLoadingIndicator(), );
]);
} }
} }
@@ -178,10 +175,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
} }
Widget buildError() { Widget buildError() {
return NetworkError( return NetworkError(message: error!, retry: retry);
message: error!,
retry: retry,
);
} }
@override @override
@@ -323,11 +317,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
} }
Widget buildError(BuildContext context, String error) { Widget buildError(BuildContext context, String error) {
return NetworkError( return NetworkError(withAppbar: false, message: error, retry: reset);
withAppbar: false,
message: error,
retry: reset,
);
} }
@override @override
@@ -388,7 +378,7 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
Colors.green, Colors.green,
Colors.blue, Colors.blue,
Colors.yellow, Colors.yellow,
Colors.purple Colors.purple,
]; ];
static const _padding = 12.0; static const _padding = 12.0;
@@ -405,11 +395,10 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
return SizedBox( return SizedBox(
width: _dotSize * 5 + _padding * 6, width: _dotSize * 5 + _padding * 6,
height: _height, height: _height,
child: Stack( child: Stack(children: List.generate(5, (index) => buildDot(index))),
children: List.generate(5, (index) => buildDot(index)), );
), },
); );
});
} }
Widget buildDot(int index) { Widget buildDot(int index) {
@@ -417,7 +406,8 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
var startValue = index * 0.8; var startValue = index * 0.8;
return Positioned( return Positioned(
left: index * _dotSize + (index + 1) * _padding, left: index * _dotSize + (index + 1) * _padding,
bottom: (math.sin(math.pi / 2 * (value - startValue).clamp(0, 2))) * bottom:
(math.sin(math.pi / 2 * (value - startValue).clamp(0, 2))) *
(_height - _dotSize), (_height - _dotSize),
child: Container( child: Container(
width: _dotSize, width: _dotSize,

View File

@@ -290,7 +290,8 @@ class ContentDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var content = Column( var content = SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -312,6 +313,7 @@ class ContentDialog extends StatelessWidget {
).paddingRight(12), ).paddingRight(12),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
),
); );
return Dialog( return Dialog(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -357,6 +359,7 @@ Future<void> showInputDialog({
String confirmText = "Confirm", String confirmText = "Confirm",
String cancelText = "Cancel", String cancelText = "Cancel",
RegExp? inputValidator, RegExp? inputValidator,
String? image,
}) { }) {
var controller = TextEditingController(text: initialValue); var controller = TextEditingController(text: initialValue);
bool isLoading = false; bool isLoading = false;
@@ -369,7 +372,14 @@ Future<void> showInputDialog({
builder: (context, setState) { builder: (context, setState) {
return ContentDialog( return ContentDialog(
title: title, title: title,
content: TextField( content: Column(
children: [
if (image != null)
SizedBox(
height: 108,
child: Image.network(image, fit: BoxFit.none),
).paddingBottom(8),
TextField(
controller: controller, controller: controller,
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText, hintText: hintText,
@@ -377,6 +387,8 @@ Future<void> showInputDialog({
errorText: error, errorText: error,
), ),
).paddingHorizontal(12), ).paddingHorizontal(12),
],
),
actions: [ actions: [
Button.filled( Button.filled(
isLoading: isLoading, isLoading: isLoading,

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.4.3"; final version = "1.4.6";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/init.dart'; import 'package:venera/utils/init.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
@@ -110,6 +111,7 @@ class Appdata with Init {
if (!await file.exists()) { if (!await file.exists()) {
return; return;
} }
try {
var json = jsonDecode(await file.readAsString()); var json = jsonDecode(await file.readAsString());
for (var key in (json['settings'] as Map<String, dynamic>).keys) { for (var key in (json['settings'] as Map<String, dynamic>).keys) {
if (json['settings'][key] != null) { if (json['settings'][key] != null) {
@@ -117,14 +119,23 @@ class Appdata with Init {
} }
} }
searchHistory = List.from(json['searchHistory']); searchHistory = List.from(json['searchHistory']);
}
catch(e) {
Log.error("Appdata", "Failed to load appdata", e);
Log.info("Appdata", "Resetting appdata");
file.deleteIgnoreError();
}
try {
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
if (await implicitDataFile.exists()) { if (await implicitDataFile.exists()) {
try {
implicitData = jsonDecode(await implicitDataFile.readAsString()); implicitData = jsonDecode(await implicitDataFile.readAsString());
} }
catch(_) {
// ignore
} }
catch (e) {
Log.error("Appdata", "Failed to load implicit data", e);
Log.info("Appdata", "Resetting implicit data");
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
implicitDataFile.deleteIgnoreError();
} }
} }
} }
@@ -178,7 +189,7 @@ class Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing, 'customImageProcessing': defaultCustomImageProcessing,
'sni': true, 'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
'comicSourceListUrl': defaultComicSourceUrl, 'comicSourceListUrl': _defaultSourceListUrl,
'preloadImageCount': 4, 'preloadImageCount': 4,
'followUpdatesFolder': null, 'followUpdatesFolder': null,
'initialPage': '0', 'initialPage': '0',
@@ -186,6 +197,8 @@ class Settings with ChangeNotifier {
'showPageNumberInReader': true, 'showPageNumberInReader': true,
'showSingleImageOnFirstPage': false, 'showSingleImageOnFirstPage': false,
'enableDoubleTapToZoom': true, 'enableDoubleTapToZoom': true,
'reverseChapterOrder': false,
'showSystemStatusBar': false,
}; };
operator [](String key) { operator [](String key) {
@@ -194,8 +207,10 @@ class Settings with ChangeNotifier {
operator []=(String key, dynamic value) { operator []=(String key, dynamic value) {
_data[key] = value; _data[key] = value;
if (key != "dataVersion") {
notifyListeners(); notifyListeners();
} }
}
@override @override
String toString() { String toString() {
@@ -221,4 +236,4 @@ function processImage(image, cid, eid, page, sourceKey) {
} }
'''; ''';
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json"; const _defaultSourceListUrl = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json";

View File

@@ -116,6 +116,26 @@ class Comic {
toString() => "$sourceKey@$id"; toString() => "$sourceKey@$id";
} }
class ComicID {
final ComicType type;
final String id;
const ComicID(this.type, this.id);
@override
bool operator ==(Object other) {
if (other is! ComicID) return false;
return other.type == type && other.id == id;
}
@override
int get hashCode => type.hashCode ^ id.hashCode;
@override
String toString() => "$type@$id";
}
class ComicDetails with HistoryMixin { class ComicDetails with HistoryMixin {
@override @override
final String title; final String title;

View File

@@ -653,6 +653,102 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void batchMoveFavorites(
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
_modifiedAfterLastCache = true;
if (!existsFolder(sourceFolder)) {
throw Exception("Source folder does not exist");
}
if (!existsFolder(targetFolder)) {
throw Exception("Target folder does not exist");
}
_db.execute("BEGIN TRANSACTION");
var displayOrder = maxValue(targetFolder) + 1;
try {
for (var item in items) {
_db.execute("""
insert or ignore into "$targetFolder" (id, name, author, type, tags, cover_path, time, display_order)
select id, name, author, type, tags, cover_path, time, ?
from "$sourceFolder"
where id == ? and type == ?;
""", [displayOrder, item.id, item.type.value]);
_db.execute("""
delete from "$sourceFolder"
where id == ? and type == ?;
""", [item.id, item.type.value]);
displayOrder++;
}
notifyListeners();
} catch (e) {
Log.error("Batch Move Favorites", e.toString());
_db.execute("ROLLBACK");
return;
}
_db.execute("COMMIT");
// Update counts
if (counts[targetFolder] == null) {
counts[targetFolder] = count(targetFolder);
} else {
counts[targetFolder] = counts[targetFolder]! + items.length;
}
if (counts[sourceFolder] != null) {
counts[sourceFolder] = counts[sourceFolder]! - items.length;
} else {
counts[sourceFolder] = count(sourceFolder);
}
notifyListeners();
}
void batchCopyFavorites(
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
_modifiedAfterLastCache = true;
if (!existsFolder(sourceFolder)) {
throw Exception("Source folder does not exist");
}
if (!existsFolder(targetFolder)) {
throw Exception("Target folder does not exist");
}
_db.execute("BEGIN TRANSACTION");
var displayOrder = maxValue(targetFolder) + 1;
try {
for (var item in items) {
_db.execute("""
insert or ignore into "$targetFolder" (id, name, author, type, tags, cover_path, time, display_order)
select id, name, author, type, tags, cover_path, time, ?
from "$sourceFolder"
where id == ? and type == ?;
""", [displayOrder, item.id, item.type.value]);
displayOrder++;
}
notifyListeners();
} catch (e) {
Log.error("Batch Copy Favorites", e.toString());
_db.execute("ROLLBACK");
return;
}
_db.execute("COMMIT");
// Update counts
if (counts[targetFolder] == null) {
counts[targetFolder] = count(targetFolder);
} else {
counts[targetFolder] = counts[targetFolder]! + items.length;
}
notifyListeners();
}
/// delete a folder /// delete a folder
void deleteFolder(String name) { void deleteFolder(String name) {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
@@ -667,11 +763,6 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void deleteComic(String folder, FavoriteItem comic) {
_modifiedAfterLastCache = true;
deleteComicWithId(folder, comic.id, comic.type);
}
void deleteComicWithId(String folder, String id, ComicType type) { void deleteComicWithId(String folder, String id, ComicType type) {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
LocalFavoriteImageProvider.delete(id, type.value); LocalFavoriteImageProvider.delete(id, type.value);
@@ -687,6 +778,55 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void batchDeleteComics(String folder, List<FavoriteItem> comics) {
_modifiedAfterLastCache = true;
_db.execute("BEGIN TRANSACTION");
try {
for (var comic in comics) {
LocalFavoriteImageProvider.delete(comic.id, comic.type.value);
_db.execute("""
delete from "$folder"
where id == ? and type == ?;
""", [comic.id, comic.type.value]);
}
if (counts[folder] != null) {
counts[folder] = counts[folder]! - comics.length;
} else {
counts[folder] = count(folder);
}
} catch (e) {
Log.error("Batch Delete Comics", e.toString());
_db.execute("ROLLBACK");
return;
}
_db.execute("COMMIT");
notifyListeners();
}
void batchDeleteComicsInAllFolders(List<ComicID> comics) {
_modifiedAfterLastCache = true;
_db.execute("BEGIN TRANSACTION");
var folderNames = _getFolderNamesWithDB();
try {
for (var comic in comics) {
LocalFavoriteImageProvider.delete(comic.id, comic.type.value);
for (var folder in folderNames) {
_db.execute("""
delete from "$folder"
where id == ? and type == ?;
""", [comic.id, comic.type.value]);
}
}
} catch (e) {
Log.error("Batch Delete Comics in All Folders", e.toString());
_db.execute("ROLLBACK");
return;
}
initCounts();
_db.execute("COMMIT");
notifyListeners();
}
Future<int> removeInvalid() async { Future<int> removeInvalid() async {
int count = 0; int count = 0;
await Future.microtask(() { await Future.microtask(() {
@@ -714,11 +854,26 @@ class LocalFavoritesManager with ChangeNotifier {
if (!existsFolder(folder)) { if (!existsFolder(folder)) {
throw Exception("Failed to reorder: folder not found"); throw Exception("Failed to reorder: folder not found");
} }
deleteFolder(folder); _db.execute("BEGIN TRANSACTION");
createFolder(folder); try {
for (int i = 0; i < newFolder.length; i++) { for (int i = 0; i < newFolder.length; i++) {
addComic(folder, newFolder[i], i); _db.execute("""
update "$folder"
set display_order = ?
where id == ? and type == ?;
""", [
i,
newFolder[i].id,
newFolder[i].type.value
]);
} }
}
catch (e) {
Log.error("Reorder", e.toString());
_db.execute("ROLLBACK");
return;
}
_db.execute("COMMIT");
notifyListeners(); notifyListeners();
} }
@@ -743,6 +898,8 @@ class LocalFavoritesManager with ChangeNotifier {
set folder_name = ? set folder_name = ?
where folder_name == ?; where folder_name == ?;
""", [after, before]); """, [after, before]);
counts[after] = counts[before] ?? 0;
counts.remove(before);
notifyListeners(); notifyListeners();
} }

View File

@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; import 'package:venera/foundation/image_provider/image_favorites_provider.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
@@ -132,6 +133,11 @@ class History implements Comic {
@override @override
String get description { String get description {
var res = ""; var res = "";
if (group != null){
res += "${"Group @group".tlParams({
"group": group!,
})} - ";
}
if (ep >= 1) { if (ep >= 1) {
res += "Chapter @ep".tlParams({ res += "Chapter @ep".tlParams({
"ep": ep, "ep": ep,
@@ -305,6 +311,31 @@ class HistoryManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void clearUnfavoritedHistory() {
_db.execute('BEGIN TRANSACTION;');
try {
final idAndTypes = _db.select("""
select id, type from history;
""");
for (var element in idAndTypes) {
final id = element["id"] as String;
final type = ComicType(element["type"] as int);
if (!LocalFavoritesManager().isExist(id, type)) {
_db.execute("""
delete from history
where id == ? and type == ?;
""", [id, type.value]);
}
}
_db.execute('COMMIT;');
} catch (e) {
_db.execute('ROLLBACK;');
rethrow;
}
updateCache();
notifyListeners();
}
void remove(String id, ComicType type) async { void remove(String id, ComicType type) async {
_db.execute(""" _db.execute("""
delete from history delete from history
@@ -380,4 +411,23 @@ class HistoryManager with ChangeNotifier {
isInitialized = false; isInitialized = false;
_db.dispose(); _db.dispose();
} }
void batchDeleteHistories(List<ComicID> histories) {
if (histories.isEmpty) return;
_db.execute('BEGIN TRANSACTION;');
try {
for (var history in histories) {
_db.execute("""
delete from history
where id == ? and type == ?;
""", [history.id, history.type.value]);
}
_db.execute('COMMIT;');
} catch (e) {
_db.execute('ROLLBACK;');
rethrow;
}
updateCache();
notifyListeners();
}
} }

View File

@@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:flutter_saf/flutter_saf.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -107,15 +109,42 @@ class LocalComic with HistoryMixin implements Comic {
void read() { void read() {
var history = HistoryManager().find(id, comicType); var history = HistoryManager().find(id, comicType);
int? firstDownloadedChapter;
int? firstDownloadedChapterGroup;
if (downloadedChapters.isNotEmpty && chapters != null) {
final chapters = this.chapters!;
if (chapters.isGrouped) {
for (int i=0; i<chapters.groupCount; i++) {
var group = chapters.getGroupByIndex(i);
var keys = group.keys.toList();
for (int j=0; j<keys.length; j++) {
var chapterId = keys[j];
if (downloadedChapters.contains(chapterId)) {
firstDownloadedChapter = j + 1;
firstDownloadedChapterGroup = i + 1;
break;
}
}
}
} else {
var keys = chapters.allChapters.keys;
for (int i = 0; i < keys.length; i++) {
if (downloadedChapters.contains(keys.elementAt(i))) {
firstDownloadedChapter = i + 1;
break;
}
}
}
}
App.rootContext.to( App.rootContext.to(
() => Reader( () => Reader(
type: comicType, type: comicType,
cid: id, cid: id,
name: title, name: title,
chapters: chapters, chapters: chapters,
initialChapter: history?.ep, initialChapter: history?.ep ?? firstDownloadedChapter,
initialPage: history?.page, initialPage: history?.page,
initialChapterGroup: history?.group, initialChapterGroup: history?.group ?? firstDownloadedChapterGroup,
history: history ?? history: history ??
History.fromModel( History.fromModel(
model: this, model: this,
@@ -461,7 +490,7 @@ class LocalManager with ChangeNotifier {
if (comic != null) { if (comic != null) {
return Directory(FilePath.join(path, comic.directory)); return Directory(FilePath.join(path, comic.directory));
} }
const comicDirectoryMaxLength = 128; const comicDirectoryMaxLength = 80;
if (name.length > comicDirectoryMaxLength) { if (name.length > comicDirectoryMaxLength) {
name = name.substring(0, comicDirectoryMaxLength); name = name.substring(0, comicDirectoryMaxLength);
} }
@@ -546,6 +575,99 @@ class LocalManager with ChangeNotifier {
remove(c.id, c.comicType); remove(c.id, c.comicType);
notifyListeners(); notifyListeners();
} }
void deleteComicChapters(LocalComic c, List<String> chapters) {
if (chapters.isEmpty) {
return;
}
var newDownloadedChapters = c.downloadedChapters
.where((e) => !chapters.contains(e))
.toList();
if (newDownloadedChapters.isNotEmpty) {
_db.execute(
'UPDATE comics SET downloadedChapters = ? WHERE id = ? AND comic_type = ?;',
[
jsonEncode(newDownloadedChapters),
c.id,
c.comicType.value,
],
);
} else {
_db.execute(
'DELETE FROM comics WHERE id = ? AND comic_type = ?;',
[c.id, c.comicType.value],
);
}
var shouldRemovedDirs = <Directory>[];
for (var chapter in chapters) {
var dir = Directory(FilePath.join(c.baseDir, chapter));
if (dir.existsSync()) {
shouldRemovedDirs.add(dir);
}
}
if (shouldRemovedDirs.isNotEmpty) {
_deleteDirectories(shouldRemovedDirs);
}
notifyListeners();
}
void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true, bool removeFavoriteAndHistory = true]) {
if (comics.isEmpty) {
return;
}
var shouldRemovedDirs = <Directory>[];
_db.execute('BEGIN TRANSACTION;');
try {
for (var c in comics) {
if (removeFileOnDisk) {
var dir = Directory(FilePath.join(path, c.directory));
if (dir.existsSync()) {
shouldRemovedDirs.add(dir);
}
}
_db.execute(
'DELETE FROM comics WHERE id = ? AND comic_type = ?;',
[c.id, c.comicType.value],
);
}
}
catch(e, s) {
Log.error("LocalManager", "Failed to batch delete comics: $e", s);
_db.execute('ROLLBACK;');
return;
}
_db.execute('COMMIT;');
var comicIDs = comics.map((e) => ComicID(e.comicType, e.id)).toList();
if (removeFavoriteAndHistory) {
LocalFavoritesManager().batchDeleteComicsInAllFolders(comicIDs);
HistoryManager().batchDeleteHistories(comicIDs);
}
notifyListeners();
if (removeFileOnDisk) {
_deleteDirectories(shouldRemovedDirs);
}
}
/// Deletes the directories in a separate isolate to avoid blocking the UI thread.
static void _deleteDirectories(List<Directory> directories) {
Isolate.run(() async {
await SAFTaskWorker().init();
for (var dir in directories) {
try {
if (dir.existsSync()) {
await dir.delete(recursive: true);
}
} catch (e) {
continue;
}
}
});
}
} }
enum LocalSortType { enum LocalSortType {

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:display_mode/display_mode.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_saf/flutter_saf.dart'; import 'package:flutter_saf/flutter_saf.dart';
@@ -15,6 +16,7 @@ import 'package:venera/pages/follow_updates_page.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/app_links.dart';
import 'package:venera/utils/handle_text_share.dart'; import 'package:venera/utils/handle_text_share.dart';
import 'package:venera/utils/opencc.dart';
import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'foundation/appdata.dart'; import 'foundation/appdata.dart';
@@ -43,6 +45,7 @@ Future<void> init() async {
TagsTranslation.readData().wait(), TagsTranslation.readData().wait(),
JsEngine().init().wait(), JsEngine().init().wait(),
ComicSourceManager().init().wait(), ComicSourceManager().init().wait(),
OpenCC.init(),
]; ];
await Future.wait(futures); await Future.wait(futures);
CacheManager().setLimitSize(appdata.settings['cacheSize']); CacheManager().setLimitSize(appdata.settings['cacheSize']);
@@ -50,6 +53,11 @@ Future<void> init() async {
if (App.isAndroid) { if (App.isAndroid) {
handleLinks(); handleLinks();
handleTextShare(); handleTextShare();
try {
await FlutterDisplayMode.setHighRefreshRate();
} catch(e) {
Log.error("Display Mode", "Failed to set high refresh rate: $e");
}
} }
FlutterError.onError = (details) { FlutterError.onError = (details) {
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}"); Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
@@ -95,8 +103,7 @@ Future<void> _checkAppUpdates() async {
appdata.writeImplicitData(); appdata.writeImplicitData();
ComicSourcePage.checkComicSourceUpdate(); ComicSourcePage.checkComicSourceUpdate();
if (appdata.settings['checkUpdateOnStart']) { if (appdata.settings['checkUpdateOnStart']) {
await Future.delayed(const Duration(milliseconds: 300)); await checkUpdateUi(false, true);
await checkUpdateUi(false);
} }
} }

View File

@@ -552,7 +552,7 @@ class _ImageDownloadWrapper {
void start() async { void start() async {
int lastBytes = 0; int lastBytes = 0;
try { try {
await for (var p in ImageDownloader.loadComicImage( await for (var p in ImageDownloader.loadComicImageUnwrapped(
image, task.source.key, task.comicId, chapter)) { image, task.source.key, task.comicId, chapter)) {
if (isCancelled) { if (isCancelled) {
return; return;

View File

@@ -111,6 +111,11 @@ abstract class ImageDownloader {
return stream.stream; return stream.stream;
} }
static Stream<ImageDownloadProgress> loadComicImageUnwrapped(
String imageKey, String? sourceKey, String cid, String eid) {
return _loadComicImage(imageKey, sourceKey, cid, eid);
}
static Stream<ImageDownloadProgress> _loadComicImage( static Stream<ImageDownloadProgress> _loadComicImage(
String imageKey, String? sourceKey, String cid, String eid) async* { String imageKey, String? sourceKey, String cid, String eid) async* {
final cacheKey = "$imageKey@$sourceKey@$cid@$eid"; final cacheKey = "$imageKey@$sourceKey@$cid@$eid";

View File

@@ -27,7 +27,7 @@ class _NormalComicChapters extends StatefulWidget {
class _NormalComicChaptersState extends State<_NormalComicChapters> { class _NormalComicChaptersState extends State<_NormalComicChapters> {
late _ComicPageState state; late _ComicPageState state;
bool reverse = false; late bool reverse;
bool showAll = false; bool showAll = false;
@@ -38,6 +38,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
reverse = appdata.settings["reverseChapterOrder"] ?? false;
history = widget.history; history = widget.history;
} }
@@ -176,7 +177,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late _ComicPageState state; late _ComicPageState state;
bool reverse = false; late bool reverse;
bool showAll = false; bool showAll = false;
@@ -191,6 +192,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
reverse = appdata.settings["reverseChapterOrder"] ?? false;
history = widget.history; history = widget.history;
if (history?.group != null) { if (history?.group != null) {
index = history!.group! - 1; index = history!.group! - 1;

View File

@@ -410,20 +410,26 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
String text; String text;
if (haveChapter) { if (haveChapter) {
var epName = "E$ep"; var epName = "E$ep";
String? groupName;
try { try {
epName = group == null if (group == null){
? comic.chapters!.titles.elementAt( epName = comic.chapters!.titles.elementAt(
math.min(ep - 1, comic.chapters!.length - 1), math.min(ep - 1, comic.chapters!.length - 1),
) );
: comic.chapters! } else {
groupName = comic.chapters!.groups.elementAt(group - 1);
epName = comic.chapters!
.getGroupByIndex(group - 1) .getGroupByIndex(group - 1)
.values .values
.elementAt(ep - 1); .elementAt(ep - 1);
} }
}
catch(e) { catch(e) {
// ignore // ignore
} }
text = "${"Last Reading".tl}: $epName P$page"; text = groupName == null
? "${"Last Reading".tl}: $epName P$page"
: "${"Last Reading".tl}: $groupName $epName P$page";
} else { } else {
text = "${"Last Reading".tl}: P$page"; text = "${"Last Reading".tl}: P$page";
} }

View File

@@ -51,9 +51,7 @@ class ComicSourcePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(body: const _Body());
body: const _Body(),
);
} }
} }
@@ -87,10 +85,7 @@ class _BodyState extends State<_Body> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SmoothCustomScrollView( return SmoothCustomScrollView(
slivers: [ slivers: [
SliverAppbar( SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow),
title: Text('Comic Source'.tl),
style: AppbarStyle.shadow,
),
buildCard(context), buildCard(context),
for (var source in ComicSource.all()) for (var source in ComicSource.all())
_SliverComicSource( _SliverComicSource(
@@ -109,9 +104,7 @@ class _BodyState extends State<_Body> {
showConfirmDialog( showConfirmDialog(
context: App.rootContext, context: App.rootContext,
title: "Delete".tl, title: "Delete".tl,
content: "Delete comic source '@n' ?".tlParams({ content: "Delete comic source '@n' ?".tlParams({"n": source.name}),
"n": source.name,
}),
btnColor: context.colorScheme.error, btnColor: context.colorScheme.error,
onConfirm: () { onConfirm: () {
var file = File(source.filePath); var file = File(source.filePath);
@@ -134,13 +127,15 @@ class _BodyState extends State<_Body> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("cancel")), child: const Text("cancel"),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await ComicSourceManager().reload(); await ComicSourceManager().reload();
App.forceRebuild(); App.forceRebuild();
}, },
child: const Text("continue")), child: const Text("continue"),
),
], ],
), ),
); );
@@ -157,8 +152,10 @@ class _BodyState extends State<_Body> {
); );
} }
static Future<void> update(ComicSource source, static Future<void> update(
[bool showLoading = true]) async { ComicSource source, [
bool showLoading = true,
]) async {
if (!source.url.isURL) { if (!source.url.isURL) {
App.rootContext.showMessage(message: "Invalid url config"); App.rootContext.showMessage(message: "Invalid url config");
return; return;
@@ -174,8 +171,10 @@ class _BodyState extends State<_Body> {
); );
} }
try { try {
var res = await AppDio().get<String>(source.url, var res = await AppDio().get<String>(
options: Options(responseType: ResponseType.plain)); source.url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return; if (cancel) return;
controller?.close(); controller?.close();
await ComicSourceParser().parse(res.data!, source.filePath); await ComicSourceParser().parse(res.data!, source.filePath);
@@ -192,14 +191,6 @@ class _BodyState extends State<_Body> {
} }
Widget buildCard(BuildContext context) { Widget buildCard(BuildContext context) {
Widget buildButton(
{required Widget child, required VoidCallback onPressed}) {
return Button.normal(
onPressed: onPressed,
child: child,
).fixHeight(32);
}
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
@@ -218,16 +209,21 @@ class _BodyState extends State<_Body> {
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
suffix: IconButton( suffix: IconButton(
onPressed: () => handleAddSource(url), onPressed: () => handleAddSource(url),
icon: const Icon(Icons.check))), icon: const Icon(Icons.check),
),
),
onChanged: (value) { onChanged: (value) {
url = value; url = value;
}, },
onSubmitted: handleAddSource, onSubmitted: handleAddSource,
).paddingHorizontal(16).paddingBottom(8), ).paddingHorizontal(16).paddingBottom(8),
ListTile( Wrap(
title: Text("Comic Source list".tl), spacing: 8,
trailing: buildButton( runSpacing: 8,
child: Text("View".tl), children: [
FilledButton.tonalIcon(
icon: Icon(Icons.article_outlined),
label: Text("Comic Source list".tl),
onPressed: () { onPressed: () {
showPopUpWidget( showPopUpWidget(
App.rootContext, App.rootContext,
@@ -235,25 +231,19 @@ class _BodyState extends State<_Body> {
); );
}, },
), ),
), FilledButton.tonalIcon(
ListTile( icon: Icon(Icons.file_open_outlined),
title: Text("Use a config file".tl), label: Text("Use a config file".tl),
trailing: buildButton(
onPressed: _selectFile, onPressed: _selectFile,
child: Text("Select".tl),
), ),
), FilledButton.tonalIcon(
ListTile( icon: Icon(Icons.help_outline),
title: Text("Help".tl), label: Text("Help".tl),
trailing: buildButton(
onPressed: help, onPressed: help,
child: Text("Open".tl),
),
),
ListTile(
title: Text("Check updates".tl),
trailing: _CheckUpdatesButton(),
), ),
_CheckUpdatesButton(),
],
).paddingHorizontal(12).paddingVertical(8),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
@@ -277,7 +267,8 @@ class _BodyState extends State<_Body> {
void help() { void help() {
launchUrlString( launchUrlString(
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); "https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
} }
Future<void> handleAddSource(String url) async { Future<void> handleAddSource(String url) async {
@@ -288,11 +279,16 @@ class _BodyState extends State<_Body> {
splits.removeWhere((element) => element == ""); splits.removeWhere((element) => element == "");
var fileName = splits.last; var fileName = splits.last;
bool cancel = false; bool cancel = false;
var controller = showLoadingDialog(App.rootContext, var controller = showLoadingDialog(
onCancel: () => cancel = true, barrierDismissible: false); App.rootContext,
onCancel: () => cancel = true,
barrierDismissible: false,
);
try { try {
var res = await AppDio() var res = await AppDio().get<String>(
.get<String>(url, options: Options(responseType: ResponseType.plain)); url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return; if (cancel) return;
controller.close(); controller.close();
await addSource(res.data!, fileName); await addSource(res.data!, fileName);
@@ -332,6 +328,12 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = null; json = null;
}); });
} }
if (controller.text.isEmpty) {
setState(() {
json = [];
});
return;
}
var dio = AppDio(); var dio = AppDio();
try { try {
var res = await dio.get<String>(controller.text); var res = await dio.get<String>(controller.text);
@@ -343,8 +345,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = jsonDecode(res.data!); json = jsonDecode(res.data!);
}); });
} }
} } catch (e) {
catch(e) {
context.showMessage(message: "Network error".tl); context.showMessage(message: "Network error".tl);
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -372,10 +373,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopUpWidgetScaffold( return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody());
title: "Comic Source".tl,
body: buildBody(),
);
} }
Widget buildBody() { Widget buildBody() {
@@ -399,32 +397,36 @@ class _ComicSourceListState extends State<_ComicSourceList> {
children: [ children: [
ListTile( ListTile(
leading: Icon(Icons.source_outlined), leading: Icon(Icons.source_outlined),
title: Text("Source URL".tl), title: Text("Repo URL".tl),
), ),
TextField( TextField(
controller: controller, controller: controller,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "URL", hintText: "URL",
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
contentPadding: contentPadding: const EdgeInsets.symmetric(horizontal: 12),
const EdgeInsets.symmetric(horizontal: 12),
), ),
onChanged: (value) { onChanged: (value) {
changed = true; changed = true;
}, },
).paddingHorizontal(16).paddingBottom(8), ).paddingHorizontal(16).paddingBottom(8),
Text("The URL should point to a 'index.json' file".tl).paddingLeft(16), Text(
Text("Do not report any issues related to sources to App repo.".tl).paddingLeft(16), "The URL should point to a 'index.json' file".tl,
).paddingLeft(16),
Text(
"Do not report any issues related to sources to App repo.".tl,
).paddingLeft(16),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: () { onPressed: () {
controller.text = defaultComicSourceUrl; launchUrlString(
changed = true; "https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
}, },
child: Text("Reset".tl), child: Text("Help".tl),
), ),
FilledButton.tonal( FilledButton.tonal(
onPressed: load, onPressed: load,
@@ -440,7 +442,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
} }
if (index == 1 && json == null) { if (index == 1 && json == null) {
return Center(child: CircularProgressIndicator()); return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(24).fixHeight(24),
);
} }
index--; index--;
@@ -551,8 +557,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
!networkFavorites.contains(source.favoriteData!.key)) { !networkFavorites.contains(source.favoriteData!.key)) {
networkFavorites.add(source.favoriteData!.key); networkFavorites.add(source.favoriteData!.key);
} }
if (source.searchPageData != null && if (source.searchPageData != null && !searchPages.contains(source.key)) {
!searchPages.contains(source.key)) {
searchPages.add(source.key); searchPages.add(source.key);
} }
@@ -594,15 +599,10 @@ class __EditFilePageState extends State<_EditFilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: Appbar( appBar: Appbar(title: Text("Edit".tl)),
title: Text("Edit".tl),
),
body: Column( body: Column(
children: [ children: [
Container( Container(height: 0.6, color: context.colorScheme.outlineVariant),
height: 0.6,
color: context.colorScheme.outlineVariant,
),
Expanded( Expanded(
child: CodeEditor( child: CodeEditor(
initialValue: current, initialValue: current,
@@ -643,9 +643,11 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
} }
void showUpdateDialog() async { void showUpdateDialog() async {
var text = ComicSourceManager().availableUpdates.entries.map((e) { var text = ComicSourceManager().availableUpdates.entries
.map((e) {
return "${ComicSource.find(e.key)!.name}: ${e.value}"; return "${ComicSource.find(e.key)!.name}: ${e.value}";
}).join("\n"); })
.join("\n");
bool doUpdate = false; bool doUpdate = false;
await showDialog( await showDialog(
context: App.rootContext, context: App.rootContext,
@@ -690,11 +692,15 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Button.normal( return FilledButton.tonalIcon(
icon: isLoading ? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
) : Icon(Icons.update),
label: Text("Check updates".tl),
onPressed: check, onPressed: check,
isLoading: isLoading, );
child: Text("Check".tl),
).fixHeight(32);
} }
} }
@@ -783,10 +789,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
child: ListTile( child: ListTile(
title: Row( title: Row(
children: [ children: [
Text( Text(source.name, style: ts.s18),
source.name,
style: ts.s18,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -819,7 +822,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
), ),
), ),
).paddingLeft(4) ).paddingLeft(4),
], ],
), ),
trailing: Row( trailing: Row(
@@ -864,15 +867,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Column( child: Column(children: buildSourceSettings().toList()),
children: buildSourceSettings().toList(),
),
),
SliverToBoxAdapter(
child: Column(
children: _buildAccount().toList(),
),
), ),
SliverToBoxAdapter(child: Column(children: _buildAccount().toList())),
], ],
); );
} }
@@ -898,8 +895,10 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
} }
} }
} else { } else {
current = item.value['options'] current =
.firstWhere((e) => e['value'] == current)['text'] ?? item.value['options'].firstWhere(
(e) => e['value'] == current,
)['text'] ??
current; current;
} }
yield ListTile( yield ListTile(
@@ -907,8 +906,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: Select( trailing: Select(
current: (current as String).ts(source.key), current: (current as String).ts(source.key),
values: (item.value['options'] as List) values: (item.value['options'] as List)
.map<String>((e) => .map<String>(
((e['text'] ?? e['value']) as String).ts(source.key)) (e) => ((e['text'] ?? e['value']) as String).ts(source.key),
)
.toList(), .toList(),
onTap: (i) { onTap: (i) {
source.data['settings'][key] = source.data['settings'][key] =
@@ -936,8 +936,11 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
source.data['settings'][key] ?? item.value['default'] ?? ''; source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile( yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)), title: Text((item.value['title'] as String).ts(source.key)),
subtitle: subtitle: Text(
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis), current,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () { onPressed: () {
@@ -978,10 +981,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () async { onTap: () async {
await context.to( await context.to(
() => _LoginPage( () => _LoginPage(config: source.account!, source: source),
config: source.account!,
source: source,
),
); );
source.saveData(); source.saveData();
setState(() {}); setState(() {});
@@ -1027,9 +1027,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: loading trailing: loading
? const SizedBox.square( ? const SizedBox.square(
dimension: 24, dimension: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(strokeWidth: 2),
strokeWidth: 2,
),
) )
: const Icon(Icons.refresh), : const Icon(Icons.refresh),
); );
@@ -1070,9 +1068,7 @@ class _LoginPageState extends State<_LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const Appbar( appBar: const Appbar(title: Text('')),
title: Text(''),
),
body: Center( body: Center(
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -1200,8 +1196,9 @@ class _LoginPageState extends State<_LoginPage> {
setState(() { setState(() {
loading = true; loading = true;
}); });
var cookies = var cookies = widget.config.cookieFields!
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); .map((e) => _cookies[e] ?? '')
.toList();
widget.config.validateCookies!(cookies).then((value) { widget.config.validateCookies!(cookies).then((value) {
if (value) { if (value) {
widget.source.data['account'] = 'ok'; widget.source.data['account'] = 'ok';

View File

@@ -20,6 +20,7 @@ import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/opencc.dart';
import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -66,6 +67,11 @@ class _FavoritesPageState extends State<FavoritesPage> {
folder = data['name']; folder = data['name'];
isNetwork = data['isNetwork'] ?? false; isNetwork = data['isNetwork'] ?? false;
} }
if (folder != null
&& !isNetwork
&& !LocalFavoritesManager().existsFolder(folder!)) {
folder = null;
}
super.initState(); super.initState();
} }

View File

@@ -52,7 +52,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
} else { } else {
searchResults = []; searchResults = [];
for (var comic in comics) { for (var comic in comics) {
if (matchKeyword(keyword, comic)) { if (matchKeyword(keyword, comic) ||
matchKeywordT(keyword, comic) ||
matchKeywordS(keyword, comic)) {
searchResults.add(comic); searchResults.add(comic);
} }
} }
@@ -130,6 +132,24 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
return true; return true;
} }
// Convert keyword to traditional Chinese to match comics
bool matchKeywordT(String keyword, FavoriteItem comic) {
if (!OpenCC.hasChineseSimplified(keyword)) {
return false;
}
keyword = OpenCC.simplifiedToTraditional(keyword);
return matchKeyword(keyword, comic);
}
// Convert keyword to simplified Chinese to match comics
bool matchKeywordS(String keyword, FavoriteItem comic) {
if (!OpenCC.hasChineseTraditional(keyword)) {
return false;
}
keyword = OpenCC.traditionalToSimplified(keyword);
return matchKeyword(keyword, comic);
}
@override @override
void initState() { void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
@@ -155,16 +175,33 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
void selectAll() { void selectAll() {
setState(() { setState(() {
if (searchMode) {
selectedComics = searchResults.asMap().map((k, v) => MapEntry(v, true));
} else {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
}
}); });
} }
void invertSelection() { void invertSelection() {
setState(() { setState(() {
comics.asMap().forEach((k, v) { if (searchMode) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); for (var c in searchResults) {
}); if (selectedComics.containsKey(c)) {
selectedComics.removeWhere((k, v) => !v); selectedComics.remove(c);
} else {
selectedComics[c] = true;
}
}
} else {
for (var c in comics) {
if (selectedComics.containsKey(c)) {
selectedComics.remove(c);
} else {
selectedComics[c] = true;
}
}
}
}); });
} }
@@ -416,10 +453,12 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
"Selected @c comics".tlParams({"c": selectedComics.length})), "Selected @c comics".tlParams({"c": selectedComics.length})),
actions: [ actions: [
MenuButton(entries: [ MenuButton(entries: [
if (!isAllFolder)
MenuEntry( MenuEntry(
icon: Icons.drive_file_move, icon: Icons.drive_file_move,
text: "Move to folder".tl, text: "Move to folder".tl,
onClick: () => favoriteOption('move')), onClick: () => favoriteOption('move')),
if (!isAllFolder)
MenuEntry( MenuEntry(
icon: Icons.copy, icon: Icons.copy,
text: "Copy to folder".tl, text: "Copy to folder".tl,
@@ -756,32 +795,26 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
return; return;
} }
if (option == 'move') { if (option == 'move') {
for (var c in selectedComics.keys) { var comics = selectedComics.keys
for (var s in selectedLocalFolders) { .map((e) => e as FavoriteItem)
LocalFavoritesManager().moveFavorite( .toList();
for (var f in selectedLocalFolders) {
LocalFavoritesManager().batchMoveFavorites(
favPage.folder as String, favPage.folder as String,
s, f,
c.id, comics,
(c as FavoriteItem).type);
}
}
} else {
for (var c in selectedComics.keys) {
for (var s in selectedLocalFolders) {
LocalFavoritesManager().addComic(
s,
FavoriteItem(
id: c.id,
name: c.title,
coverPath: c.cover,
author: c.subtitle ?? '',
type: ComicType((c.sourceKey == 'local'
? 0
: c.sourceKey.hashCode)),
tags: c.tags ?? [],
),
); );
} }
} else {
var comics = selectedComics.keys
.map((e) => e as FavoriteItem)
.toList();
for (var f in selectedLocalFolders) {
LocalFavoritesManager().batchCopyFavorites(
favPage.folder as String,
f,
comics,
);
} }
} }
App.rootContext.pop(); App.rootContext.pop();
@@ -817,13 +850,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
} }
void _deleteComicWithId() { void _deleteComicWithId() {
for (var c in selectedComics.keys) { var toBeDeleted = selectedComics.keys.map((e) => e as FavoriteItem).toList();
LocalFavoritesManager().deleteComicWithId( LocalFavoritesManager().batchDeleteComics(widget.folder, toBeDeleted);
widget.folder,
c.id,
(c as FavoriteItem).type,
);
}
_cancel(); _cancel();
} }
} }
@@ -864,7 +892,10 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
@override @override
void dispose() { void dispose() {
if (changed) { if (changed) {
// Delay to ensure navigation is completed
Future.delayed(const Duration(milliseconds: 200), () {
LocalFavoritesManager().reorder(comics, widget.name); LocalFavoritesManager().reorder(comics, widget.name);
});
} }
super.dispose(); super.dispose();
} }
@@ -899,7 +930,9 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
appBar: Appbar( appBar: Appbar(
title: Text("Reorder".tl), title: Text("Reorder".tl),
actions: [ actions: [
IconButton( Tooltip(
message: "Information".tl,
child: IconButton(
icon: const Icon(Icons.info_outline), icon: const Icon(Icons.info_outline),
onPressed: () { onPressed: () {
showInfoDialog( showInfoDialog(
@@ -909,17 +942,19 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
); );
}, },
), ),
IconButton( ),
Tooltip(
message: "Reverse".tl,
child: IconButton(
icon: const Icon(Icons.swap_vert), icon: const Icon(Icons.swap_vert),
onPressed: () { onPressed: () {
setState(() { setState(() {
comics = comics.reversed.toList(); comics = comics.reversed.toList();
changed = true; changed = true;
showToast(
message: "Reversed successfully".tl, context: context);
}); });
}, },
), ),
)
], ],
), ),
body: ReorderableBuilder<FavoriteItem>( body: ReorderableBuilder<FavoriteItem>(

View File

@@ -42,6 +42,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
folders = LocalFavoritesManager().folderNames; folders = LocalFavoritesManager().folderNames;
findNetworkFolders(); findNetworkFolders();
appdata.settings.addListener(updateFolders); appdata.settings.addListener(updateFolders);
LocalFavoritesManager().addListener(updateFolders);
super.initState(); super.initState();
} }
@@ -49,6 +50,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
void dispose() { void dispose() {
super.dispose(); super.dispose();
appdata.settings.removeListener(updateFolders); appdata.settings.removeListener(updateFolders);
LocalFavoritesManager().removeListener(updateFolders);
} }
@override @override

View File

@@ -140,6 +140,14 @@ class _HistoryPageState extends State<HistoryPage> {
title: 'Clear History'.tl, title: 'Clear History'.tl,
content: Text('Are you sure you want to clear your history?'.tl), content: Text('Are you sure you want to clear your history?'.tl),
actions: [ actions: [
Button.outlined(
onPressed: () {
HistoryManager().clearUnfavoritedHistory();
context.pop();
},
child: Text('Clear Unfavorited'.tl),
),
const SizedBox(width: 4),
Button.filled( Button.filled(
color: context.colorScheme.error, color: context.colorScheme.error,
onPressed: () { onPressed: () {

View File

@@ -361,10 +361,22 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
context: App.rootContext, context: App.rootContext,
builder: (context) { builder: (context) {
bool removeComicFile = true; bool removeComicFile = true;
bool removeFavoriteAndHistory = true;
return StatefulBuilder(builder: (context, state) { return StatefulBuilder(builder: (context, state) {
return ContentDialog( return ContentDialog(
title: "Delete".tl, title: "Delete".tl,
content: CheckboxListTile( content: Column(
children: [
CheckboxListTile(
title: Text("Remove local favorite and history".tl),
value: removeFavoriteAndHistory,
onChanged: (v) {
state(() {
removeFavoriteAndHistory = !removeFavoriteAndHistory;
});
},
),
CheckboxListTile(
title: Text("Also remove files on disk".tl), title: Text("Also remove files on disk".tl),
value: removeComicFile, value: removeComicFile,
onChanged: (v) { onChanged: (v) {
@@ -372,17 +384,26 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
removeComicFile = !removeComicFile; removeComicFile = !removeComicFile;
}); });
}, },
)
],
), ),
actions: [ actions: [
if (comics.length == 1 && comics.first.hasChapters)
TextButton(
child: Text("Delete Chapters".tl),
onPressed: () {
context.pop();
showDeleteChaptersPopWindow(context, comics.first);
},
),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
context.pop(); context.pop();
for (var comic in comics) { LocalManager().batchDeleteComics(
LocalManager().deleteComic( comics,
comic,
removeComicFile, removeComicFile,
removeFavoriteAndHistory,
); );
}
isDeleted = true; isDeleted = true;
}, },
child: Text("Confirm".tl), child: Text("Confirm".tl),
@@ -497,3 +518,59 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
typedef ExportComicFunc = Future<File> Function( typedef ExportComicFunc = Future<File> Function(
LocalComic comic, String outFilePath); LocalComic comic, String outFilePath);
void showDeleteChaptersPopWindow(BuildContext context, LocalComic comic) {
var chapters = <String>[];
showPopUpWidget(
context,
PopUpWidgetScaffold(
title: "Delete Chapters".tl,
body: StatefulBuilder(builder: (context, setState) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: comic.downloadedChapters.length,
itemBuilder: (context, index) {
var id = comic.downloadedChapters[index];
var chapter = comic.chapters![id] ?? "Unknown Chapter";
return CheckboxListTile(
title: Text(chapter),
value: chapters.contains(id),
onChanged: (v) {
setState(() {
if (v == true) {
chapters.add(id);
} else {
chapters.remove(id);
}
});
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () {
Future.delayed(const Duration(milliseconds: 200), () {
LocalManager().deleteComicChapters(comic, chapters);
});
App.rootContext.pop();
},
child: Text("Submit".tl),
)
],
),
)
],
);
}),
),
);
}

View File

@@ -40,6 +40,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
reader.images = images; reader.images = images;
reader.isLoading = false; reader.isLoading = false;
inProgress = false; inProgress = false;
Future.microtask(() {
reader.updateHistory();
});
}); });
} catch (e) { } catch (e) {
setState(() { setState(() {
@@ -65,6 +68,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
reader.images = res.data; reader.images = res.data;
reader.isLoading = false; reader.isLoading = false;
inProgress = false; inProgress = false;
Future.microtask(() {
reader.updateHistory();
});
}); });
} }
} }
@@ -79,7 +85,12 @@ class _ReaderImagesState extends State<_ReaderImages> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} else if (error != null) { } else if (error != null) {
return NetworkError( return GestureDetector(
onTap: () {
context.readerScaffold.openOrClose();
},
child: SizedBox.expand(
child: NetworkError(
message: error!, message: error!,
retry: () { retry: () {
setState(() { setState(() {
@@ -87,6 +98,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
error = null; error = null;
}); });
}, },
),
),
); );
} else { } else {
if (reader.mode.isGallery) { if (reader.mode.isGallery) {
@@ -233,7 +246,7 @@ class _GalleryModeState extends State<_GalleryMode>
photoViewControllers[index] ??= PhotoViewController(); photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage == 1) { if (reader.imagesPerPage == 1 || pageImages.length == 1) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
controller: photoViewControllers[index], controller: photoViewControllers[index],

View File

@@ -164,10 +164,9 @@ class _ReaderState extends State<Reader>
} }
mode = ReaderMode.fromKey(appdata.settings['readerMode']); mode = ReaderMode.fromKey(appdata.settings['readerMode']);
history = widget.history; history = widget.history;
Future.microtask(() { if (!appdata.settings['showSystemStatusBar']) {
updateHistory();
});
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
if (appdata.settings['enableTurnPageByVolumeKey']) { if (appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent(); handleVolumeEvent();
} }
@@ -178,10 +177,18 @@ class _ReaderState extends State<Reader>
super.initState(); super.initState();
} }
bool _isInitialized = false;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
if (!_isInitialized) {
initImagesPerPage(widget.initialPage ?? 1); initImagesPerPage(widget.initialPage ?? 1);
_isInitialized = true;
} else {
// For orientation changed
_checkImagesPerPageChange();
}
initReaderWindow(); initReaderWindow();
} }
@@ -267,7 +274,15 @@ class _ReaderState extends State<Reader>
history!.page = images?.length ?? 1; history!.page = images?.length ?? 1;
} else { } else {
/// Record the first image of the page /// Record the first image of the page
if (!showSingleImageOnFirstPage || imagesPerPage == 1) {
history!.page = (page - 1) * imagesPerPage + 1; history!.page = (page - 1) * imagesPerPage + 1;
} else {
if (page == 1) {
history!.page = 1;
} else {
history!.page = (page - 2) * imagesPerPage + 2;
}
}
} }
history!.maxPage = images?.length ?? 1; history!.maxPage = images?.length ?? 1;
if (widget.chapters?.isGrouped ?? false) { if (widget.chapters?.isGrouped ?? false) {
@@ -338,6 +353,8 @@ class _ReaderState extends State<Reader>
abstract mixin class _ImagePerPageHandler { abstract mixin class _ImagePerPageHandler {
late int _lastImagesPerPage; late int _lastImagesPerPage;
late bool _lastOrientation;
bool get isPortrait; bool get isPortrait;
int get page; int get page;
@@ -348,10 +365,15 @@ abstract mixin class _ImagePerPageHandler {
void initImagesPerPage(int initialPage) { void initImagesPerPage(int initialPage) {
_lastImagesPerPage = imagesPerPage; _lastImagesPerPage = imagesPerPage;
_lastOrientation = isPortrait;
if (imagesPerPage != 1) { if (imagesPerPage != 1) {
if (showSingleImageOnFirstPage) {
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
} else {
page = (initialPage / imagesPerPage).ceil(); page = (initialPage / imagesPerPage).ceil();
} }
} }
}
bool get showSingleImageOnFirstPage => bool get showSingleImageOnFirstPage =>
appdata.settings["showSingleImageOnFirstPage"]; appdata.settings["showSingleImageOnFirstPage"];
@@ -369,19 +391,42 @@ abstract mixin class _ImagePerPageHandler {
/// Check if the number of images per page has changed /// Check if the number of images per page has changed
void _checkImagesPerPageChange() { void _checkImagesPerPageChange() {
int currentImagesPerPage = imagesPerPage; int currentImagesPerPage = imagesPerPage;
if (_lastImagesPerPage != currentImagesPerPage) { bool currentOrientation = isPortrait;
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
_adjustPageForImagesPerPageChange( _adjustPageForImagesPerPageChange(
_lastImagesPerPage, currentImagesPerPage); _lastImagesPerPage, currentImagesPerPage);
_lastImagesPerPage = currentImagesPerPage; _lastImagesPerPage = currentImagesPerPage;
_lastOrientation = currentOrientation;
} }
} }
/// Adjust the page number when the number of images per page changes /// Adjust the page number when the number of images per page changes
void _adjustPageForImagesPerPageChange( void _adjustPageForImagesPerPageChange(
int oldImagesPerPage, int newImagesPerPage) { int oldImagesPerPage, int newImagesPerPage) {
int previousImageIndex = (page - 1) * oldImagesPerPage; int previousImageIndex = 1;
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; if (!showSingleImageOnFirstPage || oldImagesPerPage == 1) {
page = newPage; previousImageIndex = (page - 1) * oldImagesPerPage + 1;
} else {
if (page == 1) {
previousImageIndex = 1;
} else {
previousImageIndex = (page - 2) * oldImagesPerPage + 2;
}
}
int newPage;
if (newImagesPerPage != 1) {
if (showSingleImageOnFirstPage) {
newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1;
} else {
newPage = (previousImageIndex / newImagesPerPage).ceil();
}
} else {
newPage = previousImageIndex;
}
page = newPage>0 ? newPage : 1;
} }
} }

View File

@@ -107,7 +107,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (!_isOpen) { if (!_isOpen) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else { } else {
if (!appdata.settings['showSystemStatusBar']) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
} }
setState(() { setState(() {
_isOpen = !_isOpen; _isOpen = !_isOpen;

View File

@@ -96,10 +96,13 @@ Future<bool> checkUpdate() async {
return false; return false;
} }
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async { Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true, bool delay = false]) async {
try { try {
var value = await checkUpdate(); var value = await checkUpdate();
if (value) { if (value) {
if (delay) {
await Future.delayed(const Duration(seconds: 2));
}
showDialog( showDialog(
context: App.rootContext, context: App.rootContext,
builder: (context) { builder: (context) {

View File

@@ -193,12 +193,46 @@ class LogsPage extends StatefulWidget {
} }
class _LogsPageState extends State<LogsPage> { class _LogsPageState extends State<LogsPage> {
String logLevelToShow = "all";
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var logToShow = logLevelToShow == "all"
? Log.logs
: Log.logs.where((log) => log.level.name == logLevelToShow).toList();
return Scaffold( return Scaffold(
appBar: Appbar( appBar: Appbar(
title: const Text("Logs"), title: Text("Logs".tl),
actions: [ actions: [
IconButton(
onPressed: () => setState(() {
final RelativeRect position = RelativeRect.fromLTRB(
MediaQuery.of(context).size.width,
MediaQuery.of(context).padding.top + kToolbarHeight,
0.0,
0.0,
);
showMenu(context: context, position: position, items: [
PopupMenuItem(
child: Text("all"),
onTap: () => setState(() => logLevelToShow = "all")
),
PopupMenuItem(
child: Text("info"),
onTap: () => setState(() => logLevelToShow = "info")
),
PopupMenuItem(
child: Text("warning"),
onTap: () => setState(() => logLevelToShow = "warning")
),
PopupMenuItem(
child: Text("error"),
onTap: () => setState(() => logLevelToShow = "error")
),
]);
}),
icon: const Icon(Icons.filter_list_outlined)
),
IconButton( IconButton(
onPressed: () => setState(() { onPressed: () => setState(() {
final RelativeRect position = RelativeRect.fromLTRB( final RelativeRect position = RelativeRect.fromLTRB(
@@ -217,7 +251,7 @@ class _LogsPageState extends State<LogsPage> {
onTap: () { onTap: () {
Log.ignoreLimitation = true; Log.ignoreLimitation = true;
context.showMessage( context.showMessage(
message: "Only valid for this run"); message: "Only valid for this run".tl);
}, },
), ),
PopupMenuItem( PopupMenuItem(
@@ -232,9 +266,9 @@ class _LogsPageState extends State<LogsPage> {
body: ListView.builder( body: ListView.builder(
reverse: true, reverse: true,
controller: ScrollController(), controller: ScrollController(),
itemCount: Log.logs.length, itemCount: logToShow.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
index = Log.logs.length - index - 1; index = logToShow.length - index - 1;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: SelectionArea( child: SelectionArea(
@@ -253,7 +287,7 @@ class _LogsPageState extends State<LogsPage> {
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1), padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
child: Text(Log.logs[index].title), child: Text(logToShow[index].title),
), ),
), ),
const SizedBox( const SizedBox(
@@ -265,16 +299,16 @@ class _LogsPageState extends State<LogsPage> {
Theme.of(context).colorScheme.error, Theme.of(context).colorScheme.error,
Theme.of(context).colorScheme.errorContainer, Theme.of(context).colorScheme.errorContainer,
Theme.of(context).colorScheme.primaryContainer Theme.of(context).colorScheme.primaryContainer
][Log.logs[index].level.index], ][logToShow[index].level.index],
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(16)), const BorderRadius.all(Radius.circular(16)),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1), padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
child: Text( child: Text(
Log.logs[index].level.name, logToShow[index].level.name,
style: TextStyle( style: TextStyle(
color: Log.logs[index].level.index == 0 color: logToShow[index].level.index == 0
? Colors.white ? Colors.white
: Colors.black), : Colors.black),
), ),
@@ -282,14 +316,14 @@ class _LogsPageState extends State<LogsPage> {
), ),
], ],
), ),
Text(Log.logs[index].content), Text(logToShow[index].content),
Text(Log.logs[index].time Text(logToShow[index].time
.toString() .toString()
.replaceAll(RegExp(r"\.\w+"), "")), .replaceAll(RegExp(r"\.\w+"), "")),
TextButton( TextButton(
onPressed: () { onPressed: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: Log.logs[index].content)); ClipboardData(text: logToShow[index].content));
}, },
child: Text("Copy".tl), child: Text("Copy".tl),
), ),

View File

@@ -18,8 +18,8 @@ class DebugPageState extends State<DebugPage> {
slivers: [ slivers: [
SliverAppbar(title: Text("Debug".tl)), SliverAppbar(title: Text("Debug".tl)),
_CallbackSetting( _CallbackSetting(
title: "Reload Configs", title: "Reload Configs".tl,
actionTitle: "Reload", actionTitle: "Reload".tl,
callback: () { callback: () {
ComicSourceManager().reload(); ComicSourceManager().reload();
}, },

View File

@@ -52,6 +52,10 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Show history on comic tile".tl, title: "Show history on comic tile".tl,
settingKey: "showHistoryStatusOnTile", settingKey: "showHistoryStatusOnTile",
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Reverse default chapter order".tl,
settingKey: "reverseChapterOrder",
).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Keyword blocking".tl, title: "Keyword blocking".tl,
builder: () => const _ManageBlockingWordView(), builder: () => const _ManageBlockingWordView(),

View File

@@ -163,6 +163,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableClockAndBatteryInfoInReader"); widget.onChanged?.call("enableClockAndBatteryInfoInReader");
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Show system status bar".tl,
settingKey: "showSystemStatusBar",
onChanged: () {
widget.onChanged?.call("showSystemStatusBar");
},
).toSliver(),
SelectSetting( SelectSetting(
title: "Quick collect image".tl, title: "Quick collect image".tl,
settingKey: "quickCollectImage", settingKey: "quickCollectImage",

View File

@@ -22,11 +22,13 @@ class DataSync with ChangeNotifier {
} }
LocalFavoritesManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged);
ComicSourceManager().addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged);
if (App.isDesktop) {
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () {
var controller = WindowFrame.of(App.rootContext); var controller = WindowFrame.of(App.rootContext);
controller.addCloseListener(_handleWindowClose); controller.addCloseListener(_handleWindowClose);
}); });
} }
}
void onDataChanged() { void onDataChanged() {
if (isEnabled) { if (isEnabled) {

67
lib/utils/opencc.dart Normal file
View File

@@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:flutter/services.dart';
abstract class OpenCC {
static late final Map<int, int> _s2t;
static late final Map<int, int> _t2s;
static Future<void> init() async {
var data = await rootBundle.load("assets/opencc.txt");
var txt = utf8.decode(data.buffer.asUint8List());
_s2t = <int, int>{};
_t2s = <int, int>{};
for (var line in txt.split('\n')) {
if (line.isEmpty || line.startsWith('#') || line.length != 2) continue;
var s = line.runes.elementAt(0);
var t = line.runes.elementAt(1);
_s2t[s] = t;
_t2s[t] = s;
}
}
static bool hasChineseSimplified(String text) {
if (text != "监禁") {
return false;
}
for (var rune in text.runes) {
if (_s2t.containsKey(rune)) {
return true;
}
}
return false;
}
static bool hasChineseTraditional(String text) {
for (var rune in text.runes) {
if (_t2s.containsKey(rune)) {
return true;
}
}
return false;
}
static String simplifiedToTraditional(String text) {
var sb = StringBuffer();
for (var rune in text.runes) {
if (_s2t.containsKey(rune)) {
sb.write(String.fromCharCodes([_s2t[rune]!]));
} else {
sb.write(String.fromCharCodes([rune]));
}
}
return sb.toString();
}
static String traditionalToSimplified(String text) {
var sb = StringBuffer();
for (var rune in text.runes) {
if (_t2s.containsKey(rune)) {
sb.write(String.fromCharCodes([_t2s[rune]!]));
} else {
sb.write(String.fromCharCodes([rune]));
}
}
return sb.toString();
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:flutter_saf/flutter_saf.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/utils/image.dart'; import 'package:venera/utils/image.dart';
@@ -74,6 +75,9 @@ Future<Isolate> _runIsolate(
return Isolate.spawn<SendPort>( return Isolate.spawn<SendPort>(
(sendPort) => overrideIO( (sendPort) => overrideIO(
() async { () async {
if (App.isAndroid) {
await SAFTaskWorker().init();
}
var receivePort = ReceivePort(); var receivePort = ReceivePort();
sendPort.send(receivePort.sendPort); sendPort.send(receivePort.sendPort);

View File

@@ -36,7 +36,9 @@ extension TagsTranslation on String{
static String _translateTags(String tag){ static String _translateTags(String tag){
if (tag.contains('|')) { if (tag.contains('|')) {
var splits = tag.split('|'); var splits = tag.split('|');
return enTagsTranslations[splits[0]]??enTagsTranslations[splits[1]]??tag; return enTagsTranslations[splits[0].trim()]
?? enTagsTranslations[splits[1].trim()]
?? tag;
} else if(tag.contains(':')) { } else if(tag.contains(':')) {
var splits = tag.split(':'); var splits = tag.split(':');
if(_haveNamespace(splits[0])) { if(_haveNamespace(splits[0])) {

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.13.0"
battery_plus: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -170,6 +170,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
display_mode:
dependency: "direct main"
description:
name: display_mode
sha256: "8a381f3602a09dc4e96140a0df30808631468d6d0dfff7722f67b1f83757a7cc"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -190,10 +198,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -433,10 +441,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_rust_bridge name: flutter_rust_bridge
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" sha256: b416ff56002789e636244fb4cc449f587656eff995e5a7169457eb0593fcaddb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.0" version: "2.10.0"
flutter_saf: flutter_saf:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -524,10 +532,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -548,10 +556,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.9"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -766,11 +774,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: rhttp path: rhttp
ref: e7dca15ca543b5df49f3ada06016e874b79bce36 ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36 resolved-ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
url: "https://github.com/wgh136/rhttp" url: "https://github.com/wgh136/rhttp"
source: git source: git
version: "0.11.0" version: "0.12.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -1037,10 +1045,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "15.0.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@@ -1107,5 +1115,5 @@ packages:
source: hosted source: hosted
version: "0.0.12" version: "0.0.12"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.29.3" flutter: ">=3.32.6"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.4.3+143 version: 1.4.6+146
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
flutter: 3.29.3 flutter: 3.32.6
dependencies: dependencies:
flutter: flutter:
@@ -61,7 +61,7 @@ dependencies:
rhttp: rhttp:
git: git:
url: https://github.com/wgh136/rhttp url: https://github.com/wgh136/rhttp
ref: e7dca15ca543b5df49f3ada06016e874b79bce36 ref: 1f0ff50336062c5f809c256726dc55cd30b9ce59
path: rhttp path: rhttp
webdav_client: webdav_client:
git: git:
@@ -86,6 +86,7 @@ dependencies:
sdk: flutter sdk: flutter
yaml: ^3.1.3 yaml: ^3.1.3
enough_convert: ^1.6.0 enough_convert: ^1.6.0
display_mode: ^0.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -102,6 +103,7 @@ flutter:
- assets/app_icon.png - assets/app_icon.png
- assets/tags.json - assets/tags.json
- assets/tags_tw.json - assets/tags_tw.json
- assets/opencc.txt
flutter_to_arch: flutter_to_arch:
name: Venera name: Venera

View File

@@ -98,14 +98,20 @@ bool FlutterWindow::OnCreate() {
else else
result->Success(flutter::EncodableValue("No Proxy")); result->Success(flutter::EncodableValue("No Proxy"));
delete(res); delete(res);
return;
} }
#ifdef NDEBUG
else if (call.method_name() == "heartBeat") { else if (call.method_name() == "heartBeat") {
if (monitorThread == nullptr) { if (monitorThread == nullptr) {
monitorThread = new std::thread{ monitorUIThread }; monitorThread = new std::thread{ monitorUIThread };
} }
lastHeartbeat = std::chrono::steady_clock::now(); lastHeartbeat = std::chrono::steady_clock::now();
result->Success(); result->Success();
return;
} }
#endif
result->Success(); // Default response for unhandled method calls
}); });
flutter::EventChannel<> channel2( flutter::EventChannel<> channel2(