Merge pull request #34 from venera-app/dev

v1.0.5
This commit is contained in:
nyne
2024-11-13 16:27:41 +08:00
committed by GitHub
46 changed files with 1357 additions and 590 deletions

5
.gitignore vendored
View File

@@ -42,4 +42,7 @@ app.*.map.json
/android/app/profile /android/app/profile
/android/app/release /android/app/release
add_translation.py add_translation.py
*/*/generated_*
*/*/Generated*

View File

@@ -699,7 +699,7 @@ class HtmlElement {
doc: this.doc, doc: this.doc,
}) })
if(k == null) return null; if(k == null) return null;
return new HtmlElement(k); return new HtmlElement(k, this.doc);
} }
/** /**
@@ -850,6 +850,7 @@ let console = {
* @param id {string} * @param id {string}
* @param title {string} * @param title {string}
* @param subtitle {string} * @param subtitle {string}
* @param subTitle {string} - equal to subtitle
* @param cover {string} * @param cover {string}
* @param tags {string[]} * @param tags {string[]}
* @param description {string} * @param description {string}
@@ -859,10 +860,11 @@ let console = {
* @param stars {number?} - 0-5, double * @param stars {number?} - 0-5, double
* @constructor * @constructor
*/ */
function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId, stars}) { function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage, language, favoriteId, stars}) {
this.id = id; this.id = id;
this.title = title; this.title = title;
this.subtitle = subtitle; this.subtitle = subtitle;
this.subTitle = subTitle;
this.cover = cover; this.cover = cover;
this.tags = tags; this.tags = tags;
this.description = description; this.description = description;
@@ -940,6 +942,33 @@ function Comment({userName, avatar, content, time, replyCount, id, isLiked, scor
this.voteStatus = voteStatus; this.voteStatus = voteStatus;
} }
/**
* Create image loading config
* @param url {string?}
* @param method {string?} - http method, uppercase
* @param data {any} - request data, may be null
* @param headers {Object?} - request headers
* @param onResponse {((ArrayBuffer) => ArrayBuffer)?} - modify response data
* @param modifyImage {string?}
* A js script string.
* The script will be executed in a new Isolate.
* A function named `modifyImage` should be defined in the script, which receives an [Image] as the only argument, and returns an [Image]..
* @param onLoadFailed {(() => ImageLoadingConfig)?} - called when the image loading failed
* @constructor
* @since 1.0.5
*
* To keep the compatibility with the old version, do not use the constructor. Consider creating a new object with the properties directly.
*/
function ImageLoadingConfig({url, method, data, headers, onResponse, modifyImage, onLoadFailed}) {
this.url = url;
this.method = method;
this.data = data;
this.headers = headers;
this.onResponse = onResponse;
this.modifyImage = modifyImage;
this.onLoadFailed = onLoadFailed;
}
class ComicSource { class ComicSource {
name = "" name = ""

View File

@@ -143,7 +143,7 @@
"1. The directory only contains image files." : "1. 目录只包含图片文件。", "1. The directory only contains image files." : "1. 目录只包含图片文件。",
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。", "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
"Export as cbz": "导出为cbz", "Export as cbz": "导出为cbz",
"Select a cbz file." : "选择一个cbz文件", "Select a cbz file." : "选择一个cbz文件",
"A cbz file" : "一个cbz文件", "A cbz file" : "一个cbz文件",
@@ -191,7 +191,25 @@
"Quick Favorite": "快速收藏", "Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加", "Added": "已添加",
"Turn page by volume keys": "使用音量键翻页" "Turn page by volume keys": "使用音量键翻页",
"Display time & battery info in reader":"在阅读器中显示时间和电量信息",
"EhViewer downloads":"EhViewer下载",
"Select an EhViewer database and a download folder.":"选择EhViewer的下载数据导出的db文件与存放下载内容的目录",
"(EhViewer)Default": "(EhViewer)默认",
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若通过EhViewer数据库导入漫画程序将会按其中的下载标签自动创建收藏文件夹。",
"Multi-Select": "进入多选模式",
"Exit Multi-Select": "退出多选模式",
"Selected @c comics": "已选择 @c 本漫画",
"Select All": "全选",
"Deselect": "取消选择",
"Invert Selection": "反选",
"Select in range": "区间选择",
"Finished": "已完成",
"Updating": "更新中",
"Update Comics Info": "更新漫画信息",
"Create Folder": "新建文件夹",
"Select an image on screen": "选择屏幕上的图片",
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -337,7 +355,7 @@
"1. The directory only contains image files." : "1. 目錄只包含圖片文件。", "1. The directory only contains image files." : "1. 目錄只包含圖片文件。",
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。", "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
"Export as cbz": "匯出為cbz", "Export as cbz": "匯出為cbz",
"Select a cbz file." : "選擇一個cbz文件", "Select a cbz file." : "選擇一個cbz文件",
"A cbz file" : "一個cbz文件", "A cbz file" : "一個cbz文件",
@@ -385,6 +403,24 @@
"Quick Favorite": "快速收藏", "Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾", "Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
"Added": "已添加", "Added": "已添加",
"Turn page by volume keys": "使用音量鍵翻頁" "Turn page by volume keys": "使用音量鍵翻頁",
"Display time & battery info in reader": "在閱讀器中顯示時間和電量信息",
"EhViewer downloads": "EhViewer下載",
"Select an EhViewer database and a download folder.": "選擇EhViewer的下載資料匯出的db檔案與存放下載內容的目錄",
"(EhViewer)Default": "(EhViewer)預設",
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若透過EhViewer資料庫匯入漫畫程式將會按其中的下載標籤自動建立收藏資料夾。",
"Multi-Select": "進入多選模式",
"Exit Multi-Select": "退出多選模式",
"Selected @c comics": "已選擇 @c 本漫畫",
"Select All": "全選",
"Deselect": "取消選擇",
"Invert Selection": "反選",
"Select in range": "區間選擇",
"Finished": "已完成",
"Updating": "更新中",
"Update Comics Info": "更新漫畫信息",
"Create Folder": "新建文件夾",
"Select an image on screen": "選擇屏幕上的圖片",
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列"
} }
} }

View File

@@ -156,7 +156,7 @@ class _ButtonState extends State<Button> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var padding = widget.padding ?? var padding = widget.padding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 4); const EdgeInsets.symmetric(horizontal: 16);
var width = widget.width; var width = widget.width;
if (width != null) { if (width != null) {
width = width - padding.horizontal; width = width - padding.horizontal;
@@ -206,6 +206,7 @@ class _ButtonState extends State<Button> {
padding: padding, padding: padding,
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 76, minWidth: 76,
minHeight: 32,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: buttonColor, color: buttonColor,

View File

@@ -158,9 +158,16 @@ class ComicTile extends StatelessWidget {
image = FileImage(File(comic.cover.substring(7))); image = FileImage(File(comic.cover.substring(7)));
} else if (comic.sourceKey == 'local') { } else if (comic.sourceKey == 'local') {
var localComic = LocalManager().find(comic.id, ComicType.local); var localComic = LocalManager().find(comic.id, ComicType.local);
image = FileImage(localComic!.coverFile); if (localComic == null) {
return const SizedBox();
}
image = FileImage(localComic.coverFile);
} else { } else {
image = CachedImageProvider(comic.cover, sourceKey: comic.sourceKey); image = CachedImageProvider(
comic.cover,
sourceKey: comic.sourceKey,
cid: comic.id,
);
} }
return AnimatedImage( return AnimatedImage(
image: image, image: image,
@@ -476,18 +483,17 @@ class _ComicDescription extends StatelessWidget {
), ),
if (badge != null) if (badge != null)
Container( Container(
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiaryContainer, color: Theme.of(context).colorScheme.tertiaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Center(
child:Text(
"${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}",
style: const TextStyle(fontSize: 12),
), ),
) child: Center(
), child: Text(
"${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}",
style: const TextStyle(fontSize: 12),
),
)),
], ],
) )
], ],
@@ -571,17 +577,19 @@ class _ReadingHistoryPainter extends CustomPainter {
} }
class SliverGridComics extends StatefulWidget { class SliverGridComics extends StatefulWidget {
const SliverGridComics({ const SliverGridComics(
super.key, {super.key,
required this.comics, required this.comics,
this.onLastItemBuild, this.onLastItemBuild,
this.badgeBuilder, this.badgeBuilder,
this.menuBuilder, this.menuBuilder,
this.onTap, this.onTap,
}); this.selections});
final List<Comic> comics; final List<Comic> comics;
final Map<Comic, bool>? selections;
final void Function()? onLastItemBuild; final void Function()? onLastItemBuild;
final String? Function(Comic)? badgeBuilder; final String? Function(Comic)? badgeBuilder;
@@ -635,6 +643,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _SliverGridComics( return _SliverGridComics(
comics: comics, comics: comics,
selection: widget.selections,
onLastItemBuild: widget.onLastItemBuild, onLastItemBuild: widget.onLastItemBuild,
badgeBuilder: widget.badgeBuilder, badgeBuilder: widget.badgeBuilder,
menuBuilder: widget.menuBuilder, menuBuilder: widget.menuBuilder,
@@ -650,10 +659,13 @@ class _SliverGridComics extends StatelessWidget {
this.badgeBuilder, this.badgeBuilder,
this.menuBuilder, this.menuBuilder,
this.onTap, this.onTap,
this.selection,
}); });
final List<Comic> comics; final List<Comic> comics;
final Map<Comic, bool>? selection;
final void Function()? onLastItemBuild; final void Function()? onLastItemBuild;
final String? Function(Comic)? badgeBuilder; final String? Function(Comic)? badgeBuilder;
@@ -671,12 +683,24 @@ class _SliverGridComics extends StatelessWidget {
onLastItemBuild?.call(); onLastItemBuild?.call();
} }
var badge = badgeBuilder?.call(comics[index]); var badge = badgeBuilder?.call(comics[index]);
return ComicTile( var isSelected =
selection == null ? false : selection![comics[index]] ?? false;
var comic = ComicTile(
comic: comics[index], comic: comics[index],
badge: badge, badge: badge,
menuOptions: menuBuilder?.call(comics[index]), menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null ? () => onTap!(comics[index]) : null, onTap: onTap != null ? () => onTap!(comics[index]) : null,
); );
return Container(
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.surfaceContainer
: null,
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(4),
child: comic,
);
}, },
childCount: comics.length, childCount: comics.length,
), ),
@@ -874,7 +898,7 @@ class ComicListState extends State<ComicList> {
try { try {
if (widget.loadPage != null) { if (widget.loadPage != null) {
var res = await widget.loadPage!(page); var res = await widget.loadPage!(page);
if(!mounted) return; if (!mounted) return;
if (res.success) { if (res.success) {
if (res.data.isEmpty) { if (res.data.isEmpty) {
_data[page] = const []; _data[page] = const [];

View File

@@ -27,7 +27,7 @@ class NaviPane extends StatefulWidget {
required this.paneActions, required this.paneActions,
required this.pageBuilder, required this.pageBuilder,
this.initialPage = 0, this.initialPage = 0,
this.onPageChange, this.onPageChanged,
required this.observer, required this.observer,
required this.navigatorKey, required this.navigatorKey,
super.key}); super.key});
@@ -38,7 +38,7 @@ class NaviPane extends StatefulWidget {
final Widget Function(int page) pageBuilder; final Widget Function(int page) pageBuilder;
final void Function(int index)? onPageChange; final void Function(int index)? onPageChanged;
final int initialPage; final int initialPage;
@@ -59,7 +59,7 @@ class _NaviPaneState extends State<NaviPane>
set currentPage(int value) { set currentPage(int value) {
if (value == _currentPage) return; if (value == _currentPage) return;
_currentPage = value; _currentPage = value;
widget.onPageChange?.call(value); widget.onPageChanged?.call(value);
} }
void Function()? mainViewUpdateHandler; void Function()? mainViewUpdateHandler;

View File

@@ -485,8 +485,15 @@ class WindowPlacement {
} }
} }
static Rect? lastValidRect;
static Future<WindowPlacement> get current async { static Future<WindowPlacement> get current async {
var rect = await windowManager.getBounds(); var rect = await windowManager.getBounds();
if(validate(rect)) {
lastValidRect = rect;
} else {
rect = lastValidRect ?? defaultPlacement.rect;
}
var isMaximized = await windowManager.isMaximized(); var isMaximized = await windowManager.isMaximized();
return WindowPlacement(rect, isMaximized); return WindowPlacement(rect, isMaximized);
} }
@@ -501,9 +508,6 @@ class WindowPlacement {
static void loop() async { static void loop() async {
timer ??= Timer.periodic(const Duration(milliseconds: 100), (timer) async { timer ??= Timer.periodic(const Duration(milliseconds: 100), (timer) async {
var placement = await WindowPlacement.current; var placement = await WindowPlacement.current;
if (!validate(placement.rect)) {
return;
}
if (placement.rect != cache.rect || if (placement.rect != cache.rect ||
placement.isMaximized != cache.isMaximized) { placement.isMaximized != cache.isMaximized) {
cache = placement; cache = placement;

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.0.4"; final version = "1.0.5";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -118,6 +118,7 @@ class _Settings with ChangeNotifier {
'dataVersion': 0, 'dataVersion': 0,
'quickFavorite': null, 'quickFavorite': null,
'enableTurnPageByVolumeKey': true, 'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true,
}; };
operator [](String key) { operator [](String key) {

View File

@@ -10,6 +10,7 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -236,6 +237,7 @@ class ComicSource {
} }
await file.writeAsString(jsonEncode(data)); await file.writeAsString(jsonEncode(data));
_isSaving = false; _isSaving = false;
DataSync().uploadData();
} }
Future<bool> reLogin() async { Future<bool> reLogin() async {

View File

@@ -92,7 +92,7 @@ class Comic {
Comic.fromJson(Map<String, dynamic> json, this.sourceKey) Comic.fromJson(Map<String, dynamic> json, this.sourceKey)
: title = json["title"], : title = json["title"],
subtitle = json["subTitle"] ?? "", subtitle = json["subtitle"] ?? json["subTitle"] ?? "",
cover = json["cover"], cover = json["cover"],
id = json["id"], id = json["id"],
tags = List<String>.from(json["tags"] ?? []), tags = List<String>.from(json["tags"] ?? []),

View File

@@ -11,8 +11,8 @@ import 'app.dart';
import 'comic_source/comic_source.dart'; import 'comic_source/comic_source.dart';
import 'comic_type.dart'; import 'comic_type.dart';
String _getCurTime() { String _getTimeString(DateTime time) {
return DateTime.now() return time
.toIso8601String() .toIso8601String()
.replaceFirst("T", " ") .replaceFirst("T", " ")
.substring(0, 19); .substring(0, 19);
@@ -27,7 +27,7 @@ class FavoriteItem implements Comic {
@override @override
String id; String id;
String coverPath; String coverPath;
String time = _getCurTime(); late String time;
FavoriteItem({ FavoriteItem({
required this.id, required this.id,
@@ -36,7 +36,11 @@ class FavoriteItem implements Comic {
required this.author, required this.author,
required this.type, required this.type,
required this.tags, required this.tags,
}); DateTime? favoriteTime
}) {
var t = favoriteTime ?? DateTime.now();
time = _getTimeString(t);
}
FavoriteItem.fromRow(Row row) FavoriteItem.fromRow(Row row)
: name = row["name"], : name = row["name"],
@@ -167,6 +171,13 @@ class LocalFavoritesManager with ChangeNotifier {
order_value int order_value int
); );
"""); """);
_db.execute("""
create table if not exists folder_sync (
folder_name text primary key,
source_key text,
source_folder text
);
""");
} }
List<String> find(String id, ComicType type) { List<String> find(String id, ComicType type) {
@@ -227,12 +238,12 @@ class LocalFavoritesManager with ChangeNotifier {
return folders; return folders;
} }
void updateOrder(Map<String, int> order) { void updateOrder(List<String> folders) {
for (var folder in order.keys) { for (int i = 0; i < folders.length; i++) {
_db.execute(""" _db.execute("""
insert or replace into folder_order (folder_name, order_value) insert or replace into folder_order (folder_name, order_value)
values (?, ?); values (?, ?);
""", [folder, order[folder]]); """, [folders[i], i]);
} }
notifyListeners(); notifyListeners();
} }
@@ -289,12 +300,16 @@ class LocalFavoritesManager with ChangeNotifier {
return res; return res;
} }
bool existsFolder(String name) {
return folderNames.contains(name);
}
/// create a folder /// create a folder
String createFolder(String name, [bool renameWhenInvalidName = false]) { String createFolder(String name, [bool renameWhenInvalidName = false]) {
if (name.isEmpty) { if (name.isEmpty) {
if (renameWhenInvalidName) { if (renameWhenInvalidName) {
int i = 0; int i = 0;
while (folderNames.contains(i.toString())) { while (existsFolder(i.toString())) {
i++; i++;
} }
name = i.toString(); name = i.toString();
@@ -302,11 +317,11 @@ class LocalFavoritesManager with ChangeNotifier {
throw "name is empty!"; throw "name is empty!";
} }
} }
if (folderNames.contains(name)) { if (existsFolder(name)) {
if (renameWhenInvalidName) { if (renameWhenInvalidName) {
var prevName = name; var prevName = name;
int i = 0; int i = 0;
while (folderNames.contains(i.toString())) { while (existsFolder(i.toString())) {
i++; i++;
} }
name = prevName + i.toString(); name = prevName + i.toString();
@@ -355,7 +370,7 @@ class LocalFavoritesManager with ChangeNotifier {
/// This method will download cover to local, to avoid problems like changing url /// This method will download cover to local, to avoid problems like changing url
void addComic(String folder, FavoriteItem comic, [int? order]) async { void addComic(String folder, FavoriteItem comic, [int? order]) async {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
if (!folderNames.contains(folder)) { if (!existsFolder(folder)) {
throw Exception("Folder does not exists"); throw Exception("Folder does not exists");
} }
var res = _db.select(""" var res = _db.select("""
@@ -424,7 +439,7 @@ class LocalFavoritesManager with ChangeNotifier {
} }
void reorder(List<FavoriteItem> newFolder, String folder) async { void reorder(List<FavoriteItem> newFolder, String folder) async {
if (!folderNames.contains(folder)) { if (!existsFolder(folder)) {
throw Exception("Failed to reorder: folder not found"); throw Exception("Failed to reorder: folder not found");
} }
deleteFolder(folder); deleteFolder(folder);
@@ -436,7 +451,7 @@ class LocalFavoritesManager with ChangeNotifier {
} }
void rename(String before, String after) { void rename(String before, String after) {
if (folderNames.contains(after)) { if (existsFolder(after)) {
throw "Name already exists!"; throw "Name already exists!";
} }
if (after.contains('"')) { if (after.contains('"')) {
@@ -591,9 +606,9 @@ class LocalFavoritesManager with ChangeNotifier {
if (folder == null || folder is! String) { if (folder == null || folder is! String) {
throw "Invalid data"; throw "Invalid data";
} }
if (folderNames.contains(folder)) { if (existsFolder(folder)) {
int i = 0; int i = 0;
while (folderNames.contains("$folder($i)")) { while (existsFolder("$folder($i)")) {
i++; i++;
} }
folder = "$folder($i)"; folder = "$folder($i)";

View File

@@ -8,7 +8,7 @@ import 'cached_image.dart' as image_provider;
class CachedImageProvider class CachedImageProvider
extends BaseImageProvider<image_provider.CachedImageProvider> { extends BaseImageProvider<image_provider.CachedImageProvider> {
/// Image provider for normal image. /// Image provider for normal image.
const CachedImageProvider(this.url, {this.headers, this.sourceKey}); const CachedImageProvider(this.url, {this.headers, this.sourceKey, this.cid});
final String url; final String url;
@@ -16,9 +16,11 @@ class CachedImageProvider
final String? sourceKey; final String? sourceKey;
final String? cid;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey)) { await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes, expectedTotalBytes: progress.totalBytes,
@@ -36,5 +38,5 @@ class CachedImageProvider
} }
@override @override
String get key => url; String get key => url + (sourceKey ?? "") + (cid ?? "");
} }

View File

@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/download.dart'; import 'package:venera/network/download.dart';
import 'package:venera/pages/reader/reader.dart'; import 'package:venera/pages/reader/reader.dart';
@@ -346,6 +347,10 @@ class LocalManager with ChangeNotifier {
comic.cover) { comic.cover) {
continue; continue;
} }
//Hidden file in some file system
if(entity.name.startsWith('.')) {
continue;
}
files.add(entity); files.add(entity);
} }
} }
@@ -360,10 +365,10 @@ class LocalManager with ChangeNotifier {
return files.map((e) => "file://${e.path}").toList(); return files.map((e) => "file://${e.path}").toList();
} }
Future<bool> isDownloaded(String id, ComicType type, int ep) async { Future<bool> isDownloaded(String id, ComicType type, [int? ep]) async {
var comic = find(id, type); var comic = find(id, type);
if (comic == null) return false; if (comic == null) return false;
if (comic.chapters == null) return true; if (comic.chapters == null || ep == null) return true;
return comic.downloadedChapters return comic.downloadedChapters
.contains(comic.chapters!.keys.elementAt(ep-1)); .contains(comic.chapters!.keys.elementAt(ep-1));
} }
@@ -439,9 +444,20 @@ class LocalManager with ChangeNotifier {
downloadingTasks.first.resume(); downloadingTasks.first.resume();
} }
void deleteComic(LocalComic c) { void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
var dir = Directory(FilePath.join(path, c.directory)); if(removeFileOnDisk) {
dir.deleteIgnoreError(recursive: true); var dir = Directory(FilePath.join(path, c.directory));
dir.deleteIgnoreError(recursive: true);
}
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
if(HistoryManager().findSync(c.id, c.comicType) != null) {
HistoryManager().remove(c.id, c.comicType);
}
assert(c.comicType == ComicType.local);
var folders = LocalFavoritesManager().find(c.id, c.comicType);
for (var f in folders) {
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
}
remove(c.id, c.comicType); remove(c.id, c.comicType);
notifyListeners(); notifyListeners();
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -176,6 +177,8 @@ class AppDio with DioMixin {
return res; return res;
} }
static final Map<String, bool> _requests = {};
@override @override
Future<Response<T>> request<T>( Future<Response<T>> request<T>(
String path, { String path, {
@@ -186,6 +189,13 @@ class AppDio with DioMixin {
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress, ProgressCallback? onReceiveProgress,
}) async { }) async {
if(options?.headers?['prevent-parallel'] == 'true') {
while(_requests.containsKey(path)) {
await Future.delayed(const Duration(milliseconds: 20));
}
_requests[path] = true;
options!.headers!.remove('prevent-parallel');
}
proxy = await getProxy(); proxy = await getProxy();
if (_proxy != proxy) { if (_proxy != proxy) {
Log.info("Network", "Proxy changed to $proxy"); Log.info("Network", "Proxy changed to $proxy");
@@ -196,7 +206,7 @@ class AppDio with DioMixin {
: rhttp.ProxySettings.proxy(proxy!), : rhttp.ProxySettings.proxy(proxy!),
)); ));
} }
return super.request( var res = super.request<T>(
path, path,
data: data, data: data,
queryParameters: queryParameters, queryParameters: queryParameters,
@@ -205,6 +215,10 @@ class AppDio with DioMixin {
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress, onReceiveProgress: onReceiveProgress,
); );
if(_requests.containsKey(path)) {
_requests.remove(path);
}
return res;
} }
} }

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
class NetworkCache { class NetworkCache {
@@ -43,7 +42,7 @@ class NetworkCacheManager implements Interceptor {
static const _maxCacheSize = 10 * 1024 * 1024; static const _maxCacheSize = 10 * 1024 * 1024;
void setCache(NetworkCache cache) { void setCache(NetworkCache cache) {
while(size > _maxCacheSize){ while (size > _maxCacheSize) {
size -= _cache.values.first.size; size -= _cache.values.first.size;
_cache.remove(_cache.keys.first); _cache.remove(_cache.keys.first);
} }
@@ -53,7 +52,7 @@ class NetworkCacheManager implements Interceptor {
void removeCache(Uri uri) { void removeCache(Uri uri) {
var cache = _cache[uri]; var cache = _cache[uri];
if(cache != null){ if (cache != null) {
size -= cache.size; size -= cache.size;
} }
_cache.remove(uri); _cache.remove(uri);
@@ -64,41 +63,29 @@ class NetworkCacheManager implements Interceptor {
size = 0; size = 0;
} }
var preventParallel = <Uri, Completer>{};
@override @override
void onError(DioException err, ErrorInterceptorHandler handler) { void onError(DioException err, ErrorInterceptorHandler handler) {
if(err.requestOptions.method != "GET"){ if (err.requestOptions.method != "GET") {
return handler.next(err); return handler.next(err);
} }
if(preventParallel[err.requestOptions.uri] != null){
preventParallel[err.requestOptions.uri]!.complete();
preventParallel.remove(err.requestOptions.uri);
}
return handler.next(err); return handler.next(err);
} }
@override @override
void onRequest( void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async { RequestOptions options, RequestInterceptorHandler handler) async {
if(options.method != "GET"){ if (options.method != "GET") {
return handler.next(options); return handler.next(options);
} }
if(preventParallel[options.uri] != null){
await preventParallel[options.uri]!.future;
}
var cache = getCache(options.uri); var cache = getCache(options.uri);
if (cache == null || !compareHeaders(options.headers, cache.requestHeaders)) { if (cache == null ||
if(options.headers['cache-time'] != null){ !compareHeaders(options.headers, cache.requestHeaders)) {
if (options.headers['cache-time'] != null) {
options.headers.remove('cache-time'); options.headers.remove('cache-time');
} }
if(options.headers['prevent-parallel'] != null){
options.headers.remove('prevent-parallel');
preventParallel[options.uri] = Completer();
}
return handler.next(options); return handler.next(options);
} else { } else {
if(options.headers['cache-time'] == 'no'){ if (options.headers['cache-time'] == 'no') {
options.headers.remove('cache-time'); options.headers.remove('cache-time');
removeCache(options.uri); removeCache(options.uri);
return handler.next(options); return handler.next(options);
@@ -106,20 +93,21 @@ class NetworkCacheManager implements Interceptor {
} }
var time = DateTime.now(); var time = DateTime.now();
var diff = time.difference(cache.time); var diff = time.difference(cache.time);
if (options.headers['cache-time'] == 'long' if (options.headers['cache-time'] == 'long' &&
&& diff < const Duration(hours: 2)) { diff < const Duration(hours: 2)) {
return handler.resolve(Response( return handler.resolve(Response(
requestOptions: options, requestOptions: options,
data: cache.data, data: cache.data,
headers: Headers.fromMap(cache.responseHeaders), headers: Headers.fromMap(cache.responseHeaders)
..set('venera-cache', 'true'),
statusCode: 200, statusCode: 200,
)); ));
} } else if (diff < const Duration(seconds: 5)) {
else if (diff < const Duration(seconds: 5)) {
return handler.resolve(Response( return handler.resolve(Response(
requestOptions: options, requestOptions: options,
data: cache.data, data: cache.data,
headers: Headers.fromMap(cache.responseHeaders), headers: Headers.fromMap(cache.responseHeaders)
..set('venera-cache', 'true'),
statusCode: 200, statusCode: 200,
)); ));
} else if (diff < const Duration(hours: 1)) { } else if (diff < const Duration(hours: 1)) {
@@ -133,7 +121,8 @@ class NetworkCacheManager implements Interceptor {
return handler.resolve(Response( return handler.resolve(Response(
requestOptions: options, requestOptions: options,
data: cache.data, data: cache.data,
headers: Headers.fromMap(cache.responseHeaders), headers: Headers.fromMap(cache.responseHeaders)
..set('venera-cache', 'true'),
statusCode: 200, statusCode: 200,
)); ));
} }
@@ -143,6 +132,10 @@ class NetworkCacheManager implements Interceptor {
} }
static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) { static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) {
a.remove('cache-time');
a.remove('prevent-parallel');
b.remove('cache-time');
b.remove('prevent-parallel');
if (a.length != b.length) { if (a.length != b.length) {
return false; return false;
} }
@@ -160,11 +153,11 @@ class NetworkCacheManager implements Interceptor {
if (response.requestOptions.method != "GET") { if (response.requestOptions.method != "GET") {
return handler.next(response); return handler.next(response);
} }
if(response.statusCode != null && response.statusCode! >= 400){ if (response.statusCode != null && response.statusCode! >= 400) {
return handler.next(response); return handler.next(response);
} }
var size = _calculateSize(response.data); var size = _calculateSize(response.data);
if(size != null && size < 1024 * 1024 && size > 0) { if (size != null && size < 1024 * 1024 && size > 0) {
var cache = NetworkCache( var cache = NetworkCache(
uri: response.requestOptions.uri, uri: response.requestOptions.uri,
requestHeaders: response.requestOptions.headers, requestHeaders: response.requestOptions.headers,
@@ -175,30 +168,29 @@ class NetworkCacheManager implements Interceptor {
); );
setCache(cache); setCache(cache);
} }
if(preventParallel[response.requestOptions.uri] != null){
preventParallel[response.requestOptions.uri]!.complete();
preventParallel.remove(response.requestOptions.uri);
}
handler.next(response); handler.next(response);
} }
static int? _calculateSize(Object? data){ static int? _calculateSize(Object? data) {
if(data == null){ if (data == null) {
return 0; return 0;
} }
if(data is List<int>) { if (data is List<int>) {
return data.length; return data.length;
} }
if(data is String) { if (data is Uint8List) {
if(data.trim().isEmpty){ return data.length;
}
if (data is String) {
if (data.trim().isEmpty) {
return 0; return 0;
} }
if(data.length < 512 && data.contains("IP address")){ if (data.length < 512 && data.contains("IP address")) {
return 0; return 0;
} }
return data.length * 4; return data.length * 4;
} }
if(data is Map) { if (data is Map) {
return data.toString().length * 4; return data.toString().length * 4;
} }
return null; return null;

View File

@@ -76,11 +76,14 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
@override @override
ComicType get comicType => ComicType(source.key.hashCode); ComicType get comicType => ComicType(source.key.hashCode);
String? comicTitle;
ImagesDownloadTask({ ImagesDownloadTask({
required this.source, required this.source,
required this.comicId, required this.comicId,
this.comic, this.comic,
this.chapters, this.chapters,
this.comicTitle,
}); });
@override @override
@@ -357,6 +360,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
} }
LocalManager().completeTask(this); LocalManager().completeTask(this);
stopRecorder();
} }
@override @override
@@ -378,7 +382,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
int get speed => currentSpeed; int get speed => currentSpeed;
@override @override
String get title => comic?.title ?? "Loading..."; String get title => comic?.title ?? comicTitle ?? "Loading...";
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -534,6 +538,9 @@ class _ImageDownloadWrapper {
} }
} }
} catch (e, s) { } catch (e, s) {
if (isCancelled) {
return;
}
Log.error("Download", e.toString(), s); Log.error("Download", e.toString(), s);
retry--; retry--;
if (retry > 0) { if (retry > 0) {

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/consts.dart';
@@ -9,8 +10,9 @@ import 'app_dio.dart';
class ImageDownloader { class ImageDownloader {
static Stream<ImageDownloadProgress> loadThumbnail( static Stream<ImageDownloadProgress> loadThumbnail(
String url, String? sourceKey) async* { String url, String? sourceKey,
final cacheKey = "$url@$sourceKey"; [String? cid]) async* {
final cacheKey = "$url@$sourceKey${cid != null ? '@$cid' : ''}";
final cache = await CacheManager().findCache(cacheKey); final cache = await CacheManager().findCache(cacheKey);
if (cache != null) { if (cache != null) {
@@ -33,6 +35,16 @@ class ImageDownloader {
configs['headers']['user-agent'] = webUA; configs['headers']['user-agent'] = webUA;
} }
if (((configs['url'] as String?) ?? url).startsWith('cover.') &&
sourceKey != null) {
var comicSource = ComicSource.find(sourceKey);
if(comicSource != null) {
var comicInfo = await comicSource.loadComicInfo!(cid!);
yield* loadThumbnail(comicInfo.data.cover, sourceKey);
return;
}
}
var dio = AppDio(BaseOptions( var dio = AppDio(BaseOptions(
headers: Map<String, dynamic>.from(configs['headers']), headers: Map<String, dynamic>.from(configs['headers']),
method: configs['method'] ?? 'GET', method: configs['method'] ?? 'GET',
@@ -57,8 +69,9 @@ class ImageDownloader {
} }
} }
if (configs['onResponse'] != null) { if (configs['onResponse'] is JSInvokable) {
buffer = configs['onResponse'](buffer); buffer = (configs['onResponse'] as JSInvokable)([buffer]);
(configs['onResponse'] as JSInvokable).free();
} }
await CacheManager().writeCache(cacheKey, buffer); await CacheManager().writeCache(cacheKey, buffer);
@@ -83,61 +96,98 @@ class ImageDownloader {
); );
} }
Future<Map<String, dynamic>?> Function()? onLoadFailed;
var configs = <String, dynamic>{}; var configs = <String, dynamic>{};
if (sourceKey != null) { if (sourceKey != null) {
var comicSource = ComicSource.find(sourceKey); var comicSource = ComicSource.find(sourceKey);
configs = (await comicSource!.getImageLoadingConfig configs = (await comicSource!.getImageLoadingConfig
?.call(imageKey, cid, eid)) ?? {}; ?.call(imageKey, cid, eid)) ??
{};
} }
configs['headers'] ??= { var retryLimit = 5;
'user-agent': webUA, while (true) {
}; try {
configs['headers'] ??= {
'user-agent': webUA,
};
var dio = AppDio(BaseOptions( if (configs['onLoadFailed'] is JSInvokable) {
headers: configs['headers'], onLoadFailed = () async {
method: configs['method'] ?? 'GET', dynamic result = (configs['onLoadFailed'] as JSInvokable)([]);
responseType: ResponseType.stream, if (result is Future) {
)); result = await result;
}
if (result is! Map<String, dynamic>) return null;
return result;
};
}
var req = await dio.request<ResponseBody>(configs['url'] ?? imageKey, var dio = AppDio(BaseOptions(
data: configs['data']); headers: configs['headers'],
var stream = req.data?.stream ?? (throw "Error: Empty response body."); method: configs['method'] ?? 'GET',
int? expectedBytes = req.data!.contentLength; responseType: ResponseType.stream,
if (expectedBytes == -1) { ));
expectedBytes = null;
} var req = await dio.request<ResponseBody>(configs['url'] ?? imageKey,
var buffer = <int>[]; data: configs['data']);
await for (var data in stream) { var stream = req.data?.stream ?? (throw "Error: Empty response body.");
buffer.addAll(data); int? expectedBytes = req.data!.contentLength;
if (expectedBytes != null) { if (expectedBytes == -1) {
expectedBytes = null;
}
var buffer = <int>[];
await for (var data in stream) {
buffer.addAll(data);
if (expectedBytes != null) {
yield ImageDownloadProgress(
currentBytes: buffer.length,
totalBytes: expectedBytes,
);
}
}
if (configs['onResponse'] is JSInvokable) {
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
(configs['onResponse'] as JSInvokable).free();
}
var data = Uint8List.fromList(buffer);
buffer.clear();
if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript(
data,
configs['modifyImage'],
);
data = newData;
}
await CacheManager().writeCache(cacheKey, data);
yield ImageDownloadProgress( yield ImageDownloadProgress(
currentBytes: buffer.length, currentBytes: data.length,
totalBytes: expectedBytes, totalBytes: data.length,
imageBytes: data,
); );
return;
} catch (e) {
if (retryLimit < 0 || onLoadFailed == null) {
rethrow;
}
var newConfig = await onLoadFailed();
(configs['onLoadFailed'] as JSInvokable).free();
onLoadFailed = null;
if (newConfig == null) {
rethrow;
}
configs = newConfig;
retryLimit--;
} finally {
if (onLoadFailed != null) {
(configs['onLoadFailed'] as JSInvokable).free();
}
} }
} }
if (configs['onResponse'] != null) {
buffer = configs['onResponse'](buffer);
}
var data = Uint8List.fromList(buffer);
buffer.clear();
if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript(
data,
configs['modifyImage'],
);
data = newData;
}
await CacheManager().writeCache(cacheKey, data);
yield ImageDownloadProgress(
currentBytes: data.length,
totalBytes: data.length,
imageBytes: data,
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher_string.dart'; 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';
@@ -1021,6 +1022,8 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
String? error; String? error;
bool isLoading = false;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
state = context.findAncestorStateOfType<_ComicPageState>()!; state = context.findAncestorStateOfType<_ComicPageState>()!;
@@ -1034,6 +1037,11 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
if (!isInitialLoading && next == null) { if (!isInitialLoading && next == null) {
return; return;
} }
Future.microtask(() {
setState(() {
isLoading = true;
});
});
var res = await state.comicSource.loadComicThumbnail!(state.comic.id, next); var res = await state.comicSource.loadComicThumbnail!(state.comic.id, next);
if (res.success) { if (res.success) {
thumbnails.addAll(res.data); thumbnails.addAll(res.data);
@@ -1042,13 +1050,15 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
} else { } else {
error = res.errorMessage; error = res.errorMessage;
} }
setState(() {}); setState(() {
isLoading = false;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverMainAxisGroup( return MultiSliver(
slivers: [ children: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: ListTile( child: ListTile(
title: Text("Preview".tl), title: Text("Preview".tl),
@@ -1148,7 +1158,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
], ],
), ),
) )
else if (next != null || isInitialLoading) else if (isLoading)
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: ListLoadingIndicator(), child: ListLoadingIndicator(),
), ),

View File

@@ -27,7 +27,9 @@ class _DownloadingPageState extends State<DownloadingPage> {
} }
void update() { void update() {
setState(() {}); if(mounted) {
setState(() {});
}
} }
@override @override

View File

@@ -34,12 +34,11 @@ Future<void> newFolder() async {
child: Text("Import from file".tl), child: Text("Import from file".tl),
onPressed: () async { onPressed: () async {
var file = await selectFile(ext: ['json']); var file = await selectFile(ext: ['json']);
if(file == null) return; if (file == null) return;
var data = await file.readAsBytes(); var data = await file.readAsBytes();
try { try {
LocalFavoritesManager().fromJson(utf8.decode(data)); LocalFavoritesManager().fromJson(utf8.decode(data));
} } catch (e) {
catch(e) {
context.showMessage(message: "Failed to import".tl); context.showMessage(message: "Failed to import".tl);
return; return;
} }
@@ -113,7 +112,9 @@ void addFavorite(Comic comic) {
name: comic.title, name: comic.title,
coverPath: comic.cover, coverPath: comic.cover,
author: comic.subtitle ?? '', author: comic.subtitle ?? '',
type: ComicType((comic.sourceKey == 'local' ? 0 : comic.sourceKey.hashCode)), type: ComicType((comic.sourceKey == 'local'
? 0
: comic.sourceKey.hashCode)),
tags: comic.tags ?? [], tags: comic.tags ?? [],
), ),
); );
@@ -128,3 +129,162 @@ void addFavorite(Comic comic) {
}, },
); );
} }
Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
var comics = LocalFavoritesManager().getAllComics(folder);
Future<void> updateSingleComic(int index) async {
int retry = 3;
while (true) {
try {
var c = comics[index];
var comicSource = c.type.comicSource;
if (comicSource == null) return;
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
comics[index] = FavoriteItem(
id: c.id,
name: newInfo.title,
coverPath: newInfo.cover,
author: newInfo.subTitle ??
newInfo.tags['author']?.firstOrNull ??
c.author,
type: c.type,
tags: c.tags,
);
LocalFavoritesManager().updateInfo(folder, comics[index]);
return;
} catch (e) {
retry--;
if (retry == 0) {
rethrow;
}
continue;
}
}
}
var finished = ValueNotifier(0);
var errors = 0;
var index = 0;
bool isCanceled = false;
showDialog(
context: App.rootContext,
builder: (context) {
return ValueListenableBuilder(
valueListenable: finished,
builder: (context, value, child) {
var isFinished = value == comics.length;
return ContentDialog(
title: isFinished ? "Finished".tl : "Updating".tl,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
LinearProgressIndicator(
value: value / comics.length,
),
const SizedBox(height: 4),
Text("$value/${comics.length}"),
const SizedBox(height: 4),
if (errors > 0) Text("Errors: $errors"),
],
).paddingHorizontal(16),
actions: [
Button.filled(
color: isFinished ? null : context.colorScheme.error,
onPressed: () {
isCanceled = true;
context.pop();
},
child: isFinished ? Text("OK".tl) : Text("Cancel".tl),
),
],
);
},
);
},
).then((_) {
isCanceled = true;
});
while (index < comics.length) {
var futures = <Future>[];
const maxConcurrency = 4;
if (isCanceled) {
return comics;
}
for (var i = 0; i < maxConcurrency; i++) {
if (index + i >= comics.length) break;
futures.add(updateSingleComic(index + i).then((v) {
finished.value++;
}, onError: (_) {
errors++;
finished.value++;
}));
}
await Future.wait(futures);
index += maxConcurrency;
}
return comics;
}
Future<void> sortFolders() async {
var folders = LocalFavoritesManager().folderNames;
await showPopUpWidget(
App.rootContext,
StatefulBuilder(builder: (context, setState) {
return PopUpWidgetScaffold(
title: "Sort".tl,
tailing: [
Tooltip(
message: "Help".tl,
child: IconButton(
icon: const Icon(Icons.help_outline),
onPressed: () {
showInfoDialog(
context: context,
title: "Reorder".tl,
content: "Long press and drag to reorder.".tl,
);
},
),
)
],
body: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
setState(() {
var item = folders.removeAt(oldIndex);
folders.insert(newIndex, item);
});
},
itemCount: folders.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(folders[index]),
title: Text(folders[index]),
);
},
),
);
}),
);
LocalFavoritesManager().updateOrder(folders);
}

View File

@@ -9,7 +9,9 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';

View File

@@ -105,7 +105,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
}, },
).then( ).then(
(value) { (value) {
if(mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
}, },
@@ -123,6 +123,45 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
filename: "${widget.folder}.json", filename: "${widget.folder}.json",
); );
}), }),
MenuEntry(
icon: Icons.update,
text: "Update Comics Info".tl,
onClick: () {
updateComicsInfo(widget.folder).then((newComics) {
if (mounted) {
setState(() {
comics = newComics;
});
}
});
}),
MenuEntry(
icon: Icons.update,
text: "Download All".tl,
onClick: () async {
int count = 0;
for (var c in comics) {
if (await LocalManager().isDownloaded(c.id, c.type)) {
continue;
}
var comicSource = c.type.comicSource;
if (comicSource == null) {
continue;
}
LocalManager().addTask(ImagesDownloadTask(
source: comicSource,
comicId: c.id,
comic: null,
comicTitle: c.name,
));
count++;
}
context.showMessage(
message: "Added @count comics to download queue."
.tlParams({
"count": count.toString(),
}));
}),
], ],
), ),
], ],
@@ -207,7 +246,8 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
e.author, e.author,
e.tags, e.tags,
"${e.time} | ${comicSource?.name ?? "Unknown"}", "${e.time} | ${comicSource?.name ?? "Unknown"}",
comicSource?.key ?? "Unknown", comicSource?.key ??
(e.type == ComicType.local ? "local" : "Unknown"),
null, null,
null, null,
), ),

View File

@@ -80,7 +80,6 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 16),
Icon( Icon(
Icons.local_activity, Icons.local_activity,
color: context.colorScheme.secondary, color: context.colorScheme.secondary,
@@ -88,27 +87,41 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
const SizedBox(width: 12), const SizedBox(width: 12),
Text("Local".tl), Text("Local".tl),
const Spacer(), const Spacer(),
IconButton( MenuButton(
icon: const Icon(Icons.search), entries: [
color: context.colorScheme.primary, MenuEntry(
onPressed: () { icon: Icons.search,
context.to(() => const LocalSearchPage()); text: 'Search'.tl,
}, onClick: () {
context.to(() => const LocalSearchPage());
},
),
MenuEntry(
icon: Icons.add,
text: 'Create Folder'.tl,
onClick: () {
newFolder().then((value) {
setState(() {
folders = LocalFavoritesManager().folderNames;
});
});
},
),
MenuEntry(
icon: Icons.reorder,
text: 'Sort'.tl,
onClick: () {
sortFolders().then((value) {
setState(() {
folders = LocalFavoritesManager().folderNames;
});
});
},
),
],
), ),
IconButton(
icon: const Icon(Icons.add),
color: context.colorScheme.primary,
onPressed: () {
newFolder().then((value) {
setState(() {
folders = LocalFavoritesManager().folderNames;
});
});
},
),
const SizedBox(width: 16),
], ],
), ).paddingHorizontal(16),
); );
} }
index--; index--;
@@ -219,13 +232,13 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
@override @override
void update() { void update() {
if(!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
} }
@override @override
void updateFolders() { void updateFolders() {
if(!mounted) return; if (!mounted) return;
setState(() { setState(() {
folders = LocalFavoritesManager().folderNames; folders = LocalFavoritesManager().folderNames;
networkFolders = ComicSource.all() networkFolders = ComicSource.all()

View File

@@ -22,6 +22,8 @@ import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'package:sqlite3/sqlite3.dart' as sql;
import 'dart:math';
import 'local_comics_page.dart'; import 'local_comics_page.dart';
@@ -256,6 +258,7 @@ class _HistoryState extends State<_History> {
ImageProvider imageProvider = CachedImageProvider( ImageProvider imageProvider = CachedImageProvider(
cover, cover,
sourceKey: history[index].type.comicSource?.key, sourceKey: history[index].type.comicSource?.key,
cid: history[index].id,
); );
if (!cover.isURL) { if (!cover.isURL) {
var localComic = LocalManager().find( var localComic = LocalManager().find(
@@ -495,7 +498,14 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
"Select a directory which contains the comic files.".tl, "Select a directory which contains the comic files.".tl,
"Select a directory which contains the comic directories.".tl, "Select a directory which contains the comic directories.".tl,
"Select a cbz file.".tl, "Select a cbz file.".tl,
"Select an EhViewer database and a download folder.".tl
][type]; ][type];
List<String> importMethods = [
"Single Comic".tl,
"Multiple Comics".tl,
"A cbz file".tl,
"EhViewer downloads".tl
];
return ContentDialog( return ContentDialog(
dismissible: !loading, dismissible: !loading,
@@ -513,36 +523,18 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(width: 600), const SizedBox(width: 600),
RadioListTile( ...List.generate(importMethods.length, (index) {
title: Text("Single Comic".tl), return RadioListTile(
value: 0, title: Text(importMethods[index]),
groupValue: type, value: index,
onChanged: (value) { groupValue: type,
setState(() { onChanged: (value) {
type = value as int; setState(() {
}); type = value as int;
}, });
), },
RadioListTile( );
title: Text("Multiple Comics".tl), }),
value: 1,
groupValue: type,
onChanged: (value) {
setState(() {
type = value as int;
});
},
),
RadioListTile(
title: Text("A cbz file".tl),
value: 2,
groupValue: type,
onChanged: (value) {
setState(() {
type = value as int;
});
},
),
ListTile( ListTile(
title: Text("Add to favorites".tl), title: Text("Add to favorites".tl),
trailing: Select( trailing: Select(
@@ -576,7 +568,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
barrierColor: Colors.transparent, barrierColor: Colors.black.withOpacity(0.2),
builder: (context) { builder: (context) {
var help = ''; var help = '';
help += help +=
@@ -587,8 +579,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
help += help +=
'${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n'; '${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n';
help += help +=
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
.tl; .tl;
help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl;
return ContentDialog( return ContentDialog(
title: "Help".tl, title: "Help".tl,
content: Text(help).paddingHorizontal(16), content: Text(help).paddingHorizontal(16),
@@ -641,6 +634,135 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
} }
controller.close(); controller.close();
return; return;
} else if (type == 3) {
var dbFile = await selectFile(ext: ['db']);
final picker = DirectoryPicker();
final comicSrc = await picker.pickDirectory();
if (dbFile == null || comicSrc == null) {
return;
}
bool cancelled = false;
var controller = showLoadingDialog(context, onCancel: () { cancelled = true; });
try {
var cache = FilePath.join(App.cachePath, dbFile.name);
await dbFile.saveTo(cache);
var db = sql.sqlite3.open(cache);
Future<void> addTagComics(String destFolder, List<sql.Row> comics) async {
for(var comic in comics) {
if(cancelled) {
return;
}
var comicDir = Directory(FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
if(!(await comicDir.exists())) {
continue;
}
String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
String title = titleJP == "" ? comic['TITLE'] as String : titleJP;
if (LocalManager().findByName(title) != null) {
Log.info("Import Comic", "Comic already exists: $title");
continue;
}
String coverURL = await comicDir.joinFile(".thumb").exists() ?
comicDir.joinFile(".thumb").path :
(comic['THUMB'] as String).replaceAll('s.exhentai.org', 'ehgt.org');
int downloadedTimeStamp = comic['TIME'] as int;
DateTime downloadedTime =
downloadedTimeStamp != 0 ?
DateTime.fromMillisecondsSinceEpoch(downloadedTimeStamp) : DateTime.now();
var comicObj = LocalComic(
id: LocalManager().findValidId(ComicType.local),
title: title,
subtitle: '',
tags: [
//1 >> x
[
"MISC",
"DOUJINSHI",
"MANGA",
"ARTISTCG",
"GAMECG",
"IMAGE SET",
"COSPLAY",
"ASIAN PORN",
"NON-H",
"WESTERN",
][(log(comic['CATEGORY'] as int) / ln2).floor()]
],
directory: comicDir.path,
chapters: null,
cover: coverURL,
comicType: ComicType.local,
downloadedChapters: [],
createdAt: downloadedTime,
);
LocalManager().add(comicObj, comicObj.id);
LocalFavoritesManager().addComic(
destFolder,
FavoriteItem(
id: comicObj.id,
name: comicObj.title,
coverPath: comicObj.cover,
author: comicObj.subtitle,
type: comicObj.comicType,
tags: comicObj.tags,
favoriteTime: downloadedTime
),
);
}
}
//default folder
{
var defaultFolderName = '(EhViewer)Default'.tl;
if(!LocalFavoritesManager().existsFolder(defaultFolderName)) {
LocalFavoritesManager().createFolder(defaultFolderName);
}
var comicList = db.select("""
SELECT *
FROM DOWNLOAD_DIRNAME DN
LEFT JOIN DOWNLOADS DL
ON DL.GID = DN.GID
WHERE DL.LABEL IS NULL AND DL.STATE = 3
ORDER BY DL.TIME DESC
""").toList();
await addTagComics(defaultFolderName, comicList);
}
var folders = db.select("""
SELECT * FROM DOWNLOAD_LABELS;
""");
for (var folder in folders) {
if(cancelled) {
break;
}
var label = folder["LABEL"] as String;
var folderName = '(EhViewer)$label';
if(!LocalFavoritesManager().existsFolder(folderName)) {
LocalFavoritesManager().createFolder(folderName);
}
var comicList = db.select("""
SELECT *
FROM DOWNLOAD_DIRNAME DN
LEFT JOIN DOWNLOADS DL
ON DL.GID = DN.GID
WHERE DL.LABEL = ? AND DL.STATE = 3
ORDER BY DL.TIME DESC
""", [label]).toList();
await addTagComics(folderName, comicList);
}
db.dispose();
await File(cache).deleteIgnoreError();
} catch (e, s) {
Log.error("Import Comic", e.toString(), s);
context.showMessage(message: e.toString());
}
controller.close();
return;
} }
height = key.currentContext!.size!.height; height = key.currentContext!.size!.height;
setState(() { setState(() {

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.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/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/pages/downloading_page.dart'; import 'package:venera/pages/downloading_page.dart';
import 'package:venera/utils/cbz.dart'; import 'package:venera/utils/cbz.dart';
@@ -26,7 +27,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool multiSelectMode = false; bool multiSelectMode = false;
Map<LocalComic, bool> selectedComics = {}; Map<Comic, bool> selectedComics = {};
void update() { void update() {
if (keyword.isEmpty) { if (keyword.isEmpty) {
@@ -115,7 +116,66 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( void selectAll() {
setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
});
}
void deSelect() {
setState(() {
selectedComics.clear();
});
}
void invertSelection() {
setState(() {
comics.asMap().forEach((k, v) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
});
selectedComics.removeWhere((k, v) => !v);
});
}
void selectRange() {
setState(() {
List<int> l = [];
selectedComics.forEach((k, v) {
l.add(comics.indexOf(k as LocalComic));
});
if (l.isEmpty) {
return;
}
l.sort();
int start = l.first;
int end = l.last;
selectedComics.clear();
selectedComics.addEntries(List.generate(end - start + 1, (i) {
return MapEntry(comics[start + i], true);
}));
});
}
List<Widget> selectActions = [
IconButton(
icon: const Icon(Icons.select_all),
tooltip: "Select All".tl,
onPressed: selectAll),
IconButton(
icon: const Icon(Icons.deselect),
tooltip: "Deselect".tl,
onPressed: deSelect),
IconButton(
icon: const Icon(Icons.flip),
tooltip: "Invert Selection".tl,
onPressed: invertSelection),
IconButton(
icon: const Icon(Icons.border_horizontal_outlined),
tooltip: "Select in range".tl,
onPressed: selectRange),
];
var body = Scaffold(
body: SmoothCustomScrollView( body: SmoothCustomScrollView(
slivers: [ slivers: [
if (!searchMode && !multiSelectMode) if (!searchMode && !multiSelectMode)
@@ -166,11 +226,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
) )
else if (multiSelectMode) else if (multiSelectMode)
SliverAppbar( SliverAppbar(
title: Text("Selected @a comics".tlParams({ leading: Tooltip(
'a': selectedComics.length, message: "Cancel".tl,
})), child: IconButton(
actions: [
IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -179,10 +237,26 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}); });
}, },
), ),
], ),
title: Text(
"Selected @c comics".tlParams({"c": selectedComics.length})),
actions: selectActions,
) )
else if (searchMode) else if (searchMode)
SliverAppbar( SliverAppbar(
leading: Tooltip(
message: "Cancel".tl,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
),
title: TextField( title: TextField(
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
@@ -194,28 +268,17 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
update(); update();
}, },
), ),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
],
), ),
SliverGridComics( SliverGridComics(
comics: comics, comics: comics,
selections: selectedComics,
onTap: multiSelectMode onTap: multiSelectMode
? (c) { ? (c) {
setState(() { setState(() {
if (selectedComics.containsKey(c as LocalComic)) { if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c as LocalComic); selectedComics.remove(c);
} else { } else {
selectedComics[c as LocalComic] = true; selectedComics[c] = true;
} }
}); });
} }
@@ -228,25 +291,55 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: Icons.delete, icon: Icons.delete,
text: "Delete".tl, text: "Delete".tl,
onClick: () { onClick: () {
if (multiSelectMode) { showDialog(
showConfirmDialog(
context: context, context: context,
title: "Delete".tl, builder: (context) {
content: bool removeComicFile = true;
"Are you sure you want to delete @a selected comics?" return StatefulBuilder(builder: (context, state) {
.tlParams({'a': selectedComics.length}), return ContentDialog(
onConfirm: () { title: "Delete".tl,
for (var comic in selectedComics.keys) { content: Column(
LocalManager().deleteComic(comic); children: [
} Text("Delete selected comics?".tl)
setState(() { .paddingVertical(8),
selectedComics.clear(); Transform.scale(
scale: 0.9,
child: CheckboxListTile(
title: Text(
"Also remove files on disk".tl),
value: removeComicFile,
onChanged: (v) {
state(() {
removeComicFile =
!removeComicFile;
});
})),
],
).paddingHorizontal(16).paddingVertical(8),
actions: [
FilledButton(
onPressed: () {
context.pop();
if (multiSelectMode) {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(
comic as LocalComic,
removeComicFile);
}
setState(() {
selectedComics.clear();
});
} else {
LocalManager().deleteComic(
c as LocalComic, removeComicFile);
}
},
child: Text("Confirm".tl),
),
],
);
}); });
}, });
);
} else {
LocalManager().deleteComic(c as LocalComic);
}
}), }),
MenuEntry( MenuEntry(
icon: Icons.outbox_outlined, icon: Icons.outbox_outlined,
@@ -259,7 +352,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
try { try {
if (multiSelectMode) { if (multiSelectMode) {
for (var comic in selectedComics.keys) { for (var comic in selectedComics.keys) {
var file = await CBZ.export(comic); var file = await CBZ.export(comic as LocalComic);
await saveFile(filename: file.name, file: file); await saveFile(filename: file.name, file: file);
await file.delete(); await file.delete();
} }
@@ -282,5 +375,24 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
], ],
), ),
); );
return PopScope(
canPop: !multiSelectMode && !searchMode,
onPopInvokedWithResult: (didPop, result) {
if(multiSelectMode) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
} else if(searchMode) {
setState(() {
searchMode = false;
keyword = "";
update();
});
}
},
child: body,
);
} }
} }

View File

@@ -78,20 +78,25 @@ class _MainPageState extends State<MainPage> {
activeIcon: Icons.category, activeIcon: Icons.category,
), ),
], ],
onPageChanged: (i) {
setState(() {
index = i;
});
},
paneActions: [ paneActions: [
if(index != 0) if(index != 0)
PaneActionEntry( PaneActionEntry(
icon: Icons.search, icon: Icons.search,
label: "Search".tl, label: "Search".tl,
onTap: () { onTap: () {
to(() => const SearchPage()); to(() => const SearchPage(), preventDuplicate: true);
}, },
), ),
PaneActionEntry( PaneActionEntry(
icon: Icons.settings, icon: Icons.settings,
label: "Settings".tl, label: "Settings".tl,
onTap: () { onTap: () {
to(() => const SettingsPage()); to(() => const SettingsPage(), preventDuplicate: true);
}, },
) )
], ],

View File

@@ -20,11 +20,13 @@ import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/image_provider/reader_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/file_type.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'package:venera/utils/volume.dart'; import 'package:venera/utils/volume.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:battery_plus/battery_plus.dart';
part 'scaffold.dart'; part 'scaffold.dart';
part 'images.dart'; part 'images.dart';
@@ -122,6 +124,9 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
focusNode.dispose(); focusNode.dispose();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
stopVolumeEvent(); stopVolumeEvent();
Future.microtask(() {
DataSync().onDataChanged();
});
super.dispose(); super.dispose();
} }
@@ -319,6 +324,8 @@ enum ReaderMode {
bool get isGallery => key.startsWith('gallery'); bool get isGallery => key.startsWith('gallery');
bool get isContinuous => key.startsWith('continuous');
const ReaderMode(this.key); const ReaderMode(this.key);
static ReaderMode fromKey(String key) { static ReaderMode fromKey(String key) {

View File

@@ -131,10 +131,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: widget.child, child: widget.child,
), ),
buildPageInfoText(), buildPageInfoText(),
buildStatusInfo(),
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
right: 16, right: 16,
bottom: showFloatingButtonValue == 0 ? -58 : 16, bottom: showFloatingButtonValue == 0 ? -58 : 36,
child: buildEpChangeButton(), child: buildEpChangeButton(),
), ),
AnimatedPositioned( AnimatedPositioned(
@@ -424,6 +425,24 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
); );
} }
Widget buildStatusInfo() {
if (appdata.settings['enableClockAndBatteryInfoInReader']) {
return Positioned(
bottom: 13,
right: 25,
child: Row(
children: [
_ClockWidget(),
const SizedBox(width: 10),
_BatteryWidget(),
],
),
);
} else {
return const SizedBox.shrink();
}
}
void openChapterDrawer() { void openChapterDrawer() {
showSideBar( showSideBar(
context, context,
@@ -432,8 +451,73 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
); );
} }
Future<Uint8List> _getCurrentImageData() async { Future<Uint8List?> _getCurrentImageData() async {
var imageKey = context.reader.images![context.reader.page - 1]; 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![e.index - 1])
.toList();
String? selected;
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,
);
}
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
selected = images[index];
App.rootContext.pop();
},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
),
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,
),
),
),
);
if (selected == null) {
return null;
} else {
imageKey = selected!;
}
}
if (imageKey.startsWith("file://")) { if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes(); return await File(imageKey.substring(7)).readAsBytes();
} else { } else {
@@ -445,6 +529,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
void saveCurrentImage() async { void saveCurrentImage() async {
var data = await _getCurrentImageData(); var data = await _getCurrentImageData();
if (data == null) {
return;
}
var fileType = detectFileType(data); var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}"; var filename = "${context.reader.page}${fileType.ext}";
saveFile(data: data, filename: filename); saveFile(data: data, filename: filename);
@@ -452,6 +539,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
void share() async { void share() async {
var data = await _getCurrentImageData(); var data = await _getCurrentImageData();
if (data == null) {
return;
}
var fileType = detectFileType(data); var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}"; var filename = "${context.reader.page}${fileType.ext}";
Share.shareFile( Share.shareFile(
@@ -471,7 +561,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
App.rootContext.pop(); App.rootContext.pop();
} }
if (key == "enableTurnPageByVolumeKey") { if (key == "enableTurnPageByVolumeKey") {
if(appdata.settings[key]) { if (appdata.settings[key]) {
context.reader.handleVolumeEvent(); context.reader.handleVolumeEvent();
} else { } else {
context.reader.stopVolumeEvent(); context.reader.stopVolumeEvent();
@@ -569,6 +659,188 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
} }
} }
class _BatteryWidget extends StatefulWidget {
@override
_BatteryWidgetState createState() => _BatteryWidgetState();
}
class _BatteryWidgetState extends State<_BatteryWidget> {
late Battery _battery;
late int _batteryLevel = 100;
Timer? _timer;
bool _hasBattery = false;
@override
void initState() {
super.initState();
_battery = Battery();
_checkBatteryAvailability();
}
void _checkBatteryAvailability() async {
try {
_batteryLevel = await _battery.batteryLevel;
if (_batteryLevel != -1) {
setState(() {
_hasBattery = true;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_battery.batteryLevel.then((level) => {
if (_batteryLevel != level)
{
setState(() {
_batteryLevel = level;
})
}
});
});
});
} else {
setState(() {
_hasBattery = false;
});
}
} catch (e) {
setState(() {
_hasBattery = false;
});
}
}
@override
Widget build(BuildContext context) {
if (!_hasBattery) {
return const SizedBox.shrink(); //Empty Widget
}
return _batteryInfo(_batteryLevel);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
Widget _batteryInfo(int batteryLevel) {
IconData batteryIcon;
Color batteryColor = context.colorScheme.onSurface;
if (batteryLevel >= 96) {
batteryIcon = Icons.battery_full_sharp;
} else if (batteryLevel >= 84) {
batteryIcon = Icons.battery_6_bar_sharp;
} else if (batteryLevel >= 72) {
batteryIcon = Icons.battery_5_bar_sharp;
} else if (batteryLevel >= 60) {
batteryIcon = Icons.battery_4_bar_sharp;
} else if (batteryLevel >= 48) {
batteryIcon = Icons.battery_3_bar_sharp;
} else if (batteryLevel >= 36) {
batteryIcon = Icons.battery_2_bar_sharp;
} else if (batteryLevel >= 24) {
batteryIcon = Icons.battery_1_bar_sharp;
} else if (batteryLevel >= 12) {
batteryIcon = Icons.battery_0_bar_sharp;
} else {
batteryIcon = Icons.battery_alert_sharp;
batteryColor = Colors.red;
}
return Row(
children: [
Icon(
batteryIcon,
size: 16,
color: batteryColor,
// Stroke
shadows: List.generate(
9,
(index) {
if (index == 4) {
return null;
}
double offsetX = (index % 3 - 1) * 0.8;
double offsetY = ((index / 3).floor() - 1) * 0.8;
return Shadow(
color: context.colorScheme.onInverseSurface,
offset: Offset(offsetX, offsetY),
);
},
).whereType<Shadow>().toList(),
),
Stack(
children: [
Text(
'$batteryLevel%',
style: TextStyle(
fontSize: 14,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.4
..color = context.colorScheme.onInverseSurface,
),
),
Text('$batteryLevel%'),
],
),
],
);
}
}
class _ClockWidget extends StatefulWidget {
@override
_ClockWidgetState createState() => _ClockWidgetState();
}
class _ClockWidgetState extends State<_ClockWidget> {
late String _currentTime;
late Timer _timer;
@override
void initState() {
super.initState();
_currentTime = _getCurrentTime();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
final time = _getCurrentTime();
if (_currentTime != time) {
setState(() {
_currentTime = time;
});
}
});
}
String _getCurrentTime() {
final now = DateTime.now();
return "${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}";
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Text(
_currentTime,
style: TextStyle(
fontSize: 14,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.4
..color = context.colorScheme.onInverseSurface,
),
),
Text(_currentTime),
],
);
}
}
class _ChaptersView extends StatefulWidget { class _ChaptersView extends StatefulWidget {
const _ChaptersView(this.reader); const _ChaptersView(this.reader);

View File

@@ -16,12 +16,12 @@ class _AboutSettingsState extends State<AboutSettings> {
slivers: [ slivers: [
SliverAppbar(title: Text("About".tl)), SliverAppbar(title: Text("About".tl)),
SizedBox( SizedBox(
height: 136, height: 112,
width: double.infinity, width: double.infinity,
child: Center( child: Center(
child: Container( child: Container(
width: 136, width: 112,
height: 136, height: 112,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(136), borderRadius: BorderRadius.circular(136),
), ),

View File

@@ -77,6 +77,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call('enableTurnPageByVolumeKey'); widget.onChanged?.call('enableTurnPageByVolumeKey');
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Display time & battery info in reader".tl,
settingKey: "enableClockAndBatteryInfoInReader",
onChanged: () {
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
},
).toSliver(),
], ],
); );
} }

View File

@@ -10,7 +10,6 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.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';
@@ -44,7 +43,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
ColorScheme get colors => Theme.of(context).colorScheme; ColorScheme get colors => Theme.of(context).colorScheme;
bool get enableTwoViews => context.width > changePoint; bool get enableTwoViews => context.width > 720;
final categories = <String>[ final categories = <String>[
"Explore", "Explore",

View File

@@ -18,11 +18,11 @@ export 'package:flutter_inappwebview/flutter_inappwebview.dart'
extension WebviewExtension on InAppWebViewController { extension WebviewExtension on InAppWebViewController {
Future<List<io.Cookie>?> getCookies(String url) async { Future<List<io.Cookie>?> getCookies(String url) async {
if(url.contains("https://")){ if (url.contains("https://")) {
url.replaceAll("https://", ""); url.replaceAll("https://", "");
} }
if(url[url.length-1] == '/'){ if (url[url.length - 1] == '/') {
url = url.substring(0, url.length-1); url = url.substring(0, url.length - 1);
} }
CookieManager cookieManager = CookieManager.instance(); CookieManager cookieManager = CookieManager.instance();
final cookies = await cookieManager.getCookies(url: WebUri(url)); final cookies = await cookieManager.getCookies(url: WebUri(url));
@@ -89,29 +89,29 @@ class _AppWebviewState extends State<AppWebview> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
onPressed: () { onPressed: () {
showMenu( showMenuX(
context: context, context,
position: RelativeRect.fromLTRB( Offset(context.width, context.padding.top),
MediaQuery.of(context).size.width, [
0, MenuEntry(
MediaQuery.of(context).size.width, icon: Icons.open_in_browser,
0), text: "Open in browser".tl,
items: [ onClick: () async =>
PopupMenuItem( launchUrlString((await controller?.getUrl())!.toString()),
child: Text("Open in browser".tl), ),
onTap: () async => MenuEntry(
launchUrlString((await controller?.getUrl())!.toString()), icon: Icons.copy,
), text: "Copy link".tl,
PopupMenuItem( onClick: () async => Clipboard.setData(ClipboardData(
child: Text("Copy link".tl), text: (await controller?.getUrl())!.toString())),
onTap: () async => Clipboard.setData(ClipboardData( ),
text: (await controller?.getUrl())!.toString())), MenuEntry(
), icon: Icons.refresh,
PopupMenuItem( text: "Reload".tl,
child: Text("Reload".tl), onClick: () => controller?.reload(),
onTap: () => controller?.reload(), ),
), ],
]); );
}, },
), ),
) )

View File

@@ -6,6 +6,7 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/network/cookie_jar.dart';
import 'package:zip_flutter/zip_flutter.dart'; import 'package:zip_flutter/zip_flutter.dart';
import 'io.dart'; import 'io.dart';
@@ -15,7 +16,7 @@ Future<File> exportAppData() async {
var cacheFilePath = FilePath.join(App.cachePath, '$time.venera'); var cacheFilePath = FilePath.join(App.cachePath, '$time.venera');
var cacheFile = File(cacheFilePath); var cacheFile = File(cacheFilePath);
var dataPath = App.dataPath; var dataPath = App.dataPath;
if(await cacheFile.exists()) { if (await cacheFile.exists()) {
await cacheFile.delete(); await cacheFile.delete();
} }
await Isolate.run(() { await Isolate.run(() {
@@ -23,11 +24,14 @@ Future<File> exportAppData() async {
var historyFile = FilePath.join(dataPath, "history.db"); var historyFile = FilePath.join(dataPath, "history.db");
var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db"); var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db");
var appdata = FilePath.join(dataPath, "appdata.json"); var appdata = FilePath.join(dataPath, "appdata.json");
var cookies = FilePath.join(dataPath, "cookie.db");
zipFile.addFile("history.db", historyFile); zipFile.addFile("history.db", historyFile);
zipFile.addFile("local_favorite.db", localFavoriteFile); zipFile.addFile("local_favorite.db", localFavoriteFile);
zipFile.addFile("appdata.json", appdata); zipFile.addFile("appdata.json", appdata);
for(var file in Directory(FilePath.join(dataPath, "comic_source")).listSync()) { zipFile.addFile("cookie.db", cookies);
if(file is File) { for (var file
in Directory(FilePath.join(dataPath, "comic_source")).listSync()) {
if (file is File) {
zipFile.addFile("comic_source/${file.name}", file.path); zipFile.addFile("comic_source/${file.name}", file.path);
} }
} }
@@ -45,32 +49,50 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
var historyFile = cacheDir.joinFile("history.db"); var historyFile = cacheDir.joinFile("history.db");
var localFavoriteFile = cacheDir.joinFile("local_favorite.db"); var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
var appdataFile = cacheDir.joinFile("appdata.json"); var appdataFile = cacheDir.joinFile("appdata.json");
if(checkVersion && appdataFile.existsSync()) { var cookieFile = cacheDir.joinFile("cookie.db");
if (checkVersion && appdataFile.existsSync()) {
var data = jsonDecode(await appdataFile.readAsString()); var data = jsonDecode(await appdataFile.readAsString());
var version = data["settings"]["dataVersion"]; var version = data["settings"]["dataVersion"];
if(version is int && version <= appdata.settings["dataVersion"]) { if (version is int && version <= appdata.settings["dataVersion"]) {
return; return;
} }
} }
if(await historyFile.exists()) { if (await historyFile.exists()) {
HistoryManager().close(); HistoryManager().close();
historyFile.copySync(FilePath.join(App.dataPath, "history.db")); File(FilePath.join(App.dataPath, "history.db")).deleteIfExistsSync();
historyFile.renameSync(FilePath.join(App.dataPath, "history.db"));
HistoryManager().init(); HistoryManager().init();
} }
if(await localFavoriteFile.exists()) { if (await localFavoriteFile.exists()) {
LocalFavoritesManager().close(); LocalFavoritesManager().close();
localFavoriteFile.copySync(FilePath.join(App.dataPath, "local_favorite.db")); File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync();
localFavoriteFile
.renameSync(FilePath.join(App.dataPath, "local_favorite.db"));
LocalFavoritesManager().init(); LocalFavoritesManager().init();
} }
if(await appdataFile.exists()) { if (await appdataFile.exists()) {
await appdataFile.copy(FilePath.join(App.dataPath, "appdata.json")); // proxy settings should be kept
appdata.init(); var proxySettings = appdata.settings["proxy"];
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
await appdata.init();
appdata.settings["proxy"] = proxySettings;
appdata.saveData();
}
if (await cookieFile.exists()) {
SingleInstanceCookieJar.instance?.dispose();
File(FilePath.join(App.dataPath, "cookie.db")).deleteIfExistsSync();
cookieFile.renameSync(FilePath.join(App.dataPath, "cookie.db"));
SingleInstanceCookieJar.instance =
SingleInstanceCookieJar(FilePath.join(App.dataPath, "cookie.db"))
..init();
} }
var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); var comicSourceDir = FilePath.join(cacheDirPath, "comic_source");
if(Directory(comicSourceDir).existsSync()) { if (Directory(comicSourceDir).existsSync()) {
for(var file in Directory(comicSourceDir).listSync()) { for (var file in Directory(comicSourceDir).listSync()) {
if(file is File) { if (file is File) {
var targetFile = FilePath.join(App.dataPath, "comic_source", file.name); var targetFile = FilePath.join(App.dataPath, "comic_source", file.name);
File(targetFile).deleteIfExistsSync();
await file.copy(targetFile); await file.copy(targetFile);
} }
} }

View File

@@ -4,7 +4,6 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.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/app_dio.dart'; import 'package:venera/network/app_dio.dart';
@@ -19,7 +18,6 @@ class DataSync with ChangeNotifier {
if (isEnabled) { if (isEnabled) {
downloadData(); downloadData();
} }
HistoryManager().addListener(onDataChanged);
LocalFavoritesManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged);
ComicSource.addListener(onDataChanged); ComicSource.addListener(onDataChanged);
} }
@@ -57,8 +55,9 @@ class DataSync with ChangeNotifier {
} }
Future<Res<bool>> uploadData() async { Future<Res<bool>> uploadData() async {
if(isDownloading) return const Res(true);
if (haveWaitingTask) return const Res(true); if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) { while (isUploading) {
haveWaitingTask = true; haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
} }

View File

@@ -45,6 +45,18 @@ extension FileSystemEntityExt on FileSystemEntity {
// ignore // ignore
} }
} }
Future<void> deleteIfExists({bool recursive = false}) async {
if (existsSync()) {
await delete(recursive: recursive);
}
}
void deleteIfExistsSync({bool recursive = false}) {
if (existsSync()) {
deleteSync(recursive: recursive);
}
}
} }
extension FileExtension on File { extension FileExtension on File {

View File

@@ -1,43 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_qjs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin");
flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -1,34 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
file_selector_linux
flutter_qjs
gtk
screen_retriever_linux
sqlite3_flutter_libs
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -1,30 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import app_links
import desktop_webview_window
import file_selector_macos
import flutter_inappwebview_macos
import path_provider_foundation
import screen_retriever_macos
import share_plus
import sqlite3_flutter_libs
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -49,6 +49,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
battery_plus:
dependency: "direct main"
description:
name: battery_plus
sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
battery_plus_platform_interface:
dependency: transitive
description:
name: battery_plus_platform_interface
sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910
url: "https://pub.dev"
source: hosted
version: "2.0.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -109,10 +125,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@@ -121,6 +137,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
desktop_webview_window: desktop_webview_window:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -404,10 +428,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: html name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.15.4" version: "0.15.5"
http: http:
dependency: transitive dependency: transitive
description: description:
@@ -593,54 +617,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
url: "https://pub.dev"
source: hosted
version: "12.0.13"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
url: "https://pub.dev"
source: hosted
version: "9.4.5"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -788,10 +764,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3 name: sqlite3
sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.6" version: "2.4.7"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -848,6 +824,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
upower:
dependency: transitive
description:
name: upower
sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf
url: "https://pub.dev"
source: hosted
version: "0.7.0"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.0.4+104 version: 1.0.5+105
environment: environment:
sdk: '>=3.5.0 <4.0.0' sdk: '>=3.5.0 <4.0.0'
@@ -16,15 +16,15 @@ dependencies:
sdk: flutter sdk: flutter
intl: any intl: any
window_manager: ^0.4.3 window_manager: ^0.4.3
sqlite3: any sqlite3: ^2.4.7
sqlite3_flutter_libs: any sqlite3_flutter_libs: any
flutter_qjs: flutter_qjs:
git: git:
url: https://github.com/wgh136/flutter_qjs url: https://github.com/wgh136/flutter_qjs
ref: ade0b9d ref: ade0b9d
crypto: any crypto: ^3.0.6
dio: any dio: ^5.7.0
html: any html: ^0.15.5
pointycastle: any pointycastle: any
url_launcher: ^6.3.0 url_launcher: ^6.3.0
path: ^1.9.0 path: ^1.9.0
@@ -63,6 +63,7 @@ dependencies:
git: git:
url: https://github.com/wgh136/webdav_client url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
battery_plus: ^6.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -1,44 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DesktopWebviewWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterQjsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -1,37 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
desktop_webview_window
file_selector_windows
flutter_inappwebview_windows
flutter_qjs
permission_handler_windows
screen_retriever_windows
share_plus
sqlite3_flutter_libs
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -123,6 +123,26 @@ Win32Window::~Win32Window() {
bool Win32Window::Create(const std::wstring& title, bool Win32Window::Create(const std::wstring& title,
const Point& origin, const Point& origin,
const Size& size) { const Size& size) {
HWND hwnd = ::FindWindow(kWindowClassName, title.c_str());
if (hwnd) {
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
GetWindowPlacement(hwnd, &place);
SetForegroundWindow(hwnd);
switch (place.showCmd) {
case SW_SHOWMAXIMIZED:
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
break;
case SW_SHOWMINIMIZED:
ShowWindow(hwnd, SW_RESTORE);
break;
default:
ShowWindow(hwnd, SW_NORMAL);
break;
}
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
return false;
}
Destroy(); Destroy();
const wchar_t* window_class = const wchar_t* window_class =