From 9ff68d07011e0e7a66a565c3b1076b1eb13d3c73 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 28 Apr 2025 19:40:12 +0800 Subject: [PATCH] Improve local favorites performance. --- lib/foundation/favorites.dart | 73 ++++ lib/pages/favorites/favorites_page.dart | 2 + lib/pages/favorites/local_favorites_page.dart | 346 +++++++++++------- lib/utils/ext.dart | 11 + 4 files changed, 302 insertions(+), 130 deletions(-) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index ad2baf6..2fce8f5 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -1,4 +1,6 @@ import 'dart:convert'; +import 'dart:ffi'; +import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:sqlite3/sqlite3.dart'; @@ -209,7 +211,22 @@ class LocalFavoritesManager with ChangeNotifier { late Database _db; + late Map 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 init() async { + counts = {}; _db = sqlite3.open("${App.dataPath}/local_favorite.db"); _db.execute(""" create table if not exists folder_order ( @@ -256,6 +273,13 @@ class LocalFavoritesManager with ChangeNotifier { } else { appdata.settings['followUpdatesFolder'] = null; } + initCounts(); + } + + void initCounts() { + for (var folder in folderNames) { + counts[folder] = count(folder); + } } List find(String id, ComicType type) { @@ -357,6 +381,23 @@ class LocalFavoritesManager with ChangeNotifier { return rows.map((element) => FavoriteItem.fromRow(element)).toList(); } + static Future> _getFolderComicsAsync( + String folder, Pointer 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> getFolderComicsAsync(String folder) { + return _getFolderComicsAsync(folder, _db.handle); + } + List getAllComics() { var res = {}; for (final folder in folderNames) { @@ -368,6 +409,26 @@ class LocalFavoritesManager with ChangeNotifier { return res.toList(); } + static Future> _getAllComicsAsync( + List folders, Pointer p) { + return Isolate.run(() { + var db = sqlite3.fromPointer(p); + var res = {}; + 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> getAllComicsAsync() { + return _getAllComicsAsync(folderNames, _db.handle); + } + void addTagTo(String folder, String id, String tag) { _db.execute(""" update "$folder" @@ -433,6 +494,7 @@ class LocalFavoritesManager with ChangeNotifier { ); """); notifyListeners(); + counts[name] = 0; return name; } @@ -547,6 +609,11 @@ class LocalFavoritesManager with ChangeNotifier { """, [updateTime, comic.id, comic.type.value]); } } + if (counts[folder] == null) { + counts[folder] = count(folder); + } else { + counts[folder] = counts[folder]! + 1; + } notifyListeners(); return true; } @@ -596,6 +663,7 @@ class LocalFavoritesManager with ChangeNotifier { delete from folder_order where folder_name == ?; """, [name]); + counts.remove(name); notifyListeners(); } @@ -611,6 +679,11 @@ class LocalFavoritesManager with ChangeNotifier { delete from "$folder" where id == ? and type == ?; """, [id, type.value]); + if (counts[folder] != null) { + counts[folder] = counts[folder]! - 1; + } else { + counts[folder] = count(folder); + } notifyListeners(); } diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index aa14a64..664ec46 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -18,7 +18,9 @@ import 'package:venera/network/download.dart'; import 'package:venera/pages/comic_details_page/comic_page.dart'; import 'package:venera/pages/reader/reader.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/tags_translation.dart'; import 'package:venera/utils/translations.dart'; part 'favorite_actions.dart'; diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index a807b08..7acd560 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -2,6 +2,10 @@ part of 'favorites_page.dart'; 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 { const _LocalFavoritesPage({required this.folder, super.key}); @@ -35,40 +39,110 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { bool get isAllFolder => widget.folder == _localAllFolderLabel; + LocalFavoritesManager get manager => LocalFavoritesManager(); + + bool isLoading = false; + + var searchResults = []; + + 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() { - if (keyword.isEmpty) { - setState(() { - if (isAllFolder) { - comics = LocalFavoritesManager().getAllComics(); - } else { - comics = LocalFavoritesManager().getFolderComics(widget.folder); - } - }); + if (isLoading) return; + if (isAllFolder) { + var totalComics = manager.totalComics; + if (totalComics < _asyncDataFetchLimit) { + comics = manager.getAllComics(); + } else { + isLoading = true; + manager + .getAllComicsAsync() + .minTime(const Duration(milliseconds: 200)) + .then((value) { + if (mounted) { + setState(() { + isLoading = false; + comics = value; + }); + } + }); + } } else { - setState(() { - if (isAllFolder) { - comics = LocalFavoritesManager().search(keyword); - } else { - comics = - LocalFavoritesManager().searchInFolder(widget.folder, keyword); - } - }); + var folderComics = manager.folderComics(widget.folder); + if (folderComics < _asyncDataFetchLimit) { + comics = manager.getFolderComics(widget.folder); + } else { + isLoading = true; + 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 void initState() { favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; if (!isAllFolder) { - comics = LocalFavoritesManager().getFolderComics(widget.folder); var (a, b) = LocalFavoritesManager().findLinked(widget.folder); networkSource = a; networkFolder = b; } else { - comics = LocalFavoritesManager().getAllComics(); networkSource = null; networkFolder = null; } + comics = []; + updateComics(); LocalFavoritesManager().addListener(updateComics); super.initState(); } @@ -215,7 +289,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { icon: const Icon(Icons.search), onPressed: () { setState(() { + keyword = ""; searchMode = true; + updateSearchResult(); }); }, ), @@ -411,9 +487,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { icon: const Icon(Icons.close), onPressed: () { setState(() { - searchMode = false; - keyword = ""; - updateComics(); + setState(() { + searchMode = false; + }); }); }, ), @@ -422,132 +498,142 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { autofocus: true, decoration: InputDecoration( hintText: "Search".tl, - border: InputBorder.none, + border: UnderlineInputBorder(), ), onChanged: (v) { keyword = v; - updateComics(); + updateSearchResult(); }, - ), + ).paddingBottom(8).paddingRight(8), ), - SliverGridComics( - comics: comics, - selections: selectedComics, - menuBuilder: (c) { - return [ - if (!isAllFolder) + if (isLoading) + SliverToBoxAdapter( + child: SizedBox( + height: 200, + child: const Center( + 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( - icon: Icons.delete, - text: "Delete".tl, + icon: Icons.check, + text: "Select".tl, onClick: () { - LocalFavoritesManager().deleteComicWithId( - widget.folder, - c.id, - (c as FavoriteItem).type, + setState(() { + if (!multiSelectMode) { + multiSelectMode = true; + } + 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( - icon: Icons.check, - text: "Select".tl, - onClick: () { - setState(() { - if (!multiSelectMode) { - multiSelectMode = true; - } - 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, - ); - }, - ), - if (appdata.settings["onClickFavorite"] == "viewDetail") - 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); + if (appdata.settings["onClickFavorite"] == "viewDetail") + 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 { - if (lastSelectedIndex != null) { - int start = lastSelectedIndex!; - int end = comics.indexOf(c as FavoriteItem); - if (start > end) { - int temp = start; - start = end; - end = temp; + 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 { + 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++) { - if (i == lastSelectedIndex) continue; + for (int i = start; i <= end; i++) { + if (i == lastSelectedIndex) continue; - var comic = comics[i]; - if (selectedComics.containsKey(comic)) { - selectedComics.remove(comic); - } else { - selectedComics[comic] = true; + var comic = comics[i]; + if (selectedComics.containsKey(comic)) { + selectedComics.remove(comic); + } else { + selectedComics[comic] = true; + } } } + lastSelectedIndex = comics.indexOf(c as FavoriteItem); } - lastSelectedIndex = comics.indexOf(c as FavoriteItem); - } - _checkExitSelectMode(); - }); - }, - ), + _checkExitSelectMode(); + }); + }, + ), ], ); body = AppScrollBar( diff --git a/lib/utils/ext.dart b/lib/utils/ext.dart index c4fa3ed..404e179 100644 --- a/lib/utils/ext.dart +++ b/lib/utils/ext.dart @@ -107,4 +107,15 @@ abstract class MapOrNull{ static Map? from(Map? i){ return i == null ? null : Map.from(i); } +} + +extension FutureExt on Future{ + /// Wrap the future to make sure it will return at least the duration. + Future minTime(Duration duration) async { + var res = await Future.wait([ + this, + Future.delayed(duration), + ]); + return res[0]; + } } \ No newline at end of file