48 Commits

Author SHA1 Message Date
dd7154830b fix potential network issue 2024-11-13 19:28:47 +08:00
nyne
194abb82de Merge pull request #36 from venera-app/dev
v1.0.5-patch
2024-11-13 18:57:10 +08:00
a8bc097541 Update windows build script 2024-11-13 18:56:22 +08:00
d34c7c3806 fix importing data on Android 2024-11-13 18:55:25 +08:00
nyne
926437b967 Merge pull request #34 from venera-app/dev
v1.0.5
2024-11-13 16:27:41 +08:00
nyne
856ad82c55 Merge branch 'master' into dev 2024-11-13 16:27:20 +08:00
81baf53ad4 Update version code 2024-11-13 16:21:13 +08:00
71b03d744a Add feature to download all comics in a folder 2024-11-13 16:20:42 +08:00
6f2bac52e4 Improve sharing image & saving image 2024-11-13 16:08:28 +08:00
9fcc306ee0 fix HtmlElement.parent 2024-11-13 13:12:04 +08:00
5d4e8f5b84 Add sorting folders feature 2024-11-13 12:44:51 +08:00
9bdcba1270 improve ui 2024-11-13 12:21:57 +08:00
8e99e94620 Add the feature for updating local favorites info 2024-11-13 08:57:37 +08:00
nyne
00bcbaa2eb Merge pull request #32 from pkuislm/dev
EhViewer数据导入&本地下载选择优化
2024-11-12 23:13:37 +08:00
pkuislm
acb9c47657 Improve selection button display on small screen devices. 2024-11-12 23:09:53 +08:00
1636c959d0 fix #33 2024-11-12 22:37:46 +08:00
pkuislm
4ff1140bf6 Add cancellation to ehviewer import. 2024-11-12 21:28:07 +08:00
pkuislm
057d6a2f54 Update translation. 2024-11-12 19:54:47 +08:00
pkuislm
601ef68ad3 Improve local comics selection logic. 2024-11-12 19:54:34 +08:00
pkuislm
c94438d7c4 Add EhViewer database import support. 2024-11-12 19:52:34 +08:00
pkuislm
5825f88e78 Allow custom creation time of favorite items, add LocalFavoritesManager.existsFolder function. 2024-11-12 19:50:53 +08:00
pkuislm
389403c11d Ignore files starting with a dot when fetching local comic images, and improve local comic delete logic. 2024-11-12 19:48:15 +08:00
pkuislm
abd9afad6b Fix local comic cover display logic. 2024-11-12 19:45:27 +08:00
pkuislm
5119beb1fe Fix battery forground color. 2024-11-12 19:44:05 +08:00
9b98075153 fix multiple setting pages and search pages 2024-11-12 17:51:20 +08:00
775ab471f5 fix subtitle 2024-11-12 17:49:02 +08:00
293040f374 fix subtitle 2024-11-12 17:43:37 +08:00
a427bcdf84 fix search action 2024-11-12 17:37:29 +08:00
c4f531a463 Exported data should contain cookies 2024-11-12 16:36:02 +08:00
nyne
6c076bfc7a Merge pull request #31 from pkuislm/dev
给阅读界面加个时钟和电池信息
2024-11-11 22:47:55 +08:00
pkuislm
93bf99daa5 Add option to hide time and battery info. 2024-11-11 22:40:46 +08:00
pkuislm
b3e95d7162 Fix widget blinking caused by future builder. 2024-11-11 22:13:03 +08:00
pkuislm
c35bf9fb7f Merge branch 'dev' of https://github.com/pkuislm/venera into dev 2024-11-11 22:10:32 +08:00
pkuislm
189dfe5a43 Fix battery update issue. 2024-11-11 22:08:13 +08:00
pkuislm
53b9bc79dd Fix battery update issue. 2024-11-11 21:58:44 +08:00
pkuislm
bc4e0f79a5 Added clock & battery widgets in reader. 2024-11-11 21:27:40 +08:00
05bbef0b8a fix #30 2024-11-11 18:43:32 +08:00
e1df69e785 [import data] proxy settings should be kept 2024-11-11 17:46:11 +08:00
a0e3cc720a add ImageLoadingConfig constructor 2024-11-11 17:36:42 +08:00
6ae3e50a5b improve network request 2024-11-11 17:18:56 +08:00
nyne
7cf55fcb8e add onLoadFailed to imageLoadingConfig 2024-11-11 15:01:31 +08:00
nyne
d875681c4b update gitignore 2024-11-11 14:23:24 +08:00
193ecdb765 improve data sync 2024-11-11 11:52:36 +08:00
ea3cc8cc58 [windows] prevent multiple instances 2024-11-11 10:58:48 +08:00
f8eace4c31 fix an issue where a deleted comic could not be displayed in a favorite folder. 2024-11-11 10:40:56 +08:00
db2c2395de fix importing data on windows 2024-11-11 10:35:21 +08:00
nyne
fe266dcade Merge pull request #26 from boa-z/translation-typo-fix
fix: translation typo
2024-11-11 00:06:19 +08:00
boa-z
ecb657d20d fix: translation typo 2024-11-10 23:26:21 +08:00
49 changed files with 1460 additions and 607 deletions

5
.gitignore vendored
View File

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

View File

@@ -10,7 +10,6 @@ import android.view.KeyEvent
import android.Manifest
import android.os.Environment
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
@@ -34,6 +33,8 @@ class MainActivity : FlutterActivity() {
private val storageRequestCode = 0x10
private var storagePermissionRequest: ((Boolean) -> Unit)? = null
private val selectFileCode = 0x11
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == pickDirectoryCode) {
@@ -59,6 +60,41 @@ class MainActivity : FlutterActivity() {
storagePermissionRequest?.invoke(Environment.isExternalStorageManager())
}
storagePermissionRequest = null
} else if (requestCode == selectFileCode) {
if (resultCode != Activity.RESULT_OK) {
result.success(null)
return
}
val uri = data?.data
if (uri == null) {
result.success(null)
return
}
val contentResolver = context.contentResolver
val file = DocumentFile.fromSingleUri(context, uri)
if (file == null) {
result.success(null)
return
}
val fileName = file.name
if (fileName == null) {
result.success(null)
return
}
// copy file to cache directory
val cacheDir = context.cacheDir
val newFile = File(cacheDir, fileName)
val inputStream = contentResolver.openInputStream(uri)
if (inputStream == null) {
result.success(null)
return
}
val outputStream = FileOutputStream(newFile)
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
// send file path to flutter
result.success(newFile.absolutePath)
}
}
@@ -112,6 +148,12 @@ class MainActivity : FlutterActivity() {
res.success(result)
}
}
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
selectFileChannel.setMethodCallHandler { _, res ->
openFile()
result = res
}
}
private fun getProxy(): String {
@@ -223,6 +265,13 @@ class MainActivity : FlutterActivity() {
storagePermissionRequest = null
}
}
fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, selectFileCode)
}
}
class VolumeListen{

View File

@@ -699,7 +699,7 @@ class HtmlElement {
doc: this.doc,
})
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 title {string}
* @param subtitle {string}
* @param subTitle {string} - equal to subtitle
* @param cover {string}
* @param tags {string[]}
* @param description {string}
@@ -859,10 +860,11 @@ let console = {
* @param stars {number?} - 0-5, double
* @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.title = title;
this.subtitle = subtitle;
this.subTitle = subTitle;
this.cover = cover;
this.tags = tags;
this.description = description;
@@ -940,6 +942,33 @@ function Comment({userName, avatar, content, time, replyCount, id, isLiked, scor
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 {
name = ""

View File

@@ -17,6 +17,7 @@
"Multiple Comics": "多个漫画",
"help": "帮助",
"Select": "选择",
"Selected @a comics": "已选择 @a 部漫画",
"Imported @a comics": "已导入 @a 部漫画",
"Downloading": "下载中",
"Back": "后退",
@@ -41,6 +42,7 @@
"Folder": "文件夹",
"Confirm": "确认",
"Are you sure you want to delete this comic?": "您确定要删除这部漫画吗?",
"Are you sure you want to delete @a selected comics?": "您确定要删除 @a 部漫画吗?",
"Add comic source": "添加漫画来源",
"Select file": "选择文件",
"View list": "查看列表",
@@ -141,7 +143,7 @@
"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. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
"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",
"Select a cbz file." : "选择一个cbz文件",
"A cbz file" : "一个cbz文件",
@@ -189,7 +191,25 @@
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"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": {
"Home": "首頁",
@@ -210,6 +230,7 @@
"Multiple Comics": "多部漫畫",
"help": "幫助",
"Select": "選擇",
"Selected @a comics": "已選擇 @a 部漫畫",
"Imported @a comics": "已匯入 @a 部漫畫",
"Downloading": "下載中",
"Back": "後退",
@@ -235,6 +256,7 @@
"Folder": "文件夾",
"Confirm": "確認",
"Are you sure you want to delete this comic?": "您確定要刪除這部漫畫嗎?",
"Are you sure you want to delete @a selected comics?": "您確定要刪除 @a 部漫畫嗎?",
"Add comic source": "添加漫畫來源",
"Select file": "選擇文件",
"View list": "查看列表",
@@ -333,7 +355,7 @@
"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. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
"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",
"Select a cbz file." : "選擇一個cbz文件",
"A cbz file" : "一個cbz文件",
@@ -381,6 +403,24 @@
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
"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
Widget build(BuildContext context) {
var padding = widget.padding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 4);
const EdgeInsets.symmetric(horizontal: 16);
var width = widget.width;
if (width != null) {
width = width - padding.horizontal;
@@ -206,6 +206,7 @@ class _ButtonState extends State<Button> {
padding: padding,
constraints: const BoxConstraints(
minWidth: 76,
minHeight: 32,
),
decoration: BoxDecoration(
color: buttonColor,

View File

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

View File

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

View File

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

View File

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

View File

@@ -118,6 +118,7 @@ class _Settings with ChangeNotifier {
'dataVersion': 0,
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true,
};
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/history.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
@@ -236,6 +237,7 @@ class ComicSource {
}
await file.writeAsString(jsonEncode(data));
_isSaving = false;
DataSync().uploadData();
}
Future<bool> reLogin() async {

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import 'cached_image.dart' as image_provider;
class CachedImageProvider
extends BaseImageProvider<image_provider.CachedImageProvider> {
/// 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;
@@ -16,9 +16,11 @@ class CachedImageProvider
final String? sourceKey;
final String? cid;
@override
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(
cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes,
@@ -36,5 +38,5 @@ class CachedImageProvider
}
@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:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/download.dart';
import 'package:venera/pages/reader/reader.dart';
@@ -346,6 +347,10 @@ class LocalManager with ChangeNotifier {
comic.cover) {
continue;
}
//Hidden file in some file system
if(entity.name.startsWith('.')) {
continue;
}
files.add(entity);
}
}
@@ -360,10 +365,10 @@ class LocalManager with ChangeNotifier {
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);
if (comic == null) return false;
if (comic.chapters == null) return true;
if (comic.chapters == null || ep == null) return true;
return comic.downloadedChapters
.contains(comic.chapters!.keys.elementAt(ep-1));
}
@@ -439,9 +444,20 @@ class LocalManager with ChangeNotifier {
downloadingTasks.first.resume();
}
void deleteComic(LocalComic c) {
var dir = Directory(FilePath.join(path, c.directory));
dir.deleteIgnoreError(recursive: true);
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
if(removeFileOnDisk) {
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);
notifyListeners();
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -176,6 +177,8 @@ class AppDio with DioMixin {
return res;
}
static final Map<String, bool> _requests = {};
@override
Future<Response<T>> request<T>(
String path, {
@@ -186,6 +189,13 @@ class AppDio with DioMixin {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) 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();
if (_proxy != proxy) {
Log.info("Network", "Proxy changed to $proxy");
@@ -196,15 +206,22 @@ class AppDio with DioMixin {
: rhttp.ProxySettings.proxy(proxy!),
));
}
return super.request(
path,
data: data,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
try {
return super.request<T>(
path,
data: data,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
finally {
if(_requests.containsKey(path)) {
_requests.remove(path);
}
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
@@ -9,8 +10,9 @@ import 'app_dio.dart';
class ImageDownloader {
static Stream<ImageDownloadProgress> loadThumbnail(
String url, String? sourceKey) async* {
final cacheKey = "$url@$sourceKey";
String url, String? sourceKey,
[String? cid]) async* {
final cacheKey = "$url@$sourceKey${cid != null ? '@$cid' : ''}";
final cache = await CacheManager().findCache(cacheKey);
if (cache != null) {
@@ -33,6 +35,16 @@ class ImageDownloader {
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(
headers: Map<String, dynamic>.from(configs['headers']),
method: configs['method'] ?? 'GET',
@@ -57,8 +69,9 @@ class ImageDownloader {
}
}
if (configs['onResponse'] != null) {
buffer = configs['onResponse'](buffer);
if (configs['onResponse'] is JSInvokable) {
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
(configs['onResponse'] as JSInvokable).free();
}
await CacheManager().writeCache(cacheKey, buffer);
@@ -83,61 +96,98 @@ class ImageDownloader {
);
}
Future<Map<String, dynamic>?> Function()? onLoadFailed;
var configs = <String, dynamic>{};
if (sourceKey != null) {
var comicSource = ComicSource.find(sourceKey);
configs = (await comicSource!.getImageLoadingConfig
?.call(imageKey, cid, eid)) ?? {};
?.call(imageKey, cid, eid)) ??
{};
}
configs['headers'] ??= {
'user-agent': webUA,
};
var retryLimit = 5;
while (true) {
try {
configs['headers'] ??= {
'user-agent': webUA,
};
var dio = AppDio(BaseOptions(
headers: configs['headers'],
method: configs['method'] ?? 'GET',
responseType: ResponseType.stream,
));
if (configs['onLoadFailed'] is JSInvokable) {
onLoadFailed = () async {
dynamic result = (configs['onLoadFailed'] as JSInvokable)([]);
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,
data: configs['data']);
var stream = req.data?.stream ?? (throw "Error: Empty response body.");
int? expectedBytes = req.data!.contentLength;
if (expectedBytes == -1) {
expectedBytes = null;
}
var buffer = <int>[];
await for (var data in stream) {
buffer.addAll(data);
if (expectedBytes != null) {
var dio = AppDio(BaseOptions(
headers: configs['headers'],
method: configs['method'] ?? 'GET',
responseType: ResponseType.stream,
));
var req = await dio.request<ResponseBody>(configs['url'] ?? imageKey,
data: configs['data']);
var stream = req.data?.stream ?? (throw "Error: Empty response body.");
int? expectedBytes = req.data!.contentLength;
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(
currentBytes: buffer.length,
totalBytes: expectedBytes,
currentBytes: data.length,
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/services.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
@@ -1021,6 +1022,8 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
String? error;
bool isLoading = false;
@override
void didChangeDependencies() {
state = context.findAncestorStateOfType<_ComicPageState>()!;
@@ -1034,6 +1037,11 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
if (!isInitialLoading && next == null) {
return;
}
Future.microtask(() {
setState(() {
isLoading = true;
});
});
var res = await state.comicSource.loadComicThumbnail!(state.comic.id, next);
if (res.success) {
thumbnails.addAll(res.data);
@@ -1042,13 +1050,15 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
} else {
error = res.errorMessage;
}
setState(() {});
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return SliverMainAxisGroup(
slivers: [
return MultiSliver(
children: [
SliverToBoxAdapter(
child: ListTile(
title: Text("Preview".tl),
@@ -1148,7 +1158,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
],
),
)
else if (next != null || isInitialLoading)
else if (isLoading)
const SliverToBoxAdapter(
child: ListLoadingIndicator(),
),

View File

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

View File

@@ -34,12 +34,11 @@ Future<void> newFolder() async {
child: Text("Import from file".tl),
onPressed: () async {
var file = await selectFile(ext: ['json']);
if(file == null) return;
if (file == null) return;
var data = await file.readAsBytes();
try {
LocalFavoritesManager().fromJson(utf8.decode(data));
}
catch(e) {
} catch (e) {
context.showMessage(message: "Failed to import".tl);
return;
}
@@ -113,7 +112,9 @@ void addFavorite(Comic comic) {
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle ?? '',
type: ComicType((comic.sourceKey == 'local' ? 0 : comic.sourceKey.hashCode)),
type: ComicType((comic.sourceKey == 'local'
? 0
: comic.sourceKey.hashCode)),
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_type.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';

View File

@@ -105,7 +105,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
},
).then(
(value) {
if(mounted) {
if (mounted) {
setState(() {});
}
},
@@ -123,6 +123,45 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
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.tags,
"${e.time} | ${comicSource?.name ?? "Unknown"}",
comicSource?.key ?? "Unknown",
comicSource?.key ??
(e.type == ComicType.local ? "local" : "Unknown"),
null,
null,
),

View File

@@ -80,7 +80,6 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const SizedBox(width: 16),
Icon(
Icons.local_activity,
color: context.colorScheme.secondary,
@@ -88,27 +87,41 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
const SizedBox(width: 12),
Text("Local".tl),
const Spacer(),
IconButton(
icon: const Icon(Icons.search),
color: context.colorScheme.primary,
onPressed: () {
context.to(() => const LocalSearchPage());
},
MenuButton(
entries: [
MenuEntry(
icon: Icons.search,
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--;
@@ -219,13 +232,13 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
@override
void update() {
if(!mounted) return;
if (!mounted) return;
setState(() {});
}
@override
void updateFolders() {
if(!mounted) return;
if (!mounted) return;
setState(() {
folders = LocalFavoritesManager().folderNames;
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/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:sqlite3/sqlite3.dart' as sql;
import 'dart:math';
import 'local_comics_page.dart';
@@ -256,6 +258,7 @@ class _HistoryState extends State<_History> {
ImageProvider imageProvider = CachedImageProvider(
cover,
sourceKey: history[index].type.comicSource?.key,
cid: history[index].id,
);
if (!cover.isURL) {
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 directories.".tl,
"Select a cbz file.".tl,
"Select an EhViewer database and a download folder.".tl
][type];
List<String> importMethods = [
"Single Comic".tl,
"Multiple Comics".tl,
"A cbz file".tl,
"EhViewer downloads".tl
];
return ContentDialog(
dismissible: !loading,
@@ -513,36 +523,18 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 600),
RadioListTile(
title: Text("Single Comic".tl),
value: 0,
groupValue: type,
onChanged: (value) {
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;
});
},
),
...List.generate(importMethods.length, (index) {
return RadioListTile(
title: Text(importMethods[index]),
value: index,
groupValue: type,
onChanged: (value) {
setState(() {
type = value as int;
});
},
);
}),
ListTile(
title: Text("Add to favorites".tl),
trailing: Select(
@@ -576,7 +568,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.transparent,
barrierColor: Colors.black.withOpacity(0.2),
builder: (context) {
var help = '';
help +=
@@ -587,8 +579,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
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';
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;
help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl;
return ContentDialog(
title: "Help".tl,
content: Text(help).paddingHorizontal(16),
@@ -641,6 +634,135 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
}
controller.close();
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;
setState(() {

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.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/pages/downloading_page.dart';
import 'package:venera/utils/cbz.dart';
@@ -26,7 +27,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool multiSelectMode = false;
Map<LocalComic, bool> selectedComics = {};
Map<Comic, bool> selectedComics = {};
void update() {
if (keyword.isEmpty) {
@@ -99,7 +100,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
actions: [
FilledButton(
onPressed: () {
appdata.implicitData["local_sort"] =sortType.value;
appdata.implicitData["local_sort"] = sortType.value;
appdata.writeImplicitData();
Navigator.pop(context);
update();
@@ -115,7 +116,66 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
@override
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(
slivers: [
if (!searchMode && !multiSelectMode)
@@ -166,9 +226,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
)
else if (multiSelectMode)
SliverAppbar(
title: Text("Selected ${selectedComics.length} comics"),
actions: [
IconButton(
leading: Tooltip(
message: "Cancel".tl,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
@@ -177,10 +237,26 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
});
},
),
],
),
title: Text(
"Selected @c comics".tlParams({"c": selectedComics.length})),
actions: selectActions,
)
else if (searchMode)
SliverAppbar(
leading: Tooltip(
message: "Cancel".tl,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
),
title: TextField(
autofocus: true,
decoration: InputDecoration(
@@ -192,28 +268,17 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
update();
},
),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
],
),
SliverGridComics(
comics: comics,
selections: selectedComics,
onTap: multiSelectMode
? (c) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c as LocalComic);
selectedComics.remove(c);
} else {
selectedComics[c as LocalComic] = true;
selectedComics[c] = true;
}
});
}
@@ -226,23 +291,55 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
if (multiSelectMode) {
showConfirmDialog(
showDialog(
context: context,
title: "Delete".tl,
content: "Delete selected comics?".tl,
onConfirm: () {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(comic);
}
setState(() {
selectedComics.clear();
builder: (context) {
bool removeComicFile = true;
return StatefulBuilder(builder: (context, state) {
return ContentDialog(
title: "Delete".tl,
content: Column(
children: [
Text("Delete selected comics?".tl)
.paddingVertical(8),
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(
icon: Icons.outbox_outlined,
@@ -255,7 +352,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
try {
if (multiSelectMode) {
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 file.delete();
}
@@ -278,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,
),
],
onPageChanged: (i) {
setState(() {
index = i;
});
},
paneActions: [
if(index != 0)
PaneActionEntry(
icon: Icons.search,
label: "Search".tl,
onTap: () {
to(() => const SearchPage());
to(() => const SearchPage(), preventDuplicate: true);
},
),
PaneActionEntry(
icon: Icons.settings,
label: "Settings".tl,
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/local.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/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:venera/utils/volume.dart';
import 'package:window_manager/window_manager.dart';
import 'package:battery_plus/battery_plus.dart';
part 'scaffold.dart';
part 'images.dart';
@@ -122,6 +124,9 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
focusNode.dispose();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
stopVolumeEvent();
Future.microtask(() {
DataSync().onDataChanged();
});
super.dispose();
}
@@ -319,6 +324,8 @@ enum ReaderMode {
bool get isGallery => key.startsWith('gallery');
bool get isContinuous => key.startsWith('continuous');
const ReaderMode(this.key);
static ReaderMode fromKey(String key) {

View File

@@ -131,10 +131,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: widget.child,
),
buildPageInfoText(),
buildStatusInfo(),
AnimatedPositioned(
duration: const Duration(milliseconds: 180),
right: 16,
bottom: showFloatingButtonValue == 0 ? -58 : 16,
bottom: showFloatingButtonValue == 0 ? -58 : 36,
child: buildEpChangeButton(),
),
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() {
showSideBar(
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 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://")) {
return await File(imageKey.substring(7)).readAsBytes();
} else {
@@ -445,6 +529,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
void saveCurrentImage() async {
var data = await _getCurrentImageData();
if (data == null) {
return;
}
var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}";
saveFile(data: data, filename: filename);
@@ -452,6 +539,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
void share() async {
var data = await _getCurrentImageData();
if (data == null) {
return;
}
var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}";
Share.shareFile(
@@ -471,7 +561,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
App.rootContext.pop();
}
if (key == "enableTurnPageByVolumeKey") {
if(appdata.settings[key]) {
if (appdata.settings[key]) {
context.reader.handleVolumeEvent();
} else {
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 {
const _ChaptersView(this.reader);

View File

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

View File

@@ -71,12 +71,19 @@ class _ReaderSettingsState extends State<ReaderSettings> {
).toSliver(),
if(App.isAndroid)
_SwitchSetting(
title: 'Turn page by volume key'.tl,
title: 'Turn page by volume keys'.tl,
settingKey: 'enableTurnPageByVolumeKey',
onChanged: () {
widget.onChanged?.call('enableTurnPageByVolumeKey');
},
).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/cache_manager.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/local.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;
bool get enableTwoViews => context.width > changePoint;
bool get enableTwoViews => context.width > 720;
final categories = <String>[
"Explore",

View File

@@ -18,11 +18,11 @@ export 'package:flutter_inappwebview/flutter_inappwebview.dart'
extension WebviewExtension on InAppWebViewController {
Future<List<io.Cookie>?> getCookies(String url) async {
if(url.contains("https://")){
if (url.contains("https://")) {
url.replaceAll("https://", "");
}
if(url[url.length-1] == '/'){
url = url.substring(0, url.length-1);
if (url[url.length - 1] == '/') {
url = url.substring(0, url.length - 1);
}
CookieManager cookieManager = CookieManager.instance();
final cookies = await cookieManager.getCookies(url: WebUri(url));
@@ -89,29 +89,29 @@ class _AppWebviewState extends State<AppWebview> {
child: IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
MediaQuery.of(context).size.width,
0,
MediaQuery.of(context).size.width,
0),
items: [
PopupMenuItem(
child: Text("Open in browser".tl),
onTap: () async =>
launchUrlString((await controller?.getUrl())!.toString()),
),
PopupMenuItem(
child: Text("Copy link".tl),
onTap: () async => Clipboard.setData(ClipboardData(
text: (await controller?.getUrl())!.toString())),
),
PopupMenuItem(
child: Text("Reload".tl),
onTap: () => controller?.reload(),
),
]);
showMenuX(
context,
Offset(context.width, context.padding.top),
[
MenuEntry(
icon: Icons.open_in_browser,
text: "Open in browser".tl,
onClick: () async =>
launchUrlString((await controller?.getUrl())!.toString()),
),
MenuEntry(
icon: Icons.copy,
text: "Copy link".tl,
onClick: () async => Clipboard.setData(ClipboardData(
text: (await controller?.getUrl())!.toString())),
),
MenuEntry(
icon: Icons.refresh,
text: "Reload".tl,
onClick: () => 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/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/network/cookie_jar.dart';
import 'package:zip_flutter/zip_flutter.dart';
import 'io.dart';
@@ -15,7 +16,7 @@ Future<File> exportAppData() async {
var cacheFilePath = FilePath.join(App.cachePath, '$time.venera');
var cacheFile = File(cacheFilePath);
var dataPath = App.dataPath;
if(await cacheFile.exists()) {
if (await cacheFile.exists()) {
await cacheFile.delete();
}
await Isolate.run(() {
@@ -23,11 +24,14 @@ Future<File> exportAppData() async {
var historyFile = FilePath.join(dataPath, "history.db");
var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db");
var appdata = FilePath.join(dataPath, "appdata.json");
var cookies = FilePath.join(dataPath, "cookie.db");
zipFile.addFile("history.db", historyFile);
zipFile.addFile("local_favorite.db", localFavoriteFile);
zipFile.addFile("appdata.json", appdata);
for(var file in Directory(FilePath.join(dataPath, "comic_source")).listSync()) {
if(file is File) {
zipFile.addFile("cookie.db", cookies);
for (var file
in Directory(FilePath.join(dataPath, "comic_source")).listSync()) {
if (file is File) {
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 localFavoriteFile = cacheDir.joinFile("local_favorite.db");
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 version = data["settings"]["dataVersion"];
if(version is int && version <= appdata.settings["dataVersion"]) {
if (version is int && version <= appdata.settings["dataVersion"]) {
return;
}
}
if(await historyFile.exists()) {
if (await historyFile.exists()) {
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();
}
if(await localFavoriteFile.exists()) {
if (await localFavoriteFile.exists()) {
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();
}
if(await appdataFile.exists()) {
await appdataFile.copy(FilePath.join(App.dataPath, "appdata.json"));
appdata.init();
if (await appdataFile.exists()) {
// proxy settings should be kept
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");
if(Directory(comicSourceDir).existsSync()) {
for(var file in Directory(comicSourceDir).listSync()) {
if(file is File) {
if (Directory(comicSourceDir).existsSync()) {
for (var file in Directory(comicSourceDir).listSync()) {
if (file is File) {
var targetFile = FilePath.join(App.dataPath, "comic_source", file.name);
File(targetFile).deleteIfExistsSync();
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/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/app_dio.dart';
@@ -19,7 +18,6 @@ class DataSync with ChangeNotifier {
if (isEnabled) {
downloadData();
}
HistoryManager().addListener(onDataChanged);
LocalFavoritesManager().addListener(onDataChanged);
ComicSource.addListener(onDataChanged);
}
@@ -57,8 +55,9 @@ class DataSync with ChangeNotifier {
}
Future<Res<bool>> uploadData() async {
if(isDownloading) return const Res(true);
if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) {
while (isUploading) {
haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100));
}

View File

@@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:mime/mime.dart';
class FileType {
@@ -7,6 +5,14 @@ class FileType {
final String mime;
const FileType(this.ext, this.mime);
static FileType fromExtension(String ext) {
if(ext.startsWith('.')) {
ext = ext.substring(1);
}
var mime = lookupMimeType('no-file.$ext');
return FileType(".$ext", mime ?? 'application/octet-stream');
}
}
FileType detectFileType(List<int> data) {

View File

@@ -9,6 +9,7 @@ import 'package:venera/utils/ext.dart';
import 'package:path/path.dart' as p;
import 'package:share_plus/share_plus.dart' as s;
import 'package:file_selector/file_selector.dart' as file_selector;
import 'package:venera/utils/file_type.dart';
export 'dart:io';
export 'dart:typed_data';
@@ -45,6 +46,18 @@ extension FileSystemEntityExt on FileSystemEntity {
// 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 {
@@ -74,7 +87,7 @@ extension DirectoryExtension on Directory {
}
String sanitizeFileName(String fileName) {
if(fileName.endsWith('.')) {
if (fileName.endsWith('.')) {
fileName = fileName.substring(0, fileName.length - 1);
}
const maxLength = 255;
@@ -114,7 +127,8 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
}
}
Future<void> copyDirectoryIsolate(Directory source, Directory destination) async {
Future<void> copyDirectoryIsolate(
Directory source, Directory destination) async {
await Isolate.run(() {
copyDirectory(source, destination);
});
@@ -184,14 +198,32 @@ class IOSDirectoryPicker {
}
Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
var extensions = App.isMacOS || App.isIOS ? null : ext;
if (App.isAndroid) {
for (var e in ext) {
var fileType = FileType.fromExtension(e);
if (fileType.mime == "application/octet-stream") {
extensions = null;
break;
}
}
}
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
label: 'files',
extensions: App.isMacOS || App.isIOS ? null : ext,
extensions: extensions,
);
final file_selector.XFile? file = await file_selector.openFile(
acceptedTypeGroups: <file_selector.XTypeGroup>[typeGroup],
);
if (file == null) return null;
file_selector.XFile? file;
if (extensions == null && App.isAndroid) {
const selectFileChannel = MethodChannel("venera/select_file");
var filePath = await selectFileChannel.invokeMethod("selectFile");
if (filePath == null) return null;
file = file_selector.XFile(filePath);
} else {
file = await file_selector.openFile(
acceptedTypeGroups: <file_selector.XTypeGroup>[typeGroup],
);
if (file == null) return null;
}
if (!ext.contains(file.path.split(".").last)) {
App.rootContext.showMessage(message: "Invalid file type");
return null;

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"
source: hosted
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:
dependency: transitive
description:
@@ -109,10 +125,10 @@ packages:
dependency: "direct main"
description:
name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
csslib:
dependency: transitive
description:
@@ -121,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -404,10 +428,10 @@ packages:
dependency: "direct main"
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
version: "0.15.5"
http:
dependency: transitive
description:
@@ -593,54 +617,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -788,10 +764,10 @@ packages:
dependency: "direct main"
description:
name: sqlite3
sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb"
sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed
url: "https://pub.dev"
source: hosted
version: "2.4.6"
version: "2.4.7"
sqlite3_flutter_libs:
dependency: "direct main"
description:
@@ -848,6 +824,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
upower:
dependency: transitive
description:
name: upower
sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf
url: "https://pub.dev"
source: hosted
version: "0.7.0"
url_launcher:
dependency: "direct main"
description:

View File

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

View File

@@ -51,7 +51,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\desktop_webview_window_plu
Source: "{#RootPath}\build\windows\x64\runner\Release\WebView2Loader.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion

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,
const Point& origin,
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();
const wchar_t* window_class =