mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 00:07:24 +00:00
@@ -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,
|
||||
),
|
||||
|
@@ -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(() {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user