mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Compare commits
7 Commits
ce0d10aeb2
...
v1.4.7-dev
Author | SHA1 | Date | |
---|---|---|---|
894a922b8f | |||
a91d7fff2d | |||
![]() |
926a3a530e | ||
d308c2ac60 | |||
ac13807ef4 | |||
38a5b2b8cf | |||
3a7c8d5e38 |
@@ -34,4 +34,7 @@ See [Comic Source](doc/comic_source.md)
|
||||
### Tags Translation
|
||||
[](https://github.com/EhTagTranslation/Database)
|
||||
|
||||
## Headless Mode
|
||||
See [Headless Doc](doc/headless_doc.md)
|
||||
|
||||
The Chinese translation of the manga tags is from this project.
|
||||
|
@@ -409,7 +409,9 @@
|
||||
"Export logs": "导出日志",
|
||||
"Clear specific reader settings for all comics": "清除所有漫画的特殊阅读设置",
|
||||
"Clear specific reader settings for this comic": "清除该漫画的特殊阅读设置",
|
||||
"Enable comic specific settings": "为每本漫画保存特定设置"
|
||||
"Enable comic specific settings": "启用此漫画特定设置",
|
||||
"Ignore Certificate Errors": "忽略证书错误",
|
||||
"Mouse scroll speed": "鼠标滚动速度"
|
||||
},
|
||||
"zh_TW": {
|
||||
"Home": "首頁",
|
||||
@@ -821,6 +823,8 @@
|
||||
"Export logs": "匯出日誌",
|
||||
"Clear specific reader settings for all comics": "清除所有漫畫的特殊閱讀設定",
|
||||
"Clear specific reader settings for this comic": "清除該漫畫的特殊閱讀設定",
|
||||
"Enable comic specific settings": "為每本漫畫保存特定設定"
|
||||
"Enable comic specific settings": "啟用此漫畫特定設定",
|
||||
"Ignore Certificate Errors": "忽略證書錯誤",
|
||||
"Mouse scroll speed": "滑鼠滾動速度"
|
||||
}
|
||||
}
|
180
doc/headless_doc.md
Normal file
180
doc/headless_doc.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Venera Headless Mode
|
||||
|
||||
Venera's headless mode allows you to run key features from the command line, making it easy to automate tasks and integrate with other tools. This document outlines the available commands and their usage.
|
||||
|
||||
## How to Use
|
||||
|
||||
To activate headless mode, use the `--headless` flag when running the Venera executable, followed by the desired command.
|
||||
|
||||
```bash
|
||||
venera --headless <command> [subcommand] [options]
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
- **`--ignore-disheadless-log`**: Suppresses log output, providing a cleaner output for scripting.
|
||||
|
||||
## Commands
|
||||
|
||||
### `webdav`
|
||||
|
||||
Manage WebDAV data synchronization.
|
||||
|
||||
- **`webdav up`**: Uploads your local configuration to the WebDAV server.
|
||||
- **`webdav down`**: Downloads and applies the remote configuration from the WebDAV server.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
venera --headless webdav up
|
||||
```
|
||||
|
||||
### `updatescript`
|
||||
|
||||
Update comic source scripts.
|
||||
|
||||
- **`updatescript all`**: Checks for and applies all available updates for your comic source scripts.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
venera --headless updatescript all
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
|
||||
The `updatescript` command provides detailed progress and a final summary.
|
||||
|
||||
**Progress Logs:**
|
||||
|
||||
- **`Progress`**: Indicates a successful update for a single script.
|
||||
- **`ProgressError`**: Indicates a failure during a script update.
|
||||
|
||||
**Example `Progress` Log:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"message": "Progress",
|
||||
"data": {
|
||||
"current": 1,
|
||||
"total": 5,
|
||||
"source": {
|
||||
"key": "source-key",
|
||||
"name": "Source Name",
|
||||
"version": "1.0.0",
|
||||
"url": "https://example.com/source.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Final Summary:**
|
||||
|
||||
A summary is provided at the end, detailing the total number of scripts, how many were updated, and how many failed.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "All scripts updated.",
|
||||
"data": {
|
||||
"total": 5,
|
||||
"updated": 4,
|
||||
"errors": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `updatesubscribe`
|
||||
|
||||
Update your subscribed comics and retrieve a list of updated comics.
|
||||
|
||||
- **`updatesubscribe`**: Checks all subscribed comics for updates.
|
||||
- **`updatesubscribe --update-comic-by-id-type <id> <type>`**: Updates a single comic specified by its `id` and `type`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
# Update all subscriptions
|
||||
venera --headless updatesubscribe
|
||||
|
||||
# Update a single comic
|
||||
venera --headless updatesubscribe --update-comic-by-id-type "comic-id" "source-key"
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
All headless commands output JSON objects prefixed with `[CLI PRINT]`. This structured format allows for easy parsing in automated scripts. The JSON object always contains a `status` and a `message`. For commands that return data, a `data` field will also be present.
|
||||
|
||||
### `updatesubscribe` Output
|
||||
|
||||
The `updatesubscribe` command provides detailed progress and final results in JSON format.
|
||||
|
||||
**Progress Logs:**
|
||||
|
||||
During an update, you will receive `Progress` or `ProgressError` messages.
|
||||
|
||||
- **`Progress`**: Indicates a successful step in the update process.
|
||||
- **`ProgressError`**: Indicates an error occurred while updating a specific comic.
|
||||
|
||||
**Example `Progress` Log:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"message": "Progress",
|
||||
"data": {
|
||||
"current": 1,
|
||||
"total": 10,
|
||||
"comic": {
|
||||
"id": "some-comic-id",
|
||||
"name": "Some Comic Name",
|
||||
"coverUrl": "https://example.com/cover.jpg",
|
||||
"author": "Author Name",
|
||||
"type": "source-key",
|
||||
"updateTime": "2023-10-27T12:00:00Z",
|
||||
"tags": ["tag1", "tag2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example `ProgressError` Log:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"message": "ProgressError",
|
||||
"data": {
|
||||
"current": 2,
|
||||
"total": 10,
|
||||
"comic": {
|
||||
"id": "another-comic-id",
|
||||
"name": "Another Comic Name",
|
||||
...
|
||||
},
|
||||
"error": "Error message here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Final Output:**
|
||||
|
||||
Once the update process is complete, a final JSON object is returned with a list of all comics that have been updated.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Updated comics list.",
|
||||
"data": [
|
||||
{
|
||||
"id": "some-comic-id",
|
||||
"name": "Some Comic Name",
|
||||
"coverUrl": "https://example.com/cover.jpg",
|
||||
"author": "Author Name",
|
||||
"type": "source-key",
|
||||
"updateTime": "2023-10-27T12:00:00Z",
|
||||
"tags": ["tag1", "tag2"]
|
||||
}
|
||||
]
|
||||
}
|
@@ -152,7 +152,6 @@ class Settings with ChangeNotifier {
|
||||
'blockedWords': [],
|
||||
'defaultSearchTarget': null,
|
||||
'autoPageTurningInterval': 5, // in seconds
|
||||
'enableComicSpecificSettings': false,
|
||||
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
||||
'readerScreenPicNumberForLandscape': 1, // 1 - 5
|
||||
'readerScreenPicNumberForPortrait': 1, // 1 - 5
|
||||
@@ -191,6 +190,8 @@ class Settings with ChangeNotifier {
|
||||
'reverseChapterOrder': false,
|
||||
'showSystemStatusBar': false,
|
||||
'comicSpecificSettings': <String, Map<String, dynamic>>{},
|
||||
'ignoreBadCertificate': false,
|
||||
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
@@ -204,18 +205,19 @@ class Settings with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool haveComicSpecificSettings(String comicId, String sourceKey, String key) {
|
||||
return _data['comicSpecificSettings']?["$comicId@$sourceKey"]?.containsKey(
|
||||
key,
|
||||
) ??
|
||||
false;
|
||||
void setEnabledComicSpecificSettings(String comicId, String sourceKey, bool enabled) {
|
||||
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
||||
}
|
||||
|
||||
bool isComicSpecificSettingsEnabled(String? comicId, String? sourceKey) {
|
||||
if (comicId == null || sourceKey == null) {
|
||||
return false;
|
||||
}
|
||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
|
||||
}
|
||||
|
||||
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
||||
if (key == 'enableComicSpecificSettings') {
|
||||
return _data['enableComicSpecificSettings'];
|
||||
}
|
||||
if (_data['enableComicSpecificSettings'] == false) {
|
||||
if (!isComicSpecificSettingsEnabled(comicId, sourceKey)) {
|
||||
return _data[key];
|
||||
}
|
||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?[key] ??
|
||||
@@ -228,16 +230,6 @@ class Settings with ChangeNotifier {
|
||||
String key,
|
||||
dynamic value,
|
||||
) {
|
||||
if (key == 'enableComicSpecificSettings') {
|
||||
_data['enableComicSpecificSettings'] = value;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
if (_data['enableComicSpecificSettings'] == false) {
|
||||
_data[key] = value;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
(_data['comicSpecificSettings'] as Map<String, dynamic>).putIfAbsent(
|
||||
"$comicId@$sourceKey",
|
||||
() => <String, dynamic>{},
|
||||
@@ -245,16 +237,8 @@ class Settings with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetComicReaderSettings(String comicId, String sourceKey) {
|
||||
final allComicSettings = _data['comicSpecificSettings'] as Map;
|
||||
if (allComicSettings.containsKey("$comicId@$sourceKey")) {
|
||||
allComicSettings.remove("$comicId@$sourceKey");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetAllComicReaderSettings() {
|
||||
_data['comicSpecificSettings'] = <String, Map<String, dynamic>>{};
|
||||
void resetComicReaderSettings(String key) {
|
||||
(_data['comicSpecificSettings'] as Map).remove(key);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
162
lib/foundation/follow_updates.dart
Normal file
162
lib/foundation/follow_updates.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
|
||||
class ComicUpdateResult {
|
||||
final bool updated;
|
||||
final String? errorMessage;
|
||||
|
||||
ComicUpdateResult(this.updated, this.errorMessage);
|
||||
}
|
||||
|
||||
Future<ComicUpdateResult> updateComic(
|
||||
FavoriteItemWithUpdateInfo c, String folder) async {
|
||||
int retries = 3;
|
||||
while (true) {
|
||||
try {
|
||||
var comicSource = c.type.comicSource;
|
||||
if (comicSource == null) {
|
||||
return ComicUpdateResult(false, "Comic source not found");
|
||||
}
|
||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||
|
||||
var newTags = <String>[];
|
||||
for (var entry in newInfo.tags.entries) {
|
||||
const shouldIgnore = ['author', 'artist', 'time'];
|
||||
var namespace = entry.key;
|
||||
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
for (var tag in entry.value) {
|
||||
newTags.add("$namespace:$tag");
|
||||
}
|
||||
}
|
||||
|
||||
var item = FavoriteItem(
|
||||
id: c.id,
|
||||
name: newInfo.title,
|
||||
coverPath: newInfo.cover,
|
||||
author: newInfo.subTitle ??
|
||||
newInfo.tags['author']?.firstOrNull ??
|
||||
c.author,
|
||||
type: c.type,
|
||||
tags: newTags,
|
||||
);
|
||||
|
||||
LocalFavoritesManager().updateInfo(folder, item, false);
|
||||
|
||||
var updated = false;
|
||||
var updateTime = newInfo.findUpdateTime();
|
||||
if (updateTime != null && updateTime != c.updateTime) {
|
||||
LocalFavoritesManager().updateUpdateTime(
|
||||
folder,
|
||||
c.id,
|
||||
c.type,
|
||||
updateTime,
|
||||
);
|
||||
updated = true;
|
||||
} else {
|
||||
LocalFavoritesManager().updateCheckTime(folder, c.id, c.type);
|
||||
}
|
||||
return ComicUpdateResult(updated, null);
|
||||
} catch (e, s) {
|
||||
Log.error("Check Updates", e, s);
|
||||
retries--;
|
||||
if (retries == 0) {
|
||||
return ComicUpdateResult(false, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateProgress {
|
||||
final int total;
|
||||
final int current;
|
||||
final int errors;
|
||||
final int updated;
|
||||
final FavoriteItemWithUpdateInfo? comic;
|
||||
final String? errorMessage;
|
||||
|
||||
UpdateProgress(this.total, this.current, this.errors, this.updated,
|
||||
[this.comic, this.errorMessage]);
|
||||
}
|
||||
|
||||
void updateFolderBase(
|
||||
String folder,
|
||||
StreamController<UpdateProgress> stream,
|
||||
bool ignoreCheckTime,
|
||||
) async {
|
||||
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||
int total = comics.length;
|
||||
int current = 0;
|
||||
int errors = 0;
|
||||
int updated = 0;
|
||||
|
||||
stream.add(UpdateProgress(total, current, errors, updated));
|
||||
|
||||
var comicsToUpdate = <FavoriteItemWithUpdateInfo>[];
|
||||
|
||||
for (var comic in comics) {
|
||||
if (!ignoreCheckTime) {
|
||||
var lastCheckTime = comic.lastCheckTime;
|
||||
if (lastCheckTime != null &&
|
||||
DateTime.now().difference(lastCheckTime).inDays < 1) {
|
||||
current++;
|
||||
stream.add(UpdateProgress(total, current, errors, updated));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
comicsToUpdate.add(comic);
|
||||
}
|
||||
|
||||
total = comicsToUpdate.length;
|
||||
current = 0;
|
||||
stream.add(UpdateProgress(total, current, errors, updated));
|
||||
|
||||
var futures = <Future>[];
|
||||
for (var comic in comicsToUpdate) {
|
||||
var future = updateComic(comic, folder).then((result) {
|
||||
current++;
|
||||
if (result.updated) {
|
||||
updated++;
|
||||
}
|
||||
if (result.errorMessage != null) {
|
||||
errors++;
|
||||
}
|
||||
stream.add(
|
||||
UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
|
||||
});
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
|
||||
if (updated > 0) {
|
||||
LocalFavoritesManager().notifyChanges();
|
||||
}
|
||||
|
||||
stream.close();
|
||||
}
|
||||
|
||||
|
||||
Stream<UpdateProgress> updateFolder(String folder, bool ignoreCheckTime) {
|
||||
var stream = StreamController<UpdateProgress>();
|
||||
updateFolderBase(folder, stream, ignoreCheckTime);
|
||||
return stream.stream;
|
||||
}
|
||||
|
||||
Future<String> getUpdatedComicsAsJson(String folder) async {
|
||||
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||
var updatedComics = comics.where((c) => c.hasNewUpdate).toList();
|
||||
var jsonList = updatedComics.map((c) => {
|
||||
'id': c.id,
|
||||
'name': c.name,
|
||||
'coverUrl': c.coverPath,
|
||||
'author': c.author,
|
||||
'type': c.type.sourceKey,
|
||||
'updateTime': c.updateTime,
|
||||
'tags': c.tags,
|
||||
}).toList();
|
||||
return jsonEncode(jsonList);
|
||||
}
|
@@ -28,6 +28,8 @@ class Log {
|
||||
|
||||
static bool ignoreLimitation = false;
|
||||
|
||||
static bool isMuted = false;
|
||||
|
||||
static void printWarning(String text) {
|
||||
debugPrint('\x1B[33m$text\x1B[0m');
|
||||
}
|
||||
@@ -39,6 +41,7 @@ class Log {
|
||||
static IOSink? _file;
|
||||
|
||||
static void addLog(LogLevel level, String title, String content) {
|
||||
if (isMuted) return;
|
||||
if (_file == null) {
|
||||
Directory dir;
|
||||
if (App.isAndroid) {
|
||||
|
244
lib/headless.dart
Normal file
244
lib/headless.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/pages/comic_source_page.dart';
|
||||
import 'package:venera/init.dart';
|
||||
import 'package:venera/foundation/follow_updates.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
|
||||
void cliPrint(Map<String, dynamic> data) {
|
||||
print('[CLI PRINT] ${jsonEncode(data)}');
|
||||
}
|
||||
|
||||
Future<void> runHeadlessMode(List<String> args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
if (args.contains('--ignore-disheadless-log')) {
|
||||
Log.isMuted = true;
|
||||
}
|
||||
if(Platform.isLinux || Platform.isMacOS){
|
||||
Directory.current = Platform.environment['HOME']!;
|
||||
}
|
||||
// The first arg is '--headless', so we look at the next ones.
|
||||
var commandIndex = args.indexOf('--headless') + 1;
|
||||
if (commandIndex >= args.length) {
|
||||
cliPrint({'status': 'error', 'message': 'No command provided for headless mode.'});
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Need to initialize the app for some features to work
|
||||
await init();
|
||||
|
||||
var command = args[commandIndex];
|
||||
var subCommand = (commandIndex + 1 < args.length) ? args[commandIndex + 1] : null;
|
||||
|
||||
switch (command) {
|
||||
case 'webdav':
|
||||
if (subCommand == 'up') {
|
||||
cliPrint({'status': 'running', 'message': 'Uploading WebDAV data...'});
|
||||
await DataSync().uploadData();
|
||||
cliPrint({'status': 'success', 'message': 'Upload complete.'});
|
||||
} else if (subCommand == 'down') {
|
||||
cliPrint({'status': 'running', 'message': 'Downloading WebDAV data...'});
|
||||
await DataSync().downloadData();
|
||||
cliPrint({'status': 'success', 'message': 'Download complete.'});
|
||||
} else {
|
||||
cliPrint({'status': 'error', 'message': 'Invalid webdav command. Use "up" or "down".'});
|
||||
exit(1);
|
||||
}
|
||||
break;
|
||||
case 'updatescript':
|
||||
if (subCommand == 'all') {
|
||||
cliPrint({'status': 'running', 'message': 'Checking for comic source script updates...'});
|
||||
await ComicSourcePage.checkComicSourceUpdate();
|
||||
var updates = ComicSourceManager().availableUpdates;
|
||||
if (updates.isEmpty) {
|
||||
cliPrint({'status': 'success', 'message': 'No updates found.'});
|
||||
} else {
|
||||
var total = updates.length;
|
||||
var current = 0;
|
||||
var errors = 0;
|
||||
var updated = 0;
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': 'Updating all comic source scripts...',
|
||||
'data': {
|
||||
'total': total,
|
||||
'current': 0,
|
||||
'updated': 0,
|
||||
'errors': 0,
|
||||
}
|
||||
});
|
||||
for (var key in updates.keys) {
|
||||
var source = ComicSource.find(key);
|
||||
if (source != null) {
|
||||
current++;
|
||||
var data = {
|
||||
'current': current,
|
||||
'total': total,
|
||||
'source': {
|
||||
'key': source.key,
|
||||
'name': source.name,
|
||||
'version': source.version,
|
||||
'url': source.url,
|
||||
}
|
||||
};
|
||||
try {
|
||||
await ComicSourcePage.update(source, false);
|
||||
updated++;
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': 'Progress',
|
||||
'data': data,
|
||||
});
|
||||
} catch (e) {
|
||||
errors++;
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': 'ProgressError',
|
||||
'data': {
|
||||
...data,
|
||||
'error': e.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
cliPrint({
|
||||
'status': 'success',
|
||||
'message': 'All scripts updated.',
|
||||
'data': {
|
||||
'total': total,
|
||||
'updated': updated,
|
||||
'errors': errors,
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cliPrint({'status': 'error', 'message': 'Invalid updatescript command. Use "all".'});
|
||||
exit(1);
|
||||
}
|
||||
break;
|
||||
case 'updatesubscribe':
|
||||
cliPrint({'status': 'running', 'message': 'Updating subscribed comics...'});
|
||||
var folder = appdata.settings["followUpdatesFolder"];
|
||||
if (folder == null) {
|
||||
cliPrint({'status': 'error', 'message': 'Follow updates folder is not configured.'});
|
||||
exit(1);
|
||||
}
|
||||
|
||||
var updateIndex = args.indexOf('--update-comic-by-id-type');
|
||||
if (updateIndex != -1) {
|
||||
var id = args[updateIndex + 1];
|
||||
var type = args[updateIndex + 2];
|
||||
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||
var comic = comics.firstWhere((c) => c.id == id && c.type.sourceKey == type);
|
||||
|
||||
var result = await updateComic(comic, folder);
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'current': 1,
|
||||
'total': 1,
|
||||
'comic': {
|
||||
'id': comic.id,
|
||||
'name': comic.name,
|
||||
'coverUrl': comic.coverPath,
|
||||
'author': comic.author,
|
||||
'type': comic.type.sourceKey,
|
||||
'updateTime': comic.updateTime,
|
||||
'tags': comic.tags,
|
||||
}
|
||||
};
|
||||
|
||||
var message = 'Progress';
|
||||
if (result.errorMessage != null) {
|
||||
message = 'ProgressError';
|
||||
data['error'] = result.errorMessage;
|
||||
}
|
||||
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': message,
|
||||
'data': data,
|
||||
});
|
||||
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': 'Update check complete.',
|
||||
'data': {
|
||||
'total': 1,
|
||||
'updated': result.updated ? 1 : 0,
|
||||
'errors': result.errorMessage != null ? 1 : 0,
|
||||
}
|
||||
});
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
var json = await getUpdatedComicsAsJson(folder);
|
||||
cliPrint({
|
||||
'status': result.errorMessage != null ? 'error' : 'success',
|
||||
'message': 'Updated comics list.',
|
||||
'data': jsonDecode(json),
|
||||
});
|
||||
} else {
|
||||
int total = 0;
|
||||
int updated = 0;
|
||||
int errors = 0;
|
||||
await for (var progress in updateFolder(folder, true)) {
|
||||
total = progress.total;
|
||||
updated = progress.updated;
|
||||
errors = progress.errors;
|
||||
Map<String, dynamic> data = {
|
||||
'current': progress.current,
|
||||
'total': progress.total,
|
||||
};
|
||||
if (progress.comic != null) {
|
||||
data['comic'] = {
|
||||
'id': progress.comic!.id,
|
||||
'name': progress.comic!.name,
|
||||
'coverUrl': progress.comic!.coverPath,
|
||||
'author': progress.comic!.author,
|
||||
'type': progress.comic!.type.sourceKey,
|
||||
'updateTime': progress.comic!.updateTime,
|
||||
'tags': progress.comic!.tags,
|
||||
};
|
||||
}
|
||||
var message = 'Progress';
|
||||
if (progress.errorMessage != null) {
|
||||
message = 'ProgressError';
|
||||
data['error'] = progress.errorMessage;
|
||||
}
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': message,
|
||||
'data': data,
|
||||
});
|
||||
}
|
||||
cliPrint({
|
||||
'status': 'running',
|
||||
'message': 'Update check complete.',
|
||||
'data': {
|
||||
'total': total,
|
||||
'updated': updated,
|
||||
'errors': errors,
|
||||
}
|
||||
});
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
var json = await getUpdatedComicsAsJson(folder);
|
||||
cliPrint({
|
||||
'status': errors > 0 ? 'error' : 'success',
|
||||
'message': 'Updated comics list.',
|
||||
'data': jsonDecode(json),
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
cliPrint({'status': 'error', 'message': 'Unknown command: $command'});
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Exit after command execution
|
||||
exit(0);
|
||||
}
|
@@ -37,17 +37,21 @@ extension _FutureInit<T> on Future<T> {
|
||||
Future<void> init() async {
|
||||
await App.init().wait();
|
||||
await SingleInstanceCookieJar.createInstance();
|
||||
var futures = [
|
||||
Rhttp.init(),
|
||||
App.initComponents(),
|
||||
SAFTaskWorker().init().wait(),
|
||||
AppTranslation.init().wait(),
|
||||
TagsTranslation.readData().wait(),
|
||||
JsEngine().init().wait(),
|
||||
ComicSourceManager().init().wait(),
|
||||
OpenCC.init(),
|
||||
];
|
||||
await Future.wait(futures);
|
||||
try {
|
||||
var futures = [
|
||||
Rhttp.init(),
|
||||
App.initComponents(),
|
||||
SAFTaskWorker().init().wait(),
|
||||
AppTranslation.init().wait(),
|
||||
TagsTranslation.readData().wait(),
|
||||
JsEngine().init().wait(),
|
||||
ComicSourceManager().init().wait(),
|
||||
OpenCC.init(),
|
||||
];
|
||||
await Future.wait(futures);
|
||||
} catch (e, s) {
|
||||
Log.error("init", "$e\n$s");
|
||||
}
|
||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||
_checkOldConfigs();
|
||||
if (App.isAndroid) {
|
||||
|
@@ -14,9 +14,14 @@ import 'components/components.dart';
|
||||
import 'components/window_frame.dart';
|
||||
import 'foundation/app.dart';
|
||||
import 'foundation/appdata.dart';
|
||||
import 'headless.dart';
|
||||
import 'init.dart';
|
||||
|
||||
void main(List<String> args) {
|
||||
if (args.contains('--headless')) {
|
||||
runHeadlessMode(args);
|
||||
return;
|
||||
}
|
||||
if (runWebViewTitleBarWidget(args)) return;
|
||||
overrideIO(() {
|
||||
runZonedGuarded(() async {
|
||||
|
@@ -173,6 +173,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
||||
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
||||
tlsSettings: rhttp.TlsSettings(
|
||||
sni: appdata.settings['sni'] != false,
|
||||
verifyCertificates: appdata.settings['ignoreBadCertificate'] != true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -71,7 +71,8 @@ abstract class ImageDownloader {
|
||||
}
|
||||
|
||||
if (configs['onResponse'] is JSInvokable) {
|
||||
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
|
||||
final uint8List = Uint8List.fromList(buffer);
|
||||
buffer = (configs['onResponse'] as JSInvokable)([uint8List]);
|
||||
(configs['onResponse'] as JSInvokable).free();
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,54 @@ import 'package:venera/utils/translations.dart';
|
||||
class ComicSourcePage extends StatelessWidget {
|
||||
const ComicSourcePage({super.key});
|
||||
|
||||
static Future<void> update(
|
||||
ComicSource source, [
|
||||
bool showLoading = true,
|
||||
]) async {
|
||||
if (!source.url.isURL) {
|
||||
if (showLoading) {
|
||||
App.rootContext.showMessage(message: "Invalid url config");
|
||||
return;
|
||||
} else {
|
||||
throw Exception("Invalid url config");
|
||||
}
|
||||
}
|
||||
ComicSourceManager().remove(source.key);
|
||||
bool cancel = false;
|
||||
LoadingDialogController? controller;
|
||||
if (showLoading) {
|
||||
controller = showLoadingDialog(
|
||||
App.rootContext,
|
||||
onCancel: () => cancel = true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
try {
|
||||
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);
|
||||
await io.File(source.filePath).writeAsString(res.data!);
|
||||
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
||||
ComicSourceManager().availableUpdates.remove(source.key);
|
||||
}
|
||||
} catch (e) {
|
||||
if (cancel) return;
|
||||
if (showLoading) {
|
||||
App.rootContext.showMessage(message: e.toString());
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
await ComicSourceManager().reload();
|
||||
if (showLoading) {
|
||||
App.forceRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> checkComicSourceUpdate() async {
|
||||
if (ComicSource.all().isEmpty) {
|
||||
return 0;
|
||||
@@ -152,42 +200,11 @@ class _BodyState extends State<_Body> {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
void update(
|
||||
ComicSource source, [
|
||||
bool showLoading = true,
|
||||
]) async {
|
||||
if (!source.url.isURL) {
|
||||
App.rootContext.showMessage(message: "Invalid url config");
|
||||
return;
|
||||
}
|
||||
ComicSourceManager().remove(source.key);
|
||||
bool cancel = false;
|
||||
LoadingDialogController? controller;
|
||||
if (showLoading) {
|
||||
controller = showLoadingDialog(
|
||||
App.rootContext,
|
||||
onCancel: () => cancel = true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
try {
|
||||
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);
|
||||
await File(source.filePath).writeAsString(res.data!);
|
||||
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
||||
ComicSourceManager().availableUpdates.remove(source.key);
|
||||
}
|
||||
} catch (e) {
|
||||
if (cancel) return;
|
||||
App.rootContext.showMessage(message: e.toString());
|
||||
}
|
||||
await ComicSourceManager().reload();
|
||||
App.forceRebuild();
|
||||
]) {
|
||||
ComicSourcePage.update(source, showLoading);
|
||||
}
|
||||
|
||||
Widget buildCard(BuildContext context) {
|
||||
@@ -679,7 +696,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
||||
var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
|
||||
for (var key in shouldUpdate) {
|
||||
var source = ComicSource.find(key)!;
|
||||
await _BodyState.update(source, false);
|
||||
await ComicSourcePage.update(source, false);
|
||||
current++;
|
||||
loadingController.setProgress(current / total);
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@ import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import '../foundation/global_state.dart';
|
||||
import 'package:venera/foundation/follow_updates.dart';
|
||||
|
||||
class FollowUpdatesWidget extends StatefulWidget {
|
||||
const FollowUpdatesWidget({super.key});
|
||||
@@ -460,7 +460,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
||||
message: "Updating comics...".tl,
|
||||
);
|
||||
|
||||
await for (var progress in _updateFolder(folder, true)) {
|
||||
await for (var progress in updateFolder(folder, true)) {
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
@@ -497,7 +497,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
||||
|
||||
int updated = 0;
|
||||
|
||||
await for (var progress in _updateFolder(folder!, true)) {
|
||||
await for (var progress in updateFolder(folder!, true)) {
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
@@ -532,128 +532,6 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
||||
Object? get key => 'FollowUpdatesPage';
|
||||
}
|
||||
|
||||
class _UpdateProgress {
|
||||
final int total;
|
||||
final int current;
|
||||
final int errors;
|
||||
final int updated;
|
||||
|
||||
_UpdateProgress(this.total, this.current, this.errors, this.updated);
|
||||
}
|
||||
|
||||
void _updateFolderBase(
|
||||
String folder,
|
||||
StreamController<_UpdateProgress> stream,
|
||||
bool ignoreCheckTime,
|
||||
) async {
|
||||
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||
int current = 0;
|
||||
int errors = 0;
|
||||
int updated = 0;
|
||||
var futures = <Future>[];
|
||||
const maxConcurrent = 5;
|
||||
|
||||
for (int i = 0; i < comics.length; i++) {
|
||||
if (stream.isClosed) {
|
||||
return;
|
||||
}
|
||||
if (!ignoreCheckTime) {
|
||||
var lastCheckTime = comics[i].lastCheckTime;
|
||||
if (lastCheckTime != null &&
|
||||
DateTime.now().difference(lastCheckTime).inDays < 1) {
|
||||
current++;
|
||||
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (futures.length >= maxConcurrent) {
|
||||
await Future.any(futures);
|
||||
}
|
||||
|
||||
var future = () async {
|
||||
int retries = 3;
|
||||
while (true) {
|
||||
try {
|
||||
var c = comics[i];
|
||||
var comicSource = c.type.comicSource;
|
||||
if (comicSource == null) return;
|
||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||
|
||||
var newTags = <String>[];
|
||||
for (var entry in newInfo.tags.entries) {
|
||||
const shouldIgnore = ['author', 'artist', 'time'];
|
||||
var namespace = entry.key;
|
||||
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
for (var tag in entry.value) {
|
||||
newTags.add("$namespace:$tag");
|
||||
}
|
||||
}
|
||||
|
||||
var item = FavoriteItem(
|
||||
id: c.id,
|
||||
name: newInfo.title,
|
||||
coverPath: newInfo.cover,
|
||||
author: newInfo.subTitle ??
|
||||
newInfo.tags['author']?.firstOrNull ??
|
||||
c.author,
|
||||
type: c.type,
|
||||
tags: newTags,
|
||||
);
|
||||
|
||||
LocalFavoritesManager().updateInfo(folder, item, false);
|
||||
|
||||
var updateTime = newInfo.findUpdateTime();
|
||||
if (updateTime != null && updateTime != c.updateTime) {
|
||||
LocalFavoritesManager().updateUpdateTime(
|
||||
folder,
|
||||
c.id,
|
||||
c.type,
|
||||
updateTime,
|
||||
);
|
||||
} else {
|
||||
LocalFavoritesManager().updateCheckTime(folder, c.id, c.type);
|
||||
}
|
||||
updated++;
|
||||
return;
|
||||
} catch (e, s) {
|
||||
Log.error("Check Updates", e, s);
|
||||
retries--;
|
||||
if (retries == 0) {
|
||||
errors++;
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
current++;
|
||||
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
future.then((_) {
|
||||
futures.remove(future);
|
||||
});
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
|
||||
if (updated > 0) {
|
||||
LocalFavoritesManager().notifyChanges();
|
||||
}
|
||||
|
||||
stream.close();
|
||||
}
|
||||
|
||||
Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) {
|
||||
var stream = StreamController<_UpdateProgress>();
|
||||
_updateFolderBase(folder, stream, ignoreCheckTime);
|
||||
return stream.stream;
|
||||
}
|
||||
|
||||
/// Background service for checking updates
|
||||
abstract class FollowUpdatesService {
|
||||
static bool _isChecking = false;
|
||||
@@ -683,7 +561,7 @@ abstract class FollowUpdatesService {
|
||||
|
||||
int updated = 0;
|
||||
try {
|
||||
await for (var progress in _updateFolder(folder, false)) {
|
||||
await for (var progress in updateFolder(folder, false)) {
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
@@ -131,11 +131,11 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
||||
}
|
||||
if (context.reader.mode.key.startsWith('gallery')) {
|
||||
if (forward) {
|
||||
if (!context.reader.toNextPage(reader.cid, reader.type) && !context.reader.isLastChapterOfGroup) {
|
||||
if (!context.reader.toNextPage() && !context.reader.isLastChapterOfGroup) {
|
||||
context.reader.toNextChapter();
|
||||
}
|
||||
} else {
|
||||
if (!context.reader.toPrevPage(reader.cid, reader.type) && !context.reader.isFirstChapterOfGroup) {
|
||||
if (!context.reader.toPrevPage() && !context.reader.isFirstChapterOfGroup) {
|
||||
context.reader.toPrevChapter();
|
||||
}
|
||||
}
|
||||
@@ -209,12 +209,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
||||
isBottom = true;
|
||||
}
|
||||
bool isCenter = false;
|
||||
var prev = () => context.reader.toPrevPage(context.reader.cid, context.reader.type);
|
||||
var next = () => context.reader.toNextPage(context.reader.cid, context.reader.type);
|
||||
var prev = () => context.reader.toPrevPage();
|
||||
var next = () => context.reader.toNextPage();
|
||||
if (appdata.settings.getReaderSetting(
|
||||
reader.cid, reader.type.sourceKey, 'reverseTapToTurnPages')) {
|
||||
prev = () => context.reader.toNextPage(context.reader.cid, context.reader.type);
|
||||
next = () => context.reader.toPrevPage(context.reader.cid, context.reader.type);
|
||||
prev = () => context.reader.toNextPage();
|
||||
next = () => context.reader.toPrevPage();
|
||||
}
|
||||
switch (context.reader.mode) {
|
||||
case ReaderMode.galleryLeftToRight:
|
||||
|
@@ -138,14 +138,14 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
/// [totalPages] is the total number of pages in the current chapter.
|
||||
/// More than one images can be displayed on one page.
|
||||
int get totalPages {
|
||||
if (!reader.showSingleImageOnFirstPage(reader.cid, reader.type)) {
|
||||
if (!reader.showSingleImageOnFirstPage()) {
|
||||
return (reader.images!.length /
|
||||
reader.imagesPerPage(reader.cid, reader.type))
|
||||
reader.imagesPerPage())
|
||||
.ceil();
|
||||
} else {
|
||||
return 1 +
|
||||
((reader.images!.length - 1) /
|
||||
reader.imagesPerPage(reader.cid, reader.type))
|
||||
reader.imagesPerPage())
|
||||
.ceil();
|
||||
}
|
||||
}
|
||||
@@ -169,8 +169,8 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
/// Get the range of images for the given page. [page] is 1-based.
|
||||
(int start, int end) getPageImagesRange(int page) {
|
||||
var imagesPerPage = reader.imagesPerPage(reader.cid, reader.type);
|
||||
if (reader.showSingleImageOnFirstPage(reader.cid, reader.type)) {
|
||||
var imagesPerPage = reader.imagesPerPage();
|
||||
if (reader.showSingleImageOnFirstPage()) {
|
||||
if (page == 1) {
|
||||
return (0, 1);
|
||||
} else {
|
||||
@@ -259,7 +259,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage(reader.cid, reader.type) == 1 ||
|
||||
if (reader.imagesPerPage() == 1 ||
|
||||
pageImages.length == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
@@ -301,11 +301,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
onPageChanged: (i) {
|
||||
if (i == 0) {
|
||||
if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) {
|
||||
reader.toPage(reader.cid, reader.type, 1);
|
||||
reader.toPage(1);
|
||||
}
|
||||
} else if (i == totalPages + 1) {
|
||||
if (reader.isLastChapterOfGroup || !reader.toNextChapter()) {
|
||||
reader.toPage(reader.cid, reader.type, totalPages);
|
||||
reader.toPage(totalPages);
|
||||
}
|
||||
} else {
|
||||
reader.setPage(i);
|
||||
@@ -485,9 +485,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
keyRepeatTimer = null;
|
||||
}
|
||||
if (forward == true) {
|
||||
reader.toPage(reader.cid, reader.type, reader.page + 1);
|
||||
reader.toPage(reader.page + 1);
|
||||
} else if (forward == false) {
|
||||
reader.toPage(reader.cid, reader.type, reader.page - 1);
|
||||
reader.toPage(reader.page - 1);
|
||||
}
|
||||
}
|
||||
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
||||
@@ -500,9 +500,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
timer.cancel();
|
||||
return;
|
||||
} else if (forward == true) {
|
||||
reader.toPage(reader.cid, reader.type, reader.page + 1);
|
||||
reader.toPage(reader.page + 1);
|
||||
} else if (forward == false) {
|
||||
reader.toPage(reader.cid, reader.type, reader.page - 1);
|
||||
reader.toPage(reader.page - 1);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -534,7 +534,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
@override
|
||||
String? getImageKeyByOffset(Offset offset) {
|
||||
String? imageKey;
|
||||
if (reader.imagesPerPage(reader.cid, reader.type) == 1) {
|
||||
if (reader.imagesPerPage() == 1) {
|
||||
imageKey = reader.images![reader.page - 1];
|
||||
} else {
|
||||
for (var imageState in imageStates) {
|
||||
@@ -638,27 +638,52 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
cacheImages(page);
|
||||
}
|
||||
|
||||
double? futurePosition;
|
||||
double? _futurePosition;
|
||||
|
||||
void smoothTo(double offset) {
|
||||
futurePosition ??= scrollController.offset;
|
||||
if (futurePosition! > scrollController.position.maxScrollExtent &&
|
||||
offset > 0) {
|
||||
return;
|
||||
} else if (futurePosition! < scrollController.position.minScrollExtent &&
|
||||
offset < 0) {
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
return;
|
||||
}
|
||||
futurePosition = futurePosition! + offset * 1.2;
|
||||
futurePosition = futurePosition!.clamp(
|
||||
var currentLocation = scrollController.position.pixels;
|
||||
var old = _futurePosition;
|
||||
_futurePosition ??= currentLocation;
|
||||
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
||||
final customSpeed = appdata.settings.getReaderSetting(
|
||||
context.reader.cid,
|
||||
context.reader.type.sourceKey,
|
||||
"readerScrollSpeed",
|
||||
);
|
||||
if (customSpeed is num) {
|
||||
k *= customSpeed;
|
||||
}
|
||||
_futurePosition = _futurePosition! + offset * k;
|
||||
var beforeOffset = (_futurePosition! - currentLocation).abs();
|
||||
_futurePosition = _futurePosition!.clamp(
|
||||
scrollController.position.minScrollExtent,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
scrollController.animateTo(
|
||||
futurePosition!,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
var afterOffset = (_futurePosition! - currentLocation).abs();
|
||||
if (_futurePosition == old) return;
|
||||
var target = _futurePosition!;
|
||||
var duration = const Duration(milliseconds: 160);
|
||||
if (afterOffset < beforeOffset) {
|
||||
duration = duration * (afterOffset / beforeOffset);
|
||||
if (duration < Duration(milliseconds: 10)) {
|
||||
duration = Duration(milliseconds: 10);
|
||||
}
|
||||
}
|
||||
scrollController
|
||||
.animateTo(
|
||||
_futurePosition!,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
)
|
||||
.then((_) {
|
||||
var current = scrollController.position.pixels;
|
||||
if (current == target && current == _futurePosition) {
|
||||
_futurePosition = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onPointerSignal(PointerSignalEvent event) {
|
||||
@@ -787,7 +812,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
disableScroll = true;
|
||||
});
|
||||
}
|
||||
futurePosition = null;
|
||||
_futurePosition = null;
|
||||
if (_isMouseScrolling) {
|
||||
setState(() {
|
||||
_isMouseScrolling = false;
|
||||
@@ -1009,7 +1034,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
@override
|
||||
void toPage(int page) {
|
||||
itemScrollController.jumpTo(index: page);
|
||||
futurePosition = null;
|
||||
_futurePosition = null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@@ -115,15 +115,17 @@ class _ReaderState extends State<Reader>
|
||||
if (images == null) {
|
||||
return 1;
|
||||
}
|
||||
if (!showSingleImageOnFirstPage(cid, type)) {
|
||||
return (images!.length / imagesPerPage(cid, type)).ceil();
|
||||
if (!showSingleImageOnFirstPage()) {
|
||||
return (images!.length / imagesPerPage()).ceil();
|
||||
} else {
|
||||
return 1 + ((images!.length - 1) / imagesPerPage(cid, type)).ceil();
|
||||
return 1 + ((images!.length - 1) / imagesPerPage()).ceil();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ComicType get type => widget.type;
|
||||
|
||||
@override
|
||||
String get cid => widget.cid;
|
||||
|
||||
String get eid => widget.chapters?.ids.elementAtOrNull(chapter - 1) ?? '0';
|
||||
@@ -169,7 +171,7 @@ class _ReaderState extends State<Reader>
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
|
||||
handleVolumeEvent(cid, type);
|
||||
handleVolumeEvent();
|
||||
}
|
||||
setImageCacheSize();
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
@@ -184,11 +186,11 @@ class _ReaderState extends State<Reader>
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_isInitialized) {
|
||||
initImagesPerPage(cid, type, widget.initialPage ?? 1);
|
||||
initImagesPerPage(widget.initialPage ?? 1);
|
||||
_isInitialized = true;
|
||||
} else {
|
||||
// For orientation changed
|
||||
_checkImagesPerPageChange(cid, type);
|
||||
_checkImagesPerPageChange();
|
||||
}
|
||||
initReaderWindow();
|
||||
}
|
||||
@@ -230,7 +232,7 @@ class _ReaderState extends State<Reader>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_checkImagesPerPageChange(cid, type);
|
||||
_checkImagesPerPageChange();
|
||||
return KeyboardListener(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
@@ -275,13 +277,13 @@ class _ReaderState extends State<Reader>
|
||||
history!.page = images?.length ?? 1;
|
||||
} else {
|
||||
/// Record the first image of the page
|
||||
if (!showSingleImageOnFirstPage(cid, type) || imagesPerPage(cid, type) == 1) {
|
||||
history!.page = (page - 1) * imagesPerPage(cid, type) + 1;
|
||||
if (!showSingleImageOnFirstPage() || imagesPerPage() == 1) {
|
||||
history!.page = (page - 1) * imagesPerPage() + 1;
|
||||
} else {
|
||||
if (page == 1) {
|
||||
history!.page = 1;
|
||||
} else {
|
||||
history!.page = (page - 2) * imagesPerPage(cid, type) + 2;
|
||||
history!.page = (page - 2) * imagesPerPage() + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,23 +366,27 @@ abstract mixin class _ImagePerPageHandler {
|
||||
|
||||
ReaderMode get mode;
|
||||
|
||||
void initImagesPerPage(String cid, ComicType type, int initialPage) {
|
||||
_lastImagesPerPage = imagesPerPage(cid, type);
|
||||
String get cid;
|
||||
|
||||
ComicType get type;
|
||||
|
||||
void initImagesPerPage(int initialPage) {
|
||||
_lastImagesPerPage = imagesPerPage();
|
||||
_lastOrientation = isPortrait;
|
||||
if (imagesPerPage(cid, type) != 1) {
|
||||
if (showSingleImageOnFirstPage(cid, type)) {
|
||||
page = ((initialPage - 1) / imagesPerPage(cid, type)).ceil() + 1;
|
||||
if (imagesPerPage() != 1) {
|
||||
if (showSingleImageOnFirstPage()) {
|
||||
page = ((initialPage - 1) / imagesPerPage()).ceil() + 1;
|
||||
} else {
|
||||
page = (initialPage / imagesPerPage(cid, type)).ceil();
|
||||
page = (initialPage / imagesPerPage()).ceil();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool showSingleImageOnFirstPage(String cid, ComicType type) =>
|
||||
bool showSingleImageOnFirstPage() =>
|
||||
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
|
||||
|
||||
/// The number of images displayed on one screen
|
||||
int imagesPerPage(String cid, ComicType type) {
|
||||
int imagesPerPage() {
|
||||
if (mode.isContinuous) return 1;
|
||||
if (isPortrait) {
|
||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
|
||||
@@ -390,23 +396,21 @@ abstract mixin class _ImagePerPageHandler {
|
||||
}
|
||||
|
||||
/// Check if the number of images per page has changed
|
||||
void _checkImagesPerPageChange(String cid, ComicType type) {
|
||||
int currentImagesPerPage = imagesPerPage(cid, type);
|
||||
void _checkImagesPerPageChange() {
|
||||
int currentImagesPerPage = imagesPerPage();
|
||||
bool currentOrientation = isPortrait;
|
||||
|
||||
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
||||
_adjustPageForImagesPerPageChange(
|
||||
cid, type, _lastImagesPerPage, currentImagesPerPage);
|
||||
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
||||
_lastImagesPerPage = currentImagesPerPage;
|
||||
_lastOrientation = currentOrientation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust the page number when the number of images per page changes
|
||||
void _adjustPageForImagesPerPageChange(
|
||||
String cid, ComicType type, int oldImagesPerPage, int newImagesPerPage) {
|
||||
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
||||
int previousImageIndex = 1;
|
||||
if (!showSingleImageOnFirstPage(cid, type) || oldImagesPerPage == 1) {
|
||||
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
||||
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
||||
} else {
|
||||
if (page == 1) {
|
||||
@@ -418,7 +422,7 @@ abstract mixin class _ImagePerPageHandler {
|
||||
|
||||
int newPage;
|
||||
if (newImagesPerPage != 1) {
|
||||
if (showSingleImageOnFirstPage(cid, type)) {
|
||||
if (showSingleImageOnFirstPage()) {
|
||||
newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1;
|
||||
} else {
|
||||
newPage = (previousImageIndex / newImagesPerPage).ceil();
|
||||
@@ -432,9 +436,9 @@ abstract mixin class _ImagePerPageHandler {
|
||||
}
|
||||
|
||||
abstract mixin class _VolumeListener {
|
||||
bool toNextPage(String cid, ComicType type);
|
||||
bool toNextPage();
|
||||
|
||||
bool toPrevPage(String cid, ComicType type);
|
||||
bool toPrevPage();
|
||||
|
||||
bool toNextChapter();
|
||||
|
||||
@@ -442,19 +446,19 @@ abstract mixin class _VolumeListener {
|
||||
|
||||
VolumeListener? volumeListener;
|
||||
|
||||
void onDown(String cid, ComicType type) {
|
||||
if (!toNextPage(cid, type)) {
|
||||
void onDown() {
|
||||
if (!toNextPage()) {
|
||||
toNextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
void onUp(String cid, ComicType type) {
|
||||
if (!toPrevPage(cid, type)) {
|
||||
void onUp() {
|
||||
if (!toPrevPage()) {
|
||||
toPrevChapter();
|
||||
}
|
||||
}
|
||||
|
||||
void handleVolumeEvent(String cid, ComicType type) {
|
||||
void handleVolumeEvent() {
|
||||
if (!App.isAndroid) {
|
||||
// Currently only support Android
|
||||
return;
|
||||
@@ -463,8 +467,8 @@ abstract mixin class _VolumeListener {
|
||||
volumeListener?.cancel();
|
||||
}
|
||||
volumeListener = VolumeListener(
|
||||
onDown: () => onDown(cid, type),
|
||||
onUp: () => onUp(cid, type),
|
||||
onDown: onDown,
|
||||
onUp: onUp,
|
||||
)..listen();
|
||||
}
|
||||
|
||||
@@ -494,6 +498,10 @@ abstract mixin class _ReaderLocation {
|
||||
|
||||
bool get isLoading;
|
||||
|
||||
String get cid;
|
||||
|
||||
ComicType get type;
|
||||
|
||||
void update();
|
||||
|
||||
bool enablePageAnimation(String cid, ComicType type) => appdata.settings.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
||||
@@ -515,18 +523,18 @@ abstract mixin class _ReaderLocation {
|
||||
}
|
||||
|
||||
/// Returns true if the page is changed
|
||||
bool toNextPage(String cid, ComicType type) {
|
||||
return toPage(cid, type, page + 1);
|
||||
bool toNextPage() {
|
||||
return toPage(page + 1);
|
||||
}
|
||||
|
||||
/// Returns true if the page is changed
|
||||
bool toPrevPage(String cid, ComicType type) {
|
||||
return toPage(cid, type, page - 1);
|
||||
bool toPrevPage() {
|
||||
return toPage(page - 1);
|
||||
}
|
||||
|
||||
int _animationCount = 0;
|
||||
|
||||
bool toPage(String cid, ComicType type, int page) {
|
||||
bool toPage(int page) {
|
||||
if (_validatePage(page)) {
|
||||
if (page == this.page && page != 1 && page != maxPage) {
|
||||
return false;
|
||||
@@ -582,7 +590,7 @@ abstract mixin class _ReaderLocation {
|
||||
if (page == maxPage) {
|
||||
autoPageTurningTimer!.cancel();
|
||||
}
|
||||
toNextPage(cid, type);
|
||||
toNextPage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -348,6 +348,99 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
text = "P${context.reader.page}";
|
||||
}
|
||||
|
||||
final buttons = [
|
||||
Tooltip(
|
||||
message: "Collect the image".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
|
||||
onPressed: addImageFavorite,
|
||||
),
|
||||
),
|
||||
if (App.isDesktop)
|
||||
Tooltip(
|
||||
message: "${"Full Screen".tl}(F12)",
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
context.reader.fullscreen();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (App.isAndroid)
|
||||
Tooltip(
|
||||
message: "Screen Rotation".tl,
|
||||
child: IconButton(
|
||||
icon: () {
|
||||
if (rotation == null) {
|
||||
return const Icon(Icons.screen_rotation);
|
||||
} else if (rotation == false) {
|
||||
return const Icon(Icons.screen_lock_portrait);
|
||||
} else {
|
||||
return const Icon(Icons.screen_lock_landscape);
|
||||
}
|
||||
}.call(),
|
||||
onPressed: () {
|
||||
if (rotation == null) {
|
||||
setState(() {
|
||||
rotation = false;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
} else if (rotation == false) {
|
||||
setState(() {
|
||||
rotation = true;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
} else {
|
||||
setState(() {
|
||||
rotation = null;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Auto Page Turning".tl,
|
||||
child: IconButton(
|
||||
icon: context.reader.autoPageTurningTimer != null
|
||||
? const Icon(Icons.timer)
|
||||
: const Icon(Icons.timer_sharp),
|
||||
onPressed: () {
|
||||
context.reader.autoPageTurning(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
);
|
||||
update();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (context.reader.widget.chapters != null)
|
||||
Tooltip(
|
||||
message: "Chapters".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.library_books),
|
||||
onPressed: openChapterDrawer,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Save Image".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: saveCurrentImage,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Share".tl,
|
||||
child: IconButton(icon: const Icon(Icons.share), onPressed: share),
|
||||
),
|
||||
];
|
||||
|
||||
Widget child = SizedBox(
|
||||
height: kBottomBarHeight,
|
||||
child: Column(
|
||||
@@ -360,18 +453,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
onPressed: () => !isReversed
|
||||
? context.reader.chapter > 1
|
||||
? context.reader.toPrevChapter()
|
||||
: context.reader.toPage(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
1,
|
||||
)
|
||||
: context.reader.toPage(1)
|
||||
: context.reader.chapter < context.reader.maxChapter
|
||||
? context.reader.toNextChapter()
|
||||
: context.reader.toPage(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
context.reader.maxPage,
|
||||
),
|
||||
: context.reader.toPage(context.reader.maxPage),
|
||||
icon: const Icon(Icons.first_page),
|
||||
),
|
||||
Expanded(child: buildSlider()),
|
||||
@@ -379,135 +464,35 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
onPressed: () => !isReversed
|
||||
? context.reader.chapter < context.reader.maxChapter
|
||||
? context.reader.toNextChapter()
|
||||
: context.reader.toPage(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
context.reader.maxPage,
|
||||
)
|
||||
: context.reader.toPage(context.reader.maxPage)
|
||||
: context.reader.chapter > 1
|
||||
? context.reader.toPrevChapter()
|
||||
: context.reader.toPage(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
1,
|
||||
),
|
||||
: context.reader.toPage(1),
|
||||
icon: const Icon(Icons.last_page),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Container(
|
||||
height: 24,
|
||||
padding: const EdgeInsets.fromLTRB(6, 2, 6, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(child: Text(text)),
|
||||
),
|
||||
const Spacer(),
|
||||
Tooltip(
|
||||
message: "Collect the image".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isLiked() ? Icons.favorite : Icons.favorite_border,
|
||||
),
|
||||
onPressed: addImageFavorite,
|
||||
),
|
||||
),
|
||||
if (App.isDesktop)
|
||||
Tooltip(
|
||||
message: "${"Full Screen".tl}(F12)",
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
context.reader.fullscreen();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (App.isAndroid)
|
||||
Tooltip(
|
||||
message: "Screen Rotation".tl,
|
||||
child: IconButton(
|
||||
icon: () {
|
||||
if (rotation == null) {
|
||||
return const Icon(Icons.screen_rotation);
|
||||
} else if (rotation == false) {
|
||||
return const Icon(Icons.screen_lock_portrait);
|
||||
} else {
|
||||
return const Icon(Icons.screen_lock_landscape);
|
||||
}
|
||||
}.call(),
|
||||
onPressed: () {
|
||||
if (rotation == null) {
|
||||
setState(() {
|
||||
rotation = false;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
} else if (rotation == false) {
|
||||
setState(() {
|
||||
rotation = true;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
} else {
|
||||
setState(() {
|
||||
rotation = null;
|
||||
});
|
||||
SystemChrome.setPreferredOrientations(
|
||||
DeviceOrientation.values,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Auto Page Turning".tl,
|
||||
child: IconButton(
|
||||
icon: context.reader.autoPageTurningTimer != null
|
||||
? const Icon(Icons.timer)
|
||||
: const Icon(Icons.timer_sharp),
|
||||
onPressed: () {
|
||||
context.reader.autoPageTurning(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
);
|
||||
update();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (context.reader.widget.chapters != null)
|
||||
Tooltip(
|
||||
message: "Chapters".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.library_books),
|
||||
onPressed: openChapterDrawer,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Save Image".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: saveCurrentImage,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Share".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: share,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
LayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
return Row(
|
||||
children: [
|
||||
if ((constrains.maxWidth - buttons.length * 42) > 80)
|
||||
Container(
|
||||
height: 24,
|
||||
padding: const EdgeInsets.fromLTRB(6, 2, 6, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(child: Text(text)),
|
||||
).paddingLeft(16),
|
||||
const Spacer(),
|
||||
...buttons,
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -545,11 +530,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
reversed: isReversed,
|
||||
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
||||
onChanged: (i) {
|
||||
context.reader.toPage(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
i.toInt(),
|
||||
);
|
||||
context.reader.toPage(i.toInt());
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -659,10 +640,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
context.reader.type.sourceKey,
|
||||
key,
|
||||
)) {
|
||||
context.reader.handleVolumeEvent(
|
||||
context.reader.cid,
|
||||
context.reader.type,
|
||||
);
|
||||
context.reader.handleVolumeEvent();
|
||||
} else {
|
||||
context.reader.stopVolumeEvent();
|
||||
}
|
||||
|
@@ -31,6 +31,10 @@ class DebugPageState extends State<DebugPage> {
|
||||
},
|
||||
actionTitle: 'Open'.tl,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Ignore Certificate Errors".tl,
|
||||
settingKey: "ignoreBadCertificate",
|
||||
).toSliver(),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@@ -19,17 +19,57 @@ class ReaderSettings extends StatefulWidget {
|
||||
class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final comicId = widget.comicId;
|
||||
final sourceKey = widget.comicSource;
|
||||
final key = "$comicId@$sourceKey";
|
||||
|
||||
bool isEnabledSpecificSettings =
|
||||
comicId != null &&
|
||||
appdata.settings.isComicSpecificSettingsEnabled(comicId, sourceKey);
|
||||
|
||||
return SmoothCustomScrollView(
|
||||
slivers: [
|
||||
SliverAppbar(title: Text("Reading".tl)),
|
||||
if (comicId != null && sourceKey != null)
|
||||
SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SwitchListTile(
|
||||
title: Text("Enable comic specific settings".tl),
|
||||
value: isEnabledSpecificSettings,
|
||||
onChanged: (b) {
|
||||
setState(() {
|
||||
appdata.settings.setEnabledComicSpecificSettings(
|
||||
comicId,
|
||||
sourceKey,
|
||||
b,
|
||||
);
|
||||
});
|
||||
},
|
||||
).toSliver(),
|
||||
if (isEnabledSpecificSettings)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
appdata.settings.resetComicReaderSettings(key);
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
"Clear specific reader settings for this comic".tl,
|
||||
),
|
||||
),
|
||||
).toSliver(),
|
||||
Divider().toSliver(),
|
||||
],
|
||||
),
|
||||
_SwitchSetting(
|
||||
title: "Tap to turn Pages".tl,
|
||||
settingKey: "enableTapToTurnPages",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("enableTapToTurnPages");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Reverse tap to turn Pages".tl,
|
||||
@@ -37,8 +77,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("reverseTapToTurnPages");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Page animation".tl,
|
||||
@@ -46,15 +86,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("enablePageAnimation");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Enable comic specific settings".tl,
|
||||
settingKey: "enableComicSpecificSettings",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("enableComicSpecificSettings");
|
||||
},
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
SelectSetting(
|
||||
title: "Reading mode".tl,
|
||||
@@ -78,8 +111,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
}
|
||||
widget.onChanged?.call("readerMode");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SliderSetting(
|
||||
title: "Auto page turning interval".tl,
|
||||
@@ -91,8 +124,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
setState(() {});
|
||||
widget.onChanged?.call("autoPageTurningInterval");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
SliverAnimatedVisibility(
|
||||
visible: appdata.settings['readerMode']!.startsWith('gallery'),
|
||||
@@ -108,8 +141,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
setState(() {});
|
||||
widget.onChanged?.call("readerScreenPicNumberForLandscape");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
),
|
||||
),
|
||||
SliverAnimatedVisibility(
|
||||
@@ -125,8 +158,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("readerScreenPicNumberForPortrait");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
),
|
||||
),
|
||||
SliverAnimatedVisibility(
|
||||
@@ -140,8 +173,23 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showSingleImageOnFirstPage");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
),
|
||||
),
|
||||
SliverAnimatedVisibility(
|
||||
visible: appdata.settings['readerMode']!.startsWith('continuous'),
|
||||
child: _SliderSetting(
|
||||
title: "Mouse scroll speed".tl,
|
||||
settingsIndex: "readerScrollSpeed",
|
||||
interval: 0.1,
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("readerScrollSpeed");
|
||||
},
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
),
|
||||
),
|
||||
_SwitchSetting(
|
||||
@@ -151,8 +199,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
setState(() {});
|
||||
widget.onChanged?.call('enableDoubleTapToZoom');
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: 'Long press to zoom'.tl,
|
||||
@@ -161,8 +209,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
setState(() {});
|
||||
widget.onChanged?.call('enableLongPressToZoom');
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
SliverAnimatedVisibility(
|
||||
visible: appdata.settings['enableLongPressToZoom'] == true,
|
||||
@@ -173,8 +221,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
"press": "Press position".tl,
|
||||
"center": "Screen center".tl,
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
),
|
||||
),
|
||||
_SwitchSetting(
|
||||
@@ -184,8 +232,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call('limitImageWidth');
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
if (App.isAndroid)
|
||||
_SwitchSetting(
|
||||
@@ -194,8 +242,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call('enableTurnPageByVolumeKey');
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Display time & battery info in reader".tl,
|
||||
@@ -203,8 +251,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show system status bar".tl,
|
||||
@@ -212,8 +260,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showSystemStatusBar");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
SelectSetting(
|
||||
title: "Quick collect image".tl,
|
||||
@@ -229,8 +277,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
help:
|
||||
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode"
|
||||
.tl,
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_CallbackSetting(
|
||||
title: "Custom Image Processing".tl,
|
||||
@@ -243,8 +291,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
interval: 1,
|
||||
min: 1,
|
||||
max: 16,
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show Page Number".tl,
|
||||
@@ -252,39 +300,9 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showPageNumberInReader");
|
||||
},
|
||||
comicId: widget.comicId,
|
||||
comicSource: widget.comicSource,
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
// reset button
|
||||
SliverToBoxAdapter(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (widget.comicId == null) {
|
||||
appdata.settings.resetAllComicReaderSettings();
|
||||
} else {
|
||||
var keys = appdata
|
||||
.settings['comicSpecificSettings']["${widget.comicId}@${widget.comicSource}"]
|
||||
?.keys;
|
||||
appdata.settings.resetComicReaderSettings(
|
||||
widget.comicId!,
|
||||
widget.comicSource!,
|
||||
);
|
||||
if (keys != null) {
|
||||
setState(() {});
|
||||
for (var key in keys) {
|
||||
widget.onChanged?.call(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
(widget.comicId == null
|
||||
? "Clear specific reader settings for all comics"
|
||||
: "Clear specific reader settings for this comic")
|
||||
.tl,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user