mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
28 Commits
v1.4.4
...
ccb03343f4
Author | SHA1 | Date | |
---|---|---|---|
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 |
@@ -234,8 +234,10 @@
|
||||
"Please add some sources": "请添加一些源",
|
||||
"Please check your settings": "请检查您的设置",
|
||||
"No Category Pages": "没有分类页面",
|
||||
"Group @group": "第 @group 组",
|
||||
"Chapter @ep": "第 @ep 章",
|
||||
"Page @page": "第 @page 页",
|
||||
"Remove local favorite and history": "删除本地收藏和历史记录",
|
||||
"Also remove files on disk": "同时删除磁盘上的文件",
|
||||
"Copy to app local path": "将漫画复制到本地存储目录中",
|
||||
"Delete all unavailable local favorite items": "删除所有无效的本地收藏",
|
||||
@@ -388,13 +390,21 @@
|
||||
"Suggestions": "建议",
|
||||
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
|
||||
"Show single image on first page": "在首页显示单张图片",
|
||||
"Show system status bar": "显示系统状态栏",
|
||||
"Click to select an image": "点击选择一张图片",
|
||||
"Source URL": "源地址",
|
||||
"Repo URL": "仓库地址",
|
||||
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
|
||||
"Double tap to zoom": "双击缩放",
|
||||
"Clear Unfavorited": "清除未收藏",
|
||||
"Reverse": "反转",
|
||||
"Delete Chapters": "删除章节"
|
||||
"Delete Chapters": "删除章节",
|
||||
"Path copied to clipboard": "路径已复制到剪贴板",
|
||||
"Reverse default chapter order": "反转默认章节顺序",
|
||||
"Reload Configs": "重新加载配置文件",
|
||||
"Reload": "重载",
|
||||
"Disable Length Limitation": "禁用长度限制",
|
||||
"Only valid for this run": "仅对本次运行有效",
|
||||
"Logs": "日志"
|
||||
},
|
||||
"zh_TW": {
|
||||
"Home": "首頁",
|
||||
@@ -631,8 +641,10 @@
|
||||
"Please add some sources": "請添加一些源",
|
||||
"Please check your settings": "請檢查您的設定",
|
||||
"No Category Pages": "沒有分類頁面",
|
||||
"Group @group": "第 @group 組",
|
||||
"Chapter @ep": "第 @ep 章",
|
||||
"Page @page": "第 @page 頁",
|
||||
"Remove local favorite and history": "刪除本機收藏和歷史記錄",
|
||||
"Also remove files on disk": "同時刪除磁碟上的文件",
|
||||
"Copy to app local path": "將漫畫複製到本機儲存目錄中",
|
||||
"Delete all unavailable local favorite items": "刪除所有無效的本機收藏",
|
||||
@@ -785,12 +797,20 @@
|
||||
"Suggestions": "建議",
|
||||
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
|
||||
"Show single image on first page": "在首頁顯示單張圖片",
|
||||
"Show system status bar": "顯示系統狀態欄",
|
||||
"Click to select an image": "點擊選擇一張圖片",
|
||||
"Source URL": "源地址",
|
||||
"Repo URL": "倉庫地址",
|
||||
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
|
||||
"Double tap to zoom": "雙擊縮放",
|
||||
"Clear Unfavorited": "清除未收藏",
|
||||
"Reverse": "反轉",
|
||||
"Delete Chapters": "刪除章節"
|
||||
"Delete Chapters": "刪除章節",
|
||||
"Path copied to clipboard": "路徑已複製到剪貼簿",
|
||||
"Reverse default chapter order": "反轉預設章節順序",
|
||||
"Reload Configs": "重新載入設定檔",
|
||||
"Reload": "重載",
|
||||
"Disable Length Limitation": "禁用長度限制",
|
||||
"Only valid for this run": "僅對本次運行有效",
|
||||
"Logs": "日誌"
|
||||
}
|
||||
}
|
@@ -9,13 +9,45 @@ 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.
|
||||
|
||||
## Preparation
|
||||
## Comic Source List
|
||||
|
||||
Venera can display a list of comic sources in the app.
|
||||
|
||||
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.
|
||||
|
||||
Currently, you can use the following repo url:
|
||||
```
|
||||
https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/index.json
|
||||
```
|
||||
The repo is maintained by the Venera team, and you can submit a pull request to add your comic source.
|
||||
|
||||
## Create a Comic Source
|
||||
|
||||
### Preparation
|
||||
|
||||
- Install Venera. Using flutter to run the project is recommended since it's easier to debug.
|
||||
- An editor that supports javascript.
|
||||
- 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.
|
||||
|
||||
@@ -23,7 +55,7 @@ Here is a brief introduction to the template:
|
||||
|
||||
> Note: Javascript api document is [here](js_api.md).
|
||||
|
||||
### Write basic information
|
||||
#### Write basic information
|
||||
|
||||
```javascript
|
||||
class NewComicSource extends ComicSource {
|
||||
@@ -49,7 +81,7 @@ In this part, you need to do the following:
|
||||
- Change the class name to your source name.
|
||||
- Fill in the name, key, version, minAppVersion, and url fields.
|
||||
|
||||
### init function
|
||||
#### init function
|
||||
|
||||
```javascript
|
||||
/**
|
||||
@@ -64,7 +96,7 @@ The function will be called when the source is initialized. You can do some init
|
||||
|
||||
Remove this function if not used.
|
||||
|
||||
### Account
|
||||
#### Account
|
||||
|
||||
```javascript
|
||||
// [Optional] account related
|
||||
@@ -140,7 +172,7 @@ In this part, you can implement login, logout, and register functions.
|
||||
|
||||
Remove this part if not used.
|
||||
|
||||
### Explore page
|
||||
#### Explore page
|
||||
|
||||
```javascript
|
||||
// explore page list
|
||||
@@ -185,7 +217,7 @@ There are three types of explore pages:
|
||||
- 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.
|
||||
|
||||
### Category Page
|
||||
#### Category Page
|
||||
|
||||
```javascript
|
||||
// categories
|
||||
@@ -227,7 +259,7 @@ Category page is a static page that contains multiple parts, each part contains
|
||||
|
||||
A comic source can only have one category page.
|
||||
|
||||
### Category Comics Page
|
||||
#### Category Comics Page
|
||||
|
||||
```javascript
|
||||
/// category comic loading related
|
||||
@@ -280,7 +312,7 @@ When user clicks on a category, the category comics page will be displayed.
|
||||
|
||||
This part is used to load comics of a category.
|
||||
|
||||
### Search
|
||||
#### Search
|
||||
|
||||
```javascript
|
||||
/// search related
|
||||
@@ -339,7 +371,7 @@ This part is 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.
|
||||
|
||||
### Favorites
|
||||
#### Favorites
|
||||
|
||||
```javascript
|
||||
// favorite related
|
||||
@@ -411,7 +443,7 @@ This part is used to manage network favorites of the source.
|
||||
`load` and `loadNext` functions are used to load search results.
|
||||
If `load` function is implemented, `loadNext` function will be ignored.
|
||||
|
||||
### Comic Details
|
||||
#### Comic Details
|
||||
|
||||
```javascript
|
||||
/// single comic related
|
||||
@@ -576,7 +608,7 @@ If `load` function is implemented, `loadNext` function will be ignored.
|
||||
|
||||
This part is used to load comic details.
|
||||
|
||||
### Settings
|
||||
#### Settings
|
||||
|
||||
```javascript
|
||||
/*
|
||||
@@ -635,7 +667,7 @@ This part is used to load comic details.
|
||||
This part is used to provide settings for the source.
|
||||
|
||||
|
||||
### Translations
|
||||
#### Translations
|
||||
|
||||
```javascript
|
||||
// [Optional] translations for the strings in this config
|
||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.4.4";
|
||||
final version = "1.4.5";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
|
@@ -189,7 +189,7 @@ class Settings with ChangeNotifier {
|
||||
'customImageProcessing': defaultCustomImageProcessing,
|
||||
'sni': true,
|
||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||
'comicSourceListUrl': defaultComicSourceUrl,
|
||||
'comicSourceListUrl': '',
|
||||
'preloadImageCount': 4,
|
||||
'followUpdatesFolder': null,
|
||||
'initialPage': '0',
|
||||
@@ -197,6 +197,8 @@ class Settings with ChangeNotifier {
|
||||
'showPageNumberInReader': true,
|
||||
'showSingleImageOnFirstPage': false,
|
||||
'enableDoubleTapToZoom': true,
|
||||
'reverseChapterOrder': false,
|
||||
'showSystemStatusBar': false,
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
@@ -233,5 +235,3 @@ function processImage(image, cid, eid, page, sourceKey) {
|
||||
return futureImage;
|
||||
}
|
||||
''';
|
||||
|
||||
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json";
|
||||
|
@@ -133,6 +133,11 @@ class History implements Comic {
|
||||
@override
|
||||
String get description {
|
||||
var res = "";
|
||||
if (group != null){
|
||||
res += "${"Group @group".tlParams({
|
||||
"group": group!,
|
||||
})} - ";
|
||||
}
|
||||
if (ep >= 1) {
|
||||
res += "Chapter @ep".tlParams({
|
||||
"ep": ep,
|
||||
|
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
||||
import 'package:flutter_saf/flutter_saf.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
@@ -108,15 +109,42 @@ class LocalComic with HistoryMixin implements Comic {
|
||||
|
||||
void read() {
|
||||
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(
|
||||
() => Reader(
|
||||
type: comicType,
|
||||
cid: id,
|
||||
name: title,
|
||||
chapters: chapters,
|
||||
initialChapter: history?.ep,
|
||||
initialChapter: history?.ep ?? firstDownloadedChapter,
|
||||
initialPage: history?.page,
|
||||
initialChapterGroup: history?.group,
|
||||
initialChapterGroup: history?.group ?? firstDownloadedChapterGroup,
|
||||
history: history ??
|
||||
History.fromModel(
|
||||
model: this,
|
||||
@@ -583,7 +611,7 @@ class LocalManager with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true]) {
|
||||
void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true, bool removeFavoriteAndHistory = true]) {
|
||||
if (comics.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -612,8 +640,11 @@ class LocalManager with ChangeNotifier {
|
||||
_db.execute('COMMIT;');
|
||||
|
||||
var comicIDs = comics.map((e) => ComicID(e.comicType, e.id)).toList();
|
||||
|
||||
if (removeFavoriteAndHistory) {
|
||||
LocalFavoritesManager().batchDeleteComicsInAllFolders(comicIDs);
|
||||
HistoryManager().batchDeleteHistories(comicIDs);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
@@ -625,6 +656,7 @@ class LocalManager with ChangeNotifier {
|
||||
/// 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()) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:display_mode/display_mode.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_saf/flutter_saf.dart';
|
||||
@@ -50,6 +51,11 @@ Future<void> init() async {
|
||||
if (App.isAndroid) {
|
||||
handleLinks();
|
||||
handleTextShare();
|
||||
try {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
} catch(e) {
|
||||
Log.error("Display Mode", "Failed to set high refresh rate: $e");
|
||||
}
|
||||
}
|
||||
FlutterError.onError = (details) {
|
||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
@@ -95,8 +101,7 @@ Future<void> _checkAppUpdates() async {
|
||||
appdata.writeImplicitData();
|
||||
ComicSourcePage.checkComicSourceUpdate();
|
||||
if (appdata.settings['checkUpdateOnStart']) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
await checkUpdateUi(false);
|
||||
await checkUpdateUi(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -552,7 +552,7 @@ class _ImageDownloadWrapper {
|
||||
void start() async {
|
||||
int lastBytes = 0;
|
||||
try {
|
||||
await for (var p in ImageDownloader.loadComicImage(
|
||||
await for (var p in ImageDownloader.loadComicImageUnwrapped(
|
||||
image, task.source.key, task.comicId, chapter)) {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
|
@@ -111,6 +111,11 @@ abstract class ImageDownloader {
|
||||
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(
|
||||
String imageKey, String? sourceKey, String cid, String eid) async* {
|
||||
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
||||
|
@@ -27,7 +27,7 @@ class _NormalComicChapters extends StatefulWidget {
|
||||
class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
||||
late _ComicPageState state;
|
||||
|
||||
bool reverse = false;
|
||||
late bool reverse;
|
||||
|
||||
bool showAll = false;
|
||||
|
||||
@@ -38,6 +38,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
reverse = appdata.settings["reverseChapterOrder"] ?? false;
|
||||
history = widget.history;
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late _ComicPageState state;
|
||||
|
||||
bool reverse = false;
|
||||
late bool reverse;
|
||||
|
||||
bool showAll = false;
|
||||
|
||||
@@ -191,6 +192,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
reverse = appdata.settings["reverseChapterOrder"] ?? false;
|
||||
history = widget.history;
|
||||
if (history?.group != null) {
|
||||
index = history!.group! - 1;
|
||||
|
@@ -410,20 +410,26 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
String text;
|
||||
if (haveChapter) {
|
||||
var epName = "E$ep";
|
||||
String? groupName;
|
||||
try {
|
||||
epName = group == null
|
||||
? comic.chapters!.titles.elementAt(
|
||||
if (group == null){
|
||||
epName = comic.chapters!.titles.elementAt(
|
||||
math.min(ep - 1, comic.chapters!.length - 1),
|
||||
)
|
||||
: comic.chapters!
|
||||
);
|
||||
} else {
|
||||
groupName = comic.chapters!.groups.elementAt(group - 1);
|
||||
epName = comic.chapters!
|
||||
.getGroupByIndex(group - 1)
|
||||
.values
|
||||
.elementAt(ep - 1);
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
// 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 {
|
||||
text = "${"Last Reading".tl}: P$page";
|
||||
}
|
||||
|
@@ -51,9 +51,7 @@ class ComicSourcePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: const _Body(),
|
||||
);
|
||||
return Scaffold(body: const _Body());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +85,7 @@ class _BodyState extends State<_Body> {
|
||||
Widget build(BuildContext context) {
|
||||
return SmoothCustomScrollView(
|
||||
slivers: [
|
||||
SliverAppbar(
|
||||
title: Text('Comic Source'.tl),
|
||||
style: AppbarStyle.shadow,
|
||||
),
|
||||
SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow),
|
||||
buildCard(context),
|
||||
for (var source in ComicSource.all())
|
||||
_SliverComicSource(
|
||||
@@ -109,9 +104,7 @@ class _BodyState extends State<_Body> {
|
||||
showConfirmDialog(
|
||||
context: App.rootContext,
|
||||
title: "Delete".tl,
|
||||
content: "Delete comic source '@n' ?".tlParams({
|
||||
"n": source.name,
|
||||
}),
|
||||
content: "Delete comic source '@n' ?".tlParams({"n": source.name}),
|
||||
btnColor: context.colorScheme.error,
|
||||
onConfirm: () {
|
||||
var file = File(source.filePath);
|
||||
@@ -134,13 +127,15 @@ class _BodyState extends State<_Body> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("cancel")),
|
||||
child: const Text("cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ComicSourceManager().reload();
|
||||
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,
|
||||
[bool showLoading = true]) async {
|
||||
static Future<void> update(
|
||||
ComicSource source, [
|
||||
bool showLoading = true,
|
||||
]) async {
|
||||
if (!source.url.isURL) {
|
||||
App.rootContext.showMessage(message: "Invalid url config");
|
||||
return;
|
||||
@@ -174,8 +171,10 @@ class _BodyState extends State<_Body> {
|
||||
);
|
||||
}
|
||||
try {
|
||||
var res = await AppDio().get<String>(source.url,
|
||||
options: Options(responseType: ResponseType.plain));
|
||||
var res = await AppDio().get<String>(
|
||||
source.url,
|
||||
options: Options(responseType: ResponseType.plain),
|
||||
);
|
||||
if (cancel) return;
|
||||
controller?.close();
|
||||
await ComicSourceParser().parse(res.data!, source.filePath);
|
||||
@@ -192,12 +191,11 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
|
||||
Widget buildCard(BuildContext context) {
|
||||
Widget buildButton(
|
||||
{required Widget child, required VoidCallback onPressed}) {
|
||||
return Button.normal(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
).fixHeight(32);
|
||||
Widget buildButton({
|
||||
required Widget child,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return Button.normal(onPressed: onPressed, child: child).fixHeight(32);
|
||||
}
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
@@ -218,7 +216,9 @@ class _BodyState extends State<_Body> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
suffix: IconButton(
|
||||
onPressed: () => handleAddSource(url),
|
||||
icon: const Icon(Icons.check))),
|
||||
icon: const Icon(Icons.check),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
url = value;
|
||||
},
|
||||
@@ -245,10 +245,7 @@ class _BodyState extends State<_Body> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text("Help".tl),
|
||||
trailing: buildButton(
|
||||
onPressed: help,
|
||||
child: Text("Open".tl),
|
||||
),
|
||||
trailing: buildButton(onPressed: help, child: Text("Open".tl)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text("Check updates".tl),
|
||||
@@ -277,7 +274,8 @@ class _BodyState extends State<_Body> {
|
||||
|
||||
void help() {
|
||||
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 {
|
||||
@@ -288,11 +286,16 @@ class _BodyState extends State<_Body> {
|
||||
splits.removeWhere((element) => element == "");
|
||||
var fileName = splits.last;
|
||||
bool cancel = false;
|
||||
var controller = showLoadingDialog(App.rootContext,
|
||||
onCancel: () => cancel = true, barrierDismissible: false);
|
||||
var controller = showLoadingDialog(
|
||||
App.rootContext,
|
||||
onCancel: () => cancel = true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
try {
|
||||
var res = await AppDio()
|
||||
.get<String>(url, options: Options(responseType: ResponseType.plain));
|
||||
var res = await AppDio().get<String>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.plain),
|
||||
);
|
||||
if (cancel) return;
|
||||
controller.close();
|
||||
await addSource(res.data!, fileName);
|
||||
@@ -332,6 +335,12 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
json = null;
|
||||
});
|
||||
}
|
||||
if (controller.text.isEmpty) {
|
||||
setState(() {
|
||||
json = [];
|
||||
});
|
||||
return;
|
||||
}
|
||||
var dio = AppDio();
|
||||
try {
|
||||
var res = await dio.get<String>(controller.text);
|
||||
@@ -343,8 +352,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
json = jsonDecode(res.data!);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
} catch (e) {
|
||||
context.showMessage(message: "Network error".tl);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -372,10 +380,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopUpWidgetScaffold(
|
||||
title: "Comic Source".tl,
|
||||
body: buildBody(),
|
||||
);
|
||||
return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody());
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
@@ -399,32 +404,36 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(Icons.source_outlined),
|
||||
title: Text("Source URL".tl),
|
||||
title: Text("Repo URL".tl),
|
||||
),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: "URL",
|
||||
border: const UnderlineInputBorder(),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
changed = true;
|
||||
},
|
||||
).paddingHorizontal(16).paddingBottom(8),
|
||||
Text("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),
|
||||
Text(
|
||||
"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),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.text = defaultComicSourceUrl;
|
||||
changed = true;
|
||||
launchUrlString(
|
||||
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
|
||||
);
|
||||
},
|
||||
child: Text("Reset".tl),
|
||||
child: Text("Help".tl),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: load,
|
||||
@@ -440,7 +449,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
}
|
||||
|
||||
if (index == 1 && json == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
).fixWidth(24).fixHeight(24),
|
||||
);
|
||||
}
|
||||
|
||||
index--;
|
||||
@@ -551,8 +564,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
|
||||
!networkFavorites.contains(source.favoriteData!.key)) {
|
||||
networkFavorites.add(source.favoriteData!.key);
|
||||
}
|
||||
if (source.searchPageData != null &&
|
||||
!searchPages.contains(source.key)) {
|
||||
if (source.searchPageData != null && !searchPages.contains(source.key)) {
|
||||
searchPages.add(source.key);
|
||||
}
|
||||
|
||||
@@ -594,15 +606,10 @@ class __EditFilePageState extends State<_EditFilePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
title: Text("Edit".tl),
|
||||
),
|
||||
appBar: Appbar(title: Text("Edit".tl)),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 0.6,
|
||||
color: context.colorScheme.outlineVariant,
|
||||
),
|
||||
Container(height: 0.6, color: context.colorScheme.outlineVariant),
|
||||
Expanded(
|
||||
child: CodeEditor(
|
||||
initialValue: current,
|
||||
@@ -643,9 +650,11 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
||||
}
|
||||
|
||||
void showUpdateDialog() async {
|
||||
var text = ComicSourceManager().availableUpdates.entries.map((e) {
|
||||
var text = ComicSourceManager().availableUpdates.entries
|
||||
.map((e) {
|
||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||
}).join("\n");
|
||||
})
|
||||
.join("\n");
|
||||
bool doUpdate = false;
|
||||
await showDialog(
|
||||
context: App.rootContext,
|
||||
@@ -783,10 +792,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
source.name,
|
||||
style: ts.s18,
|
||||
),
|
||||
Text(source.name, style: ts.s18),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -819,7 +825,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
).paddingLeft(4)
|
||||
).paddingLeft(4),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
@@ -864,15 +870,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: buildSourceSettings().toList(),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: _buildAccount().toList(),
|
||||
),
|
||||
child: Column(children: buildSourceSettings().toList()),
|
||||
),
|
||||
SliverToBoxAdapter(child: Column(children: _buildAccount().toList())),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -898,8 +898,10 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current = item.value['options']
|
||||
.firstWhere((e) => e['value'] == current)['text'] ??
|
||||
current =
|
||||
item.value['options'].firstWhere(
|
||||
(e) => e['value'] == current,
|
||||
)['text'] ??
|
||||
current;
|
||||
}
|
||||
yield ListTile(
|
||||
@@ -907,8 +909,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
trailing: Select(
|
||||
current: (current as String).ts(source.key),
|
||||
values: (item.value['options'] as List)
|
||||
.map<String>((e) =>
|
||||
((e['text'] ?? e['value']) as String).ts(source.key))
|
||||
.map<String>(
|
||||
(e) => ((e['text'] ?? e['value']) as String).ts(source.key),
|
||||
)
|
||||
.toList(),
|
||||
onTap: (i) {
|
||||
source.data['settings'][key] =
|
||||
@@ -936,8 +939,11 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
source.data['settings'][key] ?? item.value['default'] ?? '';
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
subtitle:
|
||||
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
current,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
@@ -978,10 +984,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () async {
|
||||
await context.to(
|
||||
() => _LoginPage(
|
||||
config: source.account!,
|
||||
source: source,
|
||||
),
|
||||
() => _LoginPage(config: source.account!, source: source),
|
||||
);
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
@@ -1027,9 +1030,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
trailing: loading
|
||||
? const SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
);
|
||||
@@ -1070,9 +1071,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const Appbar(
|
||||
title: Text(''),
|
||||
),
|
||||
appBar: const Appbar(title: Text('')),
|
||||
body: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -1200,8 +1199,9 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
var cookies =
|
||||
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
|
||||
var cookies = widget.config.cookieFields!
|
||||
.map((e) => _cookies[e] ?? '')
|
||||
.toList();
|
||||
widget.config.validateCookies!(cookies).then((value) {
|
||||
if (value) {
|
||||
widget.source.data['account'] = 'ok';
|
||||
|
@@ -66,6 +66,11 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
||||
folder = data['name'];
|
||||
isNetwork = data['isNetwork'] ?? false;
|
||||
}
|
||||
if (folder != null
|
||||
&& !isNetwork
|
||||
&& !LocalFavoritesManager().existsFolder(folder!)) {
|
||||
folder = null;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@@ -361,10 +361,22 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
context: App.rootContext,
|
||||
builder: (context) {
|
||||
bool removeComicFile = true;
|
||||
bool removeFavoriteAndHistory = true;
|
||||
return StatefulBuilder(builder: (context, state) {
|
||||
return ContentDialog(
|
||||
title: "Delete".tl,
|
||||
content: CheckboxListTile(
|
||||
content: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
title: Text("Remove local favorite and history".tl),
|
||||
value: removeFavoriteAndHistory,
|
||||
onChanged: (v) {
|
||||
state(() {
|
||||
removeFavoriteAndHistory = !removeFavoriteAndHistory;
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text("Also remove files on disk".tl),
|
||||
value: removeComicFile,
|
||||
onChanged: (v) {
|
||||
@@ -372,6 +384,8 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
removeComicFile = !removeComicFile;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (comics.length == 1 && comics.first.hasChapters)
|
||||
@@ -388,6 +402,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
LocalManager().batchDeleteComics(
|
||||
comics,
|
||||
removeComicFile,
|
||||
removeFavoriteAndHistory,
|
||||
);
|
||||
isDeleted = true;
|
||||
},
|
||||
|
@@ -40,6 +40,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
reader.images = images;
|
||||
reader.isLoading = false;
|
||||
inProgress = false;
|
||||
Future.microtask(() {
|
||||
reader.updateHistory();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
@@ -65,6 +68,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
reader.images = res.data;
|
||||
reader.isLoading = false;
|
||||
inProgress = false;
|
||||
Future.microtask(() {
|
||||
reader.updateHistory();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -79,7 +85,12 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (error != null) {
|
||||
return NetworkError(
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.readerScaffold.openOrClose();
|
||||
},
|
||||
child: SizedBox.expand(
|
||||
child: NetworkError(
|
||||
message: error!,
|
||||
retry: () {
|
||||
setState(() {
|
||||
@@ -87,6 +98,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
error = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (reader.mode.isGallery) {
|
||||
@@ -233,7 +246,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage == 1) {
|
||||
if (reader.imagesPerPage == 1 || pageImages.length == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
|
@@ -164,10 +164,9 @@ class _ReaderState extends State<Reader>
|
||||
}
|
||||
mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||
history = widget.history;
|
||||
Future.microtask(() {
|
||||
updateHistory();
|
||||
});
|
||||
if (!appdata.settings['showSystemStatusBar']) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
if (appdata.settings['enableTurnPageByVolumeKey']) {
|
||||
handleVolumeEvent();
|
||||
}
|
||||
@@ -178,10 +177,18 @@ class _ReaderState extends State<Reader>
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_isInitialized) {
|
||||
initImagesPerPage(widget.initialPage ?? 1);
|
||||
_isInitialized = true;
|
||||
} else {
|
||||
// For orientation changed
|
||||
_checkImagesPerPageChange();
|
||||
}
|
||||
initReaderWindow();
|
||||
}
|
||||
|
||||
@@ -267,7 +274,15 @@ class _ReaderState extends State<Reader>
|
||||
history!.page = images?.length ?? 1;
|
||||
} else {
|
||||
/// Record the first image of the page
|
||||
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;
|
||||
if (widget.chapters?.isGrouped ?? false) {
|
||||
@@ -338,6 +353,8 @@ class _ReaderState extends State<Reader>
|
||||
abstract mixin class _ImagePerPageHandler {
|
||||
late int _lastImagesPerPage;
|
||||
|
||||
late bool _lastOrientation;
|
||||
|
||||
bool get isPortrait;
|
||||
|
||||
int get page;
|
||||
@@ -348,10 +365,15 @@ abstract mixin class _ImagePerPageHandler {
|
||||
|
||||
void initImagesPerPage(int initialPage) {
|
||||
_lastImagesPerPage = imagesPerPage;
|
||||
_lastOrientation = isPortrait;
|
||||
if (imagesPerPage != 1) {
|
||||
if (showSingleImageOnFirstPage) {
|
||||
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
|
||||
} else {
|
||||
page = (initialPage / imagesPerPage).ceil();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get showSingleImageOnFirstPage =>
|
||||
appdata.settings["showSingleImageOnFirstPage"];
|
||||
@@ -369,19 +391,42 @@ abstract mixin class _ImagePerPageHandler {
|
||||
/// Check if the number of images per page has changed
|
||||
void _checkImagesPerPageChange() {
|
||||
int currentImagesPerPage = imagesPerPage;
|
||||
if (_lastImagesPerPage != currentImagesPerPage) {
|
||||
bool currentOrientation = isPortrait;
|
||||
|
||||
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
||||
_adjustPageForImagesPerPageChange(
|
||||
_lastImagesPerPage, currentImagesPerPage);
|
||||
_lastImagesPerPage = currentImagesPerPage;
|
||||
_lastOrientation = currentOrientation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust the page number when the number of images per page changes
|
||||
void _adjustPageForImagesPerPageChange(
|
||||
int oldImagesPerPage, int newImagesPerPage) {
|
||||
int previousImageIndex = (page - 1) * oldImagesPerPage;
|
||||
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
|
||||
page = newPage;
|
||||
int previousImageIndex = 1;
|
||||
if (!showSingleImageOnFirstPage || oldImagesPerPage == 1) {
|
||||
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) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
} else {
|
||||
if (!appdata.settings['showSystemStatusBar']) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isOpen = !_isOpen;
|
||||
|
@@ -96,10 +96,13 @@ Future<bool> checkUpdate() async {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
|
||||
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true, bool delay = false]) async {
|
||||
try {
|
||||
var value = await checkUpdate();
|
||||
if (value) {
|
||||
if (delay) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
}
|
||||
showDialog(
|
||||
context: App.rootContext,
|
||||
builder: (context) {
|
||||
|
@@ -193,12 +193,46 @@ class LogsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LogsPageState extends State<LogsPage> {
|
||||
String logLevelToShow = "all";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var logToShow = logLevelToShow == "all"
|
||||
? Log.logs
|
||||
: Log.logs.where((log) => log.level.name == logLevelToShow).toList();
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
title: const Text("Logs"),
|
||||
title: Text("Logs".tl),
|
||||
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(
|
||||
onPressed: () => setState(() {
|
||||
final RelativeRect position = RelativeRect.fromLTRB(
|
||||
@@ -217,7 +251,7 @@ class _LogsPageState extends State<LogsPage> {
|
||||
onTap: () {
|
||||
Log.ignoreLimitation = true;
|
||||
context.showMessage(
|
||||
message: "Only valid for this run");
|
||||
message: "Only valid for this run".tl);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
@@ -232,9 +266,9 @@ class _LogsPageState extends State<LogsPage> {
|
||||
body: ListView.builder(
|
||||
reverse: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: Log.logs.length,
|
||||
itemCount: logToShow.length,
|
||||
itemBuilder: (context, index) {
|
||||
index = Log.logs.length - index - 1;
|
||||
index = logToShow.length - index - 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: SelectionArea(
|
||||
@@ -253,7 +287,7 @@ class _LogsPageState extends State<LogsPage> {
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||
child: Text(Log.logs[index].title),
|
||||
child: Text(logToShow[index].title),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -265,16 +299,16 @@ class _LogsPageState extends State<LogsPage> {
|
||||
Theme.of(context).colorScheme.error,
|
||||
Theme.of(context).colorScheme.errorContainer,
|
||||
Theme.of(context).colorScheme.primaryContainer
|
||||
][Log.logs[index].level.index],
|
||||
][logToShow[index].level.index],
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||
child: Text(
|
||||
Log.logs[index].level.name,
|
||||
logToShow[index].level.name,
|
||||
style: TextStyle(
|
||||
color: Log.logs[index].level.index == 0
|
||||
color: logToShow[index].level.index == 0
|
||||
? Colors.white
|
||||
: Colors.black),
|
||||
),
|
||||
@@ -282,14 +316,14 @@ class _LogsPageState extends State<LogsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(Log.logs[index].content),
|
||||
Text(Log.logs[index].time
|
||||
Text(logToShow[index].content),
|
||||
Text(logToShow[index].time
|
||||
.toString()
|
||||
.replaceAll(RegExp(r"\.\w+"), "")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: Log.logs[index].content));
|
||||
ClipboardData(text: logToShow[index].content));
|
||||
},
|
||||
child: Text("Copy".tl),
|
||||
),
|
||||
|
@@ -18,8 +18,8 @@ class DebugPageState extends State<DebugPage> {
|
||||
slivers: [
|
||||
SliverAppbar(title: Text("Debug".tl)),
|
||||
_CallbackSetting(
|
||||
title: "Reload Configs",
|
||||
actionTitle: "Reload",
|
||||
title: "Reload Configs".tl,
|
||||
actionTitle: "Reload".tl,
|
||||
callback: () {
|
||||
ComicSourceManager().reload();
|
||||
},
|
||||
|
@@ -52,6 +52,10 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
title: "Show history on comic tile".tl,
|
||||
settingKey: "showHistoryStatusOnTile",
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Reverse default chapter order".tl,
|
||||
settingKey: "reverseChapterOrder",
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "Keyword blocking".tl,
|
||||
builder: () => const _ManageBlockingWordView(),
|
||||
|
@@ -163,6 +163,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||
},
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show system status bar".tl,
|
||||
settingKey: "showSystemStatusBar",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showSystemStatusBar");
|
||||
},
|
||||
).toSliver(),
|
||||
SelectSetting(
|
||||
title: "Quick collect image".tl,
|
||||
settingKey: "quickCollectImage",
|
||||
|
@@ -22,11 +22,13 @@ class DataSync with ChangeNotifier {
|
||||
}
|
||||
LocalFavoritesManager().addListener(onDataChanged);
|
||||
ComicSourceManager().addListener(onDataChanged);
|
||||
if (App.isDesktop) {
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
var controller = WindowFrame.of(App.rootContext);
|
||||
controller.addCloseListener(_handleWindowClose);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onDataChanged() {
|
||||
if (isEnabled) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter_saf/flutter_saf.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/utils/image.dart';
|
||||
@@ -74,6 +75,9 @@ Future<Isolate> _runIsolate(
|
||||
return Isolate.spawn<SendPort>(
|
||||
(sendPort) => overrideIO(
|
||||
() async {
|
||||
if (App.isAndroid) {
|
||||
await SAFTaskWorker().init();
|
||||
}
|
||||
var receivePort = ReceivePort();
|
||||
sendPort.send(receivePort.sendPort);
|
||||
|
||||
|
@@ -35,8 +35,10 @@ extension TagsTranslation on String{
|
||||
/// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace.
|
||||
static String _translateTags(String tag){
|
||||
if (tag.contains('|')) {
|
||||
var splits = tag.split(' | ');
|
||||
return enTagsTranslations[splits[0]]??enTagsTranslations[splits[1]]??tag;
|
||||
var splits = tag.split('|');
|
||||
return enTagsTranslations[splits[0].trim()]
|
||||
?? enTagsTranslations[splits[1].trim()]
|
||||
?? tag;
|
||||
} else if(tag.contains(':')) {
|
||||
var splits = tag.split(':');
|
||||
if(_haveNamespace(splits[0])) {
|
||||
|
10
pubspec.lock
10
pubspec.lock
@@ -170,6 +170,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1108,4 +1116,4 @@ packages:
|
||||
version: "0.0.12"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
flutter: ">=3.32.4"
|
||||
|
@@ -2,11 +2,11 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.4.4+144
|
||||
version: 1.4.5+145
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
flutter: 3.32.0
|
||||
flutter: 3.32.4
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@@ -86,6 +86,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
yaml: ^3.1.3
|
||||
enough_convert: ^1.6.0
|
||||
display_mode: ^0.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user