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

@@ -581,10 +581,13 @@ class SliverGridComics extends StatefulWidget {
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;
@@ -638,6 +641,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,
@@ -653,10 +657,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;
@@ -674,11 +681,37 @@ class _SliverGridComics extends StatelessWidget {
onLastItemBuild?.call();
}
var badge = badgeBuilder?.call(comics[index]);
return ComicTile(
comic: comics[index],
badge: badge,
menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null ? () => onTap!(comics[index]) : null,
return Stack(
children: [
ComicTile(
comic: comics[index],
badge: badge,
menuOptions: menuBuilder?.call(comics[index]),
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,

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"],
@@ -296,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();
@@ -309,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();
@@ -362,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("""
@@ -431,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);
@@ -443,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('"')) {
@@ -598,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

@@ -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);
}
}
@@ -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

@@ -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;