diff --git a/lib/pages/home/main_content_page.dart b/lib/pages/home/main_content_page.dart index 3fa67d9..a93f32d 100644 --- a/lib/pages/home/main_content_page.dart +++ b/lib/pages/home/main_content_page.dart @@ -426,6 +426,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> { if (direction == SyncDirection.upload) { // 上传:先打包,再上传 setState(() => _syncStep = '正在打包数据...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 final exportResult = await WebDAVService.instance.exportLocalData(); if (!exportResult.success || exportResult.zipPath == null) { if (mounted) { @@ -436,6 +437,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> { } if (!mounted) return; setState(() => _syncStep = '正在上传到云端...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 final result = await WebDAVService.instance.uploadExportedData(exportResult); if (result.success && result.needReload && mounted) { final provider = context.read(); @@ -457,6 +459,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> { } else { // 下载 setState(() => _syncStep = '正在从云端下载...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 final config = await WebDAVService.instance.getConfig(); if (config == null) { if (mounted) { diff --git a/lib/pages/sync/webdav_sync_page.dart b/lib/pages/sync/webdav_sync_page.dart index 8f9ad86..39ce11d 100644 --- a/lib/pages/sync/webdav_sync_page.dart +++ b/lib/pages/sync/webdav_sync_page.dart @@ -131,6 +131,7 @@ class _WebDAVSyncPageState extends State { if (_syncDirection == SyncDirection.upload) { // 上传:先打包再上传,分步显示 setState(() => _syncStep = '正在打包数据...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 final exportResult = await WebDAVService.instance.exportLocalData(); if (!exportResult.success || exportResult.zipPath == null) { if (mounted) { @@ -141,10 +142,12 @@ class _WebDAVSyncPageState extends State { } if (!mounted) return; setState(() => _syncStep = '正在上传到云端...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 result = await WebDAVService.instance.uploadExportedData(exportResult); } else { // 下载 setState(() => _syncStep = '正在从云端下载...'); + await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画 result = await WebDAVService.instance.syncData(direction: SyncDirection.download); } diff --git a/lib/services/sync/backup_service.dart b/lib/services/sync/backup_service.dart index 3c64d58..392d308 100644 --- a/lib/services/sync/backup_service.dart +++ b/lib/services/sync/backup_service.dart @@ -19,8 +19,9 @@ class BackupService { // ─── 共享导出逻辑 ───────────────────────────────────── - /// 收集所有表数据和图片,构建 ZIP 字节 + /// 收集所有表数据和图片,构建 ZIP 文件 Future<_ExportData> _buildExportData() async { + // 阶段1:主线程收集数据(DB 查询、SharedPreferences 需主线程) final db = await DatabaseHelper.instance.database; final movies = await db.query('movies'); @@ -109,69 +110,25 @@ class BackupService { }, }; - // 创建 ZIP(逐文件写入磁盘,避免全部加载到内存) + // 阶段2:后台 isolate 执行 JSON 编码 + ZIP 压缩(避免阻塞主线程动画) final tempDir = await getTemporaryDirectory(); - final tempZipPath = path.join(tempDir.path, 'mooknote_backup_temp.zip'); - final encoder = ZipFileEncoder(); - encoder.create(tempZipPath); + final appDir = await getApplicationDocumentsDirectory(); - try { - // data.json - final jsonString = const JsonEncoder.withIndent(' ').convert(backupData); - final jsonBytes = Uint8List.fromList(utf8.encode(jsonString)); - final dataFile = File(path.join(tempDir.path, 'mooknote_data.json')); - await dataFile.writeAsBytes(jsonBytes); - encoder.addFile(dataFile, 'data.json'); - await dataFile.delete(); + final result = await compute(_buildZipInIsolate, _ZipComputeParams( + backupData: backupData, + imagePaths: imagePaths.toList(), + tempDirPath: tempDir.path, + appDirPath: appDir.path, + )); - int imageCount = 0; - final appDir = await getApplicationDocumentsDirectory(); - final imagesRoot = path.join(appDir.path, 'images'); - - for (final imagePath in imagePaths) { - final file = File(imagePath); - if (await file.exists()) { - String relativePath; - if (imagePath.startsWith(imagesRoot)) { - relativePath = imagePath.substring(imagesRoot.length + 1); - } else { - relativePath = path.basename(imagePath); - } - encoder.addFile(file, 'images/$relativePath'); - imageCount++; - } - } - - // 收集 epub_books 目录下的 epub 文件 - int epubCount = 0; - final epubRoot = path.join(appDir.path, 'epub_books'); - final epubDir = Directory(epubRoot); - if (await epubDir.exists()) { - await for (final entity in epubDir.list(recursive: true)) { - if (entity is File) { - final relativePath = entity.path.substring(epubRoot.length + 1); - encoder.addFile(entity, 'epub_books/$relativePath'); - epubCount++; - } - } - } - - encoder.close(); - - // 返回 zip 文件路径,不再读入内存 - return _ExportData( - zipPath: tempZipPath, - movieCount: movies.length, - bookCount: books.length, - noteCount: notes.length, - imageCount: imageCount, - epubCount: epubCount, - ); - } catch (e) { - encoder.close(); - try { await File(tempZipPath).delete(); } catch (_) {} - rethrow; - } + return _ExportData( + zipPath: result.zipPath, + movieCount: movies.length, + bookCount: books.length, + noteCount: notes.length, + imageCount: result.imageCount, + epubCount: result.epubCount, + ); } // ─── 手动导出 ───────────────────────────────────────── @@ -912,3 +869,83 @@ class ImportResult { return stats!.entries.map((e) => '${e.key}: ${e.value}').join(','); } } + +// ─── compute isolate 参数和函数 ────────────────────────── + +class _ZipComputeParams { + final Map backupData; + final List imagePaths; + final String tempDirPath; + final String appDirPath; + + _ZipComputeParams({ + required this.backupData, + required this.imagePaths, + required this.tempDirPath, + required this.appDirPath, + }); +} + +class _ZipComputeResult { + final String? zipPath; + final int imageCount; + final int epubCount; + + _ZipComputeResult({this.zipPath, required this.imageCount, required this.epubCount}); +} + +/// 在后台 isolate 中执行 JSON 编码 + ZIP 压缩,避免阻塞主线程 +_ZipComputeResult _buildZipInIsolate(_ZipComputeParams params) { + final tempZipPath = path.join(params.tempDirPath, 'mooknote_backup_temp.zip'); + final encoder = ZipFileEncoder(); + encoder.create(tempZipPath); + + try { + // data.json + final jsonString = const JsonEncoder.withIndent(' ').convert(params.backupData); + final jsonBytes = Uint8List.fromList(utf8.encode(jsonString)); + final dataFile = File(path.join(params.tempDirPath, 'mooknote_data.json')); + dataFile.writeAsBytesSync(jsonBytes); + encoder.addFile(dataFile, 'data.json'); + dataFile.deleteSync(); + + int imageCount = 0; + final imagesRoot = path.join(params.appDirPath, 'images'); + + for (final imagePath in params.imagePaths) { + final file = File(imagePath); + if (file.existsSync()) { + String relativePath; + if (imagePath.startsWith(imagesRoot)) { + relativePath = imagePath.substring(imagesRoot.length + 1); + } else { + relativePath = path.basename(imagePath); + } + encoder.addFile(file, 'images/$relativePath'); + imageCount++; + } + } + + // 收集 epub_books 目录下的 epub 文件 + int epubCount = 0; + final epubRoot = path.join(params.appDirPath, 'epub_books'); + final epubDir = Directory(epubRoot); + if (epubDir.existsSync()) { + for (final entity in epubDir.listSync(recursive: true)) { + if (entity is File) { + final relativePath = entity.path.substring(epubRoot.length + 1); + encoder.addFile(entity, 'epub_books/$relativePath'); + epubCount++; + } + } + } + + encoder.close(); + + return _ZipComputeResult(zipPath: tempZipPath, imageCount: imageCount, epubCount: epubCount); + } catch (e) { + encoder.close(); + try { File(tempZipPath).deleteSync(); } catch (_) {} + rethrow; + } +}