perf: lazy extension VM init, incremental startup maintenance, and UI optimizations

- Defer extension VM initialization until first use with lockReadyVM() pattern to eliminate TOCTOU races and reduce startup overhead
- Add validateExtensionLoad() to catch JS errors at install time without keeping VM alive
- Teardown VM on extension disable to free resources; re-init lazily on re-enable
- Replace full orphan cleanup with incremental cursor-based pagination across launches
- Batch DB writes (upsertBatch, replaceAll) with transactions for atomicity
- Parse JSON natively on Kotlin side to avoid double-serialization over MethodChannel
- Add identity-based memoization caches for unified items and path match keys in queue tab
- Use ValueListenableBuilder for targeted embedded cover refreshes instead of full setState
- Extract shared widgets (_buildAlbumGridItemCore, _buildFilterButton, _navigateWithUnfocus)
- Use libraryCollectionsProvider selector and MediaQuery.paddingOf for fewer rebuilds
- Simplify supporter chip tiers and localize remaining hardcoded strings
This commit is contained in:
zarzet
2026-03-25 19:55:02 +07:00
parent da9d64ccfd
commit 03fd734048
18 changed files with 1279 additions and 1133 deletions
+37
View File
@@ -328,6 +328,20 @@ class HistoryDatabase {
);
}
Future<void> upsertBatch(List<Map<String, dynamic>> items) async {
if (items.isEmpty) return;
final db = await database;
final batch = db.batch();
for (final json in items) {
batch.insert(
'history',
_jsonToDbRow(json),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
}
/// Get all history items ordered by download date (newest first)
Future<List<Map<String, dynamic>>> getAll({int? limit, int? offset}) async {
final db = await database;
@@ -532,6 +546,29 @@ class HistoryDatabase {
return rows.map((r) => Map<String, dynamic>.from(r)).toList();
}
Future<List<Map<String, dynamic>>> getEntriesWithPathsPage({
required int limit,
int offset = 0,
}) async {
final db = await database;
final rows = await db.query(
'history',
columns: [
'id',
'file_path',
'storage_mode',
'download_tree_uri',
'saf_relative_dir',
'saf_file_name',
],
where: 'file_path IS NOT NULL AND file_path != ""',
orderBy: 'downloaded_at DESC, id DESC',
limit: limit,
offset: offset,
);
return rows.map((r) => Map<String, dynamic>.from(r)).toList();
}
/// Delete multiple entries by IDs
Future<int> deleteByIds(List<String> ids) async {
if (ids.isEmpty) return 0;
+32 -11
View File
@@ -255,20 +255,41 @@ class LibraryDatabase {
Future<void> upsertBatch(List<Map<String, dynamic>> items) async {
if (items.isEmpty) return;
final db = await database;
final batch = db.batch();
for (final json in items) {
batch.insert(
'library',
_jsonToDbRow(json),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
await db.transaction((txn) async {
final batch = txn.batch();
for (final json in items) {
batch.insert(
'library',
_jsonToDbRow(json),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
_log.i('Batch inserted ${items.length} items');
}
Future<void> replaceAll(List<Map<String, dynamic>> items) async {
final db = await database;
await db.transaction((txn) async {
await txn.delete('library');
if (items.isEmpty) {
return;
}
final batch = txn.batch();
for (final json in items) {
batch.insert(
'library',
_jsonToDbRow(json),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
_log.i('Replaced library with ${items.length} items');
}
Future<List<Map<String, dynamic>>> getAll({int? limit, int? offset}) async {
final db = await database;
final rows = await db.query(
+23 -21
View File
@@ -83,24 +83,18 @@ class PlatformBridge {
static Future<Map<String, dynamic>> getDownloadProgress() async {
final result = await _channel.invokeMethod('getDownloadProgress');
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeMapResult(result);
}
static Future<Map<String, dynamic>> getAllDownloadProgress() async {
final result = await _channel.invokeMethod('getAllDownloadProgress');
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeMapResult(result);
}
static Stream<Map<String, dynamic>> downloadProgressStream() {
return _downloadProgressEvents.receiveBroadcastStream().map((event) {
if (event is String) {
return jsonDecode(event) as Map<String, dynamic>;
}
if (event is Map) {
return Map<String, dynamic>.from(event);
}
return const <String, dynamic>{};
});
return _downloadProgressEvents
.receiveBroadcastStream()
.map(_decodeMapResult);
}
static Future<void> exitApp() async {
@@ -1186,19 +1180,13 @@ class PlatformBridge {
/// Get current library scan progress
static Future<Map<String, dynamic>> getLibraryScanProgress() async {
final result = await _channel.invokeMethod('getLibraryScanProgress');
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeMapResult(result);
}
static Stream<Map<String, dynamic>> libraryScanProgressStream() {
return _libraryScanProgressEvents.receiveBroadcastStream().map((event) {
if (event is String) {
return jsonDecode(event) as Map<String, dynamic>;
}
if (event is Map) {
return Map<String, dynamic>.from(event);
}
return const <String, dynamic>{};
});
return _libraryScanProgressEvents
.receiveBroadcastStream()
.map(_decodeMapResult);
}
/// Cancel ongoing library scan
@@ -1206,6 +1194,20 @@ class PlatformBridge {
await _channel.invokeMethod('cancelLibraryScan');
}
static Map<String, dynamic> _decodeMapResult(dynamic result) {
if (result is Map) {
return Map<String, dynamic>.from(result);
}
if (result is String) {
if (result.isEmpty) return const <String, dynamic>{};
final decoded = jsonDecode(result);
if (decoded is Map) {
return Map<String, dynamic>.from(decoded);
}
}
return const <String, dynamic>{};
}
// MARK: - iOS Security-Scoped Bookmark
/// Create a security-scoped bookmark from a filesystem path picked by