From 5119beb1feb272983de3d78f040fbc8a9e5d450c Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:44:05 +0800 Subject: [PATCH 1/9] Fix battery forground color. --- lib/pages/reader/scaffold.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 265fc11..514351f 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -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; From abd9afad6badf49ddf52bc9f7a3815aa5f9d401b Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:45:27 +0800 Subject: [PATCH 2/9] Fix local comic cover display logic. --- lib/pages/favorites/local_favorites_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 62230c8..7d87726 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -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, ), From 389403c11dffd94af8fe9a4dbf8ac7b57b436d57 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:48:15 +0800 Subject: [PATCH 3/9] Ignore files starting with a dot when fetching local comic images, and improve local comic delete logic. --- lib/foundation/local.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 8876b84..cc2fde8 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -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(); } From 5825f88e78303debe1e041c67d275bf463fcc028 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:50:53 +0800 Subject: [PATCH 4/9] Allow custom creation time of favorite items, add LocalFavoritesManager.existsFolder function. --- lib/foundation/favorites.dart | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index a2aff14..3482601 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -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 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)"; From c94438d7c4405fd2901cc81d0bfc4550a5afb82a Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:52:34 +0800 Subject: [PATCH 5/9] Add EhViewer database import support. --- lib/pages/home_page.dart | 175 ++++++++++++++++++++++++++++++++------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index dc22b35..427c709 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -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 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,127 @@ 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; + } + var controller = showLoadingDialog(context, allowCancel: false); + + try { + var cache = FilePath.join(App.cachePath, dbFile.name); + await dbFile.saveTo(cache); + var db = sql.sqlite3.open(cache); + + Future addTagComics(String destFolder, List comics) async { + for(var comic in comics) { + 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) { + 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(() { From 601ef68ad36fea03297392c78031004495646ca6 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:54:34 +0800 Subject: [PATCH 6/9] Improve local comics selection logic. --- lib/components/comic.dart | 43 ++++++++-- lib/pages/local_comics_page.dart | 132 ++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 525bcaf..430d96f 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -581,10 +581,13 @@ class SliverGridComics extends StatefulWidget { this.badgeBuilder, this.menuBuilder, this.onTap, + this.selections }); final List comics; + final Map? selections; + final void Function()? onLastItemBuild; final String? Function(Comic)? badgeBuilder; @@ -638,6 +641,7 @@ class _SliverGridComicsState extends State { 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 comics; + final Map? 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, diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 29e4d38..151f0db 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -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 { bool multiSelectMode = false; - Map selectedComics = {}; + Map selectedComics = {}; void update() { if (keyword.isEmpty) { @@ -166,10 +167,66 @@ class _LocalComicsPageState extends State { ) 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.check_box_rounded), + tooltip: "Select All".tl, + onPressed: () { + setState(() { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + }); + }, + ), + IconButton( + icon: const Icon(Icons.check_box_outline_blank_outlined), + tooltip: "Deselect".tl, + onPressed: () { + setState(() { + selectedComics.clear(); + }); + }, + ), + IconButton( + icon: const Icon(Icons.check_box_outlined), + tooltip: "Invert Selection".tl, + onPressed: () { + setState(() { + comics.asMap().forEach((k, v) { + selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + }); + selectedComics.removeWhere((k, v) => !v); + }); + }, + ), + + IconButton( + icon: const Icon(Icons.indeterminate_check_box_rounded), + tooltip: "Select in range".tl, + onPressed: () { + setState(() { + List 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); + }) + ); + }); + }, + ), IconButton( icon: const Icon(Icons.close), + tooltip: "Exit Multi-Select".tl, onPressed: () { setState(() { multiSelectMode = false; @@ -177,6 +234,7 @@ class _LocalComicsPageState extends State { }); }, ), + ], ) else if (searchMode) @@ -207,13 +265,14 @@ class _LocalComicsPageState extends State { ), 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 +285,54 @@ class _LocalComicsPageState extends State { 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 +345,7 @@ class _LocalComicsPageState extends State { 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(); } From 057d6a2f549889d10cd90618667f113c0b75267b Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:54:47 +0800 Subject: [PATCH 7/9] Update translation. --- assets/translation.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 73539a0..721d07f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -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": "區間選擇" } } \ No newline at end of file From 4ff1140bf66e5dab610147ed2e7ee9b5402956a0 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:28:07 +0800 Subject: [PATCH 8/9] Add cancellation to ehviewer import. --- lib/pages/home_page.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 427c709..6d36958 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -640,7 +640,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { if (dbFile == null || comicSrc == null) { return; } - var controller = showLoadingDialog(context, allowCancel: false); + + bool cancelled = false; + var controller = showLoadingDialog(context, onCancel: () { cancelled = true; }); try { var cache = FilePath.join(App.cachePath, dbFile.name); @@ -649,6 +651,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { Future addTagComics(String destFolder, List 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; @@ -731,6 +736,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { """); for (var folder in folders) { + if(cancelled) { + break; + } var label = folder["LABEL"] as String; var folderName = '(EhViewer)$label'; if(!LocalFavoritesManager().existsFolder(folderName)) { From acb9c47657d8a935b6cd8042a478f2008e03f800 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:09:53 +0800 Subject: [PATCH 9/9] Improve selection button display on small screen devices. --- lib/pages/local_comics_page.dart | 174 +++++++++++++++++++------------ 1 file changed, 110 insertions(+), 64 deletions(-) diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 151f0db..b8f95bb 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -116,6 +116,106 @@ class _LocalComicsPageState extends State { @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 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 selectActions = []; + if(isScreenSmall) { + selectActions.add( + IconButton( + onPressed: () { + showMenu( + context: context, + position: RelativeRect.fromLTRB(screenWidth, App.isMobile ? 64 : 96, 0, 0), + items: [ + 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: [ @@ -169,71 +269,17 @@ class _LocalComicsPageState extends State { SliverAppbar( title: Text("Selected @c comics".tlParams({"c": selectedComics.length})), actions: [ - IconButton( - icon: const Icon(Icons.check_box_rounded), - tooltip: "Select All".tl, - onPressed: () { - setState(() { - selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); - }); - }, - ), - IconButton( - icon: const Icon(Icons.check_box_outline_blank_outlined), - tooltip: "Deselect".tl, - onPressed: () { - setState(() { - selectedComics.clear(); - }); - }, - ), - IconButton( - icon: const Icon(Icons.check_box_outlined), - tooltip: "Invert Selection".tl, - onPressed: () { - setState(() { - comics.asMap().forEach((k, v) { - selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + ...selectActions, + IconButton( + icon: const Icon(Icons.close), + tooltip: "Exit Multi-Select".tl, + onPressed: () { + setState(() { + multiSelectMode = false; + selectedComics.clear(); }); - selectedComics.removeWhere((k, v) => !v); - }); - }, - ), - - IconButton( - icon: const Icon(Icons.indeterminate_check_box_rounded), - tooltip: "Select in range".tl, - onPressed: () { - setState(() { - List 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); - }) - ); - }); - }, - ), - IconButton( - icon: const Icon(Icons.close), - tooltip: "Exit Multi-Select".tl, - onPressed: () { - setState(() { - multiSelectMode = false; - selectedComics.clear(); - }); - }, - ), + }, + ), ], )