mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
@@ -387,7 +387,10 @@
|
|||||||
"Screen center": "屏幕中心",
|
"Screen center": "屏幕中心",
|
||||||
"Suggestions": "建议",
|
"Suggestions": "建议",
|
||||||
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
|
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
|
||||||
"Click the setting icon to change the source list url.": "点击设置图标更改源列表URL"
|
"Show single image on first page": "在首页显示单张图片",
|
||||||
|
"Click to select an image": "点击选择一张图片",
|
||||||
|
"Source URL": "源地址",
|
||||||
|
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -777,6 +780,9 @@
|
|||||||
"Screen center": "螢幕中心",
|
"Screen center": "螢幕中心",
|
||||||
"Suggestions": "建議",
|
"Suggestions": "建議",
|
||||||
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
|
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
|
||||||
"Click the setting icon to change the source list url.": "點擊設定圖示更改源列表URL"
|
"Show single image on first page": "在首頁顯示單張圖片",
|
||||||
|
"Click to select an image": "點擊選擇一張圖片",
|
||||||
|
"Source URL": "源地址",
|
||||||
|
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.4.0";
|
final version = "1.4.1";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -178,13 +178,13 @@ class Settings with ChangeNotifier {
|
|||||||
'customImageProcessing': defaultCustomImageProcessing,
|
'customImageProcessing': defaultCustomImageProcessing,
|
||||||
'sni': true,
|
'sni': true,
|
||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
'comicSourceListUrl':
|
'comicSourceListUrl': defaultComicSourceUrl,
|
||||||
"https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
|
||||||
'preloadImageCount': 4,
|
'preloadImageCount': 4,
|
||||||
'followUpdatesFolder': null,
|
'followUpdatesFolder': null,
|
||||||
'initialPage': '0',
|
'initialPage': '0',
|
||||||
'comicListDisplayMode': 'paging', // paging, continuous
|
'comicListDisplayMode': 'paging', // paging, continuous
|
||||||
'showPageNumberInReader': true,
|
'showPageNumberInReader': true,
|
||||||
|
'showSingleImageOnFirstPage': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
@@ -219,3 +219,5 @@ function processImage(image, cid, eid, page, sourceKey) {
|
|||||||
return futureImage;
|
return futureImage;
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json";
|
||||||
|
@@ -21,7 +21,7 @@ class CacheManager {
|
|||||||
|
|
||||||
int _limitSize = 2 * 1024 * 1024 * 1024;
|
int _limitSize = 2 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
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');
|
||||||
_db.execute('''
|
_db.execute('''
|
||||||
@@ -33,100 +33,102 @@ class CacheManager {
|
|||||||
type TEXT
|
type TEXT
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
compute((path) => Directory(path).size, cachePath)
|
compute((path) => Directory(path).size, cachePath).then((value) {
|
||||||
.then((value) => _currentSize = value);
|
_currentSize = value;
|
||||||
|
checkCache();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the singleton instance of CacheManager.
|
||||||
factory CacheManager() => instance ??= CacheManager._create();
|
factory CacheManager() => instance ??= CacheManager._create();
|
||||||
|
|
||||||
/// set cache size limit in MB
|
/// set cache size limit in MB
|
||||||
void setLimitSize(int size){
|
void setLimitSize(int size) {
|
||||||
_limitSize = size * 1024 * 1024;
|
_limitSize = size * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setType(String key, String? type){
|
/// Write cache to disk.
|
||||||
_db.execute('''
|
Future<void> writeCache(String key, List<int> data,
|
||||||
UPDATE cache
|
[int duration = 7 * 24 * 60 * 60 * 1000]) async {
|
||||||
SET type = ?
|
|
||||||
WHERE key = ?
|
|
||||||
''', [type, key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? getType(String key){
|
|
||||||
var res = _db.select('''
|
|
||||||
SELECT type FROM cache
|
|
||||||
WHERE key = ?
|
|
||||||
''', [key]);
|
|
||||||
if(res.isEmpty){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return res.first[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeCache(String key, List<int> data, [int duration = 7 * 24 * 60 * 60 * 1000]) async{
|
|
||||||
this.dir++;
|
this.dir++;
|
||||||
this.dir %= 100;
|
this.dir %= 100;
|
||||||
var dir = this.dir;
|
var dir = this.dir;
|
||||||
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
|
var name = md5.convert(key.codeUnits).toString();
|
||||||
var file = File('$cachePath/$dir/$name');
|
var file = File('$cachePath/$dir/$name');
|
||||||
while(await file.exists()){
|
|
||||||
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
|
|
||||||
file = File('$cachePath/$dir/$name');
|
|
||||||
}
|
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
await file.writeAsBytes(data);
|
await file.writeAsBytes(data);
|
||||||
var expires = DateTime.now().millisecondsSinceEpoch + duration;
|
var expires = DateTime.now().millisecondsSinceEpoch + duration;
|
||||||
_db.execute('''
|
_db.execute('''
|
||||||
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
|
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
|
||||||
''', [key, dir.toString(), name, expires]);
|
''', [key, dir.toString(), name, expires]);
|
||||||
if(_currentSize != null) {
|
if (_currentSize != null) {
|
||||||
_currentSize = _currentSize! + data.length;
|
_currentSize = _currentSize! + data.length;
|
||||||
}
|
}
|
||||||
checkCacheIfRequired();
|
checkCacheIfRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CachingFile> openWrite(String key) async{
|
/// Find cache by key.
|
||||||
this.dir++;
|
/// If cache is expired, it will be deleted and return null.
|
||||||
this.dir %= 100;
|
/// If cache is not found, it will return null.
|
||||||
var dir = this.dir;
|
/// If cache is found, it will return the file, and update the expires time.
|
||||||
var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();
|
Future<File?> findCache(String key) async {
|
||||||
var file = File('$cachePath/$dir/$name');
|
|
||||||
while(await file.exists()){
|
|
||||||
name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();
|
|
||||||
file = File('$cachePath/$dir/$name');
|
|
||||||
}
|
|
||||||
await file.create(recursive: true);
|
|
||||||
return CachingFile._(key, dir.toString(), name, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<File?> findCache(String key) async{
|
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
SELECT * FROM cache
|
SELECT * FROM cache
|
||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
''', [key]);
|
''', [key]);
|
||||||
if(res.isEmpty){
|
if (res.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var row = res.first;
|
var row = res.first;
|
||||||
var dir = row[1] as String;
|
var dir = row[1] as String;
|
||||||
var name = row[2] as String;
|
var name = row[2] as String;
|
||||||
|
var expires = row[3] as int;
|
||||||
var file = File('$cachePath/$dir/$name');
|
var file = File('$cachePath/$dir/$name');
|
||||||
if(await file.exists()){
|
var now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (expires < now) {
|
||||||
|
// expired
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (await file.exists()) {
|
||||||
|
// update time
|
||||||
|
var expires = now + 7 * 24 * 60 * 60 * 1000;
|
||||||
|
_db.execute('''
|
||||||
|
UPDATE cache
|
||||||
|
SET expires = ?
|
||||||
|
WHERE key = ?
|
||||||
|
''', [expires, key]);
|
||||||
return file;
|
return file;
|
||||||
|
} else {
|
||||||
|
_db.execute('''
|
||||||
|
DELETE FROM cache
|
||||||
|
WHERE key = ?
|
||||||
|
''', [key]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isChecking = false;
|
bool _isChecking = false;
|
||||||
|
|
||||||
|
/// Check cache size and delete expired cache.
|
||||||
|
/// Only check cache if current size is greater than limit size.
|
||||||
void checkCacheIfRequired() {
|
void checkCacheIfRequired() {
|
||||||
if(_currentSize != null && _currentSize! > _limitSize){
|
if (_currentSize != null && _currentSize! > _limitSize) {
|
||||||
checkCache();
|
checkCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkCache() async{
|
/// Check cache size and delete expired cache.
|
||||||
if(_isChecking){
|
/// If current size is greater than limit size,
|
||||||
|
/// delete cache until current size is less than limit size.
|
||||||
|
Future<void> checkCache() async {
|
||||||
|
if (_isChecking) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isChecking = true;
|
_isChecking = true;
|
||||||
@@ -134,11 +136,13 @@ class CacheManager {
|
|||||||
SELECT * FROM cache
|
SELECT * FROM cache
|
||||||
WHERE expires < ?
|
WHERE expires < ?
|
||||||
''', [DateTime.now().millisecondsSinceEpoch]);
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
for(var row in res){
|
for (var row in res) {
|
||||||
var dir = row[1] as String;
|
var dir = row[1] as String;
|
||||||
var name = row[2] as String;
|
var name = row[2] as String;
|
||||||
var file = File('$cachePath/$dir/$name');
|
var file = File('$cachePath/$dir/$name');
|
||||||
if(await file.exists()){
|
if (await file.exists()) {
|
||||||
|
var size = await file.length();
|
||||||
|
_currentSize = _currentSize! - size;
|
||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,26 +151,18 @@ class CacheManager {
|
|||||||
WHERE expires < ?
|
WHERE expires < ?
|
||||||
''', [DateTime.now().millisecondsSinceEpoch]);
|
''', [DateTime.now().millisecondsSinceEpoch]);
|
||||||
|
|
||||||
int count = 0;
|
while (_currentSize != null && _currentSize! > _limitSize) {
|
||||||
var res2 = _db.select('''
|
|
||||||
SELECT COUNT(*) FROM cache
|
|
||||||
''');
|
|
||||||
if(res2.isNotEmpty){
|
|
||||||
count = res2.first[0] as int;
|
|
||||||
}
|
|
||||||
|
|
||||||
while((_currentSize != null && _currentSize! > _limitSize) || count > 2000){
|
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
SELECT * FROM cache
|
SELECT * FROM cache
|
||||||
ORDER BY expires ASC
|
ORDER BY expires ASC
|
||||||
limit 10
|
limit 10
|
||||||
''');
|
''');
|
||||||
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;
|
||||||
var name = row[2] as String;
|
var name = row[2] as String;
|
||||||
var file = File('$cachePath/$dir/$name');
|
var file = File('$cachePath/$dir/$name');
|
||||||
if(await file.exists()){
|
if (await file.exists()) {
|
||||||
var size = await file.length();
|
var size = await file.length();
|
||||||
await file.delete();
|
await file.delete();
|
||||||
_db.execute('''
|
_db.execute('''
|
||||||
@@ -174,7 +170,7 @@ class CacheManager {
|
|||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
''', [key]);
|
''', [key]);
|
||||||
_currentSize = _currentSize! - size;
|
_currentSize = _currentSize! - size;
|
||||||
if(_currentSize! <= _limitSize){
|
if (_currentSize! <= _limitSize) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -183,18 +179,18 @@ class CacheManager {
|
|||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
''', [key]);
|
''', [key]);
|
||||||
}
|
}
|
||||||
count--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_isChecking = false;
|
_isChecking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(String key) async{
|
/// Delete cache by key.
|
||||||
|
Future<void> delete(String key) async {
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
SELECT * FROM cache
|
SELECT * FROM cache
|
||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
''', [key]);
|
''', [key]);
|
||||||
if(res.isEmpty){
|
if (res.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var row = res.first;
|
var row = res.first;
|
||||||
@@ -202,7 +198,7 @@ class CacheManager {
|
|||||||
var name = row[2] as String;
|
var name = row[2] as String;
|
||||||
var file = File('$cachePath/$dir/$name');
|
var file = File('$cachePath/$dir/$name');
|
||||||
var fileSize = 0;
|
var fileSize = 0;
|
||||||
if(await file.exists()){
|
if (await file.exists()) {
|
||||||
fileSize = await file.length();
|
fileSize = await file.length();
|
||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
@@ -210,11 +206,12 @@ class CacheManager {
|
|||||||
DELETE FROM cache
|
DELETE FROM cache
|
||||||
WHERE key = ?
|
WHERE key = ?
|
||||||
''', [key]);
|
''', [key]);
|
||||||
if(_currentSize != null) {
|
if (_currentSize != null) {
|
||||||
_currentSize = _currentSize! - fileSize;
|
_currentSize = _currentSize! - fileSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete all cache.
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await Directory(cachePath).delete(recursive: true);
|
await Directory(cachePath).delete(recursive: true);
|
||||||
Directory(cachePath).createSync(recursive: true);
|
Directory(cachePath).createSync(recursive: true);
|
||||||
@@ -223,75 +220,4 @@ class CacheManager {
|
|||||||
''');
|
''');
|
||||||
_currentSize = 0;
|
_currentSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteKeyword(String keyword) async{
|
|
||||||
var res = _db.select('''
|
|
||||||
SELECT * FROM cache
|
|
||||||
WHERE key LIKE ?
|
|
||||||
''', ['%$keyword%']);
|
|
||||||
for(var row in res){
|
|
||||||
var key = row[0] as String;
|
|
||||||
var dir = row[1] as String;
|
|
||||||
var name = row[2] as String;
|
|
||||||
var file = File('$cachePath/$dir/$name');
|
|
||||||
var fileSize = 0;
|
|
||||||
if(await file.exists()){
|
|
||||||
fileSize = await file.length();
|
|
||||||
try {
|
|
||||||
await file.delete();
|
|
||||||
}
|
|
||||||
finally {}
|
|
||||||
}
|
|
||||||
_db.execute('''
|
|
||||||
DELETE FROM cache
|
|
||||||
WHERE key = ?
|
|
||||||
''', [key]);
|
|
||||||
if(_currentSize != null) {
|
|
||||||
_currentSize = _currentSize! - fileSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CachingFile{
|
|
||||||
CachingFile._(this.key, this.dir, this.name, this.file);
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
|
|
||||||
final String dir;
|
|
||||||
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
final File file;
|
|
||||||
|
|
||||||
final List<int> _buffer = [];
|
|
||||||
|
|
||||||
Future<void> writeBytes(List<int> data) async{
|
|
||||||
_buffer.addAll(data);
|
|
||||||
if(_buffer.length > 1024 * 1024){
|
|
||||||
await file.writeAsBytes(_buffer, mode: FileMode.append);
|
|
||||||
_buffer.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async{
|
|
||||||
if(_buffer.isNotEmpty){
|
|
||||||
await file.writeAsBytes(_buffer, mode: FileMode.append);
|
|
||||||
}
|
|
||||||
CacheManager()._db.execute('''
|
|
||||||
INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)
|
|
||||||
''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]);
|
|
||||||
CacheManager().checkCacheIfRequired();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cancel() async{
|
|
||||||
await file.deleteIgnoreError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
_buffer.clear();
|
|
||||||
if(file.existsSync()) {
|
|
||||||
file.deleteSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -234,7 +234,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
alter table "$folder"
|
alter table "$folder"
|
||||||
add column translated_tags TEXT;
|
add column translated_tags TEXT;
|
||||||
""");
|
""");
|
||||||
var comics = getAllComics(folder);
|
var comics = getFolderComics(folder);
|
||||||
for (var comic in comics) {
|
for (var comic in comics) {
|
||||||
var translatedTags = _translateTags(comic.tags);
|
var translatedTags = _translateTags(comic.tags);
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
@@ -349,7 +349,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
""").firstOrNull?["min_value"] ?? 0;
|
""").firstOrNull?["min_value"] ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FavoriteItem> getAllComics(String folder) {
|
List<FavoriteItem> getFolderComics(String folder) {
|
||||||
var rows = _db.select("""
|
var rows = _db.select("""
|
||||||
select * from "$folder"
|
select * from "$folder"
|
||||||
ORDER BY display_order;
|
ORDER BY display_order;
|
||||||
@@ -357,6 +357,17 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
|
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<FavoriteItem> getAllComics() {
|
||||||
|
var res = <FavoriteItem>{};
|
||||||
|
for (final folder in folderNames) {
|
||||||
|
var comics = _db.select("""
|
||||||
|
select * from "$folder";
|
||||||
|
""");
|
||||||
|
res.addAll(comics.map((element) => FavoriteItem.fromRow(element)));
|
||||||
|
}
|
||||||
|
return res.toList();
|
||||||
|
}
|
||||||
|
|
||||||
void addTagTo(String folder, String id, String tag) {
|
void addTagTo(String folder, String id, String tag) {
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
update "$folder"
|
update "$folder"
|
||||||
@@ -736,10 +747,10 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return comics;
|
return comics;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FavoriteItemWithFolderInfo> search(String keyword) {
|
List<FavoriteItem> search(String keyword) {
|
||||||
var keywordList = keyword.split(" ");
|
var keywordList = keyword.split(" ");
|
||||||
keyword = keywordList.first;
|
keyword = keywordList.first;
|
||||||
var comics = <FavoriteItemWithFolderInfo>[];
|
var comics = <FavoriteItem>{};
|
||||||
for (var table in folderNames) {
|
for (var table in folderNames) {
|
||||||
keyword = "%$keyword%";
|
keyword = "%$keyword%";
|
||||||
var res = _db.select("""
|
var res = _db.select("""
|
||||||
@@ -747,15 +758,18 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
|
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
|
||||||
""", [keyword, keyword, keyword, keyword]);
|
""", [keyword, keyword, keyword, keyword]);
|
||||||
for (var comic in res) {
|
for (var comic in res) {
|
||||||
comics.add(
|
comics.add(FavoriteItem.fromRow(comic));
|
||||||
FavoriteItemWithFolderInfo(FavoriteItem.fromRow(comic), table));
|
|
||||||
}
|
}
|
||||||
if (comics.length > 200) {
|
if (comics.length > 200) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool test(FavoriteItemWithFolderInfo comic, String keyword) {
|
bool test(FavoriteItem comic, String keyword) {
|
||||||
|
keyword = keyword.trim();
|
||||||
|
if (keyword.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (comic.name.contains(keyword)) {
|
if (comic.name.contains(keyword)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (comic.author.contains(keyword)) {
|
} else if (comic.author.contains(keyword)) {
|
||||||
@@ -766,12 +780,14 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 1; i < keywordList.length; i++) {
|
return comics.where((element) {
|
||||||
comics =
|
for (var i = 1; i < keywordList.length; i++) {
|
||||||
comics.where((element) => test(element, keywordList[i])).toList();
|
if (!test(element, keywordList[i])) {
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
return comics;
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void editTags(String id, String folder, List<String> tags) {
|
void editTags(String id, String folder, List<String> tags) {
|
||||||
|
@@ -25,6 +25,7 @@ import 'package:venera/components/js_ui.dart';
|
|||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
|
import 'package:venera/network/proxy.dart';
|
||||||
import 'package:venera/utils/init.dart';
|
import 'package:venera/utils/init.dart';
|
||||||
|
|
||||||
import 'comic_source/comic_source.dart';
|
import 'comic_source/comic_source.dart';
|
||||||
@@ -194,7 +195,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
responseType: ResponseType.plain,
|
responseType: ResponseType.plain,
|
||||||
validateStatus: (status) => true,
|
validateStatus: (status) => true,
|
||||||
));
|
));
|
||||||
var proxy = await AppDio.getProxy();
|
var proxy = await getProxy();
|
||||||
dio.httpClientAdapter = IOHttpClientAdapter(
|
dio.httpClientAdapter = IOHttpClientAdapter(
|
||||||
createHttpClient: () {
|
createHttpClient: () {
|
||||||
return HttpClient()
|
return HttpClient()
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_saf/flutter_saf.dart';
|
import 'package:flutter_saf/flutter_saf.dart';
|
||||||
import 'package:rhttp/rhttp.dart';
|
import 'package:rhttp/rhttp.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
@@ -51,6 +54,14 @@ Future<void> init() async {
|
|||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
};
|
};
|
||||||
|
if (App.isWindows) {
|
||||||
|
// Report to the monitor thread that the app is running
|
||||||
|
// https://github.com/venera-app/venera/issues/343
|
||||||
|
Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
const methodChannel = MethodChannel('venera/method_channel');
|
||||||
|
methodChannel.invokeMethod("heartBeat");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkOldConfigs() {
|
void _checkOldConfigs() {
|
||||||
|
@@ -7,7 +7,7 @@ import 'package:rhttp/rhttp.dart' as rhttp;
|
|||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/cache.dart';
|
import 'package:venera/network/cache.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/network/proxy.dart';
|
||||||
|
|
||||||
import '../foundation/app.dart';
|
import '../foundation/app.dart';
|
||||||
import 'cloudflare.dart';
|
import 'cloudflare.dart';
|
||||||
@@ -96,9 +96,11 @@ class MyLogInterceptor implements Interceptor {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
Log.info("Network", "${options.method} ${options.uri}\n"
|
Log.info(
|
||||||
"headers:\n${options.headers}\n"
|
"Network",
|
||||||
"data:\n${options.data}");
|
"${options.method} ${options.uri}\n"
|
||||||
|
"headers:\n${options.headers}\n"
|
||||||
|
"data:\n${options.data}");
|
||||||
options.connectTimeout = const Duration(seconds: 15);
|
options.connectTimeout = const Duration(seconds: 15);
|
||||||
options.receiveTimeout = const Duration(seconds: 15);
|
options.receiveTimeout = const Duration(seconds: 15);
|
||||||
options.sendTimeout = const Duration(seconds: 15);
|
options.sendTimeout = const Duration(seconds: 15);
|
||||||
@@ -107,64 +109,15 @@ class MyLogInterceptor implements Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppDio with DioMixin {
|
class AppDio with DioMixin {
|
||||||
String? _proxy = proxy;
|
|
||||||
|
|
||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
httpClientAdapter = RHttpAdapter(rhttp.ClientSettings(
|
httpClientAdapter = RHttpAdapter();
|
||||||
proxySettings: proxy == null
|
|
||||||
? const rhttp.ProxySettings.noProxy()
|
|
||||||
: rhttp.ProxySettings.proxy(proxy!),
|
|
||||||
));
|
|
||||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(NetworkCacheManager());
|
||||||
interceptors.add(CloudflareInterceptor());
|
interceptors.add(CloudflareInterceptor());
|
||||||
interceptors.add(MyLogInterceptor());
|
interceptors.add(MyLogInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? proxy;
|
|
||||||
|
|
||||||
static Future<String?> getProxy() async {
|
|
||||||
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
|
|
||||||
|
|
||||||
String res;
|
|
||||||
if (!App.isLinux) {
|
|
||||||
const channel = MethodChannel("venera/method_channel");
|
|
||||||
try {
|
|
||||||
res = await channel.invokeMethod("getProxy");
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = "No Proxy";
|
|
||||||
}
|
|
||||||
if (res == "No Proxy") return null;
|
|
||||||
|
|
||||||
if (res.contains(";")) {
|
|
||||||
var proxies = res.split(";");
|
|
||||||
for (String proxy in proxies) {
|
|
||||||
proxy = proxy.removeAllBlank;
|
|
||||||
if (proxy.startsWith('https=')) {
|
|
||||||
return proxy.substring(6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final RegExp regex = RegExp(
|
|
||||||
r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$',
|
|
||||||
caseSensitive: false,
|
|
||||||
multiLine: false,
|
|
||||||
);
|
|
||||||
if (!regex.hasMatch(res)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final Map<String, bool> _requests = {};
|
static final Map<String, bool> _requests = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -184,16 +137,6 @@ class AppDio with DioMixin {
|
|||||||
_requests[path] = true;
|
_requests[path] = true;
|
||||||
options!.headers!.remove('prevent-parallel');
|
options!.headers!.remove('prevent-parallel');
|
||||||
}
|
}
|
||||||
proxy = await getProxy();
|
|
||||||
if (_proxy != proxy) {
|
|
||||||
Log.info("Network", "Proxy changed to $proxy");
|
|
||||||
_proxy = proxy;
|
|
||||||
httpClientAdapter = RHttpAdapter(rhttp.ClientSettings(
|
|
||||||
proxySettings: proxy == null
|
|
||||||
? const rhttp.ProxySettings.noProxy()
|
|
||||||
: rhttp.ProxySettings.proxy(proxy!),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return super.request<T>(
|
return super.request<T>(
|
||||||
path,
|
path,
|
||||||
@@ -213,7 +156,26 @@ class AppDio with DioMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RHttpAdapter implements HttpClientAdapter {
|
class RHttpAdapter implements HttpClientAdapter {
|
||||||
rhttp.ClientSettings settings;
|
Future<rhttp.ClientSettings> get settings async {
|
||||||
|
var proxy = await getProxy();
|
||||||
|
|
||||||
|
return rhttp.ClientSettings(
|
||||||
|
proxySettings: proxy == null
|
||||||
|
? const rhttp.ProxySettings.noProxy()
|
||||||
|
: rhttp.ProxySettings.proxy(proxy),
|
||||||
|
redirectSettings: const rhttp.RedirectSettings.limited(5),
|
||||||
|
timeoutSettings: const rhttp.TimeoutSettings(
|
||||||
|
connectTimeout: Duration(seconds: 15),
|
||||||
|
keepAliveTimeout: Duration(seconds: 60),
|
||||||
|
keepAlivePing: Duration(seconds: 30),
|
||||||
|
),
|
||||||
|
throwOnStatusCode: false,
|
||||||
|
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
||||||
|
tlsSettings: rhttp.TlsSettings(
|
||||||
|
sni: appdata.settings['sni'] != false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Map<String, List<String>> _getOverrides() {
|
static Map<String, List<String>> _getOverrides() {
|
||||||
if (!appdata.settings['enableDnsOverrides'] == true) {
|
if (!appdata.settings['enableDnsOverrides'] == true) {
|
||||||
@@ -231,22 +193,6 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
|
|
||||||
settings = settings.copyWith(
|
|
||||||
redirectSettings: const rhttp.RedirectSettings.limited(5),
|
|
||||||
timeoutSettings: const rhttp.TimeoutSettings(
|
|
||||||
connectTimeout: Duration(seconds: 15),
|
|
||||||
keepAliveTimeout: Duration(seconds: 60),
|
|
||||||
keepAlivePing: Duration(seconds: 30),
|
|
||||||
),
|
|
||||||
throwOnStatusCode: false,
|
|
||||||
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
|
||||||
tlsSettings: rhttp.TlsSettings(
|
|
||||||
sni: appdata.settings['sni'] != false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void close({bool force = false}) {}
|
void close({bool force = false}) {}
|
||||||
|
|
||||||
@@ -256,10 +202,15 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
Stream<Uint8List>? requestStream,
|
Stream<Uint8List>? requestStream,
|
||||||
Future<void>? cancelFuture,
|
Future<void>? cancelFuture,
|
||||||
) async {
|
) async {
|
||||||
|
if (options.headers['User-Agent'] == null &&
|
||||||
|
options.headers['user-agent'] == null) {
|
||||||
|
options.headers['User-Agent'] = "venera/v${App.version}";
|
||||||
|
}
|
||||||
|
|
||||||
var res = await rhttp.Rhttp.request(
|
var res = await rhttp.Rhttp.request(
|
||||||
method: rhttp.HttpMethod(options.method),
|
method: rhttp.HttpMethod(options.method),
|
||||||
url: options.uri.toString(),
|
url: options.uri.toString(),
|
||||||
settings: settings,
|
settings: await settings,
|
||||||
expectBody: rhttp.HttpExpectBody.stream,
|
expectBody: rhttp.HttpExpectBody.stream,
|
||||||
body: requestStream == null ? null : rhttp.HttpBody.stream(requestStream),
|
body: requestStream == null ? null : rhttp.HttpBody.stream(requestStream),
|
||||||
headers: rhttp.HttpHeaders.rawMap(
|
headers: rhttp.HttpHeaders.rawMap(
|
||||||
@@ -289,7 +240,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String _getStatusMessage(int statusCode) {
|
static String _getStatusMessage(int statusCode) {
|
||||||
return switch(statusCode) {
|
return switch (statusCode) {
|
||||||
200 => "OK",
|
200 => "OK",
|
||||||
201 => "Created",
|
201 => "Created",
|
||||||
202 => "Accepted",
|
202 => "Accepted",
|
||||||
@@ -299,9 +250,11 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
302 => "Found",
|
302 => "Found",
|
||||||
400 => "Invalid Status Code 400: The Request is invalid.",
|
400 => "Invalid Status Code 400: The Request is invalid.",
|
||||||
401 => "Invalid Status Code 401: The Request is unauthorized.",
|
401 => "Invalid Status Code 401: The Request is unauthorized.",
|
||||||
403 => "Invalid Status Code 403: No permission to access the resource. Check your account or network.",
|
403 =>
|
||||||
|
"Invalid Status Code 403: No permission to access the resource. Check your account or network.",
|
||||||
404 => "Invalid Status Code 404: Not found.",
|
404 => "Invalid Status Code 404: Not found.",
|
||||||
429 => "Invalid Status Code 429: Too many requests. Please try again later.",
|
429 =>
|
||||||
|
"Invalid Status Code 429: Too many requests. Please try again later.",
|
||||||
_ => "Invalid Status Code $statusCode",
|
_ => "Invalid Status Code $statusCode",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
|
import 'package:venera/network/proxy.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
class FileDownloader {
|
class FileDownloader {
|
||||||
@@ -105,7 +106,7 @@ class FileDownloader {
|
|||||||
|
|
||||||
void _download(StreamController<DownloadingStatus> resultStream) async {
|
void _download(StreamController<DownloadingStatus> resultStream) async {
|
||||||
try {
|
try {
|
||||||
var proxy = await AppDio.getProxy();
|
var proxy = await getProxy();
|
||||||
_dio.httpClientAdapter = IOHttpClientAdapter(
|
_dio.httpClientAdapter = IOHttpClientAdapter(
|
||||||
createHttpClient: () {
|
createHttpClient: () {
|
||||||
return HttpClient()
|
return HttpClient()
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
@@ -8,7 +9,7 @@ import 'package:venera/utils/image.dart';
|
|||||||
|
|
||||||
import 'app_dio.dart';
|
import 'app_dio.dart';
|
||||||
|
|
||||||
class ImageDownloader {
|
abstract class ImageDownloader {
|
||||||
static Stream<ImageDownloadProgress> loadThumbnail(
|
static Stream<ImageDownloadProgress> loadThumbnail(
|
||||||
String url, String? sourceKey,
|
String url, String? sourceKey,
|
||||||
[String? cid]) async* {
|
[String? cid]) async* {
|
||||||
@@ -82,7 +83,35 @@ class ImageDownloader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final _loadingImages = <String, _StreamWrapper<ImageDownloadProgress>>{};
|
||||||
|
|
||||||
|
/// Cancel all loading images.
|
||||||
|
static void cancelAllLoadingImages() {
|
||||||
|
for (var wrapper in _loadingImages.values) {
|
||||||
|
wrapper.cancel();
|
||||||
|
}
|
||||||
|
_loadingImages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a comic image from the network or cache.
|
||||||
|
/// The function will prevent multiple requests for the same image.
|
||||||
static Stream<ImageDownloadProgress> loadComicImage(
|
static Stream<ImageDownloadProgress> loadComicImage(
|
||||||
|
String imageKey, String? sourceKey, String cid, String eid) {
|
||||||
|
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
||||||
|
if (_loadingImages.containsKey(cacheKey)) {
|
||||||
|
return _loadingImages[cacheKey]!.stream;
|
||||||
|
}
|
||||||
|
final stream = _StreamWrapper<ImageDownloadProgress>(
|
||||||
|
_loadComicImage(imageKey, sourceKey, cid, eid),
|
||||||
|
(wrapper) {
|
||||||
|
_loadingImages.remove(cacheKey);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_loadingImages[cacheKey] = stream;
|
||||||
|
return stream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<ImageDownloadProgress> _loadComicImage(
|
||||||
String imageKey, String? sourceKey, String cid, String eid) async* {
|
String imageKey, String? sourceKey, String cid, String eid) async* {
|
||||||
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
||||||
final cache = await CacheManager().findCache(cacheKey);
|
final cache = await CacheManager().findCache(cacheKey);
|
||||||
@@ -189,6 +218,63 @@ class ImageDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A wrapper class for a stream that
|
||||||
|
/// allows multiple listeners to listen to the same stream.
|
||||||
|
class _StreamWrapper<T> {
|
||||||
|
final Stream<T> _stream;
|
||||||
|
|
||||||
|
final List<StreamController> controllers = [];
|
||||||
|
|
||||||
|
final void Function(_StreamWrapper<T> wrapper) onClosed;
|
||||||
|
|
||||||
|
bool isClosed = false;
|
||||||
|
|
||||||
|
_StreamWrapper(this._stream, this.onClosed) {
|
||||||
|
_listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listen() async {
|
||||||
|
await for (var data in _stream) {
|
||||||
|
if (isClosed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (var controller in controllers) {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
controller.add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var controller in controllers) {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controllers.clear();
|
||||||
|
isClosed = true;
|
||||||
|
onClosed(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<T> get stream {
|
||||||
|
if (isClosed) {
|
||||||
|
throw Exception('Stream is closed');
|
||||||
|
}
|
||||||
|
var controller = StreamController<T>();
|
||||||
|
controllers.add(controller);
|
||||||
|
controller.onCancel = () {
|
||||||
|
controllers.remove(controller);
|
||||||
|
};
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
controllers.clear();
|
||||||
|
isClosed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ImageDownloadProgress {
|
class ImageDownloadProgress {
|
||||||
final int currentBytes;
|
final int currentBytes;
|
||||||
|
|
||||||
|
60
lib/network/proxy.dart
Normal file
60
lib/network/proxy.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
|
String? _cachedProxy;
|
||||||
|
|
||||||
|
DateTime? _cachedProxyTime;
|
||||||
|
|
||||||
|
Future<String?> getProxy() async {
|
||||||
|
if (_cachedProxyTime != null &&
|
||||||
|
DateTime.now().difference(_cachedProxyTime!).inSeconds < 1) {
|
||||||
|
return _cachedProxy;
|
||||||
|
}
|
||||||
|
String? proxy = await _getProxy();
|
||||||
|
_cachedProxy = proxy;
|
||||||
|
_cachedProxyTime = DateTime.now();
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _getProxy() async {
|
||||||
|
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
|
||||||
|
|
||||||
|
String res;
|
||||||
|
if (!App.isLinux) {
|
||||||
|
const channel = MethodChannel("venera/method_channel");
|
||||||
|
try {
|
||||||
|
res = await channel.invokeMethod("getProxy");
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = "No Proxy";
|
||||||
|
}
|
||||||
|
if (res == "No Proxy") return null;
|
||||||
|
|
||||||
|
if (res.contains(";")) {
|
||||||
|
var proxies = res.split(";");
|
||||||
|
for (String proxy in proxies) {
|
||||||
|
proxy = proxy.removeAllBlank;
|
||||||
|
if (proxy.startsWith('https=')) {
|
||||||
|
return proxy.substring(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp regex = RegExp(
|
||||||
|
r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$',
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
);
|
||||||
|
if (!regex.hasMatch(res)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
@@ -322,127 +322,168 @@ class _ComicSourceList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ComicSourceListState extends State<_ComicSourceList> {
|
class _ComicSourceListState extends State<_ComicSourceList> {
|
||||||
bool loading = true;
|
|
||||||
List? json;
|
List? json;
|
||||||
|
bool changed = false;
|
||||||
|
var controller = TextEditingController();
|
||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
var dio = AppDio();
|
if (json != null) {
|
||||||
var res = await dio.get<String>(appdata.settings['comicSourceListUrl']);
|
setState(() {
|
||||||
if (res.statusCode != 200) {
|
json = null;
|
||||||
context.showMessage(message: "Network error".tl);
|
});
|
||||||
return;
|
}
|
||||||
|
var dio = AppDio();
|
||||||
|
try {
|
||||||
|
var res = await dio.get<String>(controller.text);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw "error";
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
json = jsonDecode(res.data!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
context.showMessage(message: "Network error".tl);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
json = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller.text = appdata.settings['comicSourceListUrl'];
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
if (changed) {
|
||||||
|
appdata.settings['comicSourceListUrl'] = controller.text;
|
||||||
|
appdata.saveData();
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
json = jsonDecode(res.data!);
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopUpWidgetScaffold(
|
return PopUpWidgetScaffold(
|
||||||
title: "Comic Source".tl,
|
title: "Comic Source".tl,
|
||||||
tailing: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
onPressed: () async {
|
|
||||||
await showInputDialog(
|
|
||||||
context: context,
|
|
||||||
title: "Set comic source list url".tl,
|
|
||||||
initialValue: appdata.settings['comicSourceListUrl'],
|
|
||||||
onConfirm: (value) {
|
|
||||||
appdata.settings['comicSourceListUrl'] = value;
|
|
||||||
appdata.saveData();
|
|
||||||
setState(() {
|
|
||||||
loading = true;
|
|
||||||
json = null;
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
body: buildBody(),
|
body: buildBody(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBody() {
|
Widget buildBody() {
|
||||||
if (loading) {
|
var currentKey = ComicSource.all().map((e) => e.key).toList();
|
||||||
load();
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
return ListView.builder(
|
||||||
} else {
|
itemCount: (json?.length ?? 1) + 1,
|
||||||
var currentKey = ComicSource.all().map((e) => e.key).toList();
|
itemBuilder: (context, index) {
|
||||||
return ListView.builder(
|
if (index == 0) {
|
||||||
itemCount: json!.length + 1,
|
return Container(
|
||||||
itemBuilder: (context, index) {
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
if (index == 0) {
|
decoration: BoxDecoration(
|
||||||
return Container(
|
border: Border.all(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
padding: const EdgeInsets.all(8),
|
width: 0.6,
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: context.colorScheme.primaryContainer,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(8),
|
||||||
children: [
|
),
|
||||||
const Icon(Icons.info_outline),
|
child: Column(
|
||||||
const SizedBox(width: 8),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Expanded(
|
children: [
|
||||||
child: Column(
|
ListTile(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
leading: Icon(Icons.source_outlined),
|
||||||
children: [
|
title: Text("Source URL".tl),
|
||||||
Text("Do not report any issues related to sources to App repo.".tl),
|
),
|
||||||
Text("Click the setting icon to change the source list url.".tl),
|
TextField(
|
||||||
],
|
controller: controller,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
|
hintText: "URL",
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
],
|
onChanged: (value) {
|
||||||
),
|
changed = true;
|
||||||
);
|
|
||||||
}
|
|
||||||
index--;
|
|
||||||
|
|
||||||
var key = json![index]["key"];
|
|
||||||
var action = currentKey.contains(key)
|
|
||||||
? const Icon(Icons.check, size: 20).paddingRight(8)
|
|
||||||
: Button.filled(
|
|
||||||
child: Text("Add".tl),
|
|
||||||
onPressed: () async {
|
|
||||||
var fileName = json![index]["fileName"];
|
|
||||||
var url = json![index]["url"];
|
|
||||||
if (url == null || !(url.toString()).isURL) {
|
|
||||||
var listUrl =
|
|
||||||
appdata.settings['comicSourceListUrl'] as String;
|
|
||||||
if (listUrl
|
|
||||||
.replaceFirst("https://", "")
|
|
||||||
.replaceFirst("http://", "")
|
|
||||||
.contains("/")) {
|
|
||||||
url =
|
|
||||||
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
|
||||||
fileName;
|
|
||||||
} else {
|
|
||||||
url = '$listUrl/$fileName';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await widget.onAdd(url);
|
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
).fixHeight(32);
|
).paddingHorizontal(16).paddingBottom(8),
|
||||||
|
Text("The URL should point to a 'index.json' file".tl).paddingLeft(16),
|
||||||
var description = json![index]["version"];
|
Text("Do not report any issues related to sources to App repo.".tl).paddingLeft(16),
|
||||||
if (json![index]["description"] != null) {
|
const SizedBox(height: 8),
|
||||||
description = "$description\n${json![index]["description"]}";
|
Row(
|
||||||
}
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
return ListTile(
|
TextButton(
|
||||||
title: Text(json![index]["name"]),
|
onPressed: () {
|
||||||
subtitle: Text(description),
|
controller.text = defaultComicSourceUrl;
|
||||||
trailing: action,
|
changed = true;
|
||||||
|
},
|
||||||
|
child: Text("Reset".tl),
|
||||||
|
),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: load,
|
||||||
|
child: Text("Refresh".tl),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
}
|
if (index == 1 && json == null) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
index--;
|
||||||
|
|
||||||
|
var key = json![index]["key"];
|
||||||
|
var action = currentKey.contains(key)
|
||||||
|
? const Icon(Icons.check, size: 20).paddingRight(8)
|
||||||
|
: Button.filled(
|
||||||
|
child: Text("Add".tl),
|
||||||
|
onPressed: () async {
|
||||||
|
var fileName = json![index]["fileName"];
|
||||||
|
var url = json![index]["url"];
|
||||||
|
if (url == null || !(url.toString()).isURL) {
|
||||||
|
var listUrl =
|
||||||
|
appdata.settings['comicSourceListUrl'] as String;
|
||||||
|
if (listUrl
|
||||||
|
.replaceFirst("https://", "")
|
||||||
|
.replaceFirst("http://", "")
|
||||||
|
.contains("/")) {
|
||||||
|
url =
|
||||||
|
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
|
||||||
|
fileName;
|
||||||
|
} else {
|
||||||
|
url = '$listUrl/$fileName';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await widget.onAdd(url);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
).fixHeight(32);
|
||||||
|
|
||||||
|
var description = json![index]["version"];
|
||||||
|
if (json![index]["description"] != null) {
|
||||||
|
description = "$description\n${json![index]["description"]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(json![index]["name"]),
|
||||||
|
subtitle: Text(description),
|
||||||
|
trailing: action,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -133,7 +133,7 @@ void addFavorite(List<Comic> comics) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
||||||
var comics = LocalFavoritesManager().getAllComics(folder);
|
var comics = LocalFavoritesManager().getFolderComics(folder);
|
||||||
|
|
||||||
Future<void> updateSingleComic(int index) async {
|
Future<void> updateSingleComic(int index) async {
|
||||||
int retry = 3;
|
int retry = 3;
|
||||||
|
@@ -25,7 +25,6 @@ part 'favorite_actions.dart';
|
|||||||
part 'side_bar.dart';
|
part 'side_bar.dart';
|
||||||
part 'local_favorites_page.dart';
|
part 'local_favorites_page.dart';
|
||||||
part 'network_favorites_page.dart';
|
part 'network_favorites_page.dart';
|
||||||
part 'local_search_page.dart';
|
|
||||||
|
|
||||||
const _kLeftBarWidth = 256.0;
|
const _kLeftBarWidth = 256.0;
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
part of 'favorites_page.dart';
|
part of 'favorites_page.dart';
|
||||||
|
|
||||||
|
const _localAllFolderLabel = '^_^[%local_all%]^_^';
|
||||||
|
|
||||||
class _LocalFavoritesPage extends StatefulWidget {
|
class _LocalFavoritesPage extends StatefulWidget {
|
||||||
const _LocalFavoritesPage({required this.folder, super.key});
|
const _LocalFavoritesPage({required this.folder, super.key});
|
||||||
|
|
||||||
@@ -31,14 +33,25 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
|
|
||||||
int? lastSelectedIndex;
|
int? lastSelectedIndex;
|
||||||
|
|
||||||
|
bool get isAllFolder => widget.folder == _localAllFolderLabel;
|
||||||
|
|
||||||
void updateComics() {
|
void updateComics() {
|
||||||
if (keyword.isEmpty) {
|
if (keyword.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
if (isAllFolder) {
|
||||||
|
comics = LocalFavoritesManager().getAllComics();
|
||||||
|
} else {
|
||||||
|
comics = LocalFavoritesManager().getFolderComics(widget.folder);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
comics = LocalFavoritesManager().searchInFolder(widget.folder, keyword);
|
if (isAllFolder) {
|
||||||
|
comics = LocalFavoritesManager().search(keyword);
|
||||||
|
} else {
|
||||||
|
comics =
|
||||||
|
LocalFavoritesManager().searchInFolder(widget.folder, keyword);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,10 +59,16 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
if (!isAllFolder) {
|
||||||
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
comics = LocalFavoritesManager().getFolderComics(widget.folder);
|
||||||
networkSource = a;
|
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
||||||
networkFolder = b;
|
networkSource = a;
|
||||||
|
networkFolder = b;
|
||||||
|
} else {
|
||||||
|
comics = LocalFavoritesManager().getAllComics();
|
||||||
|
networkSource = null;
|
||||||
|
networkFolder = null;
|
||||||
|
}
|
||||||
LocalFavoritesManager().addListener(updateComics);
|
LocalFavoritesManager().addListener(updateComics);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@@ -113,6 +132,11 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var title = favPage.folder ?? "Unselected".tl;
|
||||||
|
if (title == _localAllFolderLabel) {
|
||||||
|
title = "All".tl;
|
||||||
|
}
|
||||||
|
|
||||||
Widget body = SmoothCustomScrollView(
|
Widget body = SmoothCustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -135,10 +159,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
onTap: context.width < _kTwoPanelChangeWidth
|
onTap: context.width < _kTwoPanelChangeWidth
|
||||||
? favPage.showFolderSelector
|
? favPage.showFolderSelector
|
||||||
: null,
|
: null,
|
||||||
child: Text(favPage.folder ?? "Unselected".tl),
|
child: Text(title),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (networkSource != null)
|
if (networkSource != null && !isAllFolder)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Sync".tl,
|
message: "Sync".tl,
|
||||||
child: Flyout(
|
child: Flyout(
|
||||||
@@ -196,9 +220,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuButton(
|
if (!isAllFolder)
|
||||||
entries: [
|
MenuButton(
|
||||||
MenuEntry(
|
entries: [
|
||||||
|
MenuEntry(
|
||||||
icon: Icons.edit_outlined,
|
icon: Icons.edit_outlined,
|
||||||
text: "Rename".tl,
|
text: "Rename".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
@@ -220,8 +245,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
MenuEntry(
|
),
|
||||||
|
MenuEntry(
|
||||||
icon: Icons.reorder,
|
icon: Icons.reorder,
|
||||||
text: "Reorder".tl,
|
text: "Reorder".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
@@ -241,8 +267,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
MenuEntry(
|
),
|
||||||
|
MenuEntry(
|
||||||
icon: Icons.upload_file,
|
icon: Icons.upload_file,
|
||||||
text: "Export".tl,
|
text: "Export".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
@@ -253,8 +280,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
data: utf8.encode(json),
|
data: utf8.encode(json),
|
||||||
filename: "${widget.folder}.json",
|
filename: "${widget.folder}.json",
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
MenuEntry(
|
),
|
||||||
|
MenuEntry(
|
||||||
icon: Icons.update,
|
icon: Icons.update,
|
||||||
text: "Update Comics Info".tl,
|
text: "Update Comics Info".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
@@ -265,8 +293,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
},
|
||||||
MenuEntry(
|
),
|
||||||
|
MenuEntry(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
text: "Delete Folder".tl,
|
text: "Delete Folder".tl,
|
||||||
color: context.colorScheme.error,
|
color: context.colorScheme.error,
|
||||||
@@ -284,9 +313,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
favPage.folderList?.updateFolders();
|
favPage.folderList?.updateFolders();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else if (multiSelectMode)
|
else if (multiSelectMode)
|
||||||
@@ -330,22 +360,23 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
icon: Icons.flip,
|
icon: Icons.flip,
|
||||||
text: "Invert Selection".tl,
|
text: "Invert Selection".tl,
|
||||||
onClick: invertSelection),
|
onClick: invertSelection),
|
||||||
MenuEntry(
|
if (!isAllFolder)
|
||||||
icon: Icons.delete_outline,
|
MenuEntry(
|
||||||
text: "Delete Comic".tl,
|
icon: Icons.delete_outline,
|
||||||
color: context.colorScheme.error,
|
text: "Delete Comic".tl,
|
||||||
onClick: () {
|
color: context.colorScheme.error,
|
||||||
showConfirmDialog(
|
onClick: () {
|
||||||
context: context,
|
showConfirmDialog(
|
||||||
title: "Delete".tl,
|
context: context,
|
||||||
content: "Delete @c comics?"
|
title: "Delete".tl,
|
||||||
.tlParams({"c": selectedComics.length}),
|
content: "Delete @c comics?"
|
||||||
btnColor: context.colorScheme.error,
|
.tlParams({"c": selectedComics.length}),
|
||||||
onConfirm: () {
|
btnColor: context.colorScheme.error,
|
||||||
_deleteComicWithId();
|
onConfirm: () {
|
||||||
},
|
_deleteComicWithId();
|
||||||
);
|
},
|
||||||
}),
|
);
|
||||||
|
}),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.download,
|
icon: Icons.download,
|
||||||
text: "Download".tl,
|
text: "Download".tl,
|
||||||
@@ -404,17 +435,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
selections: selectedComics,
|
selections: selectedComics,
|
||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
if (!isAllFolder)
|
||||||
icon: Icons.delete,
|
MenuEntry(
|
||||||
text: "Delete".tl,
|
icon: Icons.delete,
|
||||||
onClick: () {
|
text: "Delete".tl,
|
||||||
LocalFavoritesManager().deleteComicWithId(
|
onClick: () {
|
||||||
widget.folder,
|
LocalFavoritesManager().deleteComicWithId(
|
||||||
c.id,
|
widget.folder,
|
||||||
(c as FavoriteItem).type,
|
c.id,
|
||||||
);
|
(c as FavoriteItem).type,
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.check,
|
icon: Icons.check,
|
||||||
text: "Select".tl,
|
text: "Select".tl,
|
||||||
@@ -725,7 +757,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
final _key = GlobalKey();
|
final _key = GlobalKey();
|
||||||
var reorderWidgetKey = UniqueKey();
|
var reorderWidgetKey = UniqueKey();
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
late var comics = LocalFavoritesManager().getAllComics(widget.name);
|
late var comics = LocalFavoritesManager().getFolderComics(widget.name);
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
static int _floatToInt8(double x) {
|
static int _floatToInt8(double x) {
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
part of 'favorites_page.dart';
|
|
||||||
|
|
||||||
class LocalSearchPage extends StatefulWidget {
|
|
||||||
const LocalSearchPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LocalSearchPage> createState() => _LocalSearchPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LocalSearchPageState extends State<LocalSearchPage> {
|
|
||||||
String keyword = '';
|
|
||||||
|
|
||||||
var comics = <FavoriteItemWithFolderInfo>[];
|
|
||||||
|
|
||||||
late final SearchBarController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = SearchBarController(onSearch: (text) {
|
|
||||||
keyword = text;
|
|
||||||
comics = LocalFavoritesManager().search(keyword);
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: SmoothCustomScrollView(slivers: [
|
|
||||||
SliverSearchBar(controller: controller),
|
|
||||||
SliverGridComics(
|
|
||||||
comics: comics,
|
|
||||||
badgeBuilder: (c) {
|
|
||||||
return (c as FavoriteItemWithFolderInfo).folder;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -102,13 +102,6 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
entries: [
|
entries: [
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.search,
|
|
||||||
text: 'Search'.tl,
|
|
||||||
onClick: () {
|
|
||||||
context.to(() => const LocalSearchPage());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.add,
|
icon: Icons.add,
|
||||||
text: 'Create Folder'.tl,
|
text: 'Create Folder'.tl,
|
||||||
@@ -140,6 +133,10 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
index--;
|
index--;
|
||||||
|
if (index == 0) {
|
||||||
|
return buildLocalFolder(_localAllFolderLabel);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
if (index < folders.length) {
|
if (index < folders.length) {
|
||||||
return buildLocalFolder(folders[index]);
|
return buildLocalFolder(folders[index]);
|
||||||
}
|
}
|
||||||
@@ -214,7 +211,9 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
child: Text(name),
|
child: Text(name == _localAllFolderLabel
|
||||||
|
? "All".tl
|
||||||
|
: getFavoriteDataOrNull(name)?.title ?? name),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -306,7 +306,8 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// prevent dirty data
|
// prevent dirty data
|
||||||
var comic = LocalManager().find(c.id, ComicType.fromKey(c.sourceKey))!;
|
var comic =
|
||||||
|
LocalManager().find(c.id, ComicType.fromKey(c.sourceKey))!;
|
||||||
comic.read();
|
comic.read();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -444,7 +445,10 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
var fileName = "";
|
var fileName = "";
|
||||||
// For each comic, export it to a file
|
// For each comic, export it to a file
|
||||||
for (var comic in comics) {
|
for (var comic in comics) {
|
||||||
fileName = FilePath.join(cacheDir, sanitizeFileName(comic.title) + ext);
|
fileName = FilePath.join(
|
||||||
|
cacheDir,
|
||||||
|
sanitizeFileName(comic.title, maxLength: 100) + ext,
|
||||||
|
);
|
||||||
await export(comic, fileName);
|
await export(comic, fileName);
|
||||||
current++;
|
current++;
|
||||||
if (comics.length > 1) {
|
if (comics.length > 1) {
|
||||||
|
@@ -287,6 +287,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
text: "Copy Image".tl,
|
text: "Copy Image".tl,
|
||||||
onClick: () => copyImage(location),
|
onClick: () => copyImage(location),
|
||||||
),
|
),
|
||||||
|
if (!reader.isLoading)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.download_outlined,
|
||||||
|
text: "Save Image".tl,
|
||||||
|
onClick: () => saveImage(location),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -319,6 +325,17 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
context.showMessage(message: "No Image");
|
context.showMessage(message: "No Image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void saveImage(Offset location) async {
|
||||||
|
var controller = reader._imageViewController;
|
||||||
|
var image = await controller!.getImageByOffset(location);
|
||||||
|
if (image != null) {
|
||||||
|
var filetype = detectFileType(image);
|
||||||
|
saveFile(filename: "image${filetype.ext}", data: image);
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "No Image");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DragListener {
|
class _DragListener {
|
||||||
|
@@ -21,6 +21,12 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
ImageDownloader.cancelAllLoadingImages();
|
||||||
|
}
|
||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
if (inProgress) return;
|
if (inProgress) return;
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
@@ -104,15 +110,22 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
implements _ImageViewController {
|
implements _ImageViewController {
|
||||||
late PageController controller;
|
late PageController controller;
|
||||||
|
|
||||||
late List<bool> cached;
|
|
||||||
|
|
||||||
int get preCacheCount => appdata.settings["preloadImageCount"];
|
int get preCacheCount => appdata.settings["preloadImageCount"];
|
||||||
|
|
||||||
var photoViewControllers = <int, PhotoViewController>{};
|
var photoViewControllers = <int, PhotoViewController>{};
|
||||||
|
|
||||||
late _ReaderState reader;
|
late _ReaderState reader;
|
||||||
|
|
||||||
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
/// [totalPages] is the total number of pages in the current chapter.
|
||||||
|
/// More than one images can be displayed on one page.
|
||||||
|
int get totalPages {
|
||||||
|
if (!showSingleImageOnFirstPage) {
|
||||||
|
return (reader.images!.length / reader.imagesPerPage).ceil();
|
||||||
|
} else {
|
||||||
|
return 1 +
|
||||||
|
((reader.images!.length - 1) / reader.imagesPerPage).ceil();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var imageStates = <State<ComicImage>>{};
|
var imageStates = <State<ComicImage>>{};
|
||||||
|
|
||||||
@@ -125,24 +138,53 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
reader = context.reader;
|
reader = context.reader;
|
||||||
controller = PageController(initialPage: reader.page);
|
controller = PageController(initialPage: reader.page);
|
||||||
reader._imageViewController = this;
|
reader._imageViewController = this;
|
||||||
cached = List.filled(reader.maxPage + 2, false);
|
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
context.readerScaffold.setFloatingButton(0);
|
context.readerScaffold.setFloatingButton(0);
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cache(int current) {
|
bool get showSingleImageOnFirstPage => appdata.settings["showSingleImageOnFirstPage"];
|
||||||
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
|
||||||
if (i <= totalPages && !cached[i]) {
|
/// Get the range of images for the given page. [page] is 1-based.
|
||||||
int startIndex = (i - 1) * reader.imagesPerPage;
|
(int start, int end) getPageImagesRange(int page) {
|
||||||
int endIndex =
|
if (showSingleImageOnFirstPage) {
|
||||||
math.min(startIndex + reader.imagesPerPage, reader.images!.length);
|
if (page == 1) {
|
||||||
for (int i = startIndex; i < endIndex; i++) {
|
return (0, 1);
|
||||||
precacheImage(
|
} else {
|
||||||
_createImageProviderFromKey(reader.images![i], context), context);
|
int startIndex = (page - 2) * reader.imagesPerPage + 1;
|
||||||
}
|
int endIndex = math.min(
|
||||||
cached[i] = true;
|
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||||
|
return (startIndex, endIndex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int startIndex = (page - 1) * reader.imagesPerPage;
|
||||||
|
int endIndex = math.min(
|
||||||
|
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||||
|
return (startIndex, endIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [cache] is used to cache the images.
|
||||||
|
/// The count of images to cache is determined by the [preCacheCount] setting.
|
||||||
|
/// For previous page and next page, it will do a memory cache.
|
||||||
|
/// For current page, it will do nothing because it is already on the screen.
|
||||||
|
/// For other pages, it will do a pre-download cache.
|
||||||
|
void cache(int startPage) {
|
||||||
|
for (int i = startPage - 1; i <= startPage + preCacheCount; i++) {
|
||||||
|
if (i == startPage || i <= 0 || i > totalPages) continue;
|
||||||
|
bool shouldPreCache = i == startPage + 1 || i == startPage - 1;
|
||||||
|
_cachePage(i, shouldPreCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cachePage(int page, bool shouldPreCache) {
|
||||||
|
var (startIndex, endIndex) = getPageImagesRange(page);
|
||||||
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
|
if (shouldPreCache) {
|
||||||
|
_precacheImage(i+1, context);
|
||||||
|
} else {
|
||||||
|
_preDownloadImage(i+1, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,14 +227,10 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
child: const SizedBox(),
|
child: const SizedBox(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
int pageIndex = index - 1;
|
var (startIndex, endIndex) = getPageImagesRange(index);
|
||||||
int startIndex = pageIndex * reader.imagesPerPage;
|
|
||||||
int endIndex = math.min(
|
|
||||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
|
||||||
List<String> pageImages =
|
List<String> pageImages =
|
||||||
reader.images!.sublist(startIndex, endIndex);
|
reader.images!.sublist(startIndex, endIndex);
|
||||||
|
|
||||||
cached[index] = true;
|
|
||||||
cache(index);
|
cache(index);
|
||||||
|
|
||||||
photoViewControllers[index] ??= PhotoViewController();
|
photoViewControllers[index] ??= PhotoViewController();
|
||||||
@@ -201,8 +239,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
controller: photoViewControllers[index],
|
controller: photoViewControllers[index],
|
||||||
imageProvider:
|
imageProvider: _createImageProviderFromKey(
|
||||||
_createImageProviderFromKey(pageImages[0], context),
|
pageImages[0],
|
||||||
|
context,
|
||||||
|
startIndex + 1,
|
||||||
|
),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (_, error, s, retry) {
|
errorBuilder: (_, error, s, retry) {
|
||||||
return NetworkError(message: error.toString(), retry: retry);
|
return NetworkError(message: error.toString(), retry: retry);
|
||||||
@@ -214,7 +255,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
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,
|
||||||
child: buildPageImages(pageImages),
|
child: buildPageImages(pageImages, startIndex),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -244,12 +285,19 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
reader.setPage(i);
|
reader.setPage(i);
|
||||||
context.readerScaffold.update();
|
context.readerScaffold.update();
|
||||||
}
|
}
|
||||||
|
// Remove other pages' controllers to reset their state.
|
||||||
|
var keys = photoViewControllers.keys.toList();
|
||||||
|
for (var key in keys) {
|
||||||
|
if (key != i) {
|
||||||
|
photoViewControllers.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildPageImages(List<String> images) {
|
Widget buildPageImages(List<String> images, int startIndex) {
|
||||||
Axis axis = (reader.mode == ReaderMode.galleryTopToBottom)
|
Axis axis = (reader.mode == ReaderMode.galleryTopToBottom)
|
||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal;
|
: Axis.horizontal;
|
||||||
@@ -267,7 +315,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
child: ComicImage(
|
child: ComicImage(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
image: _createImageProviderFromKey(images[0], context),
|
image: _createImageProviderFromKey(
|
||||||
|
images[0],
|
||||||
|
context,
|
||||||
|
startIndex + 1,
|
||||||
|
),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
alignment: axis == Axis.vertical
|
alignment: axis == Axis.vertical
|
||||||
? Alignment.bottomCenter
|
? Alignment.bottomCenter
|
||||||
@@ -280,7 +332,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
child: ComicImage(
|
child: ComicImage(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
image: _createImageProviderFromKey(images[1], context),
|
image: _createImageProviderFromKey(
|
||||||
|
images[1],
|
||||||
|
context,
|
||||||
|
startIndex + 2,
|
||||||
|
),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
alignment: axis == Axis.vertical
|
alignment: axis == Axis.vertical
|
||||||
? Alignment.topCenter
|
? Alignment.topCenter
|
||||||
@@ -292,8 +348,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
imageWidgets = images.map((imageKey) {
|
imageWidgets = images.map((imageKey) {
|
||||||
|
startIndex++;
|
||||||
ImageProvider imageProvider =
|
ImageProvider imageProvider =
|
||||||
_createImageProviderFromKey(imageKey, context);
|
_createImageProviderFromKey(imageKey, context, startIndex);
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: ComicImage(
|
child: ComicImage(
|
||||||
image: imageProvider,
|
image: imageProvider,
|
||||||
@@ -402,34 +459,24 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
keyRepeatTimer = null;
|
keyRepeatTimer = null;
|
||||||
}
|
}
|
||||||
if (forward == true) {
|
if (forward == true) {
|
||||||
controller.nextPage(
|
reader.toPage(reader.page+1);
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.ease,
|
|
||||||
);
|
|
||||||
} else if (forward == false) {
|
} else if (forward == false) {
|
||||||
controller.previousPage(
|
reader.toPage(reader.page-1);
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.ease,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
||||||
keyRepeatTimer = Timer.periodic(
|
keyRepeatTimer = Timer.periodic(
|
||||||
const Duration(milliseconds: 100),
|
reader.enablePageAnimation
|
||||||
|
? const Duration(milliseconds: 200)
|
||||||
|
: const Duration(milliseconds: 50),
|
||||||
(timer) {
|
(timer) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
} else if (forward == true) {
|
} else if (forward == true) {
|
||||||
controller.nextPage(
|
reader.toPage(reader.page+1);
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
curve: Curves.ease,
|
|
||||||
);
|
|
||||||
} else if (forward == false) {
|
} else if (forward == false) {
|
||||||
controller.previousPage(
|
reader.toPage(reader.page-1);
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
curve: Curves.ease,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -447,6 +494,19 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||||
|
var imageKey = getImageKeyByOffset(offset);
|
||||||
|
if (imageKey == null) return null;
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
|
} else {
|
||||||
|
return (await CacheManager().findCache(
|
||||||
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||||
|
.readAsBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? getImageKeyByOffset(Offset offset) {
|
||||||
String? imageKey;
|
String? imageKey;
|
||||||
if (reader.imagesPerPage == 1) {
|
if (reader.imagesPerPage == 1) {
|
||||||
imageKey = reader.images![reader.page - 1];
|
imageKey = reader.images![reader.page - 1];
|
||||||
@@ -457,14 +517,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (imageKey == null) return null;
|
return imageKey;
|
||||||
if (imageKey.startsWith("file://")) {
|
|
||||||
return await File(imageKey.substring(7)).readAsBytes();
|
|
||||||
} else {
|
|
||||||
return (await CacheManager().findCache(
|
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
|
||||||
.readAsBytes();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +652,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
void cacheImages(int current) {
|
void cacheImages(int current) {
|
||||||
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
||||||
if (i <= reader.maxPage && !cached[i]) {
|
if (i <= reader.maxPage && !cached[i]) {
|
||||||
_precacheImage(i, context);
|
_preDownloadImage(i, context);
|
||||||
cached[i] = true;
|
cached[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,13 +1028,13 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
if (forward == true) {
|
if (forward == true) {
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollController.offset + context.height,
|
scrollController.offset + context.height * 0.25,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.ease,
|
curve: Curves.ease,
|
||||||
);
|
);
|
||||||
} else if (forward == false) {
|
} else if (forward == false) {
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollController.offset - context.height,
|
scrollController.offset - context.height * 0.25,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.ease,
|
curve: Curves.ease,
|
||||||
);
|
);
|
||||||
@@ -998,12 +1051,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||||
String? imageKey;
|
var imageKey = getImageKeyByOffset(offset);
|
||||||
for (var imageState in imageStates) {
|
|
||||||
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
|
||||||
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (imageKey == null) return null;
|
if (imageKey == null) return null;
|
||||||
if (imageKey.startsWith("file://")) {
|
if (imageKey.startsWith("file://")) {
|
||||||
return await File(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
@@ -1013,10 +1061,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
.readAsBytes();
|
.readAsBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? getImageKeyByOffset(Offset offset) {
|
||||||
|
String? imageKey;
|
||||||
|
for (var imageState in imageStates) {
|
||||||
|
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
||||||
|
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider _createImageProviderFromKey(
|
ImageProvider _createImageProviderFromKey(
|
||||||
String imageKey, BuildContext context) {
|
String imageKey,
|
||||||
|
BuildContext context,
|
||||||
|
int page,
|
||||||
|
) {
|
||||||
var reader = context.reader;
|
var reader = context.reader;
|
||||||
return ReaderImageProvider(
|
return ReaderImageProvider(
|
||||||
imageKey,
|
imageKey,
|
||||||
@@ -1030,16 +1092,39 @@ ImageProvider _createImageProviderFromKey(
|
|||||||
ImageProvider _createImageProvider(int page, BuildContext context) {
|
ImageProvider _createImageProvider(int page, BuildContext context) {
|
||||||
var reader = context.reader;
|
var reader = context.reader;
|
||||||
var imageKey = reader.images![page - 1];
|
var imageKey = reader.images![page - 1];
|
||||||
return _createImageProviderFromKey(imageKey, context);
|
return _createImageProviderFromKey(imageKey, context, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [_precacheImage] is used to precache the image for the given page.
|
||||||
|
/// The image is cached using the flutter's [precacheImage] method.
|
||||||
|
/// The image will be downloaded and decoded into memory.
|
||||||
void _precacheImage(int page, BuildContext context) {
|
void _precacheImage(int page, BuildContext context) {
|
||||||
|
if (page <= 0 || page > context.reader.images!.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
precacheImage(
|
precacheImage(
|
||||||
_createImageProvider(page, context),
|
_createImageProvider(page, context),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [_preDownloadImage] is used to download the image for the given page.
|
||||||
|
/// The image is downloaded using the [CacheManager] and saved to the local storage.
|
||||||
|
void _preDownloadImage(int page, BuildContext context) {
|
||||||
|
if (page <= 0 || page > context.reader.images!.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var reader = context.reader;
|
||||||
|
var imageKey = reader.images![page - 1];
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cid = reader.cid;
|
||||||
|
var eid = reader.eid;
|
||||||
|
var sourceKey = reader.type.comicSource?.key;
|
||||||
|
ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid);
|
||||||
|
}
|
||||||
|
|
||||||
class _SwipeChangeChapterProgress extends StatefulWidget {
|
class _SwipeChangeChapterProgress extends StatefulWidget {
|
||||||
const _SwipeChangeChapterProgress({
|
const _SwipeChangeChapterProgress({
|
||||||
this.controller,
|
this.controller,
|
||||||
|
@@ -29,6 +29,7 @@ import 'package:venera/foundation/image_provider/reader_image.dart';
|
|||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
|
import 'package:venera/network/images.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/clipboard_image.dart';
|
import 'package:venera/utils/clipboard_image.dart';
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
@@ -216,10 +217,16 @@ class _ReaderState extends State<Reader>
|
|||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onKeyEvent: onKeyEvent,
|
onKeyEvent: onKeyEvent,
|
||||||
child: _ReaderScaffold(
|
child: Overlay(
|
||||||
child: _ReaderGestureDetector(
|
initialEntries: [
|
||||||
child: _ReaderImages(key: Key(chapter.toString())),
|
OverlayEntry(builder: (context) {
|
||||||
),
|
return _ReaderScaffold(
|
||||||
|
child: _ReaderGestureDetector(
|
||||||
|
child: _ReaderImages(key: Key(chapter.toString())),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -603,4 +610,6 @@ abstract interface class _ImageViewController {
|
|||||||
bool handleOnTap(Offset location);
|
bool handleOnTap(Offset location);
|
||||||
|
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset);
|
Future<Uint8List?> getImageByOffset(Offset offset);
|
||||||
|
|
||||||
|
String? getImageKeyByOffset(Offset offset);
|
||||||
}
|
}
|
||||||
|
@@ -208,7 +208,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addImageFavorite() {
|
void addImageFavorite() async {
|
||||||
try {
|
try {
|
||||||
if (context.reader.images![0].contains('file://')) {
|
if (context.reader.images![0].contains('file://')) {
|
||||||
showToast(
|
showToast(
|
||||||
@@ -222,7 +222,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
String title = context.reader.history!.title;
|
String title = context.reader.history!.title;
|
||||||
String subTitle = context.reader.history!.subtitle;
|
String subTitle = context.reader.history!.subtitle;
|
||||||
int maxPage = context.reader.images!.length;
|
int maxPage = context.reader.images!.length;
|
||||||
int page = context.reader.page;
|
int? page = await selectImage();
|
||||||
|
if (page == null) return;
|
||||||
|
page += 1;
|
||||||
String sourceKey = context.reader.type.sourceKey;
|
String sourceKey = context.reader.type.sourceKey;
|
||||||
String imageKey = context.reader.images![page - 1];
|
String imageKey = context.reader.images![page - 1];
|
||||||
List<String> tags = context.reader.widget.tags;
|
List<String> tags = context.reader.widget.tags;
|
||||||
@@ -378,11 +380,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Collect the image".tl,
|
message: "Collect the image".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon:
|
||||||
isLiked() ? Icons.favorite : Icons.favorite_border),
|
Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
|
||||||
onPressed: addImageFavorite),
|
onPressed: addImageFavorite,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (App.isWindows)
|
if (App.isDesktop)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "${"Full Screen".tl}(F12)",
|
message: "${"Full Screen".tl}(F12)",
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -570,94 +573,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> _getCurrentImageData() async {
|
|
||||||
var imageKey = context.reader.images![context.reader.page - 1];
|
|
||||||
var reader = context.reader;
|
|
||||||
if (context.reader.mode.isContinuous) {
|
|
||||||
var continuesState =
|
|
||||||
context.reader._imageViewController as _ContinuousModeState;
|
|
||||||
var imagesOnScreen =
|
|
||||||
continuesState.itemPositionsListener.itemPositions.value;
|
|
||||||
var images = imagesOnScreen
|
|
||||||
.map((e) => context.reader.images!.elementAtOrNull(e.index - 1))
|
|
||||||
.whereType<String>()
|
|
||||||
.toList();
|
|
||||||
String? selected;
|
|
||||||
if (images.length > 1) {
|
|
||||||
await showPopUpWidget(
|
|
||||||
context,
|
|
||||||
PopUpWidgetScaffold(
|
|
||||||
title: "Select an image on screen".tl,
|
|
||||||
body: GridView.builder(
|
|
||||||
itemCount: images.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
ImageProvider image;
|
|
||||||
var imageKey = images[index];
|
|
||||||
if (imageKey.startsWith('file://')) {
|
|
||||||
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
|
||||||
} else {
|
|
||||||
image = ReaderImageProvider(
|
|
||||||
imageKey,
|
|
||||||
reader.type.comicSource!.key,
|
|
||||||
reader.cid,
|
|
||||||
reader.eid,
|
|
||||||
reader.page,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return InkWell(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
onTap: () {
|
|
||||||
selected = images[index];
|
|
||||||
App.rootContext.pop();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
foregroundDecoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
child: Image(
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
image: image,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(const EdgeInsets.all(8));
|
|
||||||
},
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 200,
|
|
||||||
childAspectRatio: 0.7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
selected = images.first;
|
|
||||||
}
|
|
||||||
if (selected == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
imageKey = selected!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (imageKey.startsWith("file://")) {
|
|
||||||
return await File(imageKey.substring(7)).readAsBytes();
|
|
||||||
} else {
|
|
||||||
return (await CacheManager().findCache(
|
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
|
||||||
.readAsBytes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveCurrentImage() async {
|
void saveCurrentImage() async {
|
||||||
var data = await _getCurrentImageData();
|
var data = await selectImageToData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -667,7 +584,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void share() async {
|
void share() async {
|
||||||
var data = await _getCurrentImageData();
|
var data = await selectImageToData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -750,9 +667,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
? Icons.arrow_forward_ios
|
? Icons.arrow_forward_ios
|
||||||
: Icons.arrow_back_ios_outlined,
|
: Icons.arrow_back_ios_outlined,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -761,6 +676,74 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If there is only one image on screen, return it.
|
||||||
|
///
|
||||||
|
/// If there are multiple images on screen,
|
||||||
|
/// show an overlay to let the user select an image.
|
||||||
|
///
|
||||||
|
/// The return value is the index of the selected image.
|
||||||
|
Future<int?> selectImage() async {
|
||||||
|
var reader = context.reader;
|
||||||
|
var imageViewController = context.reader._imageViewController;
|
||||||
|
if (imageViewController is _GalleryModeState && reader.imagesPerPage == 1) {
|
||||||
|
return reader.page - 1;
|
||||||
|
} else {
|
||||||
|
var location = await _showSelectImageOverlay();
|
||||||
|
if (location == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var imageKey = imageViewController!.getImageKeyByOffset(location);
|
||||||
|
if (imageKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return reader.images!.indexOf(imageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [selectImage], but return the image data.
|
||||||
|
Future<Uint8List?> selectImageToData() async {
|
||||||
|
var i = await selectImage();
|
||||||
|
if (i == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var imageKey = context.reader.images![i];
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
|
} else {
|
||||||
|
return (await CacheManager().findCache(
|
||||||
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||||
|
.readAsBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Offset?> _showSelectImageOverlay() {
|
||||||
|
if (_isOpen) {
|
||||||
|
openOrClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var completer = Completer<Offset?>();
|
||||||
|
|
||||||
|
var overlay = Overlay.of(context);
|
||||||
|
OverlayEntry? entry;
|
||||||
|
entry = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: _SelectImageOverlayContent(onTap: (offset) {
|
||||||
|
completer.complete(offset);
|
||||||
|
entry!.remove();
|
||||||
|
}, onDispose: () {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
overlay.insert(entry);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BatteryWidget extends StatefulWidget {
|
class _BatteryWidget extends StatefulWidget {
|
||||||
@@ -941,3 +924,69 @@ class _ClockWidgetState extends State<_ClockWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SelectImageOverlayContent extends StatefulWidget {
|
||||||
|
const _SelectImageOverlayContent({
|
||||||
|
required this.onTap,
|
||||||
|
required this.onDispose,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(Offset) onTap;
|
||||||
|
|
||||||
|
final void Function() onDispose;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SelectImageOverlayContent> createState() => _SelectImageOverlayContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent> {
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.onDispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTapUp: (details) {
|
||||||
|
widget.onTap(details.globalPosition);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withAlpha(50),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment(
|
||||||
|
0,
|
||||||
|
-0.8,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 232,
|
||||||
|
height: 42,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.info_outline),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
"Click to select an image".tl,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -66,6 +66,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 20,
|
max: 20,
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
|
setState(() {});
|
||||||
widget.onChanged?.call("autoPageTurningInterval");
|
widget.onChanged?.call("autoPageTurningInterval");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
@@ -80,6 +81,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
|
setState(() {});
|
||||||
widget.onChanged?.call("readerScreenPicNumberForLandscape");
|
widget.onChanged?.call("readerScreenPicNumberForLandscape");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -99,6 +101,18 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverAnimatedVisibility(
|
||||||
|
visible: appdata.settings['readerMode']!.startsWith('gallery') &&
|
||||||
|
(appdata.settings['readerScreenPicNumberForLandscape'] > 1 ||
|
||||||
|
appdata.settings['readerScreenPicNumberForPortrait'] > 1),
|
||||||
|
child: _SwitchSetting(
|
||||||
|
title: "Show single image on first page".tl,
|
||||||
|
settingKey: "showSingleImageOnFirstPage",
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("showSingleImageOnFirstPage");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Long press to zoom'.tl,
|
title: 'Long press to zoom'.tl,
|
||||||
settingKey: 'enableLongPressToZoom',
|
settingKey: 'enableLongPressToZoom',
|
||||||
|
@@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/proxy.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
@@ -308,7 +308,7 @@ class DesktopWebview {
|
|||||||
useWindowPositionAndSize: true,
|
useWindowPositionAndSize: true,
|
||||||
userDataFolderWindows: "${App.dataPath}\\webview",
|
userDataFolderWindows: "${App.dataPath}\\webview",
|
||||||
title: "webview",
|
title: "webview",
|
||||||
proxy: AppDio.proxy,
|
proxy: await getProxy(),
|
||||||
));
|
));
|
||||||
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
||||||
_webview!.setOnNavigation((s) {
|
_webview!.setOnNavigation((s) {
|
||||||
|
@@ -112,7 +112,7 @@ abstract class CBZ {
|
|||||||
var ext = e.path.split('.').last;
|
var ext = e.path.split('.').last;
|
||||||
return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext);
|
return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext);
|
||||||
});
|
});
|
||||||
if(files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
cache.deleteSync(recursive: true);
|
cache.deleteSync(recursive: true);
|
||||||
throw Exception('No images found in the archive');
|
throw Exception('No images found in the archive');
|
||||||
}
|
}
|
||||||
@@ -141,8 +141,7 @@ abstract class CBZ {
|
|||||||
FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)),
|
FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)),
|
||||||
);
|
);
|
||||||
dest.createSync();
|
dest.createSync();
|
||||||
coverFile.copyMem(
|
coverFile.copyMem(FilePath.join(dest.path, 'cover.${coverFile.extension}'));
|
||||||
FilePath.join(dest.path, 'cover.${coverFile.extension}'));
|
|
||||||
if (metaData.chapters == null) {
|
if (metaData.chapters == null) {
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var src = files[i];
|
var src = files[i];
|
||||||
@@ -233,17 +232,19 @@ abstract class CBZ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var cover = comic.coverFile;
|
var cover = comic.coverFile;
|
||||||
await cover
|
await cover.copyMem(
|
||||||
.copyMem(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}'));
|
FilePath.join(cache.path, 'cover.${cover.path.split('.').last}'));
|
||||||
|
final metaData = ComicMetaData(
|
||||||
|
title: comic.title,
|
||||||
|
author: comic.subtitle,
|
||||||
|
tags: comic.tags,
|
||||||
|
chapters: chapters,
|
||||||
|
);
|
||||||
await File(FilePath.join(cache.path, 'metadata.json')).writeAsString(
|
await File(FilePath.join(cache.path, 'metadata.json')).writeAsString(
|
||||||
jsonEncode(
|
jsonEncode(metaData),
|
||||||
ComicMetaData(
|
);
|
||||||
title: comic.title,
|
await File(FilePath.join(cache.path, 'ComicInfo.xml')).writeAsString(
|
||||||
author: comic.subtitle,
|
_buildComicInfoXml(metaData),
|
||||||
tags: comic.tags,
|
|
||||||
chapters: chapters,
|
|
||||||
).toJson(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
var cbz = File(outFilePath);
|
var cbz = File(outFilePath);
|
||||||
if (cbz.existsSync()) cbz.deleteSync();
|
if (cbz.existsSync()) cbz.deleteSync();
|
||||||
@@ -252,7 +253,54 @@ abstract class CBZ {
|
|||||||
return cbz;
|
return cbz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String _buildComicInfoXml(ComicMetaData data) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
buffer.writeln('<?xml version="1.0" encoding="utf-8"?>');
|
||||||
|
buffer.writeln('<ComicInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">');
|
||||||
|
|
||||||
|
buffer.writeln(' <Title>${_escapeXml(data.title)}</Title>');
|
||||||
|
buffer.writeln(' <Series>${_escapeXml(data.title)}</Series>');
|
||||||
|
|
||||||
|
if (data.author.isNotEmpty) {
|
||||||
|
buffer.writeln(' <Writer>${_escapeXml(data.author)}</Writer>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tags.isNotEmpty) {
|
||||||
|
var tags = data.tags;
|
||||||
|
if (tags.length > 5) {
|
||||||
|
tags = tags.sublist(0, 5);
|
||||||
|
}
|
||||||
|
buffer.writeln(' <Genre>${_escapeXml(tags.join(', '))}</Genre>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.chapters != null && data.chapters!.isNotEmpty) {
|
||||||
|
final chaptersInfo = data.chapters!.map((chapter) =>
|
||||||
|
'${_escapeXml(chapter.title)}: ${chapter.start}-${chapter.end}'
|
||||||
|
).join('; ');
|
||||||
|
buffer.writeln(' <Notes>Chapters: $chaptersInfo</Notes>');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(' <Manga>Unknown</Manga>');
|
||||||
|
buffer.writeln(' <BlackAndWhite>Unknown</BlackAndWhite>');
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
buffer.writeln(' <Year>${now.year}</Year>');
|
||||||
|
|
||||||
|
buffer.writeln('</ComicInfo>');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _escapeXml(String text) {
|
||||||
|
return text
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
static _compress(String src, String dst) async {
|
static _compress(String src, String dst) async {
|
||||||
await ZipFile.compressFolderAsync(src, dst, 4);
|
await ZipFile.compressFolderAsync(src, dst, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,6 @@ import 'package:venera/network/app_dio.dart';
|
|||||||
import 'package:venera/utils/data.dart';
|
import 'package:venera/utils/data.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:webdav_client/webdav_client.dart' hide File;
|
import 'package:webdav_client/webdav_client.dart' hide File;
|
||||||
import 'package:rhttp/rhttp.dart' as rhttp;
|
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
@@ -119,19 +118,11 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
var proxy = await AppDio.getProxy();
|
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: RHttpAdapter(
|
adapter: RHttpAdapter(),
|
||||||
rhttp.ClientSettings(
|
|
||||||
proxySettings:
|
|
||||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
|
||||||
userAgent: "venera v${App.version}",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -192,19 +183,11 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
var proxy = await AppDio.getProxy();
|
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: RHttpAdapter(
|
adapter: RHttpAdapter(),
|
||||||
rhttp.ClientSettings(
|
|
||||||
proxySettings:
|
|
||||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
|
||||||
userAgent: "venera v${App.version}",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -132,15 +132,15 @@ extension DirectoryExtension on Directory {
|
|||||||
|
|
||||||
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
||||||
String sanitizeFileName(String fileName, {String? dir, int? maxLength}) {
|
String sanitizeFileName(String fileName, {String? dir, int? maxLength}) {
|
||||||
if (fileName.endsWith('.')) {
|
while (fileName.endsWith('.')) {
|
||||||
fileName = fileName.substring(0, fileName.length - 1);
|
fileName = fileName.substring(0, fileName.length - 1);
|
||||||
}
|
}
|
||||||
var maxLength = 255;
|
var length = maxLength ?? 255;
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
if (!dir.endsWith('/') && !dir.endsWith('\\')) {
|
if (!dir.endsWith('/') && !dir.endsWith('\\')) {
|
||||||
dir = "$dir/";
|
dir = "$dir/";
|
||||||
}
|
}
|
||||||
maxLength -= dir.length;
|
length -= dir.length;
|
||||||
}
|
}
|
||||||
final invalidChars = RegExp(r'[<>:"/\\|?*]');
|
final invalidChars = RegExp(r'[<>:"/\\|?*]');
|
||||||
final sanitizedFileName = fileName.replaceAll(invalidChars, ' ');
|
final sanitizedFileName = fileName.replaceAll(invalidChars, ' ');
|
||||||
@@ -148,11 +148,11 @@ String sanitizeFileName(String fileName, {String? dir, int? maxLength}) {
|
|||||||
if (trimmedFileName.isEmpty) {
|
if (trimmedFileName.isEmpty) {
|
||||||
throw Exception('Invalid File Name: Empty length.');
|
throw Exception('Invalid File Name: Empty length.');
|
||||||
}
|
}
|
||||||
if (maxLength <= 0) {
|
if (length <= 0) {
|
||||||
throw Exception('Invalid File Name: Max length is less than 0.');
|
throw Exception('Invalid File Name: Max length is less than 0.');
|
||||||
}
|
}
|
||||||
if (trimmedFileName.length > maxLength) {
|
if (trimmedFileName.length > length) {
|
||||||
trimmedFileName = trimmedFileName.substring(0, maxLength);
|
trimmedFileName = trimmedFileName.substring(0, length);
|
||||||
}
|
}
|
||||||
return trimmedFileName;
|
return trimmedFileName;
|
||||||
}
|
}
|
||||||
|
64
pubspec.lock
64
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.12.0"
|
||||||
battery_plus:
|
battery_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -182,10 +182,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.2"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -308,18 +308,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview
|
path: flutter_inappwebview
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "6.2.0-beta.3"
|
version: "6.2.0-beta.3"
|
||||||
flutter_inappwebview_android:
|
flutter_inappwebview_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_android
|
path: flutter_inappwebview_android
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.0-beta.3"
|
version: "1.2.0-beta.3"
|
||||||
flutter_inappwebview_internal_annotations:
|
flutter_inappwebview_internal_annotations:
|
||||||
@@ -334,45 +334,45 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_ios
|
path: flutter_inappwebview_ios
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.0-beta.3"
|
version: "1.2.0-beta.3"
|
||||||
flutter_inappwebview_macos:
|
flutter_inappwebview_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_macos
|
path: flutter_inappwebview_macos
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.0-beta.3"
|
version: "1.2.0-beta.3"
|
||||||
flutter_inappwebview_platform_interface:
|
flutter_inappwebview_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_platform_interface
|
path: flutter_inappwebview_platform_interface
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "1.4.0-beta.3"
|
version: "1.4.0-beta.3"
|
||||||
flutter_inappwebview_web:
|
flutter_inappwebview_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_web
|
path: flutter_inappwebview_web
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.0-beta.3"
|
version: "1.2.0-beta.3"
|
||||||
flutter_inappwebview_windows:
|
flutter_inappwebview_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: flutter_inappwebview_windows
|
path: flutter_inappwebview_windows
|
||||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
resolved-ref: "3ef899b3db57c911b080979f1392253b835f98ab"
|
||||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
url: "https://github.com/venera-app/flutter_inappwebview"
|
||||||
source: git
|
source: git
|
||||||
version: "0.7.0-beta.3"
|
version: "0.7.0-beta.3"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
@@ -516,10 +516,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.19.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -540,10 +540,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "10.0.8"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1029,10 +1029,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "14.3.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1100,4 +1100,4 @@ packages:
|
|||||||
version: "0.0.12"
|
version: "0.0.12"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.29.2"
|
flutter: ">=3.29.3"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.4.0+140
|
version: 1.4.1+141
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
flutter: 3.29.2
|
flutter: 3.29.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -46,9 +46,9 @@ dependencies:
|
|||||||
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
|
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
|
||||||
flutter_inappwebview:
|
flutter_inappwebview:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
url: https://github.com/venera-app/flutter_inappwebview
|
||||||
path: flutter_inappwebview
|
path: flutter_inappwebview
|
||||||
ref: 0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676
|
ref: 3ef899b3db57c911b080979f1392253b835f98ab
|
||||||
app_links: ^6.4.0
|
app_links: ^6.4.0
|
||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
|
@@ -10,11 +10,16 @@
|
|||||||
#include <flutter/event_stream_handler_functions.h>
|
#include <flutter/event_stream_handler_functions.h>
|
||||||
#include <flutter/standard_method_codec.h>
|
#include <flutter/standard_method_codec.h>
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#define _CRT_SECURE_NO_WARNINGS
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
|
||||||
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& mouseEvents = nullptr;
|
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& mouseEvents = nullptr;
|
||||||
|
|
||||||
|
std::atomic<bool> mainThreadAlive(true);
|
||||||
|
std::atomic<std::chrono::steady_clock::time_point> lastHeartbeat(std::chrono::steady_clock::now());
|
||||||
|
std::thread* monitorThread = nullptr;
|
||||||
|
|
||||||
char* wideCharToMultiByte(wchar_t* pWCStrKey)
|
char* wideCharToMultiByte(wchar_t* pWCStrKey)
|
||||||
{
|
{
|
||||||
size_t pSize = WideCharToMultiByte(CP_OEMCP, 0, pWCStrKey, wcslen(pWCStrKey), NULL, 0, NULL, NULL);
|
size_t pSize = WideCharToMultiByte(CP_OEMCP, 0, pWCStrKey, wcslen(pWCStrKey), NULL, 0, NULL, NULL);
|
||||||
@@ -45,6 +50,22 @@ FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
|||||||
|
|
||||||
FlutterWindow::~FlutterWindow() {}
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
void monitorUIThread() {
|
||||||
|
const auto timeout = std::chrono::seconds(5);
|
||||||
|
|
||||||
|
while (mainThreadAlive.load()) {
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto duration = now - lastHeartbeat.load();
|
||||||
|
|
||||||
|
if (duration > timeout) {
|
||||||
|
std::cerr << "The UI thread is dead. Terminate the application.";
|
||||||
|
std::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool FlutterWindow::OnCreate() {
|
bool FlutterWindow::OnCreate() {
|
||||||
if (!Win32Window::OnCreate()) {
|
if (!Win32Window::OnCreate()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -78,6 +99,13 @@ bool FlutterWindow::OnCreate() {
|
|||||||
result->Success(flutter::EncodableValue("No Proxy"));
|
result->Success(flutter::EncodableValue("No Proxy"));
|
||||||
delete(res);
|
delete(res);
|
||||||
}
|
}
|
||||||
|
else if (call.method_name() == "heartBeat") {
|
||||||
|
if (monitorThread == nullptr) {
|
||||||
|
monitorThread = new std::thread{ monitorUIThread };
|
||||||
|
}
|
||||||
|
lastHeartbeat = std::chrono::steady_clock::now();
|
||||||
|
result->Success();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
flutter::EventChannel<> channel2(
|
flutter::EventChannel<> channel2(
|
||||||
@@ -163,6 +191,10 @@ void FlutterWindow::OnDestroy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Win32Window::OnDestroy();
|
Win32Window::OnDestroy();
|
||||||
|
if (monitorThread != nullptr) {
|
||||||
|
mainThreadAlive = false;
|
||||||
|
monitorThread->join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mouse_side_button_listener(unsigned int input)
|
void mouse_side_button_listener(unsigned int input)
|
||||||
|
Reference in New Issue
Block a user