Improve performance of deleting favorites, coping favorites, moving favorites and deleting downloads. Close #365

This commit is contained in:
2025-05-24 16:24:53 +08:00
parent ed70fdba93
commit dcd6466547
7 changed files with 266 additions and 42 deletions

View File

@@ -116,6 +116,26 @@ class Comic {
toString() => "$sourceKey@$id"; 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 { class ComicDetails with HistoryMixin {
@override @override
final String title; final String title;

View File

@@ -653,6 +653,102 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void batchMoveFavorites(
String sourceFolder, String targetFolder, List<FavoriteItem> 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<FavoriteItem> 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 /// delete a folder
void deleteFolder(String name) { void deleteFolder(String name) {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
@@ -667,11 +763,6 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void deleteComic(String folder, FavoriteItem comic) {
_modifiedAfterLastCache = true;
deleteComicWithId(folder, comic.id, comic.type);
}
void deleteComicWithId(String folder, String id, ComicType type) { void deleteComicWithId(String folder, String id, ComicType type) {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
LocalFavoriteImageProvider.delete(id, type.value); LocalFavoriteImageProvider.delete(id, type.value);
@@ -687,6 +778,55 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void batchDeleteComics(String folder, List<FavoriteItem> 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<ComicID> 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<int> removeInvalid() async { Future<int> removeInvalid() async {
int count = 0; int count = 0;
await Future.microtask(() { await Future.microtask(() {

View File

@@ -406,4 +406,23 @@ void clearUnfavoritedHistory() {
isInitialized = false; isInitialized = false;
_db.dispose(); _db.dispose();
} }
void batchDeleteHistories(List<ComicID> 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();
}
} }

View File

@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -546,6 +547,59 @@ class LocalManager with ChangeNotifier {
remove(c.id, c.comicType); remove(c.id, c.comicType);
notifyListeners(); notifyListeners();
} }
void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true]) {
if (comics.isEmpty) {
return;
}
var shouldRemovedDirs = <Directory>[];
_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<Directory> directories) {
Isolate.run(() async {
for (var dir in directories) {
try {
if (dir.existsSync()) {
await dir.delete(recursive: true);
}
} catch (e) {
continue;
}
}
});
}
} }
enum LocalSortType { enum LocalSortType {

View File

@@ -416,10 +416,12 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
"Selected @c comics".tlParams({"c": selectedComics.length})), "Selected @c comics".tlParams({"c": selectedComics.length})),
actions: [ actions: [
MenuButton(entries: [ MenuButton(entries: [
if (!isAllFolder)
MenuEntry( MenuEntry(
icon: Icons.drive_file_move, icon: Icons.drive_file_move,
text: "Move to folder".tl, text: "Move to folder".tl,
onClick: () => favoriteOption('move')), onClick: () => favoriteOption('move')),
if (!isAllFolder)
MenuEntry( MenuEntry(
icon: Icons.copy, icon: Icons.copy,
text: "Copy to folder".tl, text: "Copy to folder".tl,
@@ -756,32 +758,26 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
return; return;
} }
if (option == 'move') { if (option == 'move') {
for (var c in selectedComics.keys) { var comics = selectedComics.keys
for (var s in selectedLocalFolders) { .map((e) => e as FavoriteItem)
LocalFavoritesManager().moveFavorite( .toList();
for (var f in selectedLocalFolders) {
LocalFavoritesManager().batchMoveFavorites(
favPage.folder as String, favPage.folder as String,
s, f,
c.id, comics,
(c as FavoriteItem).type);
}
}
} 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 ?? [],
),
); );
} }
} else {
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(); App.rootContext.pop();
@@ -817,13 +813,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
} }
void _deleteComicWithId() { void _deleteComicWithId() {
for (var c in selectedComics.keys) { var toBeDeleted = selectedComics.keys.map((e) => e as FavoriteItem).toList();
LocalFavoritesManager().deleteComicWithId( LocalFavoritesManager().batchDeleteComics(widget.folder, toBeDeleted);
widget.folder,
c.id,
(c as FavoriteItem).type,
);
}
_cancel(); _cancel();
} }
} }

View File

@@ -42,6 +42,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
folders = LocalFavoritesManager().folderNames; folders = LocalFavoritesManager().folderNames;
findNetworkFolders(); findNetworkFolders();
appdata.settings.addListener(updateFolders); appdata.settings.addListener(updateFolders);
LocalFavoritesManager().addListener(updateFolders);
super.initState(); super.initState();
} }
@@ -49,6 +50,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
void dispose() { void dispose() {
super.dispose(); super.dispose();
appdata.settings.removeListener(updateFolders); appdata.settings.removeListener(updateFolders);
LocalFavoritesManager().removeListener(updateFolders);
} }
@override @override

View File

@@ -377,12 +377,10 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
FilledButton( FilledButton(
onPressed: () { onPressed: () {
context.pop(); context.pop();
for (var comic in comics) { LocalManager().batchDeleteComics(
LocalManager().deleteComic( comics,
comic,
removeComicFile, removeComicFile,
); );
}
isDeleted = true; isDeleted = true;
}, },
child: Text("Confirm".tl), child: Text("Confirm".tl),