From 9ee82975e8c6f500940ab489f09a1c297be93d42 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 20 May 2025 15:40:30 +0800 Subject: [PATCH 01/12] Handle invalid appdata file. --- lib/foundation/appdata.dart | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 9c1f210..822312f 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/init.dart'; import 'package:venera/utils/io.dart'; @@ -110,21 +111,31 @@ class Appdata with Init { if (!await file.exists()) { return; } - var json = jsonDecode(await file.readAsString()); - for (var key in (json['settings'] as Map).keys) { - if (json['settings'][key] != null) { - settings[key] = json['settings'][key]; + try { + var json = jsonDecode(await file.readAsString()); + for (var key in (json['settings'] as Map).keys) { + if (json['settings'][key] != null) { + settings[key] = json['settings'][key]; + } } + searchHistory = List.from(json['searchHistory']); } - searchHistory = List.from(json['searchHistory']); - var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); - if (await implicitDataFile.exists()) { - try { + catch(e) { + Log.error("Appdata", "Failed to load appdata", e); + Log.info("Appdata", "Resetting appdata"); + file.deleteIgnoreError(); + } + try { + var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); + if (await implicitDataFile.exists()) { implicitData = jsonDecode(await implicitDataFile.readAsString()); } - catch(_) { - // ignore - } + } + catch (e) { + Log.error("Appdata", "Failed to load implicit data", e); + Log.info("Appdata", "Resetting implicit data"); + var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); + implicitDataFile.deleteIgnoreError(); } } } From 88f093f7e50ef31f14922dac0a86665356cf2d21 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 22 May 2025 19:59:42 +0800 Subject: [PATCH 02/12] Add clear unfavorited history functionality. Close #372 --- assets/translation.json | 8 +++++--- lib/foundation/history.dart | 17 +++++++++++++++++ lib/pages/history_page.dart | 7 +++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index be1e7b4..9bb2ea3 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -391,7 +391,8 @@ "Click to select an image": "点击选择一张图片", "Source URL": "源地址", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", - "Double tap to zoom": "双击缩放" + "Double tap to zoom": "双击缩放", + "Clear Unfavorited": "清除未收藏" }, "zh_TW": { "Home": "首頁", @@ -785,6 +786,7 @@ "Click to select an image": "點擊選擇一張圖片", "Source URL": "源地址", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", - "Double tap to zoom": "雙擊縮放" + "Double tap to zoom": "雙擊縮放", + "Clear Unfavorited": "清除未收藏" } -} +} \ No newline at end of file diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 0c017cf..3c45c89 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart' show ChangeNotifier; 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/image_provider/image_favorites_provider.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/utils/ext.dart'; @@ -305,6 +306,22 @@ class HistoryManager with ChangeNotifier { notifyListeners(); } + void clearUnfavoritedHistory() { + final idAndTypes = _db.select(""" + select id, type from history; + """); + for (var element in idAndTypes) { + final id = element["id"] as String; + final type = ComicType(element["type"] as int); + if (!LocalFavoritesManager().isExist(id, type)) { + _db.execute(""" + delete from history + where id == ? and type == ?; + """, [id, type.value]); + } + } + } + void remove(String id, ComicType type) async { _db.execute(""" delete from history diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index 049021e..9a631cf 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -140,6 +140,13 @@ class _HistoryPageState extends State { title: 'Clear History'.tl, content: Text('Are you sure you want to clear your history?'.tl), actions: [ + Button.outlined( + onPressed: () { + HistoryManager().clearUnfavoritedHistory(); + context.pop(); + }, + child: Text('Clear Unfavorited'.tl), + ), Button.filled( color: context.colorScheme.error, onPressed: () { From 7dc6be622aaec2a1f1435737d6879757b53874f1 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 22 May 2025 20:01:07 +0800 Subject: [PATCH 03/12] fix clearing history. --- lib/foundation/history.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 3c45c89..412e0f5 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -320,6 +320,8 @@ class HistoryManager with ChangeNotifier { """, [id, type.value]); } } + updateCache(); + notifyListeners(); } void remove(String id, ComicType type) async { From ded0068ea6b457ff9a18b8fe061923fdbc7ae87c Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 22 May 2025 20:37:25 +0800 Subject: [PATCH 04/12] Improve performance for clearing history. --- lib/foundation/history.dart | 13 ++++++++++--- lib/pages/history_page.dart | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 412e0f5..3a1ae65 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -306,7 +306,9 @@ class HistoryManager with ChangeNotifier { notifyListeners(); } - void clearUnfavoritedHistory() { +void clearUnfavoritedHistory() { + _db.execute('BEGIN TRANSACTION;'); + try { final idAndTypes = _db.select(""" select id, type from history; """); @@ -320,9 +322,14 @@ class HistoryManager with ChangeNotifier { """, [id, type.value]); } } - updateCache(); - notifyListeners(); + _db.execute('COMMIT;'); + } catch (e) { + _db.execute('ROLLBACK;'); + rethrow; } + updateCache(); + notifyListeners(); +} void remove(String id, ComicType type) async { _db.execute(""" diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index 9a631cf..bd65ce7 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -147,6 +147,7 @@ class _HistoryPageState extends State { }, child: Text('Clear Unfavorited'.tl), ), + const SizedBox(width: 4), Button.filled( color: context.colorScheme.error, onPressed: () { From ed70fdba934c01fbce76cba0d37320356bc7ef32 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 22 May 2025 20:51:47 +0800 Subject: [PATCH 05/12] Improve reordering local comics. Close #374 --- assets/translation.json | 6 ++- lib/foundation/appdata.dart | 4 +- lib/foundation/favorites.dart | 23 +++++++-- lib/pages/favorites/local_favorites_page.dart | 49 +++++++++++-------- pubspec.lock | 20 ++++---- 5 files changed, 64 insertions(+), 38 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 9bb2ea3..9de20d2 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -392,7 +392,8 @@ "Source URL": "源地址", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "Double tap to zoom": "双击缩放", - "Clear Unfavorited": "清除未收藏" + "Clear Unfavorited": "清除未收藏", + "Reverse": "反转" }, "zh_TW": { "Home": "首頁", @@ -787,6 +788,7 @@ "Source URL": "源地址", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "Double tap to zoom": "雙擊縮放", - "Clear Unfavorited": "清除未收藏" + "Clear Unfavorited": "清除未收藏", + "Reverse": "反轉" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 822312f..5600664 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -205,7 +205,9 @@ class Settings with ChangeNotifier { operator []=(String key, dynamic value) { _data[key] = value; - notifyListeners(); + if (key != "dataVersion") { + notifyListeners(); + } } @override diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 2fce8f5..5e453f0 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -714,11 +714,26 @@ class LocalFavoritesManager with ChangeNotifier { if (!existsFolder(folder)) { throw Exception("Failed to reorder: folder not found"); } - deleteFolder(folder); - createFolder(folder); - for (int i = 0; i < newFolder.length; i++) { - addComic(folder, newFolder[i], i); + _db.execute("BEGIN TRANSACTION"); + try { + for (int i = 0; i < newFolder.length; i++) { + _db.execute(""" + update "$folder" + set display_order = ? + where id == ? and type == ?; + """, [ + i, + newFolder[i].id, + newFolder[i].type.value + ]); + } } + catch (e) { + Log.error("Reorder", e.toString()); + _db.execute("ROLLBACK"); + return; + } + _db.execute("COMMIT"); notifyListeners(); } diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 7dfe925..fc45c0f 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -864,7 +864,10 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { @override void dispose() { if (changed) { - LocalFavoritesManager().reorder(comics, widget.name); + // Delay to ensure navigation is completed + Future.delayed(const Duration(milliseconds: 200), () { + LocalFavoritesManager().reorder(comics, widget.name); + }); } super.dispose(); } @@ -899,27 +902,31 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { appBar: Appbar( title: Text("Reorder".tl), actions: [ - IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () { - showInfoDialog( - context: context, - title: "Reorder".tl, - content: "Long press and drag to reorder.".tl, - ); - }, - ), - IconButton( - icon: const Icon(Icons.swap_vert), - onPressed: () { - setState(() { - comics = comics.reversed.toList(); - changed = true; - showToast( - message: "Reversed successfully".tl, context: context); - }); - }, + Tooltip( + message: "Information".tl, + child: IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () { + showInfoDialog( + context: context, + title: "Reorder".tl, + content: "Long press and drag to reorder.".tl, + ); + }, + ), ), + Tooltip( + message: "Reverse".tl, + child: IconButton( + icon: const Icon(Icons.swap_vert), + onPressed: () { + setState(() { + comics = comics.reversed.toList(); + changed = true; + }); + }, + ), + ) ], ), body: ReorderableBuilder( diff --git a/pubspec.lock b/pubspec.lock index 1d4a5da..5deaedf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" battery_plus: dependency: "direct main" description: @@ -190,10 +190,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -524,10 +524,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -548,10 +548,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1037,10 +1037,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: From dcd646654767034af40ac7bb779d6007fabe048d Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 24 May 2025 16:24:53 +0800 Subject: [PATCH 06/12] Improve performance of deleting favorites, coping favorites, moving favorites and deleting downloads. Close #365 --- lib/foundation/comic_source/models.dart | 20 +++ lib/foundation/favorites.dart | 150 +++++++++++++++++- lib/foundation/history.dart | 19 +++ lib/foundation/local.dart | 54 +++++++ lib/pages/favorites/local_favorites_page.dart | 53 +++---- lib/pages/favorites/side_bar.dart | 2 + lib/pages/local_comics_page.dart | 10 +- 7 files changed, 266 insertions(+), 42 deletions(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 5e2bc62..52cf276 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -116,6 +116,26 @@ class Comic { toString() => "$sourceKey@$id"; } +class ComicID { + final ComicType type; + + final String id; + + const ComicID(this.type, this.id); + + @override + bool operator ==(Object other) { + if (other is! ComicID) return false; + return other.type == type && other.id == id; + } + + @override + int get hashCode => type.hashCode ^ id.hashCode; + + @override + String toString() => "$type@$id"; +} + class ComicDetails with HistoryMixin { @override final String title; diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 5e453f0..15b9e08 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -653,6 +653,102 @@ class LocalFavoritesManager with ChangeNotifier { notifyListeners(); } + void batchMoveFavorites( + String sourceFolder, String targetFolder, List items) { + _modifiedAfterLastCache = true; + + if (!existsFolder(sourceFolder)) { + throw Exception("Source folder does not exist"); + } + if (!existsFolder(targetFolder)) { + throw Exception("Target folder does not exist"); + } + + _db.execute("BEGIN TRANSACTION"); + var displayOrder = maxValue(targetFolder) + 1; + try { + for (var item in items) { + _db.execute(""" + insert or ignore into "$targetFolder" (id, name, author, type, tags, cover_path, time, display_order) + select id, name, author, type, tags, cover_path, time, ? + from "$sourceFolder" + where id == ? and type == ?; + """, [displayOrder, item.id, item.type.value]); + + _db.execute(""" + delete from "$sourceFolder" + where id == ? and type == ?; + """, [item.id, item.type.value]); + + displayOrder++; + } + notifyListeners(); + } catch (e) { + Log.error("Batch Move Favorites", e.toString()); + _db.execute("ROLLBACK"); + return; + } + _db.execute("COMMIT"); + + // Update counts + if (counts[targetFolder] == null) { + counts[targetFolder] = count(targetFolder); + } else { + counts[targetFolder] = counts[targetFolder]! + items.length; + } + + if (counts[sourceFolder] != null) { + counts[sourceFolder] = counts[sourceFolder]! - items.length; + } else { + counts[sourceFolder] = count(sourceFolder); + } + + notifyListeners(); + } + + void batchCopyFavorites( + String sourceFolder, String targetFolder, List items) { + _modifiedAfterLastCache = true; + + if (!existsFolder(sourceFolder)) { + throw Exception("Source folder does not exist"); + } + if (!existsFolder(targetFolder)) { + throw Exception("Target folder does not exist"); + } + + _db.execute("BEGIN TRANSACTION"); + var displayOrder = maxValue(targetFolder) + 1; + try { + for (var item in items) { + _db.execute(""" + insert or ignore into "$targetFolder" (id, name, author, type, tags, cover_path, time, display_order) + select id, name, author, type, tags, cover_path, time, ? + from "$sourceFolder" + where id == ? and type == ?; + """, [displayOrder, item.id, item.type.value]); + + displayOrder++; + } + notifyListeners(); + } catch (e) { + Log.error("Batch Copy Favorites", e.toString()); + _db.execute("ROLLBACK"); + return; + } + + _db.execute("COMMIT"); + + // Update counts + if (counts[targetFolder] == null) { + counts[targetFolder] = count(targetFolder); + } else { + counts[targetFolder] = counts[targetFolder]! + items.length; + } + + notifyListeners(); + } + /// delete a folder void deleteFolder(String name) { _modifiedAfterLastCache = true; @@ -667,11 +763,6 @@ class LocalFavoritesManager with ChangeNotifier { notifyListeners(); } - void deleteComic(String folder, FavoriteItem comic) { - _modifiedAfterLastCache = true; - deleteComicWithId(folder, comic.id, comic.type); - } - void deleteComicWithId(String folder, String id, ComicType type) { _modifiedAfterLastCache = true; LocalFavoriteImageProvider.delete(id, type.value); @@ -687,6 +778,55 @@ class LocalFavoritesManager with ChangeNotifier { notifyListeners(); } + void batchDeleteComics(String folder, List comics) { + _modifiedAfterLastCache = true; + _db.execute("BEGIN TRANSACTION"); + try { + for (var comic in comics) { + LocalFavoriteImageProvider.delete(comic.id, comic.type.value); + _db.execute(""" + delete from "$folder" + where id == ? and type == ?; + """, [comic.id, comic.type.value]); + } + if (counts[folder] != null) { + counts[folder] = counts[folder]! - comics.length; + } else { + counts[folder] = count(folder); + } + } catch (e) { + Log.error("Batch Delete Comics", e.toString()); + _db.execute("ROLLBACK"); + return; + } + _db.execute("COMMIT"); + notifyListeners(); + } + + void batchDeleteComicsInAllFolders(List comics) { + _modifiedAfterLastCache = true; + _db.execute("BEGIN TRANSACTION"); + var folderNames = _getFolderNamesWithDB(); + try { + for (var comic in comics) { + LocalFavoriteImageProvider.delete(comic.id, comic.type.value); + for (var folder in folderNames) { + _db.execute(""" + delete from "$folder" + where id == ? and type == ?; + """, [comic.id, comic.type.value]); + } + } + } catch (e) { + Log.error("Batch Delete Comics in All Folders", e.toString()); + _db.execute("ROLLBACK"); + return; + } + initCounts(); + _db.execute("COMMIT"); + notifyListeners(); + } + Future removeInvalid() async { int count = 0; await Future.microtask(() { diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 3a1ae65..4ea0892 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -406,4 +406,23 @@ void clearUnfavoritedHistory() { isInitialized = false; _db.dispose(); } + + void batchDeleteHistories(List histories) { + if (histories.isEmpty) return; + _db.execute('BEGIN TRANSACTION;'); + try { + for (var history in histories) { + _db.execute(""" + delete from history + where id == ? and type == ?; + """, [history.id, history.type.value]); + } + _db.execute('COMMIT;'); + } catch (e) { + _db.execute('ROLLBACK;'); + rethrow; + } + updateCache(); + notifyListeners(); + } } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index b52524f..206e8fb 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:isolate'; import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:path_provider/path_provider.dart'; @@ -546,6 +547,59 @@ class LocalManager with ChangeNotifier { remove(c.id, c.comicType); notifyListeners(); } + + void batchDeleteComics(List comics, [bool removeFileOnDisk = true]) { + if (comics.isEmpty) { + return; + } + + var shouldRemovedDirs = []; + _db.execute('BEGIN TRANSACTION;'); + try { + for (var c in comics) { + if (removeFileOnDisk) { + var dir = Directory(FilePath.join(path, c.directory)); + if (dir.existsSync()) { + shouldRemovedDirs.add(dir); + } + } + _db.execute( + 'DELETE FROM comics WHERE id = ? AND comic_type = ?;', + [c.id, c.comicType.value], + ); + } + } + catch(e, s) { + Log.error("LocalManager", "Failed to batch delete comics: $e", s); + _db.execute('ROLLBACK;'); + return; + } + _db.execute('COMMIT;'); + + var comicIDs = comics.map((e) => ComicID(e.comicType, e.id)).toList(); + LocalFavoritesManager().batchDeleteComicsInAllFolders(comicIDs); + HistoryManager().batchDeleteHistories(comicIDs); + + notifyListeners(); + + if (removeFileOnDisk) { + _deleteDirectories(shouldRemovedDirs); + } + } + + static void _deleteDirectories(List directories) { + Isolate.run(() async { + for (var dir in directories) { + try { + if (dir.existsSync()) { + await dir.delete(recursive: true); + } + } catch (e) { + continue; + } + } + }); + } } enum LocalSortType { diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index fc45c0f..602f639 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -416,10 +416,12 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { "Selected @c comics".tlParams({"c": selectedComics.length})), actions: [ MenuButton(entries: [ + if (!isAllFolder) MenuEntry( icon: Icons.drive_file_move, text: "Move to folder".tl, onClick: () => favoriteOption('move')), + if (!isAllFolder) MenuEntry( icon: Icons.copy, text: "Copy to folder".tl, @@ -756,32 +758,26 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { return; } if (option == 'move') { - for (var c in selectedComics.keys) { - for (var s in selectedLocalFolders) { - LocalFavoritesManager().moveFavorite( - favPage.folder as String, - s, - c.id, - (c as FavoriteItem).type); - } + var comics = selectedComics.keys + .map((e) => e as FavoriteItem) + .toList(); + for (var f in selectedLocalFolders) { + LocalFavoritesManager().batchMoveFavorites( + favPage.folder as String, + f, + comics, + ); } } else { - for (var c in selectedComics.keys) { - for (var s in selectedLocalFolders) { - LocalFavoritesManager().addComic( - s, - FavoriteItem( - id: c.id, - name: c.title, - coverPath: c.cover, - author: c.subtitle ?? '', - type: ComicType((c.sourceKey == 'local' - ? 0 - : c.sourceKey.hashCode)), - tags: c.tags ?? [], - ), - ); - } + var comics = selectedComics.keys + .map((e) => e as FavoriteItem) + .toList(); + for (var f in selectedLocalFolders) { + LocalFavoritesManager().batchCopyFavorites( + favPage.folder as String, + f, + comics, + ); } } App.rootContext.pop(); @@ -817,13 +813,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { } void _deleteComicWithId() { - for (var c in selectedComics.keys) { - LocalFavoritesManager().deleteComicWithId( - widget.folder, - c.id, - (c as FavoriteItem).type, - ); - } + var toBeDeleted = selectedComics.keys.map((e) => e as FavoriteItem).toList(); + LocalFavoritesManager().batchDeleteComics(widget.folder, toBeDeleted); _cancel(); } } diff --git a/lib/pages/favorites/side_bar.dart b/lib/pages/favorites/side_bar.dart index 464ad75..1125b0a 100644 --- a/lib/pages/favorites/side_bar.dart +++ b/lib/pages/favorites/side_bar.dart @@ -42,6 +42,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { folders = LocalFavoritesManager().folderNames; findNetworkFolders(); appdata.settings.addListener(updateFolders); + LocalFavoritesManager().addListener(updateFolders); super.initState(); } @@ -49,6 +50,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { void dispose() { super.dispose(); appdata.settings.removeListener(updateFolders); + LocalFavoritesManager().removeListener(updateFolders); } @override diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 22b4dd9..c958fee 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -377,12 +377,10 @@ class _LocalComicsPageState extends State { FilledButton( onPressed: () { context.pop(); - for (var comic in comics) { - LocalManager().deleteComic( - comic, - removeComicFile, - ); - } + LocalManager().batchDeleteComics( + comics, + removeComicFile, + ); isDeleted = true; }, child: Text("Confirm".tl), From 76e9ef87d4c9c075534a6e01f74b45cfb492caf7 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 25 May 2025 20:26:35 +0800 Subject: [PATCH 07/12] Add functionality to delete specific comic chapters. Close #368 --- assets/translation.json | 6 ++- lib/foundation/local.dart | 36 ++++++++++++++++++ lib/pages/local_comics_page.dart | 64 ++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 9de20d2..45079e2 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -393,7 +393,8 @@ "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "Double tap to zoom": "双击缩放", "Clear Unfavorited": "清除未收藏", - "Reverse": "反转" + "Reverse": "反转", + "Delete Chapters": "删除章节" }, "zh_TW": { "Home": "首頁", @@ -789,6 +790,7 @@ "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "Double tap to zoom": "雙擊縮放", "Clear Unfavorited": "清除未收藏", - "Reverse": "反轉" + "Reverse": "反轉", + "Delete Chapters": "刪除章節" } } \ No newline at end of file diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 206e8fb..ef7cf15 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -548,6 +548,41 @@ class LocalManager with ChangeNotifier { notifyListeners(); } + void deleteComicChapters(LocalComic c, List chapters) { + if (chapters.isEmpty) { + return; + } + var newDownloadedChapters = c.downloadedChapters + .where((e) => !chapters.contains(e)) + .toList(); + if (newDownloadedChapters.isNotEmpty) { + _db.execute( + 'UPDATE comics SET downloadedChapters = ? WHERE id = ? AND comic_type = ?;', + [ + jsonEncode(newDownloadedChapters), + c.id, + c.comicType.value, + ], + ); + } else { + _db.execute( + 'DELETE FROM comics WHERE id = ? AND comic_type = ?;', + [c.id, c.comicType.value], + ); + } + var shouldRemovedDirs = []; + for (var chapter in chapters) { + var dir = Directory(FilePath.join(c.baseDir, chapter)); + if (dir.existsSync()) { + shouldRemovedDirs.add(dir); + } + } + if (shouldRemovedDirs.isNotEmpty) { + _deleteDirectories(shouldRemovedDirs); + } + notifyListeners(); + } + void batchDeleteComics(List comics, [bool removeFileOnDisk = true]) { if (comics.isEmpty) { return; @@ -587,6 +622,7 @@ class LocalManager with ChangeNotifier { } } + /// Deletes the directories in a separate isolate to avoid blocking the UI thread. static void _deleteDirectories(List directories) { Isolate.run(() async { for (var dir in directories) { diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index c958fee..6904054 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -374,6 +374,14 @@ class _LocalComicsPageState extends State { }, ), actions: [ + if (comics.length == 1 && comics.first.hasChapters) + TextButton( + child: Text("Delete Chapters".tl), + onPressed: () { + context.pop(); + showDeleteChaptersPopWindow(context, comics.first); + }, + ), FilledButton( onPressed: () { context.pop(); @@ -495,3 +503,59 @@ class _LocalComicsPageState extends State { typedef ExportComicFunc = Future Function( LocalComic comic, String outFilePath); + +void showDeleteChaptersPopWindow(BuildContext context, LocalComic comic) { + var chapters = []; + + showPopUpWidget( + context, + PopUpWidgetScaffold( + title: "Delete Chapters".tl, + body: StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: comic.downloadedChapters.length, + itemBuilder: (context, index) { + var id = comic.downloadedChapters[index]; + var chapter = comic.chapters![id] ?? "Unknown Chapter"; + return CheckboxListTile( + title: Text(chapter), + value: chapters.contains(id), + onChanged: (v) { + setState(() { + if (v == true) { + chapters.add(id); + } else { + chapters.remove(id); + } + }); + }, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + onPressed: () { + Future.delayed(const Duration(milliseconds: 200), () { + LocalManager().deleteComicChapters(comic, chapters); + }); + App.rootContext.pop(); + }, + child: Text("Submit".tl), + ) + ], + ), + ) + ], + ); + }), + ), + ); +} From 749a1a47fbaf2c7c28904828dd5ac466d295de4e Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 25 May 2025 20:33:31 +0800 Subject: [PATCH 08/12] Fix dialog content overflow. Close #363 --- lib/components/message.dart | 46 +++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/components/message.dart b/lib/components/message.dart index 1a67f61..9c27e24 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -290,28 +290,30 @@ class ContentDialog extends StatelessWidget { @override Widget build(BuildContext context) { - var content = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title != null - ? Appbar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: dismissible ? context.pop : null, - ), - title: Text(title!), - backgroundColor: Colors.transparent, - ) - : const SizedBox.shrink(), - this.content, - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: actions, - ).paddingRight(12), - const SizedBox(height: 16), - ], + var content = SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title != null + ? Appbar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: dismissible ? context.pop : null, + ), + title: Text(title!), + backgroundColor: Colors.transparent, + ) + : const SizedBox.shrink(), + this.content, + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: actions, + ).paddingRight(12), + const SizedBox(height: 16), + ], + ), ); return Dialog( shape: RoundedRectangleBorder( From 0c46214619e7bfb2737949e1f4db8946b4902c30 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 26 May 2025 15:35:24 +0800 Subject: [PATCH 09/12] Reduce maximum length for comic directory names to improve consistency. Close #362 --- lib/foundation/local.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index ef7cf15..812d6eb 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -462,7 +462,7 @@ class LocalManager with ChangeNotifier { if (comic != null) { return Directory(FilePath.join(path, comic.directory)); } - const comicDirectoryMaxLength = 128; + const comicDirectoryMaxLength = 80; if (name.length > comicDirectoryMaxLength) { name = name.substring(0, comicDirectoryMaxLength); } From 55733ef505c55f61b8ac3ae03639d0caa38b7b58 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 26 May 2025 16:09:23 +0800 Subject: [PATCH 10/12] Update selectAll method to handle search mode for selecting comics. Close #359 --- lib/pages/favorites/local_favorites_page.dart | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 602f639..12f258c 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -155,16 +155,33 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { void selectAll() { setState(() { - selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + if (searchMode) { + selectedComics = searchResults.asMap().map((k, v) => MapEntry(v, true)); + } else { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + } }); } void invertSelection() { setState(() { - comics.asMap().forEach((k, v) { - selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); - }); - selectedComics.removeWhere((k, v) => !v); + if (searchMode) { + for (var c in searchResults) { + if (selectedComics.containsKey(c)) { + selectedComics.remove(c); + } else { + selectedComics[c] = true; + } + } + } else { + for (var c in comics) { + if (selectedComics.containsKey(c)) { + selectedComics.remove(c); + } else { + selectedComics[c] = true; + } + } + } }); } From 665f50ed2aef3ab38e7b7f6af80e7d3868ad50f5 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 26 May 2025 16:42:05 +0800 Subject: [PATCH 11/12] Fixed an issue where comic counts would become invalid after renaming a favorite folder. Close #357 --- lib/foundation/favorites.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 15b9e08..d198ec3 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -898,6 +898,8 @@ class LocalFavoritesManager with ChangeNotifier { set folder_name = ? where folder_name == ?; """, [after, before]); + counts[after] = counts[before] ?? 0; + counts.remove(before); notifyListeners(); } From 20a57c7a36d06d8468e3bcd8aff480bb44a24c42 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 26 May 2025 18:10:07 +0800 Subject: [PATCH 12/12] Update version code --- lib/foundation/app.dart | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 1257778..13ed776 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.4.3"; + final version = "1.4.4"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.lock b/pubspec.lock index 5deaedf..e96ad55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1107,5 +1107,5 @@ packages: source: hosted version: "0.0.12" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.3" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8731d77..6fc8078 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.4.3+143 +version: 1.4.4+144 environment: - sdk: '>=3.6.0 <4.0.0' - flutter: 3.29.3 + sdk: '>=3.8.0 <4.0.0' + flutter: 3.32.0 dependencies: flutter: