scope cloud backups to dedicated folder
This commit is contained in:
@@ -1281,66 +1281,17 @@ class _MediaLibraryPageState extends ConsumerState<MediaLibraryPage> {
|
||||
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<MediaLibraryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
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<void> _searchTMDB(String query) async {
|
||||
final text = query.trim();
|
||||
if (text.isEmpty || _tmdbApiKey.isEmpty) return;
|
||||
|
||||
@@ -344,7 +344,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
'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<MediaLibraryState> {
|
||||
}
|
||||
|
||||
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<String>(
|
||||
StorageKeys.cloudScrapedBackupFolderID,
|
||||
@@ -425,17 +441,9 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
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<List<CloudFile>> cloudScrapedBackups() async {
|
||||
@@ -453,17 +461,35 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
),
|
||||
);
|
||||
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<MediaLibraryState> {
|
||||
.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<MediaLibraryScanLog> _loadScanHistory() {
|
||||
final raw = StorageManager.get<dynamic>(StorageKeys.mediaScanHistory);
|
||||
if (raw is! List) return const [];
|
||||
|
||||
Reference in New Issue
Block a user