Merge pull request #410 from venera-app/v1.4.5-dev

V1.4.5
This commit is contained in:
nyne
2025-06-18 16:52:59 +08:00
committed by GitHub
18 changed files with 256 additions and 162 deletions

View File

@@ -389,7 +389,7 @@
"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": "在首页显示单张图片",
"Click to select an image": "点击选择一张图片", "Click to select an image": "点击选择一张图片",
"Source URL": "地址", "Repo URL": "仓库地址",
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
"Double tap to zoom": "双击缩放", "Double tap to zoom": "双击缩放",
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",
@@ -786,7 +786,7 @@
"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": "在首頁顯示單張圖片",
"Click to select an image": "點擊選擇一張圖片", "Click to select an image": "點擊選擇一張圖片",
"Source URL": "地址", "Repo URL": "倉庫地址",
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
"Double tap to zoom": "雙擊縮放", "Double tap to zoom": "雙擊縮放",
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",

View File

@@ -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. 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. - 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 +55,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 +81,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 +96,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 +172,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 +217,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 +259,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 +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. This part is used to load comics of a category.
### Search #### Search
```javascript ```javascript
/// search related /// search related
@@ -339,7 +371,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 +443,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 +608,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 +667,7 @@ This part is used to load comic details.
This part is used to provide settings for the source. This part is used to provide settings for the source.
### Translations #### Translations
```javascript ```javascript
// [Optional] translations for the strings in this config // [Optional] translations for the strings in this config

View File

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

View File

@@ -189,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': '',
'preloadImageCount': 4, 'preloadImageCount': 4,
'followUpdatesFolder': null, 'followUpdatesFolder': null,
'initialPage': '0', 'initialPage': '0',
@@ -233,5 +233,3 @@ function processImage(image, cid, eid, page, sourceKey) {
return futureImage; return futureImage;
} }
'''; ''';
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json";

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:isolate'; 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';
@@ -108,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,
@@ -625,6 +653,7 @@ class LocalManager with ChangeNotifier {
/// Deletes the directories in a separate isolate to avoid blocking the UI thread. /// Deletes the directories in a separate isolate to avoid blocking the UI thread.
static void _deleteDirectories(List<Directory> directories) { static void _deleteDirectories(List<Directory> directories) {
Isolate.run(() async { Isolate.run(() async {
await SAFTaskWorker().init();
for (var dir in directories) { for (var dir in directories) {
try { try {
if (dir.existsSync()) { if (dir.existsSync()) {

View File

@@ -95,8 +95,7 @@ Future<void> _checkAppUpdates() async {
appdata.writeImplicitData(); appdata.writeImplicitData();
ComicSourcePage.checkComicSourceUpdate(); ComicSourcePage.checkComicSourceUpdate();
if (appdata.settings['checkUpdateOnStart']) { if (appdata.settings['checkUpdateOnStart']) {
await Future.delayed(const Duration(milliseconds: 300)); await checkUpdateUi(false, true);
await checkUpdateUi(false);
} }
} }

View File

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

View File

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

View File

@@ -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,12 +191,11 @@ class _BodyState extends State<_Body> {
} }
Widget buildCard(BuildContext context) { Widget buildCard(BuildContext context) {
Widget buildButton( Widget buildButton({
{required Widget child, required VoidCallback onPressed}) { required Widget child,
return Button.normal( required VoidCallback onPressed,
onPressed: onPressed, }) {
child: child, return Button.normal(onPressed: onPressed, child: child).fixHeight(32);
).fixHeight(32);
} }
return SliverToBoxAdapter( return SliverToBoxAdapter(
@@ -213,12 +211,14 @@ 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;
}, },
@@ -245,10 +245,7 @@ class _BodyState extends State<_Body> {
), ),
ListTile( ListTile(
title: Text("Help".tl), title: Text("Help".tl),
trailing: buildButton( trailing: buildButton(onPressed: help, child: Text("Open".tl)),
onPressed: help,
child: Text("Open".tl),
),
), ),
ListTile( ListTile(
title: Text("Check updates".tl), title: Text("Check updates".tl),
@@ -277,7 +274,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 +286,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 +335,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 +352,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 +380,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 +404,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 +449,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 +462,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 +564,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 +606,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 +650,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,
@@ -783,10 +792,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 +825,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 +870,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 +898,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 +909,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 +939,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 +984,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 +1030,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 +1071,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 +1199,9 @@ class _LoginPageState extends State<_LoginPage> {
setState(() { setState(() {
loading = true; loading = true;
}); });
var cookies = var cookies = widget.config.cookieFields!
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); .map((e) => _cookies[e] ?? '')
.toList();
widget.config.validateCookies!(cookies).then((value) { widget.config.validateCookies!(cookies).then((value) {
if (value) { if (value) {
widget.source.data['account'] = 'ok'; widget.source.data['account'] = 'ok';

View File

@@ -66,6 +66,11 @@ class _FavoritesPageState extends State<FavoritesPage> {
folder = data['name']; folder = data['name'];
isNetwork = data['isNetwork'] ?? false; isNetwork = data['isNetwork'] ?? false;
} }
if (folder != null
&& !isNetwork
&& !LocalFavoritesManager().existsFolder(folder!)) {
folder = null;
}
super.initState(); super.initState();
} }

View File

@@ -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();
});
}); });
} }
} }
@@ -233,7 +239,7 @@ class _GalleryModeState extends State<_GalleryMode>
photoViewControllers[index] ??= PhotoViewController(); photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage == 1) { if (reader.imagesPerPage == 1 || pageImages.length == 1) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
controller: photoViewControllers[index], controller: photoViewControllers[index],

View File

@@ -164,9 +164,6 @@ 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(() {
updateHistory();
});
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
if (appdata.settings['enableTurnPageByVolumeKey']) { if (appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent(); handleVolumeEvent();
@@ -267,7 +264,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) {
@@ -349,7 +354,11 @@ abstract mixin class _ImagePerPageHandler {
void initImagesPerPage(int initialPage) { void initImagesPerPage(int initialPage) {
_lastImagesPerPage = imagesPerPage; _lastImagesPerPage = imagesPerPage;
if (imagesPerPage != 1) { if (imagesPerPage != 1) {
page = (initialPage / imagesPerPage).ceil(); if (showSingleImageOnFirstPage) {
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
} else {
page = (initialPage / imagesPerPage).ceil();
}
} }
} }

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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])) {

View File

@@ -1108,4 +1108,4 @@ packages:
version: "0.0.12" version: "0.0.12"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.32.4"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.4.4+144 version: 1.4.5+145
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
flutter: 3.32.0 flutter: 3.32.4
dependencies: dependencies:
flutter: flutter: