Merge pull request #32 from pkuislm/dev

EhViewer数据导入&本地下载选择优化
This commit is contained in:
nyne
2024-11-12 23:13:37 +08:00
committed by GitHub
8 changed files with 423 additions and 87 deletions

View File

@@ -207,7 +207,7 @@ 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

@@ -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';
@@ -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 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 +522,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(
@@ -587,8 +578,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 +633,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) {
@@ -115,6 +116,106 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
@override
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(
body: SmoothCustomScrollView(
slivers: [
@@ -166,17 +267,20 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
)
else if (multiSelectMode)
SliverAppbar(
title: Text("Selected ${selectedComics.length} comics"),
title: Text("Selected @c comics".tlParams({"c": selectedComics.length})),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
},
),
...selectActions,
IconButton(
icon: const Icon(Icons.close),
tooltip: "Exit Multi-Select".tl,
onPressed: () {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
},
),
],
)
else if (searchMode)
@@ -207,13 +311,14 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
),
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 +331,54 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
if (multiSelectMode) {
showConfirmDialog(
context: context,
title: "Delete".tl,
content: "Delete selected comics?".tl,
onConfirm: () {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(comic);
showDialog(
context: context,
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),
),
],
);
}
setState(() {
selectedComics.clear();
});
},
);
} else {
LocalManager().deleteComic(c as LocalComic);
}
);
}
);
}),
MenuEntry(
icon: Icons.outbox_outlined,
@@ -255,7 +391,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();
}

View File

@@ -650,7 +650,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
Widget _batteryInfo(int batteryLevel) {
IconData batteryIcon;
Color batteryColor = Colors.black;
Color batteryColor = context.colorScheme.onSurface;
if (batteryLevel >= 96) {
batteryIcon = Icons.battery_full_sharp;