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

@@ -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": "區間選擇"
} }
} }

View File

@@ -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(
comic: comics[index], children: [
badge: badge, ComicTile(
menuOptions: menuBuilder?.call(comics[index]), comic: comics[index],
onTap: onTap != null ? () => onTap!(comics[index]) : null, 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, childCount: comics.length,

View File

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

View File

@@ -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]) {
var dir = Directory(FilePath.join(path, c.directory)); if(removeFileOnDisk) {
dir.deleteIgnoreError(recursive: true); 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); remove(c.id, c.comicType);
notifyListeners(); notifyListeners();
} }

View File

@@ -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,
), ),

View File

@@ -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]),
groupValue: type, value: index,
onChanged: (value) { groupValue: type,
setState(() { onChanged: (value) {
type = value as int; 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;
});
},
),
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(() {

View File

@@ -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,17 +267,20 @@ 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: [
IconButton( ...selectActions,
icon: const Icon(Icons.close), IconButton(
onPressed: () { icon: const Icon(Icons.close),
setState(() { tooltip: "Exit Multi-Select".tl,
multiSelectMode = false; onPressed: () {
selectedComics.clear(); setState(() {
}); multiSelectMode = false;
}, selectedComics.clear();
), });
},
),
], ],
) )
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) {
title: "Delete".tl, bool removeComicFile = true;
content: "Delete selected comics?".tl, return StatefulBuilder(
onConfirm: () { builder: (context, state) {
for (var comic in selectedComics.keys) { return ContentDialog(
LocalManager().deleteComic(comic); 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( 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();
} }

View File

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