mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
17 Commits
v1.4.4-dev
...
v1.4.5-dev
Author | SHA1 | Date | |
---|---|---|---|
179f7643bb | |||
defd4b8624 | |||
b2a164e066 | |||
a46ceebf19 | |||
cc08445f13 | |||
93f7f72d07 | |||
20f7ab4866 | |||
54363919cd | |||
182a821fc5 | |||
8868c6edb3 | |||
![]() |
fffbb4ed23 | ||
![]() |
b057be0311 | ||
![]() |
fc5fed1707 | ||
![]() |
8525f5318f | ||
![]() |
d58cafc4a0 | ||
23afafd1d6 | |||
![]() |
3b6e0adbbb |
@@ -389,7 +389,7 @@
|
||||
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
|
||||
"Show single image on first page": "在首页显示单张图片",
|
||||
"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": "清除未收藏",
|
||||
@@ -786,7 +786,7 @@
|
||||
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
|
||||
"Show single image on first page": "在首頁顯示單張圖片",
|
||||
"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": "清除未收藏",
|
||||
|
@@ -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',
|
||||
@@ -233,5 +233,3 @@ function processImage(image, cid, eid, page, sourceKey) {
|
||||
return futureImage;
|
||||
}
|
||||
''';
|
||||
|
||||
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json";
|
||||
|
@@ -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,
|
||||
@@ -625,6 +653,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";
|
||||
|
@@ -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);
|
||||
@@ -133,14 +126,16 @@ class _BodyState extends State<_Body> {
|
||||
title: const Text("Reload Configs"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("cancel")),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ComicSourceManager().reload();
|
||||
App.forceRebuild();
|
||||
},
|
||||
child: const Text("continue")),
|
||||
onPressed: () async {
|
||||
await ComicSourceManager().reload();
|
||||
App.forceRebuild();
|
||||
},
|
||||
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(
|
||||
@@ -213,12 +211,14 @@ class _BodyState extends State<_Body> {
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "URL",
|
||||
border: const UnderlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
suffix: IconButton(
|
||||
onPressed: () => handleAddSource(url),
|
||||
icon: const Icon(Icons.check))),
|
||||
hintText: "URL",
|
||||
border: const UnderlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
suffix: IconButton(
|
||||
onPressed: () => handleAddSource(url),
|
||||
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--;
|
||||
@@ -449,28 +462,28 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
||||
var action = currentKey.contains(key)
|
||||
? const Icon(Icons.check, size: 20).paddingRight(8)
|
||||
: Button.filled(
|
||||
child: Text("Add".tl),
|
||||
onPressed: () async {
|
||||
var fileName = json![index]["fileName"];
|
||||
var url = json![index]["url"];
|
||||
if (url == null || !(url.toString()).isURL) {
|
||||
var listUrl =
|
||||
appdata.settings['comicSourceListUrl'] as String;
|
||||
if (listUrl
|
||||
.replaceFirst("https://", "")
|
||||
.replaceFirst("http://", "")
|
||||
.contains("/")) {
|
||||
url =
|
||||
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
||||
fileName;
|
||||
} else {
|
||||
url = '$listUrl/$fileName';
|
||||
}
|
||||
}
|
||||
await widget.onAdd(url);
|
||||
setState(() {});
|
||||
},
|
||||
).fixHeight(32);
|
||||
child: Text("Add".tl),
|
||||
onPressed: () async {
|
||||
var fileName = json![index]["fileName"];
|
||||
var url = json![index]["url"];
|
||||
if (url == null || !(url.toString()).isURL) {
|
||||
var listUrl =
|
||||
appdata.settings['comicSourceListUrl'] as String;
|
||||
if (listUrl
|
||||
.replaceFirst("https://", "")
|
||||
.replaceFirst("http://", "")
|
||||
.contains("/")) {
|
||||
url =
|
||||
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
||||
fileName;
|
||||
} else {
|
||||
url = '$listUrl/$fileName';
|
||||
}
|
||||
}
|
||||
await widget.onAdd(url);
|
||||
setState(() {});
|
||||
},
|
||||
).fixHeight(32);
|
||||
|
||||
var description = json![index]["version"];
|
||||
if (json![index]["description"] != null) {
|
||||
@@ -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) {
|
||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||
}).join("\n");
|
||||
var text = ComicSourceManager().availableUpdates.entries
|
||||
.map((e) {
|
||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||
})
|
||||
.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();
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -233,7 +239,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,9 +164,6 @@ class _ReaderState extends State<Reader>
|
||||
}
|
||||
mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||
history = widget.history;
|
||||
Future.microtask(() {
|
||||
updateHistory();
|
||||
});
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
if (appdata.settings['enableTurnPageByVolumeKey']) {
|
||||
handleVolumeEvent();
|
||||
@@ -267,7 +264,15 @@ class _ReaderState extends State<Reader>
|
||||
history!.page = images?.length ?? 1;
|
||||
} else {
|
||||
/// 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;
|
||||
if (widget.chapters?.isGrouped ?? false) {
|
||||
@@ -349,7 +354,11 @@ abstract mixin class _ImagePerPageHandler {
|
||||
void initImagesPerPage(int initialPage) {
|
||||
_lastImagesPerPage = imagesPerPage;
|
||||
if (imagesPerPage != 1) {
|
||||
page = (initialPage / imagesPerPage).ceil();
|
||||
if (showSingleImageOnFirstPage) {
|
||||
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
|
||||
} else {
|
||||
page = (initialPage / imagesPerPage).ceil();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -22,10 +22,12 @@ class DataSync with ChangeNotifier {
|
||||
}
|
||||
LocalFavoritesManager().addListener(onDataChanged);
|
||||
ComicSourceManager().addListener(onDataChanged);
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
var controller = WindowFrame.of(App.rootContext);
|
||||
controller.addCloseListener(_handleWindowClose);
|
||||
});
|
||||
if (App.isDesktop) {
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
var controller = WindowFrame.of(App.rootContext);
|
||||
controller.addCloseListener(_handleWindowClose);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onDataChanged() {
|
||||
|
@@ -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])) {
|
||||
|
20
pubspec.lock
20
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:
|
||||
@@ -433,10 +441,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_rust_bridge
|
||||
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611"
|
||||
sha256: b416ff56002789e636244fb4cc449f587656eff995e5a7169457eb0593fcaddb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.10.0"
|
||||
flutter_saf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -766,11 +774,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
|
||||
resolved-ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
|
||||
url: "https://github.com/wgh136/rhttp"
|
||||
source: git
|
||||
version: "0.11.0"
|
||||
version: "0.12.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
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"
|
||||
|
11
pubspec.yaml
11
pubspec.yaml
@@ -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:
|
||||
@@ -58,10 +58,10 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/venera-app/lodepng_flutter
|
||||
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||
rhttp:
|
||||
git:
|
||||
rhttp:
|
||||
git:
|
||||
url: https://github.com/wgh136/rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
ref: 1f0ff50336062c5f809c256726dc55cd30b9ce59
|
||||
path: rhttp
|
||||
webdav_client:
|
||||
git:
|
||||
@@ -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