mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
@@ -141,7 +141,7 @@
|
||||
"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. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
|
||||
"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",
|
||||
"Select a cbz file." : "选择一个cbz文件",
|
||||
"A cbz file" : "一个cbz文件",
|
||||
@@ -190,7 +190,18 @@
|
||||
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
|
||||
"Added": "已添加",
|
||||
"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": {
|
||||
"Home": "首頁",
|
||||
@@ -334,7 +345,7 @@
|
||||
"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. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
|
||||
"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",
|
||||
"Select a cbz file." : "選擇一個cbz文件",
|
||||
"A cbz file" : "一個cbz文件",
|
||||
@@ -383,6 +394,17 @@
|
||||
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
|
||||
"Added": "已添加",
|
||||
"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.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,
|
||||
|
@@ -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)";
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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