mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
17 Commits
v1.4.1-dev
...
v1.4.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9f048685e4 | ||
![]() |
bc1f5e11b5 | ||
1f2147ef72 | |||
fba365fd93 | |||
a5e3fbaee5 | |||
190e645a12 | |||
![]() |
8a83ff5367 | ||
6e14942dab | |||
146fc70143 | |||
b37ea01aca | |||
bf7b90313a | |||
929c1a9d91 | |||
9ff68d0701 | |||
dfd15ed34a | |||
![]() |
dfe2a0db6a | ||
c6714f79b6 | |||
6877aa120f |
@@ -39,6 +39,32 @@ let Convert = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param str {string}
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
encodeGbk: (str) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "gbk",
|
||||||
|
value: str,
|
||||||
|
isEncode: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value {ArrayBuffer}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
decodeGbk: (value) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "gbk",
|
||||||
|
value: value,
|
||||||
|
isEncode: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} value
|
* @param {ArrayBuffer} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
@@ -176,7 +202,7 @@ let Convert = {
|
|||||||
decryptAesCbc: (value, key, iv) => {
|
decryptAesCbc: (value, key, iv) => {
|
||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "convert",
|
method: "convert",
|
||||||
type: "aes-ecb",
|
type: "aes-cbc",
|
||||||
value: value,
|
value: value,
|
||||||
key: key,
|
key: key,
|
||||||
iv: iv,
|
iv: iv,
|
||||||
|
@@ -390,7 +390,8 @@
|
|||||||
"Show single image on first page": "在首页显示单张图片",
|
"Show single image on first page": "在首页显示单张图片",
|
||||||
"Click to select an image": "点击选择一张图片",
|
"Click to select an image": "点击选择一张图片",
|
||||||
"Source URL": "源地址",
|
"Source URL": "源地址",
|
||||||
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件"
|
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
|
||||||
|
"Double tap to zoom": "双击缩放"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -783,6 +784,7 @@
|
|||||||
"Show single image on first page": "在首頁顯示單張圖片",
|
"Show single image on first page": "在首頁顯示單張圖片",
|
||||||
"Click to select an image": "點擊選擇一張圖片",
|
"Click to select an image": "點擊選擇一張圖片",
|
||||||
"Source URL": "源地址",
|
"Source URL": "源地址",
|
||||||
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件"
|
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
|
||||||
|
"Double tap to zoom": "雙擊縮放"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,12 +46,14 @@
|
|||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Choose images</string>
|
<string>Choose images</string>
|
||||||
<key>UIFileSharingEnabled</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Ensure that the operation is being performed by the user themselves.</string>
|
<string>Ensure that the operation is being performed by the user themselves.</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.books</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.4.1";
|
final version = "1.4.3";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -185,6 +185,7 @@ class Settings with ChangeNotifier {
|
|||||||
'comicListDisplayMode': 'paging', // paging, continuous
|
'comicListDisplayMode': 'paging', // paging, continuous
|
||||||
'showPageNumberInReader': true,
|
'showPageNumberInReader': true,
|
||||||
'showSingleImageOnFirstPage': false,
|
'showSingleImageOnFirstPage': false,
|
||||||
|
'enableDoubleTapToZoom': true,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
@@ -21,6 +23,51 @@ class CacheManager {
|
|||||||
|
|
||||||
int _limitSize = 2 * 1024 * 1024 * 1024;
|
int _limitSize = 2 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
static Future<int> _scanDir(Pointer<void> dbP, String dir) async {
|
||||||
|
var res = await Isolate.run(() async {
|
||||||
|
int totalSize = 0;
|
||||||
|
List<String> unmanagedFiles = [];
|
||||||
|
var db = sqlite3.fromPointer(dbP);
|
||||||
|
await for (var file in Directory(dir).list(recursive: true)) {
|
||||||
|
if (file is File) {
|
||||||
|
var size = await file.length();
|
||||||
|
var segments = file.uri.pathSegments;
|
||||||
|
var name = segments.last;
|
||||||
|
var dir = segments.elementAtOrNull(segments.length - 2) ?? "*";
|
||||||
|
var res = db.select('''
|
||||||
|
SELECT * FROM cache
|
||||||
|
WHERE dir = ? AND name = ?
|
||||||
|
''', [dir, name]);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
unmanagedFiles.add(file.path);
|
||||||
|
} else {
|
||||||
|
totalSize += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'totalSize': totalSize,
|
||||||
|
'unmanagedFiles': unmanagedFiles,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// delete unmanaged files
|
||||||
|
// Only modify the database in the main isolate to avoid deadlock
|
||||||
|
for (var filePath in res['unmanagedFiles'] as List<String>) {
|
||||||
|
var file = File(filePath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
var segments = file.uri.pathSegments;
|
||||||
|
var name = segments.last;
|
||||||
|
var dir = segments.elementAtOrNull(segments.length - 2) ?? "*";
|
||||||
|
CacheManager()._db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE dir = ? AND name = ?
|
||||||
|
''', [dir, name]);
|
||||||
|
}
|
||||||
|
return res['totalSize'] as int;
|
||||||
|
}
|
||||||
|
|
||||||
CacheManager._create() {
|
CacheManager._create() {
|
||||||
Directory(cachePath).createSync(recursive: true);
|
Directory(cachePath).createSync(recursive: true);
|
||||||
_db = sqlite3.open('${App.dataPath}/cache.db');
|
_db = sqlite3.open('${App.dataPath}/cache.db');
|
||||||
@@ -33,7 +80,7 @@ class CacheManager {
|
|||||||
type TEXT
|
type TEXT
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
compute((path) => Directory(path).size, cachePath).then((value) {
|
_scanDir(_db.handle, cachePath).then((value) {
|
||||||
_currentSize = value;
|
_currentSize = value;
|
||||||
checkCache();
|
checkCache();
|
||||||
});
|
});
|
||||||
@@ -50,6 +97,7 @@ class CacheManager {
|
|||||||
/// Write cache to disk.
|
/// Write cache to disk.
|
||||||
Future<void> writeCache(String key, List<int> data,
|
Future<void> writeCache(String key, List<int> data,
|
||||||
[int duration = 7 * 24 * 60 * 60 * 1000]) async {
|
[int duration = 7 * 24 * 60 * 60 * 1000]) async {
|
||||||
|
await delete(key);
|
||||||
this.dir++;
|
this.dir++;
|
||||||
this.dir %= 100;
|
this.dir %= 100;
|
||||||
var dir = this.dir;
|
var dir = this.dir;
|
||||||
@@ -146,10 +194,12 @@ class CacheManager {
|
|||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_db.execute('''
|
if (res.isNotEmpty) {
|
||||||
|
_db.execute('''
|
||||||
DELETE FROM cache
|
DELETE FROM cache
|
||||||
WHERE expires < ?
|
WHERE expires < ?
|
||||||
''', [DateTime.now().millisecondsSinceEpoch]);
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
|
}
|
||||||
|
|
||||||
while (_currentSize != null && _currentSize! > _limitSize) {
|
while (_currentSize != null && _currentSize! > _limitSize) {
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
@@ -157,6 +207,13 @@ class CacheManager {
|
|||||||
ORDER BY expires ASC
|
ORDER BY expires ASC
|
||||||
limit 10
|
limit 10
|
||||||
''');
|
''');
|
||||||
|
if (res.isEmpty) {
|
||||||
|
// There are many files unmanaged by the cache manager.
|
||||||
|
// Clear all cache.
|
||||||
|
await Directory(cachePath).delete(recursive: true);
|
||||||
|
Directory(cachePath).createSync(recursive: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
for (var row in res) {
|
for (var row in res) {
|
||||||
var key = row[0] as String;
|
var key = row[0] as String;
|
||||||
var dir = row[1] as String;
|
var dir = row[1] as String;
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
@@ -209,7 +211,22 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
|
|
||||||
late Database _db;
|
late Database _db;
|
||||||
|
|
||||||
|
late Map<String, int> counts;
|
||||||
|
|
||||||
|
int get totalComics {
|
||||||
|
int total = 0;
|
||||||
|
for (var t in counts.values) {
|
||||||
|
total += t;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int folderComics(String folder) {
|
||||||
|
return counts[folder] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
counts = {};
|
||||||
_db = sqlite3.open("${App.dataPath}/local_favorite.db");
|
_db = sqlite3.open("${App.dataPath}/local_favorite.db");
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
create table if not exists folder_order (
|
create table if not exists folder_order (
|
||||||
@@ -256,6 +273,13 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
appdata.settings['followUpdatesFolder'] = null;
|
appdata.settings['followUpdatesFolder'] = null;
|
||||||
}
|
}
|
||||||
|
initCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initCounts() {
|
||||||
|
for (var folder in folderNames) {
|
||||||
|
counts[folder] = count(folder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> find(String id, ComicType type) {
|
List<String> find(String id, ComicType type) {
|
||||||
@@ -357,6 +381,23 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
|
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<FavoriteItem>> _getFolderComicsAsync(
|
||||||
|
String folder, Pointer<void> p) {
|
||||||
|
return Isolate.run(() {
|
||||||
|
var db = sqlite3.fromPointer(p);
|
||||||
|
var rows = db.select("""
|
||||||
|
select * from "$folder"
|
||||||
|
ORDER BY display_order;
|
||||||
|
""");
|
||||||
|
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a new isolate to get the comics in the folder
|
||||||
|
Future<List<FavoriteItem>> getFolderComicsAsync(String folder) {
|
||||||
|
return _getFolderComicsAsync(folder, _db.handle);
|
||||||
|
}
|
||||||
|
|
||||||
List<FavoriteItem> getAllComics() {
|
List<FavoriteItem> getAllComics() {
|
||||||
var res = <FavoriteItem>{};
|
var res = <FavoriteItem>{};
|
||||||
for (final folder in folderNames) {
|
for (final folder in folderNames) {
|
||||||
@@ -368,6 +409,26 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return res.toList();
|
return res.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<FavoriteItem>> _getAllComicsAsync(
|
||||||
|
List<String> folders, Pointer<void> p) {
|
||||||
|
return Isolate.run(() {
|
||||||
|
var db = sqlite3.fromPointer(p);
|
||||||
|
var res = <FavoriteItem>{};
|
||||||
|
for (final folder in folders) {
|
||||||
|
var comics = db.select("""
|
||||||
|
select * from "$folder";
|
||||||
|
""");
|
||||||
|
res.addAll(comics.map((element) => FavoriteItem.fromRow(element)));
|
||||||
|
}
|
||||||
|
return res.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a new isolate to get all the comics
|
||||||
|
Future<List<FavoriteItem>> getAllComicsAsync() {
|
||||||
|
return _getAllComicsAsync(folderNames, _db.handle);
|
||||||
|
}
|
||||||
|
|
||||||
void addTagTo(String folder, String id, String tag) {
|
void addTagTo(String folder, String id, String tag) {
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
update "$folder"
|
update "$folder"
|
||||||
@@ -433,6 +494,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
""");
|
""");
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
counts[name] = 0;
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +609,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
""", [updateTime, comic.id, comic.type.value]);
|
""", [updateTime, comic.id, comic.type.value]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (counts[folder] == null) {
|
||||||
|
counts[folder] = count(folder);
|
||||||
|
} else {
|
||||||
|
counts[folder] = counts[folder]! + 1;
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -596,6 +663,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
delete from folder_order
|
delete from folder_order
|
||||||
where folder_name == ?;
|
where folder_name == ?;
|
||||||
""", [name]);
|
""", [name]);
|
||||||
|
counts.remove(name);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,6 +679,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
delete from "$folder"
|
delete from "$folder"
|
||||||
where id == ? and type == ?;
|
where id == ? and type == ?;
|
||||||
""", [id, type.value]);
|
""", [id, type.value]);
|
||||||
|
if (counts[folder] != null) {
|
||||||
|
counts[folder] = counts[folder]! - 1;
|
||||||
|
} else {
|
||||||
|
counts[folder] = count(folder);
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:enough_convert/enough_convert.dart';
|
||||||
import 'package:flutter/foundation.dart' show protected;
|
import 'package:flutter/foundation.dart' show protected;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:html/parser.dart' as html;
|
import 'package:html/parser.dart' as html;
|
||||||
@@ -372,6 +373,11 @@ mixin class _JSEngineApi {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "utf8":
|
case "utf8":
|
||||||
return isEncode ? utf8.encode(value) : utf8.decode(value);
|
return isEncode ? utf8.encode(value) : utf8.decode(value);
|
||||||
|
case "gbk":
|
||||||
|
final codec = const GbkCodec();
|
||||||
|
return isEncode
|
||||||
|
? Uint8List.fromList(codec.encode(value))
|
||||||
|
: codec.decode(value);
|
||||||
case "base64":
|
case "base64":
|
||||||
return isEncode ? base64Encode(value) : base64Decode(value);
|
return isEncode ? base64Encode(value) : base64Decode(value);
|
||||||
case "md5":
|
case "md5":
|
||||||
|
@@ -234,19 +234,30 @@ class _StreamWrapper<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _listen() async {
|
void _listen() async {
|
||||||
await for (var data in _stream) {
|
try {
|
||||||
if (isClosed) {
|
await for (var data in _stream) {
|
||||||
break;
|
if (isClosed) {
|
||||||
}
|
break;
|
||||||
for (var controller in controllers) {
|
}
|
||||||
if (!controller.isClosed) {
|
for (var controller in controllers) {
|
||||||
controller.add(data);
|
if (!controller.isClosed) {
|
||||||
|
controller.add(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var controller in controllers) {
|
catch (e) {
|
||||||
if (!controller.isClosed) {
|
for (var controller in controllers) {
|
||||||
controller.close();
|
if (!controller.isClosed) {
|
||||||
|
controller.addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
for (var controller in controllers) {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controllers.clear();
|
controllers.clear();
|
||||||
|
@@ -18,7 +18,9 @@ import 'package:venera/network/download.dart';
|
|||||||
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
part 'favorite_actions.dart';
|
part 'favorite_actions.dart';
|
||||||
|
@@ -2,6 +2,10 @@ part of 'favorites_page.dart';
|
|||||||
|
|
||||||
const _localAllFolderLabel = '^_^[%local_all%]^_^';
|
const _localAllFolderLabel = '^_^[%local_all%]^_^';
|
||||||
|
|
||||||
|
/// If the number of comics in a folder exceeds this limit, it will be
|
||||||
|
/// fetched asynchronously.
|
||||||
|
const _asyncDataFetchLimit = 500;
|
||||||
|
|
||||||
class _LocalFavoritesPage extends StatefulWidget {
|
class _LocalFavoritesPage extends StatefulWidget {
|
||||||
const _LocalFavoritesPage({required this.folder, super.key});
|
const _LocalFavoritesPage({required this.folder, super.key});
|
||||||
|
|
||||||
@@ -35,40 +39,110 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
|
|
||||||
bool get isAllFolder => widget.folder == _localAllFolderLabel;
|
bool get isAllFolder => widget.folder == _localAllFolderLabel;
|
||||||
|
|
||||||
|
LocalFavoritesManager get manager => LocalFavoritesManager();
|
||||||
|
|
||||||
|
bool isLoading = false;
|
||||||
|
|
||||||
|
var searchResults = <FavoriteItem>[];
|
||||||
|
|
||||||
|
void updateSearchResult() {
|
||||||
|
setState(() {
|
||||||
|
if (keyword.trim().isEmpty) {
|
||||||
|
searchResults = comics;
|
||||||
|
} else {
|
||||||
|
searchResults = [];
|
||||||
|
for (var comic in comics) {
|
||||||
|
if (matchKeyword(keyword, comic)) {
|
||||||
|
searchResults.add(comic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void updateComics() {
|
void updateComics() {
|
||||||
if (keyword.isEmpty) {
|
if (isLoading) return;
|
||||||
setState(() {
|
if (isAllFolder) {
|
||||||
if (isAllFolder) {
|
var totalComics = manager.totalComics;
|
||||||
comics = LocalFavoritesManager().getAllComics();
|
if (totalComics < _asyncDataFetchLimit) {
|
||||||
} else {
|
comics = manager.getAllComics();
|
||||||
comics = LocalFavoritesManager().getFolderComics(widget.folder);
|
} else {
|
||||||
}
|
isLoading = true;
|
||||||
});
|
manager
|
||||||
|
.getAllComicsAsync()
|
||||||
|
.minTime(const Duration(milliseconds: 200))
|
||||||
|
.then((value) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
comics = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
var folderComics = manager.folderComics(widget.folder);
|
||||||
if (isAllFolder) {
|
if (folderComics < _asyncDataFetchLimit) {
|
||||||
comics = LocalFavoritesManager().search(keyword);
|
comics = manager.getFolderComics(widget.folder);
|
||||||
} else {
|
} else {
|
||||||
comics =
|
isLoading = true;
|
||||||
LocalFavoritesManager().searchInFolder(widget.folder, keyword);
|
manager
|
||||||
}
|
.getFolderComicsAsync(widget.folder)
|
||||||
});
|
.minTime(const Duration(milliseconds: 200))
|
||||||
|
.then((value) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
comics = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matchKeyword(String keyword, FavoriteItem comic) {
|
||||||
|
var list = keyword.split(" ");
|
||||||
|
for (var k in list) {
|
||||||
|
if (k.isEmpty) continue;
|
||||||
|
if (comic.title.contains(k)) {
|
||||||
|
continue;
|
||||||
|
} else if (comic.subtitle != null && comic.subtitle!.contains(k)) {
|
||||||
|
continue;
|
||||||
|
} else if (comic.tags.any((tag) {
|
||||||
|
if (tag == k) {
|
||||||
|
return true;
|
||||||
|
} else if (tag.contains(':') && tag.split(':')[1] == k) {
|
||||||
|
return true;
|
||||||
|
} else if (App.locale.languageCode != 'en' &&
|
||||||
|
tag.translateTagsToCN == k) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})) {
|
||||||
|
continue;
|
||||||
|
} else if (comic.author == k) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
if (!isAllFolder) {
|
if (!isAllFolder) {
|
||||||
comics = LocalFavoritesManager().getFolderComics(widget.folder);
|
|
||||||
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
||||||
networkSource = a;
|
networkSource = a;
|
||||||
networkFolder = b;
|
networkFolder = b;
|
||||||
} else {
|
} else {
|
||||||
comics = LocalFavoritesManager().getAllComics();
|
|
||||||
networkSource = null;
|
networkSource = null;
|
||||||
networkFolder = null;
|
networkFolder = null;
|
||||||
}
|
}
|
||||||
|
comics = [];
|
||||||
|
updateComics();
|
||||||
LocalFavoritesManager().addListener(updateComics);
|
LocalFavoritesManager().addListener(updateComics);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@@ -215,7 +289,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
keyword = "";
|
||||||
searchMode = true;
|
searchMode = true;
|
||||||
|
updateSearchResult();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -411,9 +487,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
searchMode = false;
|
setState(() {
|
||||||
keyword = "";
|
searchMode = false;
|
||||||
updateComics();
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -422,132 +498,142 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search".tl,
|
hintText: "Search".tl,
|
||||||
border: InputBorder.none,
|
border: UnderlineInputBorder(),
|
||||||
),
|
),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
keyword = v;
|
keyword = v;
|
||||||
updateComics();
|
updateSearchResult();
|
||||||
},
|
},
|
||||||
),
|
).paddingBottom(8).paddingRight(8),
|
||||||
),
|
),
|
||||||
SliverGridComics(
|
if (isLoading)
|
||||||
comics: comics,
|
SliverToBoxAdapter(
|
||||||
selections: selectedComics,
|
child: SizedBox(
|
||||||
menuBuilder: (c) {
|
height: 200,
|
||||||
return [
|
child: const Center(
|
||||||
if (!isAllFolder)
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverGridComics(
|
||||||
|
comics: searchMode ? searchResults : comics,
|
||||||
|
selections: selectedComics,
|
||||||
|
menuBuilder: (c) {
|
||||||
|
return [
|
||||||
|
if (!isAllFolder)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.delete,
|
||||||
|
text: "Delete".tl,
|
||||||
|
onClick: () {
|
||||||
|
LocalFavoritesManager().deleteComicWithId(
|
||||||
|
widget.folder,
|
||||||
|
c.id,
|
||||||
|
(c as FavoriteItem).type,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.delete,
|
icon: Icons.check,
|
||||||
text: "Delete".tl,
|
text: "Select".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
LocalFavoritesManager().deleteComicWithId(
|
setState(() {
|
||||||
widget.folder,
|
if (!multiSelectMode) {
|
||||||
c.id,
|
multiSelectMode = true;
|
||||||
(c as FavoriteItem).type,
|
}
|
||||||
|
if (selectedComics.containsKey(c as FavoriteItem)) {
|
||||||
|
selectedComics.remove(c);
|
||||||
|
_checkExitSelectMode();
|
||||||
|
} else {
|
||||||
|
selectedComics[c] = true;
|
||||||
|
}
|
||||||
|
lastSelectedIndex = comics.indexOf(c);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.download,
|
||||||
|
text: "Download".tl,
|
||||||
|
onClick: () {
|
||||||
|
downloadComic(c as FavoriteItem);
|
||||||
|
context.showMessage(
|
||||||
|
message: "Download started".tl,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuEntry(
|
if (appdata.settings["onClickFavorite"] == "viewDetail")
|
||||||
icon: Icons.check,
|
MenuEntry(
|
||||||
text: "Select".tl,
|
icon: Icons.menu_book_outlined,
|
||||||
onClick: () {
|
text: "Read".tl,
|
||||||
setState(() {
|
onClick: () {
|
||||||
if (!multiSelectMode) {
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
multiSelectMode = true;
|
() => ReaderWithLoading(
|
||||||
}
|
id: c.id,
|
||||||
if (selectedComics.containsKey(c as FavoriteItem)) {
|
sourceKey: c.sourceKey,
|
||||||
selectedComics.remove(c);
|
),
|
||||||
_checkExitSelectMode();
|
);
|
||||||
} else {
|
},
|
||||||
selectedComics[c] = true;
|
),
|
||||||
}
|
];
|
||||||
lastSelectedIndex = comics.indexOf(c);
|
},
|
||||||
});
|
onTap: (c) {
|
||||||
},
|
if (multiSelectMode) {
|
||||||
),
|
setState(() {
|
||||||
MenuEntry(
|
if (selectedComics.containsKey(c as FavoriteItem)) {
|
||||||
icon: Icons.download,
|
selectedComics.remove(c);
|
||||||
text: "Download".tl,
|
_checkExitSelectMode();
|
||||||
onClick: () {
|
} else {
|
||||||
downloadComic(c as FavoriteItem);
|
selectedComics[c] = true;
|
||||||
context.showMessage(
|
}
|
||||||
message: "Download started".tl,
|
lastSelectedIndex = comics.indexOf(c);
|
||||||
);
|
});
|
||||||
},
|
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
|
||||||
),
|
App.mainNavigatorKey?.currentContext
|
||||||
if (appdata.settings["onClickFavorite"] == "viewDetail")
|
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.menu_book_outlined,
|
|
||||||
text: "Read".tl,
|
|
||||||
onClick: () {
|
|
||||||
App.mainNavigatorKey?.currentContext?.to(
|
|
||||||
() => ReaderWithLoading(
|
|
||||||
id: c.id,
|
|
||||||
sourceKey: c.sourceKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
onTap: (c) {
|
|
||||||
if (multiSelectMode) {
|
|
||||||
setState(() {
|
|
||||||
if (selectedComics.containsKey(c as FavoriteItem)) {
|
|
||||||
selectedComics.remove(c);
|
|
||||||
_checkExitSelectMode();
|
|
||||||
} else {
|
|
||||||
selectedComics[c] = true;
|
|
||||||
}
|
|
||||||
lastSelectedIndex = comics.indexOf(c);
|
|
||||||
});
|
|
||||||
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
|
|
||||||
App.mainNavigatorKey?.currentContext
|
|
||||||
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
|
|
||||||
} else {
|
|
||||||
App.mainNavigatorKey?.currentContext?.to(
|
|
||||||
() => ReaderWithLoading(
|
|
||||||
id: c.id,
|
|
||||||
sourceKey: c.sourceKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPressed: (c) {
|
|
||||||
setState(() {
|
|
||||||
if (!multiSelectMode) {
|
|
||||||
multiSelectMode = true;
|
|
||||||
if (!selectedComics.containsKey(c as FavoriteItem)) {
|
|
||||||
selectedComics[c] = true;
|
|
||||||
}
|
|
||||||
lastSelectedIndex = comics.indexOf(c);
|
|
||||||
} else {
|
} else {
|
||||||
if (lastSelectedIndex != null) {
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
int start = lastSelectedIndex!;
|
() => ReaderWithLoading(
|
||||||
int end = comics.indexOf(c as FavoriteItem);
|
id: c.id,
|
||||||
if (start > end) {
|
sourceKey: c.sourceKey,
|
||||||
int temp = start;
|
),
|
||||||
start = end;
|
);
|
||||||
end = temp;
|
}
|
||||||
|
},
|
||||||
|
onLongPressed: (c) {
|
||||||
|
setState(() {
|
||||||
|
if (!multiSelectMode) {
|
||||||
|
multiSelectMode = true;
|
||||||
|
if (!selectedComics.containsKey(c as FavoriteItem)) {
|
||||||
|
selectedComics[c] = true;
|
||||||
}
|
}
|
||||||
|
lastSelectedIndex = comics.indexOf(c);
|
||||||
|
} else {
|
||||||
|
if (lastSelectedIndex != null) {
|
||||||
|
int start = lastSelectedIndex!;
|
||||||
|
int end = comics.indexOf(c as FavoriteItem);
|
||||||
|
if (start > end) {
|
||||||
|
int temp = start;
|
||||||
|
start = end;
|
||||||
|
end = temp;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = start; i <= end; i++) {
|
for (int i = start; i <= end; i++) {
|
||||||
if (i == lastSelectedIndex) continue;
|
if (i == lastSelectedIndex) continue;
|
||||||
|
|
||||||
var comic = comics[i];
|
var comic = comics[i];
|
||||||
if (selectedComics.containsKey(comic)) {
|
if (selectedComics.containsKey(comic)) {
|
||||||
selectedComics.remove(comic);
|
selectedComics.remove(comic);
|
||||||
} else {
|
} else {
|
||||||
selectedComics[comic] = true;
|
selectedComics[comic] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lastSelectedIndex = comics.indexOf(c as FavoriteItem);
|
||||||
}
|
}
|
||||||
lastSelectedIndex = comics.indexOf(c as FavoriteItem);
|
_checkExitSelectMode();
|
||||||
}
|
});
|
||||||
_checkExitSelectMode();
|
},
|
||||||
});
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
body = AppScrollBar(
|
body = AppScrollBar(
|
||||||
|
@@ -86,51 +86,10 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
padding: widget.withAppbar
|
padding: widget.withAppbar
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: EdgeInsets.only(top: context.padding.top),
|
: EdgeInsets.only(top: context.padding.top),
|
||||||
itemCount: folders.length + networkFolders.length + 2,
|
itemCount: folders.length + networkFolders.length + 3,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Container(
|
return buildLocalTitle();
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.local_activity,
|
|
||||||
color: context.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text("Local".tl),
|
|
||||||
const Spacer(),
|
|
||||||
MenuButton(
|
|
||||||
entries: [
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.add,
|
|
||||||
text: 'Create Folder'.tl,
|
|
||||||
onClick: () {
|
|
||||||
newFolder().then((value) {
|
|
||||||
setState(() {
|
|
||||||
folders =
|
|
||||||
LocalFavoritesManager().folderNames;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.reorder,
|
|
||||||
text: 'Sort'.tl,
|
|
||||||
onClick: () {
|
|
||||||
sortFolders().then((value) {
|
|
||||||
setState(() {
|
|
||||||
folders =
|
|
||||||
LocalFavoritesManager().folderNames;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingHorizontal(16),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
index--;
|
index--;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
@@ -142,38 +101,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
}
|
}
|
||||||
index -= folders.length;
|
index -= folders.length;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Container(
|
return buildNetworkTitle();
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
margin: const EdgeInsets.only(top: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
top: BorderSide(
|
|
||||||
color: context.colorScheme.outlineVariant,
|
|
||||||
width: 0.6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.cloud,
|
|
||||||
color: context.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text("Network".tl),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.settings),
|
|
||||||
onPressed: () {
|
|
||||||
showPopUpWidget(
|
|
||||||
App.rootContext,
|
|
||||||
setFavoritesPagesWidget(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingHorizontal(16),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
index--;
|
index--;
|
||||||
return buildNetworkFolder(networkFolders[index]);
|
return buildNetworkFolder(networkFolders[index]);
|
||||||
@@ -185,8 +113,95 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildLocalTitle() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_activity,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text("Local".tl),
|
||||||
|
const Spacer(),
|
||||||
|
MenuButton(
|
||||||
|
entries: [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.add,
|
||||||
|
text: 'Create Folder'.tl,
|
||||||
|
onClick: () {
|
||||||
|
newFolder().then((value) {
|
||||||
|
setState(() {
|
||||||
|
folders = LocalFavoritesManager().folderNames;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.reorder,
|
||||||
|
text: 'Sort'.tl,
|
||||||
|
onClick: () {
|
||||||
|
sortFolders().then((value) {
|
||||||
|
setState(() {
|
||||||
|
folders = LocalFavoritesManager().folderNames;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNetworkTitle() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
margin: const EdgeInsets.only(top: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text("Network".tl),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: () {
|
||||||
|
showPopUpWidget(
|
||||||
|
App.rootContext,
|
||||||
|
setFavoritesPagesWidget(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildLocalFolder(String name) {
|
Widget buildLocalFolder(String name) {
|
||||||
bool isSelected = name == favPage.folder && !favPage.isNetwork;
|
bool isSelected = name == favPage.folder && !favPage.isNetwork;
|
||||||
|
int count = 0;
|
||||||
|
if (name == _localAllFolderLabel) {
|
||||||
|
count = LocalFavoritesManager().totalComics;
|
||||||
|
} else {
|
||||||
|
count = LocalFavoritesManager().folderComics(name);
|
||||||
|
}
|
||||||
|
var folderName = name == _localAllFolderLabel
|
||||||
|
? "All".tl
|
||||||
|
: getFavoriteDataOrNull(name)?.title ?? name;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@@ -211,9 +226,25 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
child: Text(name == _localAllFolderLabel
|
child: Row(
|
||||||
? "All".tl
|
children: [
|
||||||
: getFavoriteDataOrNull(name)?.title ?? name),
|
Expanded(
|
||||||
|
child: Text(folderName),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(right: 8),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(count.toString()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -152,12 +152,18 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
|
|
||||||
bool _dragInProgress = false;
|
bool _dragInProgress = false;
|
||||||
|
|
||||||
|
bool get _enableDoubleTapToZoom => appdata.settings["enableDoubleTapToZoom"];
|
||||||
|
|
||||||
void onTapUp(TapUpDetails event) {
|
void onTapUp(TapUpDetails event) {
|
||||||
if (_longPressInProgress) {
|
if (_longPressInProgress) {
|
||||||
_longPressInProgress = false;
|
_longPressInProgress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final location = event.globalPosition;
|
final location = event.globalPosition;
|
||||||
|
if (!_enableDoubleTapToZoom) {
|
||||||
|
onTap(location);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final previousLocation = _previousEvent?.globalPosition;
|
final previousLocation = _previousEvent?.globalPosition;
|
||||||
if (previousLocation != null) {
|
if (previousLocation != null) {
|
||||||
if ((location - previousLocation).distanceSquared <
|
if ((location - previousLocation).distanceSquared <
|
||||||
|
@@ -119,7 +119,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
/// [totalPages] is the total number of pages in the current chapter.
|
/// [totalPages] is the total number of pages in the current chapter.
|
||||||
/// More than one images can be displayed on one page.
|
/// More than one images can be displayed on one page.
|
||||||
int get totalPages {
|
int get totalPages {
|
||||||
if (!showSingleImageOnFirstPage) {
|
if (!reader.showSingleImageOnFirstPage) {
|
||||||
return (reader.images!.length / reader.imagesPerPage).ceil();
|
return (reader.images!.length / reader.imagesPerPage).ceil();
|
||||||
} else {
|
} else {
|
||||||
return 1 +
|
return 1 +
|
||||||
@@ -144,11 +144,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get showSingleImageOnFirstPage => appdata.settings["showSingleImageOnFirstPage"];
|
|
||||||
|
|
||||||
/// Get the range of images for the given page. [page] is 1-based.
|
/// Get the range of images for the given page. [page] is 1-based.
|
||||||
(int start, int end) getPageImagesRange(int page) {
|
(int start, int end) getPageImagesRange(int page) {
|
||||||
if (showSingleImageOnFirstPage) {
|
if (reader.showSingleImageOnFirstPage) {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
return (0, 1);
|
return (0, 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -252,6 +250,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
|
childSize: reader.size * 2,
|
||||||
controller: photoViewControllers[index],
|
controller: photoViewControllers[index],
|
||||||
minScale: PhotoViewComputedScale.contained * 1.0,
|
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||||
maxScale: PhotoViewComputedScale.covered * 10.0,
|
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||||
|
@@ -111,7 +111,16 @@ class _ReaderState extends State<Reader>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil();
|
int get maxPage {
|
||||||
|
if (images == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!showSingleImageOnFirstPage) {
|
||||||
|
return (images!.length / imagesPerPage).ceil();
|
||||||
|
} else {
|
||||||
|
return 1 + ((images!.length - 1) / imagesPerPage).ceil();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ComicType get type => widget.type;
|
ComicType get type => widget.type;
|
||||||
|
|
||||||
@@ -125,7 +134,8 @@ class _ReaderState extends State<Reader>
|
|||||||
late ReaderMode mode;
|
late ReaderMode mode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait;
|
bool get isPortrait =>
|
||||||
|
MediaQuery.of(context).orientation == Orientation.portrait;
|
||||||
|
|
||||||
History? history;
|
History? history;
|
||||||
|
|
||||||
@@ -343,6 +353,9 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get showSingleImageOnFirstPage =>
|
||||||
|
appdata.settings["showSingleImageOnFirstPage"];
|
||||||
|
|
||||||
/// The number of images displayed on one screen
|
/// The number of images displayed on one screen
|
||||||
int get imagesPerPage {
|
int get imagesPerPage {
|
||||||
if (mode.isContinuous) return 1;
|
if (mode.isContinuous) return 1;
|
||||||
|
@@ -113,6 +113,14 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: 'Double tap to zoom'.tl,
|
||||||
|
settingKey: 'enableDoubleTapToZoom',
|
||||||
|
onChanged: () {
|
||||||
|
setState(() {});
|
||||||
|
widget.onChanged?.call('enableDoubleTapToZoom');
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Long press to zoom'.tl,
|
title: 'Long press to zoom'.tl,
|
||||||
settingKey: 'enableLongPressToZoom',
|
settingKey: 'enableLongPressToZoom',
|
||||||
|
@@ -107,4 +107,15 @@ abstract class MapOrNull{
|
|||||||
static Map<K, V>? from<K, V>(Map<dynamic, dynamic>? i){
|
static Map<K, V>? from<K, V>(Map<dynamic, dynamic>? i){
|
||||||
return i == null ? null : Map<K, V>.from(i);
|
return i == null ? null : Map<K, V>.from(i);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FutureExt<T> on Future<T>{
|
||||||
|
/// Wrap the future to make sure it will return at least the duration.
|
||||||
|
Future<T> minTime(Duration duration) async {
|
||||||
|
var res = await Future.wait([
|
||||||
|
this,
|
||||||
|
Future.delayed(duration),
|
||||||
|
]);
|
||||||
|
return res[0];
|
||||||
|
}
|
||||||
}
|
}
|
@@ -178,6 +178,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
enough_convert:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: enough_convert
|
||||||
|
sha256: c67d85ca21aaa0648f155907362430701db41f7ec8e6501a58ad9cd9d8569d01
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.4.1+141
|
version: 1.4.3+143
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
@@ -85,6 +85,7 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
yaml: ^3.1.3
|
yaml: ^3.1.3
|
||||||
|
enough_convert: ^1.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user