mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
59 Commits
v1.4.2-dev
...
9d8ade6fe0
Author | SHA1 | Date | |
---|---|---|---|
9d8ade6fe0 | |||
6245399810 | |||
c074e7f9d1 | |||
f822e198ea | |||
7035f11eb5 | |||
f2f5a4f573 | |||
2acf234f7d | |||
9ed8f351c7 | |||
7c35dc7cf7 | |||
ccb03343f4 | |||
![]() |
b9817ec030 | ||
![]() |
5ebb554e54 | ||
23ee79fe9d | |||
![]() |
85baac657a | ||
![]() |
cceca6b96f | ||
![]() |
b5b0dc85e3 | ||
![]() |
50044c4372 | ||
![]() |
5fd7f1b880 | ||
![]() |
058fde3f5a | ||
![]() |
a2d46123dd | ||
![]() |
01acc4f9de | ||
![]() |
856aae0769 | ||
![]() |
8eda8adcc8 | ||
defd4b8624 | |||
b2a164e066 | |||
a46ceebf19 | |||
cc08445f13 | |||
93f7f72d07 | |||
20f7ab4866 | |||
54363919cd | |||
182a821fc5 | |||
8868c6edb3 | |||
![]() |
fffbb4ed23 | ||
![]() |
b057be0311 | ||
![]() |
fc5fed1707 | ||
![]() |
8525f5318f | ||
![]() |
d58cafc4a0 | ||
23afafd1d6 | |||
![]() |
3b6e0adbbb | ||
20a57c7a36 | |||
665f50ed2a | |||
55733ef505 | |||
0c46214619 | |||
749a1a47fb | |||
76e9ef87d4 | |||
dcd6466547 | |||
ed70fdba93 | |||
ded0068ea6 | |||
![]() |
7dc6be622a | ||
![]() |
88f093f7e5 | ||
8f357b3e6c | |||
9ee82975e8 | |||
![]() |
9f048685e4 | ||
![]() |
bc1f5e11b5 | ||
1f2147ef72 | |||
fba365fd93 | |||
a5e3fbaee5 | |||
190e645a12 | |||
![]() |
8a83ff5367 |
@@ -39,6 +39,32 @@ let Convert = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param str {string}
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
encodeGbk: (str) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "gbk",
|
||||||
|
value: str,
|
||||||
|
isEncode: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value {ArrayBuffer}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
decodeGbk: (value) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "gbk",
|
||||||
|
value: value,
|
||||||
|
isEncode: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} value
|
* @param {ArrayBuffer} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
@@ -176,7 +202,7 @@ let Convert = {
|
|||||||
decryptAesCbc: (value, key, iv) => {
|
decryptAesCbc: (value, key, iv) => {
|
||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "convert",
|
method: "convert",
|
||||||
type: "aes-ecb",
|
type: "aes-cbc",
|
||||||
value: value,
|
value: value,
|
||||||
key: key,
|
key: key,
|
||||||
iv: iv,
|
iv: iv,
|
||||||
@@ -1296,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
3982
assets/opencc.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,9 +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": "双击缩放",
|
||||||
|
"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": "首頁",
|
||||||
@@ -627,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": "刪除所有無效的本機收藏",
|
||||||
@@ -781,8 +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": "雙擊縮放",
|
||||||
|
"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": "匯出日誌"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -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]);
|
||||||
|
@@ -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;
|
||||||
@@ -400,16 +390,15 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
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,
|
||||||
|
@@ -290,28 +290,30 @@ class ContentDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var content = Column(
|
var content = SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
title != null
|
children: [
|
||||||
? Appbar(
|
title != null
|
||||||
leading: IconButton(
|
? Appbar(
|
||||||
icon: const Icon(Icons.close),
|
leading: IconButton(
|
||||||
onPressed: dismissible ? context.pop : null,
|
icon: const Icon(Icons.close),
|
||||||
),
|
onPressed: dismissible ? context.pop : null,
|
||||||
title: Text(title!),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
title: Text(title!),
|
||||||
)
|
backgroundColor: Colors.transparent,
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
this.content,
|
: const SizedBox.shrink(),
|
||||||
const SizedBox(height: 16),
|
this.content,
|
||||||
Row(
|
const SizedBox(height: 16),
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
Row(
|
||||||
children: actions,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
).paddingRight(12),
|
children: actions,
|
||||||
const SizedBox(height: 16),
|
).paddingRight(12),
|
||||||
],
|
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,14 +372,23 @@ Future<void> showInputDialog({
|
|||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: title,
|
title: title,
|
||||||
content: TextField(
|
content: Column(
|
||||||
controller: controller,
|
children: [
|
||||||
decoration: InputDecoration(
|
if (image != null)
|
||||||
hintText: hintText,
|
SizedBox(
|
||||||
border: const OutlineInputBorder(),
|
height: 108,
|
||||||
errorText: error,
|
child: Image.network(image, fit: BoxFit.none),
|
||||||
),
|
).paddingBottom(8),
|
||||||
).paddingHorizontal(12),
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: error,
|
||||||
|
),
|
||||||
|
).paddingHorizontal(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.4.2";
|
final version = "1.4.6";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -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,21 +111,31 @@ class Appdata with Init {
|
|||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var json = jsonDecode(await file.readAsString());
|
try {
|
||||||
for (var key in (json['settings'] as Map<String, dynamic>).keys) {
|
var json = jsonDecode(await file.readAsString());
|
||||||
if (json['settings'][key] != null) {
|
for (var key in (json['settings'] as Map<String, dynamic>).keys) {
|
||||||
settings[key] = json['settings'][key];
|
if (json['settings'][key] != null) {
|
||||||
|
settings[key] = json['settings'][key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
searchHistory = List.from(json['searchHistory']);
|
||||||
}
|
}
|
||||||
searchHistory = List.from(json['searchHistory']);
|
catch(e) {
|
||||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
Log.error("Appdata", "Failed to load appdata", e);
|
||||||
if (await implicitDataFile.exists()) {
|
Log.info("Appdata", "Resetting appdata");
|
||||||
try {
|
file.deleteIgnoreError();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||||
|
if (await implicitDataFile.exists()) {
|
||||||
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,7 +207,9 @@ class Settings with ChangeNotifier {
|
|||||||
|
|
||||||
operator []=(String key, dynamic value) {
|
operator []=(String key, dynamic value) {
|
||||||
_data[key] = value;
|
_data[key] = value;
|
||||||
notifyListeners();
|
if (key != "dataVersion") {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -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";
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
@@ -21,6 +23,51 @@ class CacheManager {
|
|||||||
|
|
||||||
int _limitSize = 2 * 1024 * 1024 * 1024;
|
int _limitSize = 2 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
static Future<int> _scanDir(Pointer<void> dbP, String dir) async {
|
||||||
|
var res = await Isolate.run(() async {
|
||||||
|
int totalSize = 0;
|
||||||
|
List<String> unmanagedFiles = [];
|
||||||
|
var db = sqlite3.fromPointer(dbP);
|
||||||
|
await for (var file in Directory(dir).list(recursive: true)) {
|
||||||
|
if (file is File) {
|
||||||
|
var size = await file.length();
|
||||||
|
var segments = file.uri.pathSegments;
|
||||||
|
var name = segments.last;
|
||||||
|
var dir = segments.elementAtOrNull(segments.length - 2) ?? "*";
|
||||||
|
var res = db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE dir = ? AND name = ?
|
||||||
|
''', [dir, name]);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
unmanagedFiles.add(file.path);
|
||||||
|
} else {
|
||||||
|
totalSize += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'totalSize': totalSize,
|
||||||
|
'unmanagedFiles': unmanagedFiles,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// delete unmanaged files
|
||||||
|
// Only modify the database in the main isolate to avoid deadlock
|
||||||
|
for (var filePath in res['unmanagedFiles'] as List<String>) {
|
||||||
|
var file = File(filePath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
var segments = file.uri.pathSegments;
|
||||||
|
var name = segments.last;
|
||||||
|
var dir = segments.elementAtOrNull(segments.length - 2) ?? "*";
|
||||||
|
CacheManager()._db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE dir = ? AND name = ?
|
||||||
|
''', [dir, name]);
|
||||||
|
}
|
||||||
|
return res['totalSize'] as int;
|
||||||
|
}
|
||||||
|
|
||||||
CacheManager._create() {
|
CacheManager._create() {
|
||||||
Directory(cachePath).createSync(recursive: true);
|
Directory(cachePath).createSync(recursive: true);
|
||||||
_db = sqlite3.open('${App.dataPath}/cache.db');
|
_db = sqlite3.open('${App.dataPath}/cache.db');
|
||||||
@@ -33,7 +80,7 @@ class CacheManager {
|
|||||||
type TEXT
|
type TEXT
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
compute((path) => Directory(path).size, cachePath).then((value) {
|
_scanDir(_db.handle, cachePath).then((value) {
|
||||||
_currentSize = value;
|
_currentSize = value;
|
||||||
checkCache();
|
checkCache();
|
||||||
});
|
});
|
||||||
@@ -50,6 +97,7 @@ class CacheManager {
|
|||||||
/// Write cache to disk.
|
/// Write cache to disk.
|
||||||
Future<void> writeCache(String key, List<int> data,
|
Future<void> writeCache(String key, List<int> data,
|
||||||
[int duration = 7 * 24 * 60 * 60 * 1000]) async {
|
[int duration = 7 * 24 * 60 * 60 * 1000]) async {
|
||||||
|
await delete(key);
|
||||||
this.dir++;
|
this.dir++;
|
||||||
this.dir %= 100;
|
this.dir %= 100;
|
||||||
var dir = this.dir;
|
var dir = this.dir;
|
||||||
@@ -146,10 +194,12 @@ class CacheManager {
|
|||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_db.execute('''
|
if (res.isNotEmpty) {
|
||||||
|
_db.execute('''
|
||||||
DELETE FROM cache
|
DELETE FROM cache
|
||||||
WHERE expires < ?
|
WHERE expires < ?
|
||||||
''', [DateTime.now().millisecondsSinceEpoch]);
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
|
}
|
||||||
|
|
||||||
while (_currentSize != null && _currentSize! > _limitSize) {
|
while (_currentSize != null && _currentSize! > _limitSize) {
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
@@ -157,6 +207,13 @@ class CacheManager {
|
|||||||
ORDER BY expires ASC
|
ORDER BY expires ASC
|
||||||
limit 10
|
limit 10
|
||||||
''');
|
''');
|
||||||
|
if (res.isEmpty) {
|
||||||
|
// There are many files unmanaged by the cache manager.
|
||||||
|
// Clear all cache.
|
||||||
|
await Directory(cachePath).delete(recursive: true);
|
||||||
|
Directory(cachePath).createSync(recursive: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
for (var row in res) {
|
for (var row in res) {
|
||||||
var key = row[0] as String;
|
var key = row[0] as String;
|
||||||
var dir = row[1] as String;
|
var dir = row[1] as String;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:enough_convert/enough_convert.dart';
|
||||||
import 'package:flutter/foundation.dart' show protected;
|
import 'package:flutter/foundation.dart' show protected;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:html/parser.dart' as html;
|
import 'package:html/parser.dart' as html;
|
||||||
@@ -372,6 +373,11 @@ mixin class _JSEngineApi {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "utf8":
|
case "utf8":
|
||||||
return isEncode ? utf8.encode(value) : utf8.decode(value);
|
return isEncode ? utf8.encode(value) : utf8.decode(value);
|
||||||
|
case "gbk":
|
||||||
|
final codec = const GbkCodec();
|
||||||
|
return isEncode
|
||||||
|
? Uint8List.fromList(codec.encode(value))
|
||||||
|
: codec.decode(value);
|
||||||
case "base64":
|
case "base64":
|
||||||
return isEncode ? base64Encode(value) : base64Decode(value);
|
return isEncode ? base64Encode(value) : base64Decode(value);
|
||||||
case "md5":
|
case "md5":
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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";
|
||||||
@@ -234,19 +239,30 @@ class _StreamWrapper<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _listen() async {
|
void _listen() async {
|
||||||
await for (var data in _stream) {
|
try {
|
||||||
if (isClosed) {
|
await for (var data in _stream) {
|
||||||
break;
|
if (isClosed) {
|
||||||
}
|
break;
|
||||||
for (var controller in controllers) {
|
}
|
||||||
if (!controller.isClosed) {
|
for (var controller in controllers) {
|
||||||
controller.add(data);
|
if (!controller.isClosed) {
|
||||||
|
controller.add(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var controller in controllers) {
|
catch (e) {
|
||||||
if (!controller.isClosed) {
|
for (var controller in controllers) {
|
||||||
controller.close();
|
if (!controller.isClosed) {
|
||||||
|
controller.addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
for (var controller in controllers) {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controllers.clear();
|
controllers.clear();
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 {
|
||||||
.getGroupByIndex(group - 1)
|
groupName = comic.chapters!.groups.elementAt(group - 1);
|
||||||
.values
|
epName = comic.chapters!
|
||||||
.elementAt(ep - 1);
|
.getGroupByIndex(group - 1)
|
||||||
|
.values
|
||||||
|
.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";
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
@@ -133,14 +126,16 @@ class _BodyState extends State<_Body> {
|
|||||||
title: const Text("Reload Configs"),
|
title: const Text("Reload Configs"),
|
||||||
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,
|
||||||
@@ -213,47 +204,46 @@ class _BodyState extends State<_Body> {
|
|||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "URL",
|
hintText: "URL",
|
||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
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: [
|
||||||
onPressed: () {
|
FilledButton.tonalIcon(
|
||||||
showPopUpWidget(
|
icon: Icon(Icons.article_outlined),
|
||||||
App.rootContext,
|
label: Text("Comic Source list".tl),
|
||||||
_ComicSourceList(handleAddSource),
|
onPressed: () {
|
||||||
);
|
showPopUpWidget(
|
||||||
},
|
App.rootContext,
|
||||||
),
|
_ComicSourceList(handleAddSource),
|
||||||
),
|
);
|
||||||
ListTile(
|
},
|
||||||
title: Text("Use a config file".tl),
|
),
|
||||||
trailing: buildButton(
|
FilledButton.tonalIcon(
|
||||||
onPressed: _selectFile,
|
icon: Icon(Icons.file_open_outlined),
|
||||||
child: Text("Select".tl),
|
label: Text("Use a config file".tl),
|
||||||
),
|
onPressed: _selectFile,
|
||||||
),
|
),
|
||||||
ListTile(
|
FilledButton.tonalIcon(
|
||||||
title: Text("Help".tl),
|
icon: Icon(Icons.help_outline),
|
||||||
trailing: buildButton(
|
label: Text("Help".tl),
|
||||||
onPressed: help,
|
onPressed: help,
|
||||||
child: Text("Open".tl),
|
),
|
||||||
),
|
_CheckUpdatesButton(),
|
||||||
),
|
],
|
||||||
ListTile(
|
).paddingHorizontal(12).paddingVertical(8),
|
||||||
title: Text("Check updates".tl),
|
|
||||||
trailing: _CheckUpdatesButton(),
|
|
||||||
),
|
|
||||||
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--;
|
||||||
@@ -449,28 +455,28 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
|||||||
var action = currentKey.contains(key)
|
var action = currentKey.contains(key)
|
||||||
? const Icon(Icons.check, size: 20).paddingRight(8)
|
? const Icon(Icons.check, size: 20).paddingRight(8)
|
||||||
: Button.filled(
|
: Button.filled(
|
||||||
child: Text("Add".tl),
|
child: Text("Add".tl),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var fileName = json![index]["fileName"];
|
var fileName = json![index]["fileName"];
|
||||||
var url = json![index]["url"];
|
var url = json![index]["url"];
|
||||||
if (url == null || !(url.toString()).isURL) {
|
if (url == null || !(url.toString()).isURL) {
|
||||||
var listUrl =
|
var listUrl =
|
||||||
appdata.settings['comicSourceListUrl'] as String;
|
appdata.settings['comicSourceListUrl'] as String;
|
||||||
if (listUrl
|
if (listUrl
|
||||||
.replaceFirst("https://", "")
|
.replaceFirst("https://", "")
|
||||||
.replaceFirst("http://", "")
|
.replaceFirst("http://", "")
|
||||||
.contains("/")) {
|
.contains("/")) {
|
||||||
url =
|
url =
|
||||||
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
||||||
fileName;
|
fileName;
|
||||||
} else {
|
} else {
|
||||||
url = '$listUrl/$fileName';
|
url = '$listUrl/$fileName';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await widget.onAdd(url);
|
await widget.onAdd(url);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
).fixHeight(32);
|
).fixHeight(32);
|
||||||
|
|
||||||
var description = json![index]["version"];
|
var description = json![index]["version"];
|
||||||
if (json![index]["description"] != null) {
|
if (json![index]["description"] != null) {
|
||||||
@@ -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
|
||||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
.map((e) {
|
||||||
}).join("\n");
|
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||||
|
})
|
||||||
|
.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';
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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(() {
|
||||||
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
|
if (searchMode) {
|
||||||
|
selectedComics = searchResults.asMap().map((k, v) => MapEntry(v, true));
|
||||||
|
} else {
|
||||||
|
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();
|
||||||
favPage.folder as String,
|
for (var f in selectedLocalFolders) {
|
||||||
s,
|
LocalFavoritesManager().batchMoveFavorites(
|
||||||
c.id,
|
favPage.folder as String,
|
||||||
(c as FavoriteItem).type);
|
f,
|
||||||
}
|
comics,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (var c in selectedComics.keys) {
|
var comics = selectedComics.keys
|
||||||
for (var s in selectedLocalFolders) {
|
.map((e) => e as FavoriteItem)
|
||||||
LocalFavoritesManager().addComic(
|
.toList();
|
||||||
s,
|
for (var f in selectedLocalFolders) {
|
||||||
FavoriteItem(
|
LocalFavoritesManager().batchCopyFavorites(
|
||||||
id: c.id,
|
favPage.folder as String,
|
||||||
name: c.title,
|
f,
|
||||||
coverPath: c.cover,
|
comics,
|
||||||
author: c.subtitle ?? '',
|
);
|
||||||
type: ComicType((c.sourceKey == 'local'
|
|
||||||
? 0
|
|
||||||
: c.sourceKey.hashCode)),
|
|
||||||
tags: c.tags ?? [],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
LocalFavoritesManager().reorder(comics, widget.name);
|
// Delay to ensure navigation is completed
|
||||||
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
LocalFavoritesManager().reorder(comics, widget.name);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -899,27 +930,31 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
appBar: Appbar(
|
appBar: Appbar(
|
||||||
title: Text("Reorder".tl),
|
title: Text("Reorder".tl),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Tooltip(
|
||||||
icon: const Icon(Icons.info_outline),
|
message: "Information".tl,
|
||||||
onPressed: () {
|
child: IconButton(
|
||||||
showInfoDialog(
|
icon: const Icon(Icons.info_outline),
|
||||||
context: context,
|
onPressed: () {
|
||||||
title: "Reorder".tl,
|
showInfoDialog(
|
||||||
content: "Long press and drag to reorder.".tl,
|
context: context,
|
||||||
);
|
title: "Reorder".tl,
|
||||||
},
|
content: "Long press and drag to reorder.".tl,
|
||||||
),
|
);
|
||||||
IconButton(
|
},
|
||||||
icon: const Icon(Icons.swap_vert),
|
),
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
comics = comics.reversed.toList();
|
|
||||||
changed = true;
|
|
||||||
showToast(
|
|
||||||
message: "Reversed successfully".tl, context: context);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Reverse".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.swap_vert),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
comics = comics.reversed.toList();
|
||||||
|
changed = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ReorderableBuilder<FavoriteItem>(
|
body: ReorderableBuilder<FavoriteItem>(
|
||||||
|
@@ -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
|
||||||
|
@@ -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: () {
|
||||||
|
@@ -361,28 +361,49 @@ 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(
|
||||||
title: Text("Also remove files on disk".tl),
|
children: [
|
||||||
value: removeComicFile,
|
CheckboxListTile(
|
||||||
onChanged: (v) {
|
title: Text("Remove local favorite and history".tl),
|
||||||
state(() {
|
value: removeFavoriteAndHistory,
|
||||||
removeComicFile = !removeComicFile;
|
onChanged: (v) {
|
||||||
});
|
state(() {
|
||||||
},
|
removeFavoriteAndHistory = !removeFavoriteAndHistory;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text("Also remove files on disk".tl),
|
||||||
|
value: removeComicFile,
|
||||||
|
onChanged: (v) {
|
||||||
|
state(() {
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -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,14 +85,21 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
} else if (error != null) {
|
} else if (error != null) {
|
||||||
return NetworkError(
|
return GestureDetector(
|
||||||
message: error!,
|
onTap: () {
|
||||||
retry: () {
|
context.readerScaffold.openOrClose();
|
||||||
setState(() {
|
|
||||||
reader.isLoading = true;
|
|
||||||
error = null;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: NetworkError(
|
||||||
|
message: error!,
|
||||||
|
retry: () {
|
||||||
|
setState(() {
|
||||||
|
reader.isLoading = true;
|
||||||
|
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],
|
||||||
|
@@ -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();
|
||||||
initImagesPerPage(widget.initialPage ?? 1);
|
if (!_isInitialized) {
|
||||||
|
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
|
||||||
history!.page = (page - 1) * imagesPerPage + 1;
|
if (!showSingleImageOnFirstPage || 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,8 +365,13 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
|
|
||||||
void initImagesPerPage(int initialPage) {
|
void initImagesPerPage(int initialPage) {
|
||||||
_lastImagesPerPage = imagesPerPage;
|
_lastImagesPerPage = imagesPerPage;
|
||||||
|
_lastOrientation = isPortrait;
|
||||||
if (imagesPerPage != 1) {
|
if (imagesPerPage != 1) {
|
||||||
page = (initialPage / imagesPerPage).ceil();
|
if (showSingleImageOnFirstPage) {
|
||||||
|
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
|
||||||
|
} else {
|
||||||
|
page = (initialPage / imagesPerPage).ceil();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -107,7 +107,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
if (!_isOpen) {
|
if (!_isOpen) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
} else {
|
} else {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
if (!appdata.settings['showSystemStatusBar']) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isOpen = !_isOpen;
|
_isOpen = !_isOpen;
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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),
|
||||||
),
|
),
|
||||||
|
@@ -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();
|
||||||
},
|
},
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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",
|
||||||
|
@@ -22,10 +22,12 @@ class DataSync with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
LocalFavoritesManager().addListener(onDataChanged);
|
LocalFavoritesManager().addListener(onDataChanged);
|
||||||
ComicSourceManager().addListener(onDataChanged);
|
ComicSourceManager().addListener(onDataChanged);
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
if (App.isDesktop) {
|
||||||
var controller = WindowFrame.of(App.rootContext);
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
controller.addCloseListener(_handleWindowClose);
|
var controller = WindowFrame.of(App.rootContext);
|
||||||
});
|
controller.addCloseListener(_handleWindowClose);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDataChanged() {
|
void onDataChanged() {
|
||||||
|
67
lib/utils/opencc.dart
Normal file
67
lib/utils/opencc.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -35,8 +35,10 @@ extension TagsTranslation on String{
|
|||||||
/// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace.
|
/// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace.
|
||||||
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])) {
|
||||||
|
50
pubspec.lock
50
pubspec.lock
@@ -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:
|
||||||
@@ -178,14 +186,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
enough_convert:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: enough_convert
|
||||||
|
sha256: c67d85ca21aaa0648f155907362430701db41f7ec8e6501a58ad9cd9d8569d01
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
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:
|
||||||
@@ -425,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:
|
||||||
@@ -516,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:
|
||||||
@@ -540,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:
|
||||||
@@ -758,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:
|
||||||
@@ -1029,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:
|
||||||
@@ -1099,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"
|
||||||
|
15
pubspec.yaml
15
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.4.2+142
|
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:
|
||||||
@@ -58,10 +58,10 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||||
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:
|
||||||
@@ -85,6 +85,8 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
yaml: ^3.1.3
|
yaml: ^3.1.3
|
||||||
|
enough_convert: ^1.6.0
|
||||||
|
display_mode: ^0.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -101,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
|
||||||
|
@@ -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(
|
||||||
|
Reference in New Issue
Block a user