refresh cloud file index on startup
This commit is contained in:
+3
-3
@@ -22,9 +22,9 @@ class GuangyaApp extends ConsumerWidget {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final fp = ref.read(fileProvider.notifier);
|
final fp = ref.read(fileProvider.notifier);
|
||||||
fp.api = ref.read(authProvider.notifier).api;
|
fp.api = ref.read(authProvider.notifier).api;
|
||||||
ref.read(mediaLibraryProvider.notifier).api = ref
|
final media = ref.read(mediaLibraryProvider.notifier);
|
||||||
.read(authProvider.notifier)
|
media.api = ref.read(authProvider.notifier).api;
|
||||||
.api;
|
media.load();
|
||||||
final fileState = ref.read(fileProvider);
|
final fileState = ref.read(fileProvider);
|
||||||
if (fileState.files.isEmpty && !fileState.isLoading) {
|
if (fileState.files.isEmpty && !fileState.isLoading) {
|
||||||
fp.loadFiles();
|
fp.loadFiles();
|
||||||
|
|||||||
@@ -13,9 +13,16 @@ class FileMetadataCache {
|
|||||||
static Future<void> cacheFiles(List<CloudFile> files) =>
|
static Future<void> cacheFiles(List<CloudFile> files) =>
|
||||||
_store.cacheFiles(files);
|
_store.cacheFiles(files);
|
||||||
|
|
||||||
|
static Future<void> cacheFolderChildrenBatch(
|
||||||
|
Map<String?, List<CloudFile>> folders,
|
||||||
|
) => _store.cacheFolderChildrenBatch(folders);
|
||||||
|
|
||||||
static Future<List<CloudFile>?> folderChildren(String? folderID) =>
|
static Future<List<CloudFile>?> folderChildren(String? folderID) =>
|
||||||
_store.folderChildren(folderID);
|
_store.folderChildren(folderID);
|
||||||
|
|
||||||
|
static Future<List<CloudFile>> allCachedFolderChildren() =>
|
||||||
|
_store.allCachedFolderChildren();
|
||||||
|
|
||||||
static Future<List<CloudFile>?> siblingFiles(String fileID) =>
|
static Future<List<CloudFile>?> siblingFiles(String fileID) =>
|
||||||
_store.siblingFiles(fileID);
|
_store.siblingFiles(fileID);
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,37 @@ class MediaLibraryStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cacheFolderChildrenBatch(
|
||||||
|
Map<String?, List<CloudFile>> 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<List<CloudFile>?> folderChildren(String? folderID) async {
|
Future<List<CloudFile>?> folderChildren(String? folderID) async {
|
||||||
final rows = await (await _db).query(
|
final rows = await (await _db).query(
|
||||||
'folder_children',
|
'folder_children',
|
||||||
@@ -333,6 +364,27 @@ class MediaLibraryStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CloudFile>> allCachedFolderChildren() async {
|
||||||
|
final rows = await (await _db).query(
|
||||||
|
'folder_children',
|
||||||
|
columns: const ['children_json'],
|
||||||
|
);
|
||||||
|
final values = <String, CloudFile>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
try {
|
||||||
|
final raw = jsonDecode(row['children_json']?.toString() ?? '[]');
|
||||||
|
if (raw is! List) continue;
|
||||||
|
for (final value in raw.whereType<Map>()) {
|
||||||
|
final file = CloudFile.fromJson(Map<String, dynamic>.from(value));
|
||||||
|
values[file.id] = file;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore a malformed stale folder row and keep the remaining index.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<CloudFile>?> siblingFiles(String fileID) async {
|
Future<List<CloudFile>?> siblingFiles(String fileID) async {
|
||||||
final rows = await (await _db).query(
|
final rows = await (await _db).query(
|
||||||
'folder_children',
|
'folder_children',
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class StorageKeys {
|
|||||||
static const String defaultFilePageSize = 'guangya.defaultFilePageSize';
|
static const String defaultFilePageSize = 'guangya.defaultFilePageSize';
|
||||||
static const String fastTransferSession = 'guangya.fastTransferSession';
|
static const String fastTransferSession = 'guangya.fastTransferSession';
|
||||||
static const String mediaScanHistory = 'guangya.mediaScanHistory';
|
static const String mediaScanHistory = 'guangya.mediaScanHistory';
|
||||||
|
static const String cloudIndexRefreshMinutes =
|
||||||
|
'guangya.cloudIndexRefreshMinutes';
|
||||||
|
static const String cloudIndexLastUpdatedAt =
|
||||||
|
'guangya.cloudIndexLastUpdatedAt';
|
||||||
}
|
}
|
||||||
|
|
||||||
class StorageManager {
|
class StorageManager {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
|
import '../providers/media_library_provider.dart';
|
||||||
import '../core/storage/storage_manager.dart';
|
import '../core/storage/storage_manager.dart';
|
||||||
|
|
||||||
class SettingsDialog extends ConsumerStatefulWidget {
|
class SettingsDialog extends ConsumerStatefulWidget {
|
||||||
@@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
final _scanConcurrencyController = TextEditingController();
|
final _scanConcurrencyController = TextEditingController();
|
||||||
final _transferConcurrencyController = TextEditingController();
|
final _transferConcurrencyController = TextEditingController();
|
||||||
final _cacheTTLController = TextEditingController();
|
final _cacheTTLController = TextEditingController();
|
||||||
|
final _cloudIndexRefreshController = TextEditingController();
|
||||||
final _pageSizeController = TextEditingController();
|
final _pageSizeController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,6 +47,9 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
StorageManager.get<String>(StorageKeys.fastTransferConcurrency) ?? '3';
|
StorageManager.get<String>(StorageKeys.fastTransferConcurrency) ?? '3';
|
||||||
_cacheTTLController.text =
|
_cacheTTLController.text =
|
||||||
StorageManager.get<String>(StorageKeys.fileCacheTTLMinutes) ?? '3';
|
StorageManager.get<String>(StorageKeys.fileCacheTTLMinutes) ?? '3';
|
||||||
|
_cloudIndexRefreshController.text =
|
||||||
|
StorageManager.get<String>(StorageKeys.cloudIndexRefreshMinutes) ??
|
||||||
|
'30';
|
||||||
_pageSizeController.text =
|
_pageSizeController.text =
|
||||||
StorageManager.get<String>(StorageKeys.defaultFilePageSize) ?? '50';
|
StorageManager.get<String>(StorageKeys.defaultFilePageSize) ?? '50';
|
||||||
}
|
}
|
||||||
@@ -60,6 +65,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
_scanConcurrencyController.dispose();
|
_scanConcurrencyController.dispose();
|
||||||
_transferConcurrencyController.dispose();
|
_transferConcurrencyController.dispose();
|
||||||
_cacheTTLController.dispose();
|
_cacheTTLController.dispose();
|
||||||
|
_cloudIndexRefreshController.dispose();
|
||||||
_pageSizeController.dispose();
|
_pageSizeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -78,9 +84,9 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
ShadButton(
|
ShadButton(
|
||||||
child: const Text('完成'),
|
child: const Text('完成'),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
_saveSettings();
|
await _saveSettings();
|
||||||
Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -217,6 +223,17 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_SettingsRow(
|
||||||
|
icon: Icons.cloud_sync_rounded,
|
||||||
|
label: '全盘索引刷新分钟',
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: ShadInput(
|
||||||
|
controller: _cloudIndexRefreshController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.format_list_numbered_rounded,
|
icon: Icons.format_list_numbered_rounded,
|
||||||
label: '默认分页大小',
|
label: '默认分页大小',
|
||||||
@@ -313,7 +330,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _saveSettings() {
|
Future<void> _saveSettings() async {
|
||||||
StorageManager.set(
|
StorageManager.set(
|
||||||
StorageKeys.tmdbApiKey,
|
StorageKeys.tmdbApiKey,
|
||||||
_tmdbApiKeyController.text.trim(),
|
_tmdbApiKeyController.text.trim(),
|
||||||
@@ -350,6 +367,11 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||||||
StorageKeys.fileCacheTTLMinutes,
|
StorageKeys.fileCacheTTLMinutes,
|
||||||
_cacheTTLController.text.trim(),
|
_cacheTTLController.text.trim(),
|
||||||
);
|
);
|
||||||
|
await StorageManager.set(
|
||||||
|
StorageKeys.cloudIndexRefreshMinutes,
|
||||||
|
_cloudIndexRefreshController.text.trim(),
|
||||||
|
);
|
||||||
|
ref.read(mediaLibraryProvider.notifier).updateCloudIndexRefreshSchedule();
|
||||||
StorageManager.set(
|
StorageManager.set(
|
||||||
StorageKeys.defaultFilePageSize,
|
StorageKeys.defaultFilePageSize,
|
||||||
_pageSizeController.text.trim(),
|
_pageSizeController.text.trim(),
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
bool _loaded = false;
|
bool _loaded = false;
|
||||||
bool _cancelScan = false;
|
bool _cancelScan = false;
|
||||||
bool _cancelDetailSync = false;
|
bool _cancelDetailSync = false;
|
||||||
|
bool _refreshingCloudIndex = false;
|
||||||
|
Timer? _cloudIndexTimer;
|
||||||
|
|
||||||
MediaLibraryNotifier() : super(const MediaLibraryState());
|
MediaLibraryNotifier() : super(const MediaLibraryState());
|
||||||
|
|
||||||
@@ -151,6 +153,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
if (selectedID != null) {
|
if (selectedID != null) {
|
||||||
unawaited(_hydrateMissingArtwork(selectedID, items));
|
unawaited(_hydrateMissingArtwork(selectedID, items));
|
||||||
}
|
}
|
||||||
|
unawaited(refreshGlobalCloudIndex());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = state.copyWith(errorMessage: e.toString());
|
state = state.copyWith(errorMessage: e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
@@ -430,7 +433,8 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
|
|
||||||
if (forceRemote) {
|
if (forceRemote) {
|
||||||
_appendScanLog('正在获取云盘全量文件索引…');
|
_appendScanLog('正在获取云盘全量文件索引…');
|
||||||
final globalFiles = await _allGlobalRemoteFiles();
|
await refreshGlobalCloudIndex(force: true);
|
||||||
|
final globalFiles = await _cachedGlobalFiles();
|
||||||
final seen = <String>{};
|
final seen = <String>{};
|
||||||
final mediaFiles = <CloudFile>[];
|
final mediaFiles = <CloudFile>[];
|
||||||
for (final source in library.sources) {
|
for (final source in library.sources) {
|
||||||
@@ -544,6 +548,76 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
_appendScanLog('[同步识别][DEBUG] 已请求取消同步识别');
|
_appendScanLog('[同步识别][DEBUG] 已请求取消同步识别');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> refreshGlobalCloudIndex({bool force = false}) async {
|
||||||
|
if (_api == null || _refreshingCloudIndex) return;
|
||||||
|
final minutes =
|
||||||
|
(int.tryParse(
|
||||||
|
StorageManager.get<String>(
|
||||||
|
StorageKeys.cloudIndexRefreshMinutes,
|
||||||
|
) ??
|
||||||
|
'30',
|
||||||
|
) ??
|
||||||
|
30)
|
||||||
|
.clamp(5, 1440);
|
||||||
|
final lastUpdated = int.tryParse(
|
||||||
|
StorageManager.get<String>(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 = <String?, List<CloudFile>>{};
|
||||||
|
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<String>(
|
||||||
|
StorageKeys.cloudIndexRefreshMinutes,
|
||||||
|
) ??
|
||||||
|
'30',
|
||||||
|
) ??
|
||||||
|
30)
|
||||||
|
.clamp(5, 1440);
|
||||||
|
_scheduleCloudIndexRefresh(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cloudIndexTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _appendScanLog(String message, {bool isError = false}) {
|
void _appendScanLog(String message, {bool isError = false}) {
|
||||||
final logs = [
|
final logs = [
|
||||||
...state.scanLogs,
|
...state.scanLogs,
|
||||||
@@ -1494,6 +1568,12 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CloudFile>> _cachedGlobalFiles() async {
|
||||||
|
final values = await FileMetadataCache.allCachedFolderChildren();
|
||||||
|
if (values.isEmpty) return _allGlobalRemoteFiles();
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<CloudFile>> _allGlobalRemoteFilesByType({int? resType}) async {
|
Future<List<CloudFile>> _allGlobalRemoteFilesByType({int? resType}) async {
|
||||||
const pageSize = 10000;
|
const pageSize = 10000;
|
||||||
final values = <CloudFile>[];
|
final values = <CloudFile>[];
|
||||||
|
|||||||
Reference in New Issue
Block a user