diff --git a/lib/pages/media_library_page.dart b/lib/pages/media_library_page.dart index 0cc6de7..3c1229d 100644 --- a/lib/pages/media_library_page.dart +++ b/lib/pages/media_library_page.dart @@ -1281,66 +1281,17 @@ class _MediaLibraryPageState extends ConsumerState { height: (MediaQuery.sizeOf(dialogContext).height - 260) .clamp(240.0, 360.0) .toDouble(), - child: ListView.separated( - itemCount: backups.length, - separatorBuilder: (_, _) => const ShadSeparator.horizontal(), - itemBuilder: (itemContext, index) { - final backup = backups[index]; - final cs = ShadTheme.of(itemContext).colorScheme; - return LayoutBuilder( - builder: (context, constraints) => ShadButton.ghost( - width: constraints.maxWidth, - expands: false, - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 9, - ), - onPressed: () => Navigator.of(dialogContext).pop(backup), - child: Row( - children: [ - Icon( - Icons.storage_rounded, - size: 18, - color: cs.mutedForeground, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - backup.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: cs.foreground, - ), - ), - const SizedBox(height: 3), - Text( - '${backup.formattedSize} · ${backup.modifiedAt}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 11, - color: cs.mutedForeground, - ), - ), - ], - ), - ), - Icon( - Icons.chevron_right_rounded, - color: cs.mutedForeground, - ), - ], - ), - ), - ); - }, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (var index = 0; index < backups.length; index++) ...[ + _cloudBackupRestoreRow(dialogContext, backup: backups[index]), + if (index < backups.length - 1) + const ShadSeparator.horizontal(), + ], + ], + ), ), ), ), @@ -1354,6 +1305,53 @@ class _MediaLibraryPageState extends ConsumerState { } } + Widget _cloudBackupRestoreRow( + BuildContext context, { + required CloudFile backup, + }) { + final cs = ShadTheme.of(context).colorScheme; + return LayoutBuilder( + builder: (context, constraints) => ShadButton.ghost( + width: constraints.maxWidth, + expands: false, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9), + onPressed: () => Navigator.of(context).pop(backup), + child: Row( + children: [ + Icon(Icons.storage_rounded, size: 18, color: cs.mutedForeground), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + backup.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: cs.foreground, + ), + ), + const SizedBox(height: 3), + Text( + '${backup.formattedSize} · ${backup.modifiedAt}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 11, color: cs.mutedForeground), + ), + ], + ), + ), + Icon(Icons.chevron_right_rounded, color: cs.mutedForeground), + ], + ), + ), + ); + } + Future _searchTMDB(String query) async { final text = query.trim(); if (text.isEmpty || _tmdbApiKey.isEmpty) return; diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index a452e4c..f20551f 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -344,7 +344,7 @@ class MediaLibraryNotifier extends StateNotifier { 'guangya-media-', ); final backup = File( - '${temporaryDirectory.path}/media-library-${DateTime.now().millisecondsSinceEpoch}.sqlite3', + '${temporaryDirectory.path}/${_cloudBackupFileName(DateTime.now())}', ); _appendBackupLog('正在导出本地刮削数据库'); await _store.exportBackupTo(backup.path); @@ -413,6 +413,22 @@ class MediaLibraryNotifier extends StateNotifier { } Future<_CloudBackupDestination> _resolveCloudBackupDestination() async { + const folderName = '小黄鸭备份'; + final existing = await _findCloudBackupDestination(); + if (existing != null) return existing; + final folderID = _findStringDeep( + await _api!.fsCreateDir(folderName, parentID: null), + const ['fileId', 'file_id', 'resId', 'res_id', 'id'], + ); + if (folderID == null || folderID.isEmpty) { + throw Exception('无法创建云盘备份目录「$folderName」'); + } + await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folderID); + _appendBackupLog('已创建云盘备份目录:云盘根目录/$folderName'); + return _CloudBackupDestination(id: folderID, path: '云盘根目录/$folderName'); + } + + Future<_CloudBackupDestination?> _findCloudBackupDestination() async { const folderName = '小黄鸭备份'; final storedID = StorageManager.get( StorageKeys.cloudScrapedBackupFolderID, @@ -425,17 +441,9 @@ class MediaLibraryNotifier extends StateNotifier { root, ).where((file) => file.isDirectory && file.name == folderName); final folder = existing.isEmpty ? null : existing.first; - final folderID = - folder?.id ?? - _findStringDeep( - await _api!.fsCreateDir(folderName, parentID: null), - const ['fileId', 'file_id', 'resId', 'res_id', 'id'], - ); - if (folderID == null || folderID.isEmpty) { - throw Exception('无法创建云盘备份目录「$folderName」'); - } - await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folderID); - return _CloudBackupDestination(id: folderID, path: '云盘根目录/$folderName'); + if (folder == null) return null; + await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folder.id); + return _CloudBackupDestination(id: folder.id, path: '云盘根目录/$folderName'); } Future> cloudScrapedBackups() async { @@ -453,17 +461,35 @@ class MediaLibraryNotifier extends StateNotifier { ), ); try { - final response = await _api!.searchFiles( - 'media-library.sqlite3', - pageSize: 100, + final destination = await _findCloudBackupDestination(); + if (destination == null) { + _appendBackupLog('未找到云盘备份目录,当前没有可恢复的备份'); + state = state.copyWith( + cloudBackupSync: const CloudBackupSyncProgress( + phase: '未找到云盘备份', + destination: '云盘根目录/小黄鸭备份', + transferredBytes: 0, + totalBytes: 0, + isActive: false, + ), + ); + return const []; + } + _appendBackupLog('正在读取备份目录:${destination.path}'); + final response = await _api!.fsFiles( + parentID: destination.id, + pageSize: 1000, ); - final backups = _extractFiles(response) - .where( - (file) => - !file.isDirectory && - file.name.toLowerCase().endsWith('.sqlite3'), - ) - .toList(); + final backups = + _extractFiles(response) + .where( + (file) => + !file.isDirectory && + file.name.toLowerCase().endsWith('.sqlite3') && + file.name.startsWith('media-library-'), + ) + .toList() + ..sort((a, b) => b.modifiedAt.compareTo(a.modifiedAt)); _appendBackupLog('云盘备份查找完成,找到 ${backups.length} 个文件'); state = state.copyWith( cloudBackupSync: const CloudBackupSyncProgress( @@ -1190,6 +1216,12 @@ class MediaLibraryNotifier extends StateNotifier { .replaceFirst(RegExp(r'^DioException \[[^\]]+\]:\s*'), '网络请求失败:'); } + String _cloudBackupFileName(DateTime value) { + String two(int number) => number.toString().padLeft(2, '0'); + return 'media-library-${value.year}${two(value.month)}${two(value.day)}-' + '${two(value.hour)}${two(value.minute)}${two(value.second)}.sqlite3'; + } + List _loadScanHistory() { final raw = StorageManager.get(StorageKeys.mediaScanHistory); if (raw is! List) return const [];