From b5e4b0e4418f37bdf369e49bfead0c11fdbb4e75 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 22:08:17 +0800 Subject: [PATCH] refresh cloud file index on startup --- lib/app/app.dart | 6 +- lib/core/storage/file_metadata_cache.dart | 7 ++ lib/core/storage/media_library_store.dart | 52 ++++++++++++++ lib/core/storage/storage_manager.dart | 4 ++ lib/pages/settings_page.dart | 30 +++++++-- lib/providers/media_library_provider.dart | 82 ++++++++++++++++++++++- 6 files changed, 173 insertions(+), 8 deletions(-) diff --git a/lib/app/app.dart b/lib/app/app.dart index 76f5c70..6e1649f 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -22,9 +22,9 @@ class GuangyaApp extends ConsumerWidget { WidgetsBinding.instance.addPostFrameCallback((_) { final fp = ref.read(fileProvider.notifier); fp.api = ref.read(authProvider.notifier).api; - ref.read(mediaLibraryProvider.notifier).api = ref - .read(authProvider.notifier) - .api; + final media = ref.read(mediaLibraryProvider.notifier); + media.api = ref.read(authProvider.notifier).api; + media.load(); final fileState = ref.read(fileProvider); if (fileState.files.isEmpty && !fileState.isLoading) { fp.loadFiles(); diff --git a/lib/core/storage/file_metadata_cache.dart b/lib/core/storage/file_metadata_cache.dart index 4a08e8f..57c539b 100644 --- a/lib/core/storage/file_metadata_cache.dart +++ b/lib/core/storage/file_metadata_cache.dart @@ -13,9 +13,16 @@ class FileMetadataCache { static Future cacheFiles(List files) => _store.cacheFiles(files); + static Future cacheFolderChildrenBatch( + Map> folders, + ) => _store.cacheFolderChildrenBatch(folders); + static Future?> folderChildren(String? folderID) => _store.folderChildren(folderID); + static Future> allCachedFolderChildren() => + _store.allCachedFolderChildren(); + static Future?> siblingFiles(String fileID) => _store.siblingFiles(fileID); diff --git a/lib/core/storage/media_library_store.dart b/lib/core/storage/media_library_store.dart index ee9de6b..36bc6c7 100644 --- a/lib/core/storage/media_library_store.dart +++ b/lib/core/storage/media_library_store.dart @@ -310,6 +310,37 @@ class MediaLibraryStore { }); } + Future cacheFolderChildrenBatch( + Map> folders, + ) async { + if (folders.isEmpty) return; + final db = await _db; + await db.transaction((txn) async { + for (final entry in folders.entries) { + final files = entry.value; + for (final file in files) { + final gcid = file.gcid?.trim(); + if (gcid == null || gcid.isEmpty) continue; + await txn.insert('file_index', { + 'file_id': file.id, + 'gcid': gcid, + }, conflictAlgorithm: ConflictAlgorithm.replace); + await txn.insert('gcid_details', { + 'gcid': gcid, + 'file_json': jsonEncode(file.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + await txn.insert('folder_children', { + 'folder_id': _folderID(entry.key), + 'child_ids': jsonEncode(files.map((file) => file.id).toList()), + 'children_json': jsonEncode( + files.map((file) => file.toJson()).toList(), + ), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + }); + } + Future?> folderChildren(String? folderID) async { final rows = await (await _db).query( 'folder_children', @@ -333,6 +364,27 @@ class MediaLibraryStore { } } + Future> allCachedFolderChildren() async { + final rows = await (await _db).query( + 'folder_children', + columns: const ['children_json'], + ); + final values = {}; + for (final row in rows) { + try { + final raw = jsonDecode(row['children_json']?.toString() ?? '[]'); + if (raw is! List) continue; + for (final value in raw.whereType()) { + final file = CloudFile.fromJson(Map.from(value)); + values[file.id] = file; + } + } catch (_) { + // Ignore a malformed stale folder row and keep the remaining index. + } + } + return values.values.toList(); + } + Future?> siblingFiles(String fileID) async { final rows = await (await _db).query( 'folder_children', diff --git a/lib/core/storage/storage_manager.dart b/lib/core/storage/storage_manager.dart index 9ac702c..efffbec 100644 --- a/lib/core/storage/storage_manager.dart +++ b/lib/core/storage/storage_manager.dart @@ -30,6 +30,10 @@ class StorageKeys { static const String defaultFilePageSize = 'guangya.defaultFilePageSize'; static const String fastTransferSession = 'guangya.fastTransferSession'; static const String mediaScanHistory = 'guangya.mediaScanHistory'; + static const String cloudIndexRefreshMinutes = + 'guangya.cloudIndexRefreshMinutes'; + static const String cloudIndexLastUpdatedAt = + 'guangya.cloudIndexLastUpdatedAt'; } class StorageManager { diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index e3dbdfe..4ca1937 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../providers/theme_provider.dart'; +import '../providers/media_library_provider.dart'; import '../core/storage/storage_manager.dart'; class SettingsDialog extends ConsumerStatefulWidget { @@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState { final _scanConcurrencyController = TextEditingController(); final _transferConcurrencyController = TextEditingController(); final _cacheTTLController = TextEditingController(); + final _cloudIndexRefreshController = TextEditingController(); final _pageSizeController = TextEditingController(); @override @@ -45,6 +47,9 @@ class _SettingsDialogState extends ConsumerState { StorageManager.get(StorageKeys.fastTransferConcurrency) ?? '3'; _cacheTTLController.text = StorageManager.get(StorageKeys.fileCacheTTLMinutes) ?? '3'; + _cloudIndexRefreshController.text = + StorageManager.get(StorageKeys.cloudIndexRefreshMinutes) ?? + '30'; _pageSizeController.text = StorageManager.get(StorageKeys.defaultFilePageSize) ?? '50'; } @@ -60,6 +65,7 @@ class _SettingsDialogState extends ConsumerState { _scanConcurrencyController.dispose(); _transferConcurrencyController.dispose(); _cacheTTLController.dispose(); + _cloudIndexRefreshController.dispose(); _pageSizeController.dispose(); super.dispose(); } @@ -78,9 +84,9 @@ class _SettingsDialogState extends ConsumerState { actions: [ ShadButton( child: const Text('完成'), - onPressed: () { - _saveSettings(); - Navigator.of(context).pop(); + onPressed: () async { + await _saveSettings(); + if (context.mounted) Navigator.of(context).pop(); }, ), ], @@ -217,6 +223,17 @@ class _SettingsDialogState extends ConsumerState { ), ), ), + _SettingsRow( + icon: Icons.cloud_sync_rounded, + label: '全盘索引刷新分钟', + child: SizedBox( + width: 100, + child: ShadInput( + controller: _cloudIndexRefreshController, + keyboardType: TextInputType.number, + ), + ), + ), _SettingsRow( icon: Icons.format_list_numbered_rounded, label: '默认分页大小', @@ -313,7 +330,7 @@ class _SettingsDialogState extends ConsumerState { ); } - void _saveSettings() { + Future _saveSettings() async { StorageManager.set( StorageKeys.tmdbApiKey, _tmdbApiKeyController.text.trim(), @@ -350,6 +367,11 @@ class _SettingsDialogState extends ConsumerState { StorageKeys.fileCacheTTLMinutes, _cacheTTLController.text.trim(), ); + await StorageManager.set( + StorageKeys.cloudIndexRefreshMinutes, + _cloudIndexRefreshController.text.trim(), + ); + ref.read(mediaLibraryProvider.notifier).updateCloudIndexRefreshSchedule(); StorageManager.set( StorageKeys.defaultFilePageSize, _pageSizeController.text.trim(), diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 3c3661c..67b3651 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -120,6 +120,8 @@ class MediaLibraryNotifier extends StateNotifier { bool _loaded = false; bool _cancelScan = false; bool _cancelDetailSync = false; + bool _refreshingCloudIndex = false; + Timer? _cloudIndexTimer; MediaLibraryNotifier() : super(const MediaLibraryState()); @@ -151,6 +153,7 @@ class MediaLibraryNotifier extends StateNotifier { if (selectedID != null) { unawaited(_hydrateMissingArtwork(selectedID, items)); } + unawaited(refreshGlobalCloudIndex()); } catch (e) { state = state.copyWith(errorMessage: e.toString()); } finally { @@ -430,7 +433,8 @@ class MediaLibraryNotifier extends StateNotifier { if (forceRemote) { _appendScanLog('正在获取云盘全量文件索引…'); - final globalFiles = await _allGlobalRemoteFiles(); + await refreshGlobalCloudIndex(force: true); + final globalFiles = await _cachedGlobalFiles(); final seen = {}; final mediaFiles = []; for (final source in library.sources) { @@ -544,6 +548,76 @@ class MediaLibraryNotifier extends StateNotifier { _appendScanLog('[同步识别][DEBUG] 已请求取消同步识别'); } + Future refreshGlobalCloudIndex({bool force = false}) async { + if (_api == null || _refreshingCloudIndex) return; + final minutes = + (int.tryParse( + StorageManager.get( + StorageKeys.cloudIndexRefreshMinutes, + ) ?? + '30', + ) ?? + 30) + .clamp(5, 1440); + final lastUpdated = int.tryParse( + StorageManager.get(StorageKeys.cloudIndexLastUpdatedAt) ?? '', + ); + if (!force && lastUpdated != null) { + final elapsed = DateTime.now().difference( + DateTime.fromMillisecondsSinceEpoch(lastUpdated), + ); + if (elapsed < Duration(minutes: minutes)) { + _scheduleCloudIndexRefresh(minutes); + return; + } + } + _refreshingCloudIndex = true; + try { + final files = await _allGlobalRemoteFiles(); + final folders = >{}; + for (final file in files) { + (folders[file.parentID] ??= []).add(file); + } + await FileMetadataCache.cacheFolderChildrenBatch(folders); + await StorageManager.set( + StorageKeys.cloudIndexLastUpdatedAt, + DateTime.now().millisecondsSinceEpoch.toString(), + ); + _appendScanLog('[云盘索引] 已刷新 ${files.length} 个文件与目录缓存'); + } catch (error) { + _appendScanLog('[云盘索引] 刷新失败:$error', isError: true); + } finally { + _refreshingCloudIndex = false; + _scheduleCloudIndexRefresh(minutes); + } + } + + void _scheduleCloudIndexRefresh(int minutes) { + _cloudIndexTimer?.cancel(); + _cloudIndexTimer = Timer(Duration(minutes: minutes), () { + unawaited(refreshGlobalCloudIndex(force: true)); + }); + } + + void updateCloudIndexRefreshSchedule() { + final minutes = + (int.tryParse( + StorageManager.get( + StorageKeys.cloudIndexRefreshMinutes, + ) ?? + '30', + ) ?? + 30) + .clamp(5, 1440); + _scheduleCloudIndexRefresh(minutes); + } + + @override + void dispose() { + _cloudIndexTimer?.cancel(); + super.dispose(); + } + void _appendScanLog(String message, {bool isError = false}) { final logs = [ ...state.scanLogs, @@ -1494,6 +1568,12 @@ class MediaLibraryNotifier extends StateNotifier { return values; } + Future> _cachedGlobalFiles() async { + final values = await FileMetadataCache.allCachedFolderChildren(); + if (values.isEmpty) return _allGlobalRemoteFiles(); + return values; + } + Future> _allGlobalRemoteFilesByType({int? resType}) async { const pageSize = 10000; final values = [];