mirror of
https://github.com/venera-app/venera.git
synced 2025-12-16 23:11:15 +00:00
Compare commits
26 Commits
5a76a10fb2
...
feat/js-di
| Author | SHA1 | Date | |
|---|---|---|---|
| e8d98e8274 | |||
| 09a1d2821c | |||
|
|
7842b5a1ac | ||
|
|
079f574e2f | ||
|
|
b08f11f6ac | ||
|
|
cd925df125 | ||
|
|
8c87c4a906 | ||
|
|
c234a53518 | ||
| 49fd64358c | |||
| 3426d707fe | |||
| ebc106d45b | |||
| 0cda9a2921 | |||
| 0eb5d76687 | |||
| 29d25f7fcd | |||
| 7d60e78f27 | |||
|
|
e93b56a008 | ||
|
|
d10873a903 | ||
|
|
2d27f7d650 | ||
| e1fbdfbd50 | |||
| 0a5b70b161 | |||
| 9173665afe | |||
| e0ea449c17 | |||
| c438a84537 | |||
| 8c625e212a | |||
| ab786ed2ab | |||
| d9303aab2e |
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -116,6 +116,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
choco install yq -y
|
choco install yq -y
|
||||||
pip install httpx
|
pip install httpx
|
||||||
|
- name: Install Inno Setup
|
||||||
|
run: choco install innosetup --no-progress
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
@@ -170,6 +172,9 @@ jobs:
|
|||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate flutter_to_debian
|
||||||
|
- name: "Patch font"
|
||||||
|
run: |
|
||||||
|
dart run patch/font.dart
|
||||||
- run: python3 debian/build.py arm64
|
- run: python3 debian/build.py arm64
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
13
.github/workflows/update_alt_store.yml
vendored
13
.github/workflows/update_alt_store.yml
vendored
@@ -40,8 +40,19 @@ jobs:
|
|||||||
if git diff --staged --quiet; then
|
if git diff --staged --quiet; then
|
||||||
echo "changes=false" >> $GITHUB_OUTPUT
|
echo "changes=false" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
|
# Create a new branch for the PR
|
||||||
|
branch_name="update-altstore-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
git checkout -b "$branch_name"
|
||||||
git commit -m "Updated source with latest release"
|
git commit -m "Updated source with latest release"
|
||||||
git push
|
git push -u origin "$branch_name"
|
||||||
|
|
||||||
|
# Create PR using GitHub CLI
|
||||||
|
gh pr create \
|
||||||
|
--title "Update AltStore source with latest release" \
|
||||||
|
--body "This PR updates the alt_store.json file with the latest release information." \
|
||||||
|
--head "$branch_name" \
|
||||||
|
--base master
|
||||||
|
|
||||||
echo "changes=true" >> $GITHUB_OUTPUT
|
echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@
|
|||||||
"bundleIdentifier": "com.github.wgh136.venera",
|
"bundleIdentifier": "com.github.wgh136.venera",
|
||||||
"developerName": "wgh136",
|
"developerName": "wgh136",
|
||||||
"subtitle": "A comic reader that supports reading local and network comics",
|
"subtitle": "A comic reader that supports reading local and network comics",
|
||||||
"version": "1.4.5",
|
"version": "1.5.3",
|
||||||
"versionDate": "2025-06-18",
|
"versionDate": "2025-10-13",
|
||||||
"versionDescription": "1. Fixed an abnormal single image height issue when \"imagesPerPage > 1\". 379 \r\n2. Fixed an invalid page calculation issue when \"showSingleImageOnFirstPage\" is enabled. \r\n3. Fixed an issue with incorrect reading history when displaying a single image on the first page. \r\n4. Fixed abnormal history recording when pages are not flipped. 392 \r\n5. Fixed an issue where the download task would stop after exiting the reader. 387 \r\n6. Fixed a \"RangeError\" when translating tags. 356 \r\n7. Reset the current folder to null on the favorites page if the folder is invalid. 389 \r\n8. Fixed various issues when using a custom download path on Android. 400 \r\n9. Set the initial chapter to the first downloaded chapter if no history exists when starting to read a local comic. 405 \r\n10. Removed the config file repository URL from the app.",
|
"versionDescription": "1. Fix an issue where the app freezes after swiping back on Android. 544\r\n2. Enable minification when building for Android. 547\r\n3. Prevent the app from creating an archive download task when the archive URL is an empty string.",
|
||||||
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.4.5/venera-ios-1.4.5%2B145.ipa",
|
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.5.3/venera-ios-1.5.3%2B153.ipa",
|
||||||
"localizedDescription": "A comic reader that supports reading local and network comics",
|
"localizedDescription": "A comic reader that supports reading local and network comics",
|
||||||
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
|
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
|
||||||
"tintColor": "#0784FC",
|
"tintColor": "#0784FC",
|
||||||
"category": "utilities",
|
"category": "utilities",
|
||||||
"size": 14960268,
|
"size": 15047841,
|
||||||
"appPermissions": {
|
"appPermissions": {
|
||||||
"entitlements": [
|
"entitlements": [
|
||||||
"application-identifier",
|
"application-identifier",
|
||||||
@@ -39,6 +39,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.5.3",
|
||||||
|
"date": "2025-10-13",
|
||||||
|
"localizedDescription": "1. Fix an issue where the app freezes after swiping back on Android. 544\r\n2. Enable minification when building for Android. 547\r\n3. Prevent the app from creating an archive download task when the archive URL is an empty string.",
|
||||||
|
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.5.3/venera-ios-1.5.3%2B153.ipa",
|
||||||
|
"size": 15047841
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"date": "2025-06-18",
|
"date": "2025-06-18",
|
||||||
@@ -59,6 +66,16 @@
|
|||||||
"tintColor": "#0784FC",
|
"tintColor": "#0784FC",
|
||||||
"title": "v1.4.5 - Venera 18/06/25",
|
"title": "v1.4.5 - Venera 18/06/25",
|
||||||
"url": "https://github.com/venera-app/venera/releases/tag/v1.4.5"
|
"url": "https://github.com/venera-app/venera/releases/tag/v1.4.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appID": "com.github.wgh136.venera",
|
||||||
|
"caption": "Update of Venera just got released!",
|
||||||
|
"date": "2025-10-13T12:47:27Z",
|
||||||
|
"identifier": "release-v1.5.3",
|
||||||
|
"notify": true,
|
||||||
|
"tintColor": "#0784FC",
|
||||||
|
"title": "v1.5.3 - Venera 13/10/25",
|
||||||
|
"url": "https://github.com/venera-app/venera/releases/tag/v1.5.3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ linter:
|
|||||||
rules:
|
rules:
|
||||||
collection_methods_unrelated_type: false
|
collection_methods_unrelated_type: false
|
||||||
use_build_context_synchronously: false
|
use_build_context_synchronously: false
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
avoid_print: false
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
|
|||||||
@@ -84,9 +84,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// Temporarily solution to fix crash
|
minifyEnabled true
|
||||||
minifyEnabled false
|
shrinkResources true
|
||||||
shrinkResources false
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1334,7 +1334,7 @@ let UI = {
|
|||||||
* Show an input dialog
|
* Show an input dialog
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
||||||
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha.
|
* @param image {string | ArrayBuffer | null | undefined} - Since 1.4.6, you can pass an image url to show an image in the dialog. Since 1.5.3, you can also pass an ArrayBuffer to show a custom image.
|
||||||
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
||||||
*/
|
*/
|
||||||
showInputDialog: (title, validator, image) => {
|
showInputDialog: (title, validator, image) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -40,7 +42,6 @@ mixin class JsUiApi {
|
|||||||
var image = message['image'];
|
var image = message['image'];
|
||||||
if (title is! String) return;
|
if (title is! String) return;
|
||||||
if (validator != null && validator is! JSInvokable) return;
|
if (validator != null && validator is! JSInvokable) return;
|
||||||
if (image != null && image is! String) return;
|
|
||||||
return _showInputDialog(title, validator, image);
|
return _showInputDialog(title, validator, image);
|
||||||
case 'showSelectDialog':
|
case 'showSelectDialog':
|
||||||
var title = message['title'];
|
var title = message['title'];
|
||||||
@@ -126,13 +127,25 @@ mixin class JsUiApi {
|
|||||||
controller?.close();
|
controller?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _showInputDialog(String title, JSInvokable? validator, String? image) async {
|
Future<String?> _showInputDialog(String title, JSInvokable? validator, dynamic image) async {
|
||||||
String? result;
|
String? result;
|
||||||
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
||||||
|
String? imageUrl;
|
||||||
|
Uint8List? imageData;
|
||||||
|
if (image != null) {
|
||||||
|
if (image is String) {
|
||||||
|
imageUrl = image;
|
||||||
|
} else if (image is Uint8List) {
|
||||||
|
imageData = image;
|
||||||
|
} else if (image is List<int>) {
|
||||||
|
imageData = Uint8List.fromList(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
await showInputDialog(
|
await showInputDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
title: title,
|
title: title,
|
||||||
image: image,
|
image: imageUrl,
|
||||||
|
imageData: imageData,
|
||||||
onConfirm: (v) {
|
onConfirm: (v) {
|
||||||
if (func != null) {
|
if (func != null) {
|
||||||
var res = func.call([v]);
|
var res = func.call([v]);
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ Future<void> showInputDialog({
|
|||||||
String cancelText = "Cancel",
|
String cancelText = "Cancel",
|
||||||
RegExp? inputValidator,
|
RegExp? inputValidator,
|
||||||
String? image,
|
String? image,
|
||||||
|
Uint8List? imageData,
|
||||||
}) {
|
}) {
|
||||||
var controller = TextEditingController(text: initialValue);
|
var controller = TextEditingController(text: initialValue);
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
@@ -379,6 +380,11 @@ Future<void> showInputDialog({
|
|||||||
height: 108,
|
height: 108,
|
||||||
child: Image.network(image, fit: BoxFit.none),
|
child: Image.network(image, fit: BoxFit.none),
|
||||||
).paddingBottom(8),
|
).paddingBottom(8),
|
||||||
|
if (image == null && imageData != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 108,
|
||||||
|
child: Image.memory(imageData, fit: BoxFit.none),
|
||||||
|
).paddingBottom(8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.5.1";
|
final version = "1.5.3";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
context,
|
context,
|
||||||
animation,
|
animation,
|
||||||
secondaryAnimation,
|
secondaryAnimation,
|
||||||
enableIOSGesture
|
enableIOSGesture && App.isIOS
|
||||||
? IOSBackGestureDetector(
|
? IOSBackGestureDetector(
|
||||||
gestureWidth: _kBackGestureWidth,
|
gestureWidth: _kBackGestureWidth,
|
||||||
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
||||||
@@ -302,7 +302,7 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
|
|||||||
assert(mounted);
|
assert(mounted);
|
||||||
assert(_backGestureController != null);
|
assert(_backGestureController != null);
|
||||||
_backGestureController!.dragUpdate(
|
_backGestureController!.dragUpdate(
|
||||||
_convertToLogical(details.primaryDelta! / context.size!.width));
|
_convertToLogical(details.primaryDelta! / context.size!.width));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
@@ -213,12 +214,10 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
|
|
||||||
late Map<String, int> counts;
|
late Map<String, int> counts;
|
||||||
|
|
||||||
|
var _hashedIds = <int, int>{};
|
||||||
|
|
||||||
int get totalComics {
|
int get totalComics {
|
||||||
int total = 0;
|
return _hashedIds.length;
|
||||||
for (var t in counts.values) {
|
|
||||||
total += t;
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int folderComics(String folder) {
|
int folderComics(String folder) {
|
||||||
@@ -280,6 +279,48 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
for (var folder in folderNames) {
|
for (var folder in folderNames) {
|
||||||
counts[folder] = count(folder);
|
counts[folder] = count(folder);
|
||||||
}
|
}
|
||||||
|
_initHashedIds(folderNames, _db.handle).then((value) {
|
||||||
|
_hashedIds = value;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshHashedIds() {
|
||||||
|
_initHashedIds(folderNames, _db.handle).then((value) {
|
||||||
|
_hashedIds = value;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void reduceHashedId(String id, int type) {
|
||||||
|
var hash = id.hashCode ^ type;
|
||||||
|
if (_hashedIds.containsKey(hash)) {
|
||||||
|
if (_hashedIds[hash]! > 1) {
|
||||||
|
_hashedIds[hash] = _hashedIds[hash]! - 1;
|
||||||
|
} else {
|
||||||
|
_hashedIds.remove(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<int, int>> _initHashedIds(
|
||||||
|
List<String> folders, Pointer<void> p) {
|
||||||
|
return Isolate.run(() {
|
||||||
|
var db = sqlite3.fromPointer(p);
|
||||||
|
var hashedIds = <int, int>{};
|
||||||
|
for (var folder in folders) {
|
||||||
|
var rows = db.select("""
|
||||||
|
select id, type from "$folder";
|
||||||
|
""");
|
||||||
|
for (var row in rows) {
|
||||||
|
var id = row["id"] as String;
|
||||||
|
var type = row["type"] as int;
|
||||||
|
var hash = id.hashCode ^ type;
|
||||||
|
hashedIds[hash] = (hashedIds[hash] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashedIds;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> find(String id, ComicType type) {
|
List<String> find(String id, ComicType type) {
|
||||||
@@ -559,7 +600,6 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
/// return true if success, false if already exists
|
/// return true if success, false if already exists
|
||||||
bool addComic(String folder, FavoriteItem comic,
|
bool addComic(String folder, FavoriteItem comic,
|
||||||
[int? order, String? updateTime]) {
|
[int? order, String? updateTime]) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
if (!existsFolder(folder)) {
|
if (!existsFolder(folder)) {
|
||||||
throw Exception("Folder does not exists");
|
throw Exception("Folder does not exists");
|
||||||
}
|
}
|
||||||
@@ -614,14 +654,14 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
counts[folder] = counts[folder]! + 1;
|
counts[folder] = counts[folder]! + 1;
|
||||||
}
|
}
|
||||||
|
var hash = comic.id.hashCode ^ comic.type.value;
|
||||||
|
_hashedIds[hash] = (_hashedIds[hash] ?? 0) + 1;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void moveFavorite(
|
void moveFavorite(
|
||||||
String sourceFolder, String targetFolder, String id, ComicType type) {
|
String sourceFolder, String targetFolder, String id, ComicType type) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
|
|
||||||
if (!existsFolder(sourceFolder)) {
|
if (!existsFolder(sourceFolder)) {
|
||||||
throw Exception("Source folder does not exist");
|
throw Exception("Source folder does not exist");
|
||||||
}
|
}
|
||||||
@@ -655,8 +695,6 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
|
|
||||||
void batchMoveFavorites(
|
void batchMoveFavorites(
|
||||||
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
|
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
|
|
||||||
if (!existsFolder(sourceFolder)) {
|
if (!existsFolder(sourceFolder)) {
|
||||||
throw Exception("Source folder does not exist");
|
throw Exception("Source folder does not exist");
|
||||||
}
|
}
|
||||||
@@ -691,25 +729,15 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
_db.execute("COMMIT");
|
_db.execute("COMMIT");
|
||||||
|
|
||||||
// Update counts
|
// Update counts
|
||||||
if (counts[targetFolder] == null) {
|
counts[targetFolder] = count(targetFolder);
|
||||||
counts[targetFolder] = count(targetFolder);
|
counts[sourceFolder] = count(sourceFolder);
|
||||||
} else {
|
refreshHashedIds();
|
||||||
counts[targetFolder] = counts[targetFolder]! + items.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counts[sourceFolder] != null) {
|
|
||||||
counts[sourceFolder] = counts[sourceFolder]! - items.length;
|
|
||||||
} else {
|
|
||||||
counts[sourceFolder] = count(sourceFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void batchCopyFavorites(
|
void batchCopyFavorites(
|
||||||
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
|
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
|
|
||||||
if (!existsFolder(sourceFolder)) {
|
if (!existsFolder(sourceFolder)) {
|
||||||
throw Exception("Source folder does not exist");
|
throw Exception("Source folder does not exist");
|
||||||
}
|
}
|
||||||
@@ -740,18 +768,14 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
_db.execute("COMMIT");
|
_db.execute("COMMIT");
|
||||||
|
|
||||||
// Update counts
|
// Update counts
|
||||||
if (counts[targetFolder] == null) {
|
counts[targetFolder] = count(targetFolder);
|
||||||
counts[targetFolder] = count(targetFolder);
|
refreshHashedIds();
|
||||||
} else {
|
|
||||||
counts[targetFolder] = counts[targetFolder]! + items.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// delete a folder
|
/// delete a folder
|
||||||
void deleteFolder(String name) {
|
void deleteFolder(String name) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
drop table "$name";
|
drop table "$name";
|
||||||
""");
|
""");
|
||||||
@@ -760,11 +784,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
where folder_name == ?;
|
where folder_name == ?;
|
||||||
""", [name]);
|
""", [name]);
|
||||||
counts.remove(name);
|
counts.remove(name);
|
||||||
|
refreshHashedIds();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteComicWithId(String folder, String id, ComicType type) {
|
void deleteComicWithId(String folder, String id, ComicType type) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
LocalFavoriteImageProvider.delete(id, type.value);
|
LocalFavoriteImageProvider.delete(id, type.value);
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
delete from "$folder"
|
delete from "$folder"
|
||||||
@@ -775,11 +799,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
counts[folder] = count(folder);
|
counts[folder] = count(folder);
|
||||||
}
|
}
|
||||||
|
reduceHashedId(id, type.value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void batchDeleteComics(String folder, List<FavoriteItem> comics) {
|
void batchDeleteComics(String folder, List<FavoriteItem> comics) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
_db.execute("BEGIN TRANSACTION");
|
_db.execute("BEGIN TRANSACTION");
|
||||||
try {
|
try {
|
||||||
for (var comic in comics) {
|
for (var comic in comics) {
|
||||||
@@ -800,11 +824,13 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_db.execute("COMMIT");
|
_db.execute("COMMIT");
|
||||||
|
for (var comic in comics) {
|
||||||
|
reduceHashedId(comic.id, comic.type.value);
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void batchDeleteComicsInAllFolders(List<ComicID> comics) {
|
void batchDeleteComicsInAllFolders(List<ComicID> comics) {
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
_db.execute("BEGIN TRANSACTION");
|
_db.execute("BEGIN TRANSACTION");
|
||||||
var folderNames = _getFolderNamesWithDB();
|
var folderNames = _getFolderNamesWithDB();
|
||||||
try {
|
try {
|
||||||
@@ -824,6 +850,10 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
initCounts();
|
initCounts();
|
||||||
_db.execute("COMMIT");
|
_db.execute("COMMIT");
|
||||||
|
for (var comic in comics) {
|
||||||
|
var hash = comic.id.hashCode ^ comic.type.value;
|
||||||
|
_hashedIds.remove(hash);
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,7 +938,6 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
markAsRead(id, type);
|
markAsRead(id, type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_modifiedAfterLastCache = true;
|
|
||||||
var followUpdatesFolder = appdata.settings['followUpdatesFolder'];
|
var followUpdatesFolder = appdata.settings['followUpdatesFolder'];
|
||||||
for (final folder in folderNames) {
|
for (final folder in folderNames) {
|
||||||
var rows = _db.select("""
|
var rows = _db.select("""
|
||||||
@@ -1029,28 +1058,9 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _cachedFavoritedIds = <String, bool>{};
|
|
||||||
|
|
||||||
bool isExist(String id, ComicType type) {
|
bool isExist(String id, ComicType type) {
|
||||||
if (_modifiedAfterLastCache) {
|
var hash = id.hashCode ^ type.value;
|
||||||
_cacheFavoritedIds();
|
return _hashedIds.containsKey(hash);
|
||||||
}
|
|
||||||
return _cachedFavoritedIds.containsKey("$id@${type.value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _modifiedAfterLastCache = true;
|
|
||||||
|
|
||||||
void _cacheFavoritedIds() {
|
|
||||||
_modifiedAfterLastCache = false;
|
|
||||||
_cachedFavoritedIds.clear();
|
|
||||||
for (var folder in folderNames) {
|
|
||||||
var rows = _db.select("""
|
|
||||||
select id, type from "$folder";
|
|
||||||
""");
|
|
||||||
for (var row in rows) {
|
|
||||||
_cachedFavoritedIds["${row["id"]}@${row["type"]}"] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) {
|
void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/utils/channel.dart';
|
||||||
|
|
||||||
class ComicUpdateResult {
|
class ComicUpdateResult {
|
||||||
final bool updated;
|
final bool updated;
|
||||||
@@ -62,6 +63,7 @@ Future<ComicUpdateResult> updateComic(
|
|||||||
return ComicUpdateResult(updated, null);
|
return ComicUpdateResult(updated, null);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Check Updates", e, s);
|
Log.error("Check Updates", e, s);
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
retries--;
|
retries--;
|
||||||
if (retries == 0) {
|
if (retries == 0) {
|
||||||
return ComicUpdateResult(false, e.toString());
|
return ComicUpdateResult(false, e.toString());
|
||||||
@@ -114,23 +116,50 @@ void updateFolderBase(
|
|||||||
current = 0;
|
current = 0;
|
||||||
stream.add(UpdateProgress(total, current, errors, updated));
|
stream.add(UpdateProgress(total, current, errors, updated));
|
||||||
|
|
||||||
var futures = <Future>[];
|
var channel = Channel<FavoriteItemWithUpdateInfo>(10);
|
||||||
for (var comic in comicsToUpdate) {
|
|
||||||
var future = updateComic(comic, folder).then((result) {
|
// Producer
|
||||||
current++;
|
() async {
|
||||||
if (result.updated) {
|
var c = 0;
|
||||||
updated++;
|
for (var comic in comicsToUpdate) {
|
||||||
|
await channel.push(comic);
|
||||||
|
c++;
|
||||||
|
// Throttle
|
||||||
|
if (c % 5 == 0) {
|
||||||
|
var delay = c % 100 + 1;
|
||||||
|
if (delay > 10) {
|
||||||
|
delay = 10;
|
||||||
|
}
|
||||||
|
await Future.delayed(Duration(seconds: delay));
|
||||||
}
|
}
|
||||||
if (result.errorMessage != null) {
|
}
|
||||||
errors++;
|
channel.close();
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Consumers
|
||||||
|
var updateFutures = <Future>[];
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
var f = () async {
|
||||||
|
while (true) {
|
||||||
|
var comic = await channel.pop();
|
||||||
|
if (comic == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var result = await updateComic(comic, folder);
|
||||||
|
current++;
|
||||||
|
if (result.updated) {
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
if (result.errorMessage != null) {
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
stream.add(UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
|
||||||
}
|
}
|
||||||
stream.add(
|
}();
|
||||||
UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
|
updateFutures.add(f);
|
||||||
});
|
|
||||||
futures.add(future);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(futures);
|
await Future.wait(updateFutures);
|
||||||
|
|
||||||
if (updated > 0) {
|
if (updated > 0) {
|
||||||
LocalFavoritesManager().notifyChanges();
|
LocalFavoritesManager().notifyChanges();
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (comic.hasChapters) {
|
if (comic.hasChapters) {
|
||||||
var cid =
|
var cid =
|
||||||
ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String);
|
ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String);
|
||||||
|
cid = getChapterDirectoryName(cid);
|
||||||
directory = Directory(FilePath.join(directory.path, cid));
|
directory = Directory(FilePath.join(directory.path, cid));
|
||||||
}
|
}
|
||||||
var files = <File>[];
|
var files = <File>[];
|
||||||
@@ -600,7 +601,10 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
var shouldRemovedDirs = <Directory>[];
|
var shouldRemovedDirs = <Directory>[];
|
||||||
for (var chapter in chapters) {
|
for (var chapter in chapters) {
|
||||||
var dir = Directory(FilePath.join(c.baseDir, chapter));
|
var dir = Directory(FilePath.join(
|
||||||
|
c.baseDir,
|
||||||
|
getChapterDirectoryName(chapter),
|
||||||
|
));
|
||||||
if (dir.existsSync()) {
|
if (dir.existsSync()) {
|
||||||
shouldRemovedDirs.add(dir);
|
shouldRemovedDirs.add(dir);
|
||||||
}
|
}
|
||||||
@@ -668,6 +672,21 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String getChapterDirectoryName(String name) {
|
||||||
|
var builder = StringBuffer();
|
||||||
|
for (var i = 0; i < name.length; i++) {
|
||||||
|
var char = name[i];
|
||||||
|
if (char == '/' || char == '\\' || char == ':' || char == '*' ||
|
||||||
|
char == '?'
|
||||||
|
|| char == '"' || char == '<' || char == '>' || char == '|') {
|
||||||
|
builder.write('_');
|
||||||
|
} else {
|
||||||
|
builder.write(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocalSortType {
|
enum LocalSortType {
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
tertiary = light.tertiary;
|
tertiary = light.tertiary;
|
||||||
}
|
}
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
title: "venera",
|
||||||
home: home,
|
home: home,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: getTheme(primary, secondary, tertiary, Brightness.light),
|
theme: getTheme(primary, secondary, tertiary, Brightness.light),
|
||||||
@@ -246,7 +247,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
/// https://github.com/flutter/flutter/issues/161086
|
/// https://github.com/flutter/flutter/issues/161086
|
||||||
var isPaddingCheckError =
|
var isPaddingCheckError =
|
||||||
MediaQuery.of(context).viewPadding.top <= 0 ||
|
MediaQuery.of(context).viewPadding.top <= 0 ||
|
||||||
MediaQuery.of(context).viewPadding.top > 50;
|
MediaQuery.of(context).viewPadding.top > 200;
|
||||||
|
|
||||||
if (isPaddingCheckError && Platform.isAndroid) {
|
if (isPaddingCheckError && Platform.isAndroid) {
|
||||||
widget = MediaQuery(
|
widget = MediaQuery(
|
||||||
|
|||||||
@@ -107,7 +107,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
var local = LocalManager().find(id, comicType);
|
var local = LocalManager().find(id, comicType);
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
if (local == null) {
|
if (local == null) {
|
||||||
Directory(path!).deleteIgnoreError(recursive: true);
|
Future.sync(() async {
|
||||||
|
var tasks = this.tasks.values.toList();
|
||||||
|
for (var i = 0; i < tasks.length; i++) {
|
||||||
|
if (!tasks[i].isComplete) {
|
||||||
|
tasks[i].cancel();
|
||||||
|
await tasks[i].wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Directory(path!).delete(recursive: true);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
Log.error("Download", "Failed to delete directory: $e");
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (chapters != null) {
|
} else if (chapters != null) {
|
||||||
for (var c in chapters!) {
|
for (var c in chapters!) {
|
||||||
var dir = Directory(FilePath.join(path!, c));
|
var dir = Directory(FilePath.join(path!, c));
|
||||||
@@ -197,7 +211,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
if (comic!.chapters != null) {
|
if (comic!.chapters != null) {
|
||||||
saveTo = Directory(FilePath.join(
|
saveTo = Directory(FilePath.join(
|
||||||
path!,
|
path!,
|
||||||
_images!.keys.elementAt(_chapter),
|
LocalManager.getChapterDirectoryName(
|
||||||
|
_images!.keys.elementAt(_chapter),
|
||||||
|
),
|
||||||
));
|
));
|
||||||
if (!saveTo.existsSync()) {
|
if (!saveTo.existsSync()) {
|
||||||
saveTo.createSync(recursive: true);
|
saveTo.createSync(recursive: true);
|
||||||
|
|||||||
@@ -181,7 +181,15 @@ abstract class ImageDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configs['onResponse'] is JSInvokable) {
|
if (configs['onResponse'] is JSInvokable) {
|
||||||
buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
|
dynamic result = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
|
||||||
|
if (result is Future) {
|
||||||
|
result = await result;
|
||||||
|
}
|
||||||
|
if (result is List<int>) {
|
||||||
|
buffer = result;
|
||||||
|
} else {
|
||||||
|
throw "Error: Invalid onResponse result.";
|
||||||
|
}
|
||||||
(configs['onResponse'] as JSInvokable).free();
|
(configs['onResponse'] as JSInvokable).free();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,39 +17,50 @@ class CategoriesPage extends StatefulWidget {
|
|||||||
State<CategoriesPage> createState() => _CategoriesPageState();
|
State<CategoriesPage> createState() => _CategoriesPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CategoriesPageState extends State<CategoriesPage> {
|
class _CategoriesPageState extends State<CategoriesPage>
|
||||||
|
with
|
||||||
|
TickerProviderStateMixin,
|
||||||
|
AutomaticKeepAliveClientMixin<CategoriesPage> {
|
||||||
var categories = <String>[];
|
var categories = <String>[];
|
||||||
|
|
||||||
|
late TabController controller;
|
||||||
|
|
||||||
void onSettingsChanged() {
|
void onSettingsChanged() {
|
||||||
var categories =
|
var categories = List.from(
|
||||||
List.from(appdata.settings["categories"]).whereType<String>().toList();
|
appdata.settings["categories"],
|
||||||
|
).whereType<String>().toList();
|
||||||
var allCategories = ComicSource.all()
|
var allCategories = ComicSource.all()
|
||||||
.map((e) => e.categoryData?.key)
|
.map((e) => e.categoryData?.key)
|
||||||
.where((element) => element != null)
|
.where((element) => element != null)
|
||||||
.map((e) => e!)
|
.map((e) => e!)
|
||||||
.toList();
|
.toList();
|
||||||
categories =
|
categories = categories
|
||||||
categories.where((element) => allCategories.contains(element)).toList();
|
.where((element) => allCategories.contains(element))
|
||||||
|
.toList();
|
||||||
if (!categories.isEqualTo(this.categories)) {
|
if (!categories.isEqualTo(this.categories)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
this.categories = categories;
|
this.categories = categories;
|
||||||
});
|
});
|
||||||
|
controller = TabController(length: categories.length, vsync: this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
var categories =
|
var categories = List.from(
|
||||||
List.from(appdata.settings["categories"]).whereType<String>().toList();
|
appdata.settings["categories"],
|
||||||
|
).whereType<String>().toList();
|
||||||
var allCategories = ComicSource.all()
|
var allCategories = ComicSource.all()
|
||||||
.map((e) => e.categoryData?.key)
|
.map((e) => e.categoryData?.key)
|
||||||
.where((element) => element != null)
|
.where((element) => element != null)
|
||||||
.map((e) => e!)
|
.map((e) => e!)
|
||||||
.toList();
|
.toList();
|
||||||
this.categories =
|
this.categories = categories
|
||||||
categories.where((element) => allCategories.contains(element)).toList();
|
.where((element) => allCategories.contains(element))
|
||||||
|
.toList();
|
||||||
appdata.settings.addListener(onSettingsChanged);
|
appdata.settings.addListener(onSettingsChanged);
|
||||||
|
controller = TabController(length: categories.length, vsync: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPage() {
|
void addPage() {
|
||||||
@@ -59,6 +70,7 @@ class _CategoriesPageState extends State<CategoriesPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
controller.dispose();
|
||||||
appdata.settings.removeListener(onSettingsChanged);
|
appdata.settings.removeListener(onSettingsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,46 +97,45 @@ class _CategoriesPageState extends State<CategoriesPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
if (categories.isEmpty) {
|
if (categories.isEmpty) {
|
||||||
return buildEmpty();
|
return buildEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
child: DefaultTabController(
|
child: Column(
|
||||||
length: categories.length,
|
children: [
|
||||||
key: Key(categories.toString()),
|
AppTabBar(
|
||||||
child: Column(
|
controller: controller,
|
||||||
children: [
|
key: PageStorageKey(categories.toString()),
|
||||||
AppTabBar(
|
tabs: categories.map((e) {
|
||||||
key: PageStorageKey(categories.toString()),
|
String title = e;
|
||||||
tabs: categories.map((e) {
|
try {
|
||||||
String title = e;
|
title = getCategoryDataWithKey(e).title;
|
||||||
try {
|
} catch (e) {
|
||||||
title = getCategoryDataWithKey(e).title;
|
//
|
||||||
} catch (e) {
|
}
|
||||||
//
|
return Tab(text: title, key: Key(e));
|
||||||
}
|
}).toList(),
|
||||||
return Tab(
|
actionButton: TabActionButton(
|
||||||
text: title,
|
icon: const Icon(Icons.add),
|
||||||
key: Key(e),
|
text: "Add".tl,
|
||||||
);
|
onPressed: addPage,
|
||||||
}).toList(),
|
),
|
||||||
actionButton: TabActionButton(
|
).paddingTop(context.padding.top),
|
||||||
icon: const Icon(Icons.add),
|
Expanded(
|
||||||
text: "Add".tl,
|
child: TabBarView(
|
||||||
onPressed: addPage,
|
controller: controller,
|
||||||
),
|
children: categories.map((e) => _CategoryPage(e)).toList(),
|
||||||
).paddingTop(context.padding.top),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TabBarView(
|
],
|
||||||
children: categories.map((e) => _CategoryPage(e)).toList(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef ClickTagCallback = void Function(String, String?);
|
typedef ClickTagCallback = void Function(String, String?);
|
||||||
@@ -150,38 +161,42 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
if (data.enableRankingPage || data.buttons.isNotEmpty) {
|
if (data.enableRankingPage || data.buttons.isNotEmpty) {
|
||||||
children.add(buildTitle(data.title));
|
children.add(buildTitle(data.title));
|
||||||
children.add(Padding(
|
children.add(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
Padding(
|
||||||
child: Wrap(
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
||||||
children: [
|
child: Wrap(
|
||||||
if (data.enableRankingPage)
|
children: [
|
||||||
buildTag("Ranking".tl, () {
|
if (data.enableRankingPage)
|
||||||
context.to(() => RankingPage(categoryKey: data.key));
|
buildTag("Ranking".tl, () {
|
||||||
}),
|
context.to(() => RankingPage(categoryKey: data.key));
|
||||||
for (var buttonData in data.buttons)
|
}),
|
||||||
buildTag(buttonData.label.tl, buttonData.onTap)
|
for (var buttonData in data.buttons)
|
||||||
],
|
buildTag(buttonData.label.tl, buttonData.onTap),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var part in data.categories) {
|
for (var part in data.categories) {
|
||||||
if (part.enableRandom) {
|
if (part.enableRandom) {
|
||||||
children.add(StatefulBuilder(builder: (context, updater) {
|
children.add(
|
||||||
return Column(
|
StatefulBuilder(
|
||||||
mainAxisSize: MainAxisSize.min,
|
builder: (context, updater) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
buildTitleWithRefresh(part.title, () => updater(() {})),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
buildTags(part.categories)
|
children: [
|
||||||
],
|
buildTitleWithRefresh(part.title, () => updater(() {})),
|
||||||
);
|
buildTags(part.categories),
|
||||||
}));
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
children.add(buildTitle(part.title));
|
children.add(buildTitle(part.title));
|
||||||
children.add(
|
children.add(buildTags(part.categories));
|
||||||
buildTags(part.categories),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
@@ -195,8 +210,10 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
Widget buildTitle(String title) {
|
Widget buildTitle(String title) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(16, 10, 5, 10),
|
||||||
child: Text(title.tl,
|
child: Text(
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500)),
|
title.tl,
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,21 +224,16 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title.tl,
|
title.tl,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(onPressed: onRefresh, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: onRefresh, icon: const Icon(Icons.refresh)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTags(
|
Widget buildTags(List<CategoryItem> categories) {
|
||||||
List<CategoryItem> categories,
|
|
||||||
) {
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
|||||||
@@ -155,64 +155,60 @@ abstract mixin class _ComicPageActions {
|
|||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Download".tl,
|
title: "Download".tl,
|
||||||
content: Column(
|
content: RadioGroup<int>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
groupValue: selected,
|
||||||
children: [
|
onChanged: (v) {
|
||||||
RadioListTile<int>(
|
setState(() {
|
||||||
value: -1,
|
selected = v ?? selected;
|
||||||
groupValue: selected,
|
});
|
||||||
title: Text("Normal".tl),
|
},
|
||||||
onChanged: (v) {
|
child: Column(
|
||||||
setState(() {
|
mainAxisSize: MainAxisSize.min,
|
||||||
selected = v!;
|
children: [
|
||||||
});
|
RadioListTile<int>(
|
||||||
},
|
value: -1,
|
||||||
),
|
title: Text("Normal".tl),
|
||||||
ExpansionTile(
|
|
||||||
title: Text("Archive".tl),
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
),
|
),
|
||||||
collapsedShape: const RoundedRectangleBorder(
|
ExpansionTile(
|
||||||
borderRadius: BorderRadius.zero,
|
title: Text("Archive".tl),
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
onExpansionChanged: (b) {
|
borderRadius: BorderRadius.zero,
|
||||||
if (!isLoading && b && archives == null) {
|
),
|
||||||
isLoading = true;
|
collapsedShape: const RoundedRectangleBorder(
|
||||||
comicSource.archiveDownloader!
|
borderRadius: BorderRadius.zero,
|
||||||
.getArchives(comic.id)
|
),
|
||||||
.then((value) {
|
onExpansionChanged: (b) {
|
||||||
if (value.success) {
|
if (!isLoading && b && archives == null) {
|
||||||
archives = value.data;
|
isLoading = true;
|
||||||
} else {
|
comicSource.archiveDownloader!
|
||||||
App.rootContext
|
.getArchives(comic.id)
|
||||||
.showMessage(message: value.errorMessage!);
|
.then((value) {
|
||||||
}
|
if (value.success) {
|
||||||
setState(() {
|
archives = value.data;
|
||||||
isLoading = false;
|
} else {
|
||||||
|
App.rootContext
|
||||||
|
.showMessage(message: value.errorMessage!);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
},
|
children: [
|
||||||
children: [
|
if (archives == null)
|
||||||
if (archives == null)
|
const ListLoadingIndicator().toCenter()
|
||||||
const ListLoadingIndicator().toCenter()
|
else
|
||||||
else
|
for (int i = 0; i < archives!.length; i++)
|
||||||
for (int i = 0; i < archives!.length; i++)
|
RadioListTile<int>(
|
||||||
RadioListTile<int>(
|
value: i,
|
||||||
value: i,
|
title: Text(archives![i].title),
|
||||||
groupValue: selected,
|
subtitle: Text(archives![i].description),
|
||||||
onChanged: (v) {
|
)
|
||||||
setState(() {
|
],
|
||||||
selected = v!;
|
)
|
||||||
});
|
],
|
||||||
},
|
),
|
||||||
title: Text(archives![i].title),
|
|
||||||
subtitle: Text(archives![i].description),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
@@ -237,10 +233,12 @@ abstract mixin class _ComicPageActions {
|
|||||||
isGettingLink = false;
|
isGettingLink = false;
|
||||||
});
|
});
|
||||||
} else if (context.mounted) {
|
} else if (context.mounted) {
|
||||||
LocalManager()
|
if (res.data.isNotEmpty) {
|
||||||
|
LocalManager()
|
||||||
.addTask(ArchiveDownloadTask(res.data, comic));
|
.addTask(ArchiveDownloadTask(res.data, comic));
|
||||||
App.rootContext
|
App.rootContext
|
||||||
.showMessage(message: "Download started".tl);
|
.showMessage(message: "Download started".tl);
|
||||||
|
}
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -197,11 +197,12 @@ class _NetworkSectionState extends State<_NetworkSection> {
|
|||||||
if (res.subData is List) {
|
if (res.subData is List) {
|
||||||
final list = List<String>.from(res.subData);
|
final list = List<String>.from(res.subData);
|
||||||
if (list.isNotEmpty) {
|
if (list.isNotEmpty) {
|
||||||
addedFolders = {list.first};
|
addedFolders = list.toSet();
|
||||||
|
localIsFavorite = true;
|
||||||
} else {
|
} else {
|
||||||
addedFolders.clear();
|
addedFolders.clear();
|
||||||
|
localIsFavorite = false;
|
||||||
}
|
}
|
||||||
localIsFavorite = addedFolders.isNotEmpty;
|
|
||||||
} else {
|
} else {
|
||||||
addedFolders.clear();
|
addedFolders.clear();
|
||||||
localIsFavorite = false;
|
localIsFavorite = false;
|
||||||
@@ -352,62 +353,6 @@ class _NetworkSectionState extends State<_NetworkSection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMultiFolder() {
|
Widget _buildMultiFolder() {
|
||||||
if (localIsFavorite == true &&
|
|
||||||
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
|
|
||||||
return ListTile(
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Text("Network Favorites".tl),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.primaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text("Added".tl, style: ts.s12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: isLoading
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: _HoverButton(
|
|
||||||
isFavorite: true,
|
|
||||||
onTap: () async {
|
|
||||||
setState(() {
|
|
||||||
isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
var res = await widget
|
|
||||||
.comicSource
|
|
||||||
.favoriteData!
|
|
||||||
.addOrDelFavorite!(widget.cid, '', false, null);
|
|
||||||
if (res.success) {
|
|
||||||
// Invalidate network cache so subsequent loads see latest
|
|
||||||
NetworkCacheManager().clear();
|
|
||||||
setState(() {
|
|
||||||
localIsFavorite = false;
|
|
||||||
});
|
|
||||||
widget.onFavorite(false);
|
|
||||||
App.rootContext.showMessage(message: "Removed".tl);
|
|
||||||
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.showMessage(message: res.errorMessage!);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -425,8 +370,10 @@ class _NetworkSectionState extends State<_NetworkSection> {
|
|||||||
var name = entry.value;
|
var name = entry.value;
|
||||||
var id = entry.key;
|
var id = entry.key;
|
||||||
var isAdded = addedFolders.contains(id);
|
var isAdded = addedFolders.contains(id);
|
||||||
var hasSelection = addedFolders.isNotEmpty;
|
// When `singleFolderForSingleComic` is `false`, all add and remove buttons are clickable.
|
||||||
var enabled = !hasSelection || isAdded;
|
// When `singleFolderForSingleComic` is `true`, the remove button is always clickable,
|
||||||
|
// while the add button is only clickable if the comic has not been added to any list.
|
||||||
|
var enabled = !(widget.comicSource.favoriteData!.singleFolderForSingleComic && addedFolders.isNotEmpty && !isAdded);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -469,11 +416,9 @@ class _NetworkSectionState extends State<_NetworkSection> {
|
|||||||
NetworkCacheManager().clear();
|
NetworkCacheManager().clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
addedFolders.clear();
|
addedFolders.remove(id);
|
||||||
} else {
|
} else {
|
||||||
addedFolders
|
addedFolders.add(id);
|
||||||
..clear()
|
|
||||||
..add(id);
|
|
||||||
}
|
}
|
||||||
// sync local flag for single-folder-per-comic logic and parent
|
// sync local flag for single-folder-per-comic logic and parent
|
||||||
localIsFavorite = addedFolders.isNotEmpty;
|
localIsFavorite = addedFolders.isNotEmpty;
|
||||||
|
|||||||
@@ -514,51 +514,53 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Column(
|
: RadioGroup<int>(
|
||||||
key: key,
|
groupValue: type,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onChanged: (value) {
|
||||||
children: [
|
setState(() {
|
||||||
const SizedBox(width: 600),
|
type = value ?? type;
|
||||||
...List.generate(importMethods.length, (index) {
|
});
|
||||||
return RadioListTile(
|
},
|
||||||
title: Text(importMethods[index]),
|
child: Column(
|
||||||
value: index,
|
key: key,
|
||||||
groupValue: type,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onChanged: (value) {
|
children: [
|
||||||
setState(() {
|
const SizedBox(width: 600),
|
||||||
type = value as int;
|
...List.generate(importMethods.length, (index) {
|
||||||
});
|
return RadioListTile<int>(
|
||||||
},
|
title: Text(importMethods[index]),
|
||||||
);
|
value: index,
|
||||||
}),
|
);
|
||||||
if (type != 4)
|
}),
|
||||||
ListTile(
|
if (type != 4)
|
||||||
title: Text("Add to favorites".tl),
|
ListTile(
|
||||||
trailing: Select(
|
title: Text("Add to favorites".tl),
|
||||||
current: selectedFolder,
|
trailing: Select(
|
||||||
values: folders,
|
current: selectedFolder,
|
||||||
minWidth: 112,
|
values: folders,
|
||||||
onTap: (v) {
|
minWidth: 112,
|
||||||
setState(() {
|
onTap: (v) {
|
||||||
selectedFolder = folders[v];
|
setState(() {
|
||||||
});
|
selectedFolder = folders[v];
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
).paddingHorizontal(8),
|
),
|
||||||
if (!App.isIOS && !App.isMacOS && type != 2 && type != 3)
|
).paddingHorizontal(8),
|
||||||
CheckboxListTile(
|
if (!App.isIOS && !App.isMacOS && type != 2 && type != 3)
|
||||||
enabled: true,
|
CheckboxListTile(
|
||||||
title: Text("Copy to app local path".tl),
|
enabled: true,
|
||||||
value: copyToLocalFolder,
|
title: Text("Copy to app local path".tl),
|
||||||
onChanged: (v) {
|
value: copyToLocalFolder,
|
||||||
setState(() {
|
onChanged: (v) {
|
||||||
copyToLocalFolder = !copyToLocalFolder;
|
setState(() {
|
||||||
});
|
copyToLocalFolder = !copyToLocalFolder;
|
||||||
}).paddingHorizontal(8),
|
});
|
||||||
const SizedBox(height: 8),
|
}).paddingHorizontal(8),
|
||||||
Text(info).paddingHorizontal(24),
|
const SizedBox(height: 8),
|
||||||
],
|
Text(info).paddingHorizontal(24),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.text(
|
Button.text(
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -404,21 +404,23 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> {
|
|||||||
children: [
|
children: [
|
||||||
tabBar,
|
tabBar,
|
||||||
TabViewBody(children: [
|
TabViewBody(children: [
|
||||||
Column(
|
RadioGroup<ImageFavoriteSortType>(
|
||||||
children: ImageFavoriteSortType.values
|
groupValue: sortType,
|
||||||
.map(
|
onChanged: (v) {
|
||||||
(e) => RadioListTile<ImageFavoriteSortType>(
|
setState(() {
|
||||||
title: Text(e.value.tl),
|
sortType = v ?? sortType;
|
||||||
value: e,
|
});
|
||||||
groupValue: sortType,
|
},
|
||||||
onChanged: (v) {
|
child: Column(
|
||||||
setState(() {
|
children: ImageFavoriteSortType.values
|
||||||
sortType = v!;
|
.map(
|
||||||
});
|
(e) => RadioListTile<ImageFavoriteSortType>(
|
||||||
},
|
title: Text(e.value.tl),
|
||||||
),
|
value: e,
|
||||||
)
|
),
|
||||||
.toList(),
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -70,39 +70,29 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Sort".tl,
|
title: "Sort".tl,
|
||||||
content: Column(
|
content: RadioGroup<LocalSortType>(
|
||||||
children: [
|
groupValue: sortType,
|
||||||
RadioListTile<LocalSortType>(
|
onChanged: (v) {
|
||||||
title: Text("Name".tl),
|
setState(() {
|
||||||
value: LocalSortType.name,
|
sortType = v ?? sortType;
|
||||||
groupValue: sortType,
|
});
|
||||||
onChanged: (v) {
|
},
|
||||||
setState(() {
|
child: Column(
|
||||||
sortType = v!;
|
children: [
|
||||||
});
|
RadioListTile<LocalSortType>(
|
||||||
},
|
title: Text("Name".tl),
|
||||||
),
|
value: LocalSortType.name,
|
||||||
RadioListTile<LocalSortType>(
|
),
|
||||||
title: Text("Date".tl),
|
RadioListTile<LocalSortType>(
|
||||||
value: LocalSortType.timeAsc,
|
title: Text("Date".tl),
|
||||||
groupValue: sortType,
|
value: LocalSortType.timeAsc,
|
||||||
onChanged: (v) {
|
),
|
||||||
setState(() {
|
RadioListTile<LocalSortType>(
|
||||||
sortType = v!;
|
title: Text("Date Desc".tl),
|
||||||
});
|
value: LocalSortType.timeDesc,
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
RadioListTile<LocalSortType>(
|
),
|
||||||
title: Text("Date Desc".tl),
|
|
||||||
value: LocalSortType.timeDesc,
|
|
||||||
groupValue: sortType,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
sortType = v!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
|
|||||||
@@ -428,30 +428,26 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
RadioGroup<bool>(
|
||||||
children: [
|
groupValue: upload,
|
||||||
Text("Operation".tl),
|
onChanged: (value) {
|
||||||
Radio<bool>(
|
setState(() {
|
||||||
groupValue: upload,
|
upload = value ?? upload;
|
||||||
value: true,
|
});
|
||||||
onChanged: (value) {
|
},
|
||||||
setState(() {
|
child: Row(
|
||||||
upload = value!;
|
children: [
|
||||||
});
|
Text("Operation".tl),
|
||||||
},
|
Radio<bool>(
|
||||||
),
|
value: true,
|
||||||
Text("Upload".tl),
|
),
|
||||||
Radio<bool>(
|
Text("Upload".tl),
|
||||||
groupValue: upload,
|
Radio<bool>(
|
||||||
value: false,
|
value: false,
|
||||||
onChanged: (value) {
|
),
|
||||||
setState(() {
|
Text("Download".tl),
|
||||||
upload = value!;
|
],
|
||||||
});
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
Text("Download".tl),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
|
|||||||
@@ -111,44 +111,34 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
return PopUpWidgetScaffold(
|
return PopUpWidgetScaffold(
|
||||||
title: "Proxy".tl,
|
title: "Proxy".tl,
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: RadioGroup<String>(
|
||||||
children: [
|
groupValue: type,
|
||||||
RadioListTile<String>(
|
onChanged: (v) {
|
||||||
title: Text("Direct".tl),
|
setState(() {
|
||||||
value: 'direct',
|
type = v ?? type;
|
||||||
groupValue: type,
|
});
|
||||||
onChanged: (v) {
|
if (type != 'manual') {
|
||||||
setState(() {
|
appdata.settings['proxy'] = toProxyStr();
|
||||||
type = v!;
|
appdata.saveData();
|
||||||
});
|
}
|
||||||
appdata.settings['proxy'] = toProxyStr();
|
},
|
||||||
appdata.saveData();
|
child: Column(
|
||||||
},
|
children: [
|
||||||
),
|
RadioListTile<String>(
|
||||||
RadioListTile<String>(
|
title: Text("Direct".tl),
|
||||||
title: Text("System".tl),
|
value: 'direct',
|
||||||
value: 'system',
|
),
|
||||||
groupValue: type,
|
RadioListTile<String>(
|
||||||
onChanged: (v) {
|
title: Text("System".tl),
|
||||||
setState(() {
|
value: 'system',
|
||||||
type = v!;
|
),
|
||||||
});
|
RadioListTile(
|
||||||
appdata.settings['proxy'] = toProxyStr();
|
title: Text("Manual".tl),
|
||||||
appdata.saveData();
|
value: 'manual',
|
||||||
},
|
),
|
||||||
),
|
if (type == 'manual') buildManualProxy(),
|
||||||
RadioListTile(
|
],
|
||||||
title: Text("Manual".tl),
|
),
|
||||||
value: 'manual',
|
|
||||||
groupValue: type,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
type = v!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (type == 'manual') buildManualProxy(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
58
lib/utils/channel.dart
Normal file
58
lib/utils/channel.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
class Channel<T> {
|
||||||
|
final Queue<T> _queue;
|
||||||
|
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
Channel(this.size) : _queue = Queue<T>();
|
||||||
|
|
||||||
|
Completer? _releaseCompleter;
|
||||||
|
|
||||||
|
Completer? _pushCompleter;
|
||||||
|
|
||||||
|
var currentSize = 0;
|
||||||
|
|
||||||
|
var isClosed = false;
|
||||||
|
|
||||||
|
Future<void> push(T item) async {
|
||||||
|
if (currentSize >= size) {
|
||||||
|
_releaseCompleter ??= Completer();
|
||||||
|
return _releaseCompleter!.future.then((_) {
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_queue.addLast(item);
|
||||||
|
currentSize++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_queue.addLast(item);
|
||||||
|
currentSize++;
|
||||||
|
_pushCompleter?.complete();
|
||||||
|
_pushCompleter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> pop() async {
|
||||||
|
while (_queue.isEmpty) {
|
||||||
|
if (isClosed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_pushCompleter ??= Completer();
|
||||||
|
await _pushCompleter!.future;
|
||||||
|
}
|
||||||
|
var item = _queue.removeFirst();
|
||||||
|
currentSize--;
|
||||||
|
if (_releaseCompleter != null && currentSize < size) {
|
||||||
|
_releaseCompleter!.complete();
|
||||||
|
_releaseCompleter = null;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
isClosed = true;
|
||||||
|
_pushCompleter?.complete();
|
||||||
|
_releaseCompleter?.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
patch/font.dart
Normal file
28
patch/font.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
const harmonySansLink = "https://developer.huawei.com/images/download/general/HarmonyOS-Sans.zip";
|
||||||
|
|
||||||
|
var dio = Dio();
|
||||||
|
await dio.download(harmonySansLink, "HarmonyOS-Sans.zip");
|
||||||
|
await extractFileToDisk("HarmonyOS-Sans.zip", "./assets/");
|
||||||
|
File("HarmonyOS-Sans.zip").deleteSync();
|
||||||
|
|
||||||
|
var pubspec = await File("pubspec.yaml").readAsString();
|
||||||
|
pubspec = pubspec.replaceFirst("# fonts:",
|
||||||
|
""" fonts:
|
||||||
|
- family: HarmonyOS Sans
|
||||||
|
fonts:
|
||||||
|
- asset: assets/HarmonyOS Sans/HarmonyOS_Sans_SC/HarmonyOS_Sans_SC_Regular.ttf
|
||||||
|
""");
|
||||||
|
await File("pubspec.yaml").writeAsString(pubspec);
|
||||||
|
|
||||||
|
var mainDart = await File("lib/main.dart").readAsString();
|
||||||
|
mainDart = mainDart.replaceFirst("Noto Sans CJK", "HarmonyOS Sans");
|
||||||
|
await File("lib/main.dart").writeAsString(mainDart);
|
||||||
|
|
||||||
|
print("Successfully patched font.");
|
||||||
|
}
|
||||||
22
pubspec.lock
22
pubspec.lock
@@ -33,6 +33,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
archive:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -408,10 +416,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_memory_info
|
name: flutter_memory_info
|
||||||
sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74"
|
sha256: eacfd0dd01ff596b4e5bf022442769a1807a73f2af43d62802436f0a5de99137
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1"
|
version: "0.0.3"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -770,6 +778,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
rhttp:
|
rhttp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1116,4 +1132,4 @@ packages:
|
|||||||
version: "0.0.12"
|
version: "0.0.12"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.35.2"
|
flutter: ">=3.35.5"
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.5.1+151
|
version: 1.5.3+153
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
flutter: 3.35.3
|
flutter: 3.35.5
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -75,7 +75,7 @@ dependencies:
|
|||||||
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
|
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
shimmer_animation: ^2.1.0
|
shimmer_animation: ^2.1.0
|
||||||
flutter_memory_info: ^0.0.1
|
flutter_memory_info: ^0.0.3
|
||||||
syntax_highlight: ^0.4.0
|
syntax_highlight: ^0.4.0
|
||||||
flutter_7zip:
|
flutter_7zip:
|
||||||
git:
|
git:
|
||||||
@@ -94,6 +94,7 @@ dev_dependencies:
|
|||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_to_arch: ^1.0.1
|
flutter_to_arch: ^1.0.1
|
||||||
flutter_to_debian: ^2.0.2
|
flutter_to_debian: ^2.0.2
|
||||||
|
archive: any
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
@@ -104,6 +105,7 @@ flutter:
|
|||||||
- assets/tags.json
|
- assets/tags.json
|
||||||
- assets/tags_tw.json
|
- assets/tags_tw.json
|
||||||
- assets/opencc.txt
|
- assets/opencc.txt
|
||||||
|
# fonts:
|
||||||
|
|
||||||
flutter_to_arch:
|
flutter_to_arch:
|
||||||
name: Venera
|
name: Venera
|
||||||
|
|||||||
115
test/channel_test.dart
Normal file
115
test/channel_test.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:venera/utils/channel.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test("1-1-1", () async {
|
||||||
|
var channel = Channel<int>(1);
|
||||||
|
await channel.push(1);
|
||||||
|
var item = await channel.pop();
|
||||||
|
expect(item, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1-3-1", () async {
|
||||||
|
var channel = Channel<int>(1);
|
||||||
|
|
||||||
|
// producer
|
||||||
|
() async {
|
||||||
|
await channel.push(1);
|
||||||
|
}();
|
||||||
|
() async {
|
||||||
|
await channel.push(2);
|
||||||
|
}();
|
||||||
|
() async {
|
||||||
|
await channel.push(3);
|
||||||
|
}();
|
||||||
|
|
||||||
|
// consumer
|
||||||
|
var results = <int>[];
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var item = await channel.pop();
|
||||||
|
if (item != null) {
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(results.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("2-3-1", () async {
|
||||||
|
var channel = Channel<int>(2);
|
||||||
|
|
||||||
|
// producer
|
||||||
|
() async {
|
||||||
|
await channel.push(1);
|
||||||
|
}();
|
||||||
|
() async {
|
||||||
|
await channel.push(2);
|
||||||
|
}();
|
||||||
|
() async {
|
||||||
|
await channel.push(3);
|
||||||
|
}();
|
||||||
|
|
||||||
|
// consumer
|
||||||
|
var results = <int>[];
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var item = await channel.pop();
|
||||||
|
if (item != null) {
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(results.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1-1-3", () async {
|
||||||
|
var channel = Channel<int>(1);
|
||||||
|
|
||||||
|
// producer
|
||||||
|
() async {
|
||||||
|
print("push 1");
|
||||||
|
await channel.push(1);
|
||||||
|
print("push 2");
|
||||||
|
await channel.push(2);
|
||||||
|
print("push 3");
|
||||||
|
await channel.push(3);
|
||||||
|
print("push done");
|
||||||
|
channel.close();
|
||||||
|
}();
|
||||||
|
|
||||||
|
// consumer
|
||||||
|
var consumers = <Future>[];
|
||||||
|
var results = <int>[];
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
consumers.add(() async {
|
||||||
|
while (true) {
|
||||||
|
var item = await channel.pop();
|
||||||
|
if (item == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
print("pop $item");
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait(consumers);
|
||||||
|
expect(results.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("close", () async {
|
||||||
|
var channel = Channel<int>(2);
|
||||||
|
|
||||||
|
// producer
|
||||||
|
() async {
|
||||||
|
await channel.push(1);
|
||||||
|
await channel.push(2);
|
||||||
|
await channel.push(3);
|
||||||
|
channel.close();
|
||||||
|
}();
|
||||||
|
|
||||||
|
// consumer
|
||||||
|
await channel.pop();
|
||||||
|
await channel.pop();
|
||||||
|
await channel.pop();
|
||||||
|
var item4 = await channel.pop();
|
||||||
|
expect(item4, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user