perf: optimize polling, progress caching, staggered warmup, and snapshot-based library scan

- Reduce polling interval from 800ms to 1200ms across download progress, library scan, and Android native stream
- Add dirty-flag caching to Go GetMultiProgress() to skip redundant JSON marshaling
- Replace eager provider initialization with staggered Timer-based warmup (400/900/1600ms)
- Add snapshot-based incremental library scan to avoid large MethodChannel payloads
- Move history stats and grouped album filtering to Riverpod providers for better cache invalidation
- Cap home tab history preview to 48 items with deep equality wrapper to reduce rebuilds
- Throttle foreground service notification updates to 2% progress buckets
- Migrate PageView to PageView.builder with AutomaticKeepAliveClientMixin
- Add comparison table to README
This commit is contained in:
zarzet committed 2026-03-14 16:52:33 +07:00
1 parent 10bc29e347
commit aa9854fc0a
12 files changed
+711 -392

No files matched your search

+30
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
@@ -470,6 +472,34 @@ class LibraryDatabase {
return result;
}
/// Export file modification times to a compact line-based snapshot that
/// native code can read without receiving a large method-channel payload.
Future<String> writeFileModTimesSnapshot() async {
final db = await database;
final rows = await db.rawQuery(
'SELECT file_path, COALESCE(file_mod_time, 0) AS file_mod_time FROM library',
);
final tempDir = await getTemporaryDirectory();
final file = File(
join(
tempDir.path,
'library_file_mod_times_${DateTime.now().microsecondsSinceEpoch}.tsv',
),
);
final buffer = StringBuffer();
for (final row in rows) {
final path = row['file_path'] as String?;
if (path == null || path.isEmpty) continue;
final modTime = (row['file_mod_time'] as num?)?.toInt() ?? 0;
buffer
..write(modTime)
..write('\t')
..writeln(path);
}
await file.writeAsString(buffer.toString(), flush: true);
return file.path;
}
/// Update file_mod_time for existing rows using file_path as key.
Future<void> updateFileModTimes(Map<String, int> fileModTimes) async {
if (fileModTimes.isEmpty) return;
+22
View File
@@ -1090,6 +1090,17 @@ class PlatformBridge {
return jsonDecode(result as String) as Map<String, dynamic>;
}
static Future<Map<String, dynamic>> scanLibraryFolderIncrementalFromSnapshot(
String folderPath,
String snapshotPath,
) async {
final result = await _channel.invokeMethod(
'scanLibraryFolderIncrementalFromSnapshot',
{'folder_path': folderPath, 'snapshot_path': snapshotPath},
);
return jsonDecode(result as String) as Map<String, dynamic>;
}
static Future<List<Map<String, dynamic>>> scanSafTree(String treeUri) async {
_log.i('scanSafTree: $treeUri');
final result = await _channel.invokeMethod('scanSafTree', {
@@ -1115,6 +1126,17 @@ class PlatformBridge {
return jsonDecode(result as String) as Map<String, dynamic>;
}
static Future<Map<String, dynamic>> scanSafTreeIncrementalFromSnapshot(
String treeUri,
String snapshotPath,
) async {
final result = await _channel.invokeMethod(
'scanSafTreeIncrementalFromSnapshot',
{'tree_uri': treeUri, 'snapshot_path': snapshotPath},
);
return jsonDecode(result as String) as Map<String, dynamic>;
}
/// Get last-modified timestamps for a list of SAF file URIs.
/// Returns map uri -> modTime (unix millis), only for files that still exist.
static Future<Map<String, int>> getSafFileModTimes(List<String> uris) async {