mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
@@ -141,7 +141,7 @@
|
|||||||
"1. The directory only contains image files." : "1. 目录只包含图片文件。",
|
"1. The directory only contains image files." : "1. 目录只包含图片文件。",
|
||||||
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
|
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
|
||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
|
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。",
|
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
|
||||||
"Export as cbz": "导出为cbz",
|
"Export as cbz": "导出为cbz",
|
||||||
"Select a cbz file." : "选择一个cbz文件",
|
"Select a cbz file." : "选择一个cbz文件",
|
||||||
"A cbz file" : "一个cbz文件",
|
"A cbz file" : "一个cbz文件",
|
||||||
@@ -190,7 +190,18 @@
|
|||||||
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
|
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
|
||||||
"Added": "已添加",
|
"Added": "已添加",
|
||||||
"Turn page by volume keys": "使用音量键翻页",
|
"Turn page by volume keys": "使用音量键翻页",
|
||||||
"Display time & battery info in reader":"在阅读器中显示时间和电量信息"
|
"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": "区间选择"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -334,7 +345,7 @@
|
|||||||
"1. The directory only contains image files." : "1. 目錄只包含圖片文件。",
|
"1. The directory only contains image files." : "1. 目錄只包含圖片文件。",
|
||||||
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
|
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
|
||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
|
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。",
|
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
|
||||||
"Export as cbz": "匯出為cbz",
|
"Export as cbz": "匯出為cbz",
|
||||||
"Select a cbz file." : "選擇一個cbz文件",
|
"Select a cbz file." : "選擇一個cbz文件",
|
||||||
"A cbz file" : "一個cbz文件",
|
"A cbz file" : "一個cbz文件",
|
||||||
@@ -383,6 +394,17 @@
|
|||||||
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
|
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
|
||||||
"Added": "已添加",
|
"Added": "已添加",
|
||||||
"Turn page by volume keys": "使用音量鍵翻頁",
|
"Turn page by volume keys": "使用音量鍵翻頁",
|
||||||
"Display time & battery info in reader":"在閱讀器中顯示時間和電量信息"
|
"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": "區間選擇"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -581,10 +581,13 @@ class SliverGridComics extends StatefulWidget {
|
|||||||
this.badgeBuilder,
|
this.badgeBuilder,
|
||||||
this.menuBuilder,
|
this.menuBuilder,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.selections
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Comic> comics;
|
final List<Comic> comics;
|
||||||
|
|
||||||
|
final Map<Comic, bool>? selections;
|
||||||
|
|
||||||
final void Function()? onLastItemBuild;
|
final void Function()? onLastItemBuild;
|
||||||
|
|
||||||
final String? Function(Comic)? badgeBuilder;
|
final String? Function(Comic)? badgeBuilder;
|
||||||
@@ -638,6 +641,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _SliverGridComics(
|
return _SliverGridComics(
|
||||||
comics: comics,
|
comics: comics,
|
||||||
|
selection: widget.selections,
|
||||||
onLastItemBuild: widget.onLastItemBuild,
|
onLastItemBuild: widget.onLastItemBuild,
|
||||||
badgeBuilder: widget.badgeBuilder,
|
badgeBuilder: widget.badgeBuilder,
|
||||||
menuBuilder: widget.menuBuilder,
|
menuBuilder: widget.menuBuilder,
|
||||||
@@ -653,10 +657,13 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
this.badgeBuilder,
|
this.badgeBuilder,
|
||||||
this.menuBuilder,
|
this.menuBuilder,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.selection,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Comic> comics;
|
final List<Comic> comics;
|
||||||
|
|
||||||
|
final Map<Comic, bool>? selection;
|
||||||
|
|
||||||
final void Function()? onLastItemBuild;
|
final void Function()? onLastItemBuild;
|
||||||
|
|
||||||
final String? Function(Comic)? badgeBuilder;
|
final String? Function(Comic)? badgeBuilder;
|
||||||
@@ -674,11 +681,37 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
onLastItemBuild?.call();
|
onLastItemBuild?.call();
|
||||||
}
|
}
|
||||||
var badge = badgeBuilder?.call(comics[index]);
|
var badge = badgeBuilder?.call(comics[index]);
|
||||||
return ComicTile(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
ComicTile(
|
||||||
comic: comics[index],
|
comic: comics[index],
|
||||||
badge: badge,
|
badge: badge,
|
||||||
menuOptions: menuBuilder?.call(comics[index]),
|
menuOptions: menuBuilder?.call(comics[index]),
|
||||||
onTap: onTap != null ? () => onTap!(comics[index]) : null,
|
onTap: onTap != null ? () => onTap!(comics[index]) : null,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
right: 8,
|
||||||
|
child: Visibility(
|
||||||
|
visible: selection == null ? false : selection![comics[index]] ?? false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Transform.scale(
|
||||||
|
scale: 0.9,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.circle_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: comics.length,
|
childCount: comics.length,
|
||||||
|
@@ -11,8 +11,8 @@ import 'app.dart';
|
|||||||
import 'comic_source/comic_source.dart';
|
import 'comic_source/comic_source.dart';
|
||||||
import 'comic_type.dart';
|
import 'comic_type.dart';
|
||||||
|
|
||||||
String _getCurTime() {
|
String _getTimeString(DateTime time) {
|
||||||
return DateTime.now()
|
return time
|
||||||
.toIso8601String()
|
.toIso8601String()
|
||||||
.replaceFirst("T", " ")
|
.replaceFirst("T", " ")
|
||||||
.substring(0, 19);
|
.substring(0, 19);
|
||||||
@@ -27,7 +27,7 @@ class FavoriteItem implements Comic {
|
|||||||
@override
|
@override
|
||||||
String id;
|
String id;
|
||||||
String coverPath;
|
String coverPath;
|
||||||
String time = _getCurTime();
|
late String time;
|
||||||
|
|
||||||
FavoriteItem({
|
FavoriteItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -36,7 +36,11 @@ class FavoriteItem implements Comic {
|
|||||||
required this.author,
|
required this.author,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.tags,
|
required this.tags,
|
||||||
});
|
DateTime? favoriteTime
|
||||||
|
}) {
|
||||||
|
var t = favoriteTime ?? DateTime.now();
|
||||||
|
time = _getTimeString(t);
|
||||||
|
}
|
||||||
|
|
||||||
FavoriteItem.fromRow(Row row)
|
FavoriteItem.fromRow(Row row)
|
||||||
: name = row["name"],
|
: name = row["name"],
|
||||||
@@ -296,12 +300,16 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool existsFolder(String name) {
|
||||||
|
return folderNames.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// create a folder
|
/// create a folder
|
||||||
String createFolder(String name, [bool renameWhenInvalidName = false]) {
|
String createFolder(String name, [bool renameWhenInvalidName = false]) {
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
if (renameWhenInvalidName) {
|
if (renameWhenInvalidName) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (folderNames.contains(i.toString())) {
|
while (existsFolder(i.toString())) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
name = i.toString();
|
name = i.toString();
|
||||||
@@ -309,11 +317,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
throw "name is empty!";
|
throw "name is empty!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (folderNames.contains(name)) {
|
if (existsFolder(name)) {
|
||||||
if (renameWhenInvalidName) {
|
if (renameWhenInvalidName) {
|
||||||
var prevName = name;
|
var prevName = name;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (folderNames.contains(i.toString())) {
|
while (existsFolder(i.toString())) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
name = prevName + i.toString();
|
name = prevName + i.toString();
|
||||||
@@ -362,7 +370,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
/// This method will download cover to local, to avoid problems like changing url
|
/// This method will download cover to local, to avoid problems like changing url
|
||||||
void addComic(String folder, FavoriteItem comic, [int? order]) async {
|
void addComic(String folder, FavoriteItem comic, [int? order]) async {
|
||||||
_modifiedAfterLastCache = true;
|
_modifiedAfterLastCache = true;
|
||||||
if (!folderNames.contains(folder)) {
|
if (!existsFolder(folder)) {
|
||||||
throw Exception("Folder does not exists");
|
throw Exception("Folder does not exists");
|
||||||
}
|
}
|
||||||
var res = _db.select("""
|
var res = _db.select("""
|
||||||
@@ -431,7 +439,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reorder(List<FavoriteItem> newFolder, String folder) async {
|
void reorder(List<FavoriteItem> newFolder, String folder) async {
|
||||||
if (!folderNames.contains(folder)) {
|
if (!existsFolder(folder)) {
|
||||||
throw Exception("Failed to reorder: folder not found");
|
throw Exception("Failed to reorder: folder not found");
|
||||||
}
|
}
|
||||||
deleteFolder(folder);
|
deleteFolder(folder);
|
||||||
@@ -443,7 +451,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rename(String before, String after) {
|
void rename(String before, String after) {
|
||||||
if (folderNames.contains(after)) {
|
if (existsFolder(after)) {
|
||||||
throw "Name already exists!";
|
throw "Name already exists!";
|
||||||
}
|
}
|
||||||
if (after.contains('"')) {
|
if (after.contains('"')) {
|
||||||
@@ -598,9 +606,9 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
if (folder == null || folder is! String) {
|
if (folder == null || folder is! String) {
|
||||||
throw "Invalid data";
|
throw "Invalid data";
|
||||||
}
|
}
|
||||||
if (folderNames.contains(folder)) {
|
if (existsFolder(folder)) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (folderNames.contains("$folder($i)")) {
|
while (existsFolder("$folder($i)")) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
folder = "$folder($i)";
|
folder = "$folder($i)";
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
@@ -346,6 +347,10 @@ class LocalManager with ChangeNotifier {
|
|||||||
comic.cover) {
|
comic.cover) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
//Hidden file in some file system
|
||||||
|
if(entity.name.startsWith('.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
files.add(entity);
|
files.add(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,9 +444,20 @@ class LocalManager with ChangeNotifier {
|
|||||||
downloadingTasks.first.resume();
|
downloadingTasks.first.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteComic(LocalComic c) {
|
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
||||||
|
if(removeFileOnDisk) {
|
||||||
var dir = Directory(FilePath.join(path, c.directory));
|
var dir = Directory(FilePath.join(path, c.directory));
|
||||||
dir.deleteIgnoreError(recursive: true);
|
dir.deleteIgnoreError(recursive: true);
|
||||||
|
}
|
||||||
|
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
||||||
|
if(HistoryManager().findSync(c.id, c.comicType) != null) {
|
||||||
|
HistoryManager().remove(c.id, c.comicType);
|
||||||
|
}
|
||||||
|
assert(c.comicType == ComicType.local);
|
||||||
|
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
||||||
|
for (var f in folders) {
|
||||||
|
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
|
||||||
|
}
|
||||||
remove(c.id, c.comicType);
|
remove(c.id, c.comicType);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
@@ -207,7 +207,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
e.author,
|
e.author,
|
||||||
e.tags,
|
e.tags,
|
||||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||||
comicSource?.key ?? "Unknown",
|
comicSource?.key ?? (e.type == ComicType.local ? "local" : "Unknown"),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
@@ -22,6 +22,8 @@ import 'package:venera/utils/data_sync.dart';
|
|||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
import 'package:sqlite3/sqlite3.dart' as sql;
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'local_comics_page.dart';
|
import 'local_comics_page.dart';
|
||||||
|
|
||||||
@@ -495,7 +497,14 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
"Select a directory which contains the comic files.".tl,
|
"Select a directory which contains the comic files.".tl,
|
||||||
"Select a directory which contains the comic directories.".tl,
|
"Select a directory which contains the comic directories.".tl,
|
||||||
"Select a cbz file.".tl,
|
"Select a cbz file.".tl,
|
||||||
|
"Select an EhViewer database and a download folder.".tl
|
||||||
][type];
|
][type];
|
||||||
|
List<String> importMethods = [
|
||||||
|
"Single Comic".tl,
|
||||||
|
"Multiple Comics".tl,
|
||||||
|
"A cbz file".tl,
|
||||||
|
"EhViewer downloads".tl
|
||||||
|
];
|
||||||
|
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
dismissible: !loading,
|
dismissible: !loading,
|
||||||
@@ -513,36 +522,18 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 600),
|
const SizedBox(width: 600),
|
||||||
RadioListTile(
|
...List.generate(importMethods.length, (index) {
|
||||||
title: Text("Single Comic".tl),
|
return RadioListTile(
|
||||||
value: 0,
|
title: Text(importMethods[index]),
|
||||||
|
value: index,
|
||||||
groupValue: type,
|
groupValue: type,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
type = value as int;
|
type = value as int;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
RadioListTile(
|
}),
|
||||||
title: Text("Multiple Comics".tl),
|
|
||||||
value: 1,
|
|
||||||
groupValue: type,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
type = value as int;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RadioListTile(
|
|
||||||
title: Text("A cbz file".tl),
|
|
||||||
value: 2,
|
|
||||||
groupValue: type,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
type = value as int;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Add to favorites".tl),
|
title: Text("Add to favorites".tl),
|
||||||
trailing: Select(
|
trailing: Select(
|
||||||
@@ -587,8 +578,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
help +=
|
help +=
|
||||||
'${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n';
|
'${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n';
|
||||||
help +=
|
help +=
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles."
|
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
|
||||||
.tl;
|
.tl;
|
||||||
|
help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl;
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(help).paddingHorizontal(16),
|
content: Text(help).paddingHorizontal(16),
|
||||||
@@ -641,6 +633,135 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
return;
|
return;
|
||||||
|
} else if (type == 3) {
|
||||||
|
var dbFile = await selectFile(ext: ['db']);
|
||||||
|
final picker = DirectoryPicker();
|
||||||
|
final comicSrc = await picker.pickDirectory();
|
||||||
|
if (dbFile == null || comicSrc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cancelled = false;
|
||||||
|
var controller = showLoadingDialog(context, onCancel: () { cancelled = true; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
var cache = FilePath.join(App.cachePath, dbFile.name);
|
||||||
|
await dbFile.saveTo(cache);
|
||||||
|
var db = sql.sqlite3.open(cache);
|
||||||
|
|
||||||
|
Future<void> addTagComics(String destFolder, List<sql.Row> comics) async {
|
||||||
|
for(var comic in comics) {
|
||||||
|
if(cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var comicDir = Directory(FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
||||||
|
if(!(await comicDir.exists())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
||||||
|
String title = titleJP == "" ? comic['TITLE'] as String : titleJP;
|
||||||
|
if (LocalManager().findByName(title) != null) {
|
||||||
|
Log.info("Import Comic", "Comic already exists: $title");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String coverURL = await comicDir.joinFile(".thumb").exists() ?
|
||||||
|
comicDir.joinFile(".thumb").path :
|
||||||
|
(comic['THUMB'] as String).replaceAll('s.exhentai.org', 'ehgt.org');
|
||||||
|
int downloadedTimeStamp = comic['TIME'] as int;
|
||||||
|
DateTime downloadedTime =
|
||||||
|
downloadedTimeStamp != 0 ?
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(downloadedTimeStamp) : DateTime.now();
|
||||||
|
var comicObj = LocalComic(
|
||||||
|
id: LocalManager().findValidId(ComicType.local),
|
||||||
|
title: title,
|
||||||
|
subtitle: '',
|
||||||
|
tags: [
|
||||||
|
//1 >> x
|
||||||
|
[
|
||||||
|
"MISC",
|
||||||
|
"DOUJINSHI",
|
||||||
|
"MANGA",
|
||||||
|
"ARTISTCG",
|
||||||
|
"GAMECG",
|
||||||
|
"IMAGE SET",
|
||||||
|
"COSPLAY",
|
||||||
|
"ASIAN PORN",
|
||||||
|
"NON-H",
|
||||||
|
"WESTERN",
|
||||||
|
][(log(comic['CATEGORY'] as int) / ln2).floor()]
|
||||||
|
],
|
||||||
|
directory: comicDir.path,
|
||||||
|
chapters: null,
|
||||||
|
cover: coverURL,
|
||||||
|
comicType: ComicType.local,
|
||||||
|
downloadedChapters: [],
|
||||||
|
createdAt: downloadedTime,
|
||||||
|
);
|
||||||
|
LocalManager().add(comicObj, comicObj.id);
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
destFolder,
|
||||||
|
FavoriteItem(
|
||||||
|
id: comicObj.id,
|
||||||
|
name: comicObj.title,
|
||||||
|
coverPath: comicObj.cover,
|
||||||
|
author: comicObj.subtitle,
|
||||||
|
type: comicObj.comicType,
|
||||||
|
tags: comicObj.tags,
|
||||||
|
favoriteTime: downloadedTime
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//default folder
|
||||||
|
{
|
||||||
|
var defaultFolderName = '(EhViewer)Default'.tl;
|
||||||
|
if(!LocalFavoritesManager().existsFolder(defaultFolderName)) {
|
||||||
|
LocalFavoritesManager().createFolder(defaultFolderName);
|
||||||
|
}
|
||||||
|
var comicList = db.select("""
|
||||||
|
SELECT *
|
||||||
|
FROM DOWNLOAD_DIRNAME DN
|
||||||
|
LEFT JOIN DOWNLOADS DL
|
||||||
|
ON DL.GID = DN.GID
|
||||||
|
WHERE DL.LABEL IS NULL AND DL.STATE = 3
|
||||||
|
ORDER BY DL.TIME DESC
|
||||||
|
""").toList();
|
||||||
|
await addTagComics(defaultFolderName, comicList);
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = db.select("""
|
||||||
|
SELECT * FROM DOWNLOAD_LABELS;
|
||||||
|
""");
|
||||||
|
|
||||||
|
for (var folder in folders) {
|
||||||
|
if(cancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var label = folder["LABEL"] as String;
|
||||||
|
var folderName = '(EhViewer)$label';
|
||||||
|
if(!LocalFavoritesManager().existsFolder(folderName)) {
|
||||||
|
LocalFavoritesManager().createFolder(folderName);
|
||||||
|
}
|
||||||
|
var comicList = db.select("""
|
||||||
|
SELECT *
|
||||||
|
FROM DOWNLOAD_DIRNAME DN
|
||||||
|
LEFT JOIN DOWNLOADS DL
|
||||||
|
ON DL.GID = DN.GID
|
||||||
|
WHERE DL.LABEL = ? AND DL.STATE = 3
|
||||||
|
ORDER BY DL.TIME DESC
|
||||||
|
""", [label]).toList();
|
||||||
|
await addTagComics(folderName, comicList);
|
||||||
|
}
|
||||||
|
db.dispose();
|
||||||
|
await File(cache).deleteIgnoreError();
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Import Comic", e.toString(), s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
height = key.currentContext!.size!.height;
|
height = key.currentContext!.size!.height;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
import 'package:venera/utils/cbz.dart';
|
import 'package:venera/utils/cbz.dart';
|
||||||
@@ -26,7 +27,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
|
|
||||||
bool multiSelectMode = false;
|
bool multiSelectMode = false;
|
||||||
|
|
||||||
Map<LocalComic, bool> selectedComics = {};
|
Map<Comic, bool> selectedComics = {};
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
if (keyword.isEmpty) {
|
if (keyword.isEmpty) {
|
||||||
@@ -115,6 +116,106 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final bool isScreenSmall = screenWidth < 500.0;
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
if(isScreenSmall) {
|
||||||
|
selectActions.add(
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showMenu(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(screenWidth, App.isMobile ? 64 : 96, 0, 0),
|
||||||
|
items: <PopupMenuEntry>[
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: selectAll,
|
||||||
|
child: Text("Select All".tl),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: deSelect,
|
||||||
|
child: Text("Deselect".tl),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: invertSelection,
|
||||||
|
child: Text("Invert Selection".tl),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: selectRange,
|
||||||
|
child: Text("Select in range".tl),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.list
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
selectActions = [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.check_box_rounded),
|
||||||
|
tooltip: "Select All".tl,
|
||||||
|
onPressed: selectAll
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.check_box_outline_blank_outlined),
|
||||||
|
tooltip: "Deselect".tl,
|
||||||
|
onPressed: deSelect
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.check_box_outlined),
|
||||||
|
tooltip: "Invert Selection".tl,
|
||||||
|
onPressed: invertSelection
|
||||||
|
),
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.indeterminate_check_box_rounded),
|
||||||
|
tooltip: "Select in range".tl,
|
||||||
|
onPressed: selectRange
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SmoothCustomScrollView(
|
body: SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -166,10 +267,12 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
)
|
)
|
||||||
else if (multiSelectMode)
|
else if (multiSelectMode)
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
title: Text("Selected ${selectedComics.length} comics"),
|
title: Text("Selected @c comics".tlParams({"c": selectedComics.length})),
|
||||||
actions: [
|
actions: [
|
||||||
|
...selectActions,
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: "Exit Multi-Select".tl,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
multiSelectMode = false;
|
multiSelectMode = false;
|
||||||
@@ -177,6 +280,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else if (searchMode)
|
else if (searchMode)
|
||||||
@@ -207,13 +311,14 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
),
|
),
|
||||||
SliverGridComics(
|
SliverGridComics(
|
||||||
comics: comics,
|
comics: comics,
|
||||||
|
selections: selectedComics,
|
||||||
onTap: multiSelectMode
|
onTap: multiSelectMode
|
||||||
? (c) {
|
? (c) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as LocalComic)) {
|
if (selectedComics.containsKey(c as LocalComic)) {
|
||||||
selectedComics.remove(c as LocalComic);
|
selectedComics.remove(c);
|
||||||
} else {
|
} else {
|
||||||
selectedComics[c as LocalComic] = true;
|
selectedComics[c] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -226,23 +331,54 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
text: "Delete".tl,
|
text: "Delete".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
if (multiSelectMode) {
|
showDialog(
|
||||||
showConfirmDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
bool removeComicFile = true;
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, state) {
|
||||||
|
return ContentDialog(
|
||||||
title: "Delete".tl,
|
title: "Delete".tl,
|
||||||
content: "Delete selected comics?".tl,
|
content: Column(
|
||||||
onConfirm: () {
|
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) {
|
for (var comic in selectedComics.keys) {
|
||||||
LocalManager().deleteComic(comic);
|
LocalManager().deleteComic(comic as LocalComic, removeComicFile);
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedComics.clear();
|
selectedComics.clear();
|
||||||
});
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
LocalManager().deleteComic(c as LocalComic);
|
LocalManager().deleteComic(c as LocalComic, removeComicFile);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
child: Text("Confirm".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.outbox_outlined,
|
icon: Icons.outbox_outlined,
|
||||||
@@ -255,7 +391,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
try {
|
try {
|
||||||
if (multiSelectMode) {
|
if (multiSelectMode) {
|
||||||
for (var comic in selectedComics.keys) {
|
for (var comic in selectedComics.keys) {
|
||||||
var file = await CBZ.export(comic);
|
var file = await CBZ.export(comic as LocalComic);
|
||||||
await saveFile(filename: file.name, file: file);
|
await saveFile(filename: file.name, file: file);
|
||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
|
@@ -650,7 +650,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
|
|||||||
|
|
||||||
Widget _batteryInfo(int batteryLevel) {
|
Widget _batteryInfo(int batteryLevel) {
|
||||||
IconData batteryIcon;
|
IconData batteryIcon;
|
||||||
Color batteryColor = Colors.black;
|
Color batteryColor = context.colorScheme.onSurface;
|
||||||
|
|
||||||
if (batteryLevel >= 96) {
|
if (batteryLevel >= 96) {
|
||||||
batteryIcon = Icons.battery_full_sharp;
|
batteryIcon = Icons.battery_full_sharp;
|
||||||
|
Reference in New Issue
Block a user