Improve local favorites performance.

This commit is contained in:
2025-04-28 19:40:12 +08:00
parent dfd15ed34a
commit 9ff68d0701
4 changed files with 302 additions and 130 deletions

View File

@@ -1,4 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:isolate';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
@@ -209,7 +211,22 @@ class LocalFavoritesManager with ChangeNotifier {
late Database _db; late Database _db;
late Map<String, int> counts;
int get totalComics {
int total = 0;
for (var t in counts.values) {
total += t;
}
return total;
}
int folderComics(String folder) {
return counts[folder] ?? 0;
}
Future<void> init() async { Future<void> init() async {
counts = {};
_db = sqlite3.open("${App.dataPath}/local_favorite.db"); _db = sqlite3.open("${App.dataPath}/local_favorite.db");
_db.execute(""" _db.execute("""
create table if not exists folder_order ( create table if not exists folder_order (
@@ -256,6 +273,13 @@ class LocalFavoritesManager with ChangeNotifier {
} else { } else {
appdata.settings['followUpdatesFolder'] = null; appdata.settings['followUpdatesFolder'] = null;
} }
initCounts();
}
void initCounts() {
for (var folder in folderNames) {
counts[folder] = count(folder);
}
} }
List<String> find(String id, ComicType type) { List<String> find(String id, ComicType type) {
@@ -357,6 +381,23 @@ class LocalFavoritesManager with ChangeNotifier {
return rows.map((element) => FavoriteItem.fromRow(element)).toList(); return rows.map((element) => FavoriteItem.fromRow(element)).toList();
} }
static Future<List<FavoriteItem>> _getFolderComicsAsync(
String folder, Pointer<void> p) {
return Isolate.run(() {
var db = sqlite3.fromPointer(p);
var rows = db.select("""
select * from "$folder"
ORDER BY display_order;
""");
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
});
}
/// Start a new isolate to get the comics in the folder
Future<List<FavoriteItem>> getFolderComicsAsync(String folder) {
return _getFolderComicsAsync(folder, _db.handle);
}
List<FavoriteItem> getAllComics() { List<FavoriteItem> getAllComics() {
var res = <FavoriteItem>{}; var res = <FavoriteItem>{};
for (final folder in folderNames) { for (final folder in folderNames) {
@@ -368,6 +409,26 @@ class LocalFavoritesManager with ChangeNotifier {
return res.toList(); return res.toList();
} }
static Future<List<FavoriteItem>> _getAllComicsAsync(
List<String> folders, Pointer<void> p) {
return Isolate.run(() {
var db = sqlite3.fromPointer(p);
var res = <FavoriteItem>{};
for (final folder in folders) {
var comics = db.select("""
select * from "$folder";
""");
res.addAll(comics.map((element) => FavoriteItem.fromRow(element)));
}
return res.toList();
});
}
/// Start a new isolate to get all the comics
Future<List<FavoriteItem>> getAllComicsAsync() {
return _getAllComicsAsync(folderNames, _db.handle);
}
void addTagTo(String folder, String id, String tag) { void addTagTo(String folder, String id, String tag) {
_db.execute(""" _db.execute("""
update "$folder" update "$folder"
@@ -433,6 +494,7 @@ class LocalFavoritesManager with ChangeNotifier {
); );
"""); """);
notifyListeners(); notifyListeners();
counts[name] = 0;
return name; return name;
} }
@@ -547,6 +609,11 @@ class LocalFavoritesManager with ChangeNotifier {
""", [updateTime, comic.id, comic.type.value]); """, [updateTime, comic.id, comic.type.value]);
} }
} }
if (counts[folder] == null) {
counts[folder] = count(folder);
} else {
counts[folder] = counts[folder]! + 1;
}
notifyListeners(); notifyListeners();
return true; return true;
} }
@@ -596,6 +663,7 @@ class LocalFavoritesManager with ChangeNotifier {
delete from folder_order delete from folder_order
where folder_name == ?; where folder_name == ?;
""", [name]); """, [name]);
counts.remove(name);
notifyListeners(); notifyListeners();
} }
@@ -611,6 +679,11 @@ class LocalFavoritesManager with ChangeNotifier {
delete from "$folder" delete from "$folder"
where id == ? and type == ?; where id == ? and type == ?;
""", [id, type.value]); """, [id, type.value]);
if (counts[folder] != null) {
counts[folder] = counts[folder]! - 1;
} else {
counts[folder] = count(folder);
}
notifyListeners(); notifyListeners();
} }

View File

@@ -18,7 +18,9 @@ import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_details_page/comic_page.dart'; import 'package:venera/pages/comic_details_page/comic_page.dart';
import 'package:venera/pages/reader/reader.dart'; import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
part 'favorite_actions.dart'; part 'favorite_actions.dart';

View File

@@ -2,6 +2,10 @@ part of 'favorites_page.dart';
const _localAllFolderLabel = '^_^[%local_all%]^_^'; const _localAllFolderLabel = '^_^[%local_all%]^_^';
/// If the number of comics in a folder exceeds this limit, it will be
/// fetched asynchronously.
const _asyncDataFetchLimit = 200;
class _LocalFavoritesPage extends StatefulWidget { class _LocalFavoritesPage extends StatefulWidget {
const _LocalFavoritesPage({required this.folder, super.key}); const _LocalFavoritesPage({required this.folder, super.key});
@@ -35,40 +39,110 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
bool get isAllFolder => widget.folder == _localAllFolderLabel; bool get isAllFolder => widget.folder == _localAllFolderLabel;
LocalFavoritesManager get manager => LocalFavoritesManager();
bool isLoading = false;
var searchResults = <FavoriteItem>[];
void updateSearchResult() {
setState(() {
if (keyword.trim().isEmpty) {
searchResults = comics;
} else {
searchResults = [];
for (var comic in comics) {
if (matchKeyword(keyword, comic)) {
searchResults.add(comic);
}
}
}
});
}
void updateComics() { void updateComics() {
if (keyword.isEmpty) { if (isLoading) return;
setState(() { if (isAllFolder) {
if (isAllFolder) { var totalComics = manager.totalComics;
comics = LocalFavoritesManager().getAllComics(); if (totalComics < _asyncDataFetchLimit) {
} else { comics = manager.getAllComics();
comics = LocalFavoritesManager().getFolderComics(widget.folder); } else {
} isLoading = true;
}); manager
.getAllComicsAsync()
.minTime(const Duration(milliseconds: 200))
.then((value) {
if (mounted) {
setState(() {
isLoading = false;
comics = value;
});
}
});
}
} else { } else {
setState(() { var folderComics = manager.folderComics(widget.folder);
if (isAllFolder) { if (folderComics < _asyncDataFetchLimit) {
comics = LocalFavoritesManager().search(keyword); comics = manager.getFolderComics(widget.folder);
} else { } else {
comics = isLoading = true;
LocalFavoritesManager().searchInFolder(widget.folder, keyword); manager
} .getFolderComicsAsync(widget.folder)
}); .minTime(const Duration(milliseconds: 200))
.then((value) {
if (mounted) {
setState(() {
isLoading = false;
comics = value;
});
}
});
}
} }
setState(() {});
}
bool matchKeyword(String keyword, FavoriteItem comic) {
var list = keyword.split(" ");
for (var k in list) {
if (k.isEmpty) continue;
if (comic.title.contains(k)) {
continue;
} else if (comic.subtitle != null && comic.subtitle!.contains(k)) {
continue;
} else if (comic.tags.any((tag) {
if (tag == k) {
return true;
} else if (tag.contains(':') && tag.split(':')[1] == k) {
return true;
} else if (App.locale.languageCode != 'en' &&
tag.translateTagsToCN == k) {
return true;
}
return false;
})) {
continue;
} else if (comic.author == k) {
continue;
}
return false;
}
return true;
} }
@override @override
void initState() { void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
if (!isAllFolder) { if (!isAllFolder) {
comics = LocalFavoritesManager().getFolderComics(widget.folder);
var (a, b) = LocalFavoritesManager().findLinked(widget.folder); var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
networkSource = a; networkSource = a;
networkFolder = b; networkFolder = b;
} else { } else {
comics = LocalFavoritesManager().getAllComics();
networkSource = null; networkSource = null;
networkFolder = null; networkFolder = null;
} }
comics = [];
updateComics();
LocalFavoritesManager().addListener(updateComics); LocalFavoritesManager().addListener(updateComics);
super.initState(); super.initState();
} }
@@ -215,7 +289,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
onPressed: () { onPressed: () {
setState(() { setState(() {
keyword = "";
searchMode = true; searchMode = true;
updateSearchResult();
}); });
}, },
), ),
@@ -411,9 +487,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
setState(() { setState(() {
searchMode = false; setState(() {
keyword = ""; searchMode = false;
updateComics(); });
}); });
}, },
), ),
@@ -422,132 +498,142 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Search".tl, hintText: "Search".tl,
border: InputBorder.none, border: UnderlineInputBorder(),
), ),
onChanged: (v) { onChanged: (v) {
keyword = v; keyword = v;
updateComics(); updateSearchResult();
}, },
), ).paddingBottom(8).paddingRight(8),
), ),
SliverGridComics( if (isLoading)
comics: comics, SliverToBoxAdapter(
selections: selectedComics, child: SizedBox(
menuBuilder: (c) { height: 200,
return [ child: const Center(
if (!isAllFolder) child: CircularProgressIndicator(),
),
),
)
else
SliverGridComics(
comics: searchMode ? searchResults : comics,
selections: selectedComics,
menuBuilder: (c) {
return [
if (!isAllFolder)
MenuEntry(
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
LocalFavoritesManager().deleteComicWithId(
widget.folder,
c.id,
(c as FavoriteItem).type,
);
},
),
MenuEntry( MenuEntry(
icon: Icons.delete, icon: Icons.check,
text: "Delete".tl, text: "Select".tl,
onClick: () { onClick: () {
LocalFavoritesManager().deleteComicWithId( setState(() {
widget.folder, if (!multiSelectMode) {
c.id, multiSelectMode = true;
(c as FavoriteItem).type, }
if (selectedComics.containsKey(c as FavoriteItem)) {
selectedComics.remove(c);
_checkExitSelectMode();
} else {
selectedComics[c] = true;
}
lastSelectedIndex = comics.indexOf(c);
});
},
),
MenuEntry(
icon: Icons.download,
text: "Download".tl,
onClick: () {
downloadComic(c as FavoriteItem);
context.showMessage(
message: "Download started".tl,
); );
}, },
), ),
MenuEntry( if (appdata.settings["onClickFavorite"] == "viewDetail")
icon: Icons.check, MenuEntry(
text: "Select".tl, icon: Icons.menu_book_outlined,
onClick: () { text: "Read".tl,
setState(() { onClick: () {
if (!multiSelectMode) { App.mainNavigatorKey?.currentContext?.to(
multiSelectMode = true; () => ReaderWithLoading(
} id: c.id,
if (selectedComics.containsKey(c as FavoriteItem)) { sourceKey: c.sourceKey,
selectedComics.remove(c); ),
_checkExitSelectMode(); );
} else { },
selectedComics[c] = true; ),
} ];
lastSelectedIndex = comics.indexOf(c); },
}); onTap: (c) {
}, if (multiSelectMode) {
), setState(() {
MenuEntry( if (selectedComics.containsKey(c as FavoriteItem)) {
icon: Icons.download, selectedComics.remove(c);
text: "Download".tl, _checkExitSelectMode();
onClick: () { } else {
downloadComic(c as FavoriteItem); selectedComics[c] = true;
context.showMessage( }
message: "Download started".tl, lastSelectedIndex = comics.indexOf(c);
); });
}, } else if (appdata.settings["onClickFavorite"] == "viewDetail") {
), App.mainNavigatorKey?.currentContext
if (appdata.settings["onClickFavorite"] == "viewDetail") ?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
MenuEntry(
icon: Icons.menu_book_outlined,
text: "Read".tl,
onClick: () {
App.mainNavigatorKey?.currentContext?.to(
() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
);
},
),
];
},
onTap: (c) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as FavoriteItem)) {
selectedComics.remove(c);
_checkExitSelectMode();
} else {
selectedComics[c] = true;
}
lastSelectedIndex = comics.indexOf(c);
});
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
} else {
App.mainNavigatorKey?.currentContext?.to(
() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
);
}
},
onLongPressed: (c) {
setState(() {
if (!multiSelectMode) {
multiSelectMode = true;
if (!selectedComics.containsKey(c as FavoriteItem)) {
selectedComics[c] = true;
}
lastSelectedIndex = comics.indexOf(c);
} else { } else {
if (lastSelectedIndex != null) { App.mainNavigatorKey?.currentContext?.to(
int start = lastSelectedIndex!; () => ReaderWithLoading(
int end = comics.indexOf(c as FavoriteItem); id: c.id,
if (start > end) { sourceKey: c.sourceKey,
int temp = start; ),
start = end; );
end = temp; }
},
onLongPressed: (c) {
setState(() {
if (!multiSelectMode) {
multiSelectMode = true;
if (!selectedComics.containsKey(c as FavoriteItem)) {
selectedComics[c] = true;
} }
lastSelectedIndex = comics.indexOf(c);
} else {
if (lastSelectedIndex != null) {
int start = lastSelectedIndex!;
int end = comics.indexOf(c as FavoriteItem);
if (start > end) {
int temp = start;
start = end;
end = temp;
}
for (int i = start; i <= end; i++) { for (int i = start; i <= end; i++) {
if (i == lastSelectedIndex) continue; if (i == lastSelectedIndex) continue;
var comic = comics[i]; var comic = comics[i];
if (selectedComics.containsKey(comic)) { if (selectedComics.containsKey(comic)) {
selectedComics.remove(comic); selectedComics.remove(comic);
} else { } else {
selectedComics[comic] = true; selectedComics[comic] = true;
}
} }
} }
lastSelectedIndex = comics.indexOf(c as FavoriteItem);
} }
lastSelectedIndex = comics.indexOf(c as FavoriteItem); _checkExitSelectMode();
} });
_checkExitSelectMode(); },
}); ),
},
),
], ],
); );
body = AppScrollBar( body = AppScrollBar(

View File

@@ -107,4 +107,15 @@ abstract class MapOrNull{
static Map<K, V>? from<K, V>(Map<dynamic, dynamic>? i){ static Map<K, V>? from<K, V>(Map<dynamic, dynamic>? i){
return i == null ? null : Map<K, V>.from(i); return i == null ? null : Map<K, V>.from(i);
} }
}
extension FutureExt<T> on Future<T>{
/// Wrap the future to make sure it will return at least the duration.
Future<T> minTime(Duration duration) async {
var res = await Future.wait([
this,
Future.delayed(duration),
]);
return res[0];
}
} }