generated from dellevin/template
优化备份卡顿问题
This commit is contained in:
@@ -426,6 +426,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
|
|||||||
if (direction == SyncDirection.upload) {
|
if (direction == SyncDirection.upload) {
|
||||||
// 上传:先打包,再上传
|
// 上传:先打包,再上传
|
||||||
setState(() => _syncStep = '正在打包数据...');
|
setState(() => _syncStep = '正在打包数据...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
final exportResult = await WebDAVService.instance.exportLocalData();
|
final exportResult = await WebDAVService.instance.exportLocalData();
|
||||||
if (!exportResult.success || exportResult.zipPath == null) {
|
if (!exportResult.success || exportResult.zipPath == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -436,6 +437,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
|
|||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _syncStep = '正在上传到云端...');
|
setState(() => _syncStep = '正在上传到云端...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
final result = await WebDAVService.instance.uploadExportedData(exportResult);
|
final result = await WebDAVService.instance.uploadExportedData(exportResult);
|
||||||
if (result.success && result.needReload && mounted) {
|
if (result.success && result.needReload && mounted) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
@@ -457,6 +459,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
|
|||||||
} else {
|
} else {
|
||||||
// 下载
|
// 下载
|
||||||
setState(() => _syncStep = '正在从云端下载...');
|
setState(() => _syncStep = '正在从云端下载...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
final config = await WebDAVService.instance.getConfig();
|
final config = await WebDAVService.instance.getConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
if (_syncDirection == SyncDirection.upload) {
|
if (_syncDirection == SyncDirection.upload) {
|
||||||
// 上传:先打包再上传,分步显示
|
// 上传:先打包再上传,分步显示
|
||||||
setState(() => _syncStep = '正在打包数据...');
|
setState(() => _syncStep = '正在打包数据...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
final exportResult = await WebDAVService.instance.exportLocalData();
|
final exportResult = await WebDAVService.instance.exportLocalData();
|
||||||
if (!exportResult.success || exportResult.zipPath == null) {
|
if (!exportResult.success || exportResult.zipPath == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -141,10 +142,12 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _syncStep = '正在上传到云端...');
|
setState(() => _syncStep = '正在上传到云端...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
result = await WebDAVService.instance.uploadExportedData(exportResult);
|
result = await WebDAVService.instance.uploadExportedData(exportResult);
|
||||||
} else {
|
} else {
|
||||||
// 下载
|
// 下载
|
||||||
setState(() => _syncStep = '正在从云端下载...');
|
setState(() => _syncStep = '正在从云端下载...');
|
||||||
|
await Future.delayed(Duration.zero); // 让 UI 先渲染进度动画
|
||||||
result = await WebDAVService.instance.syncData(direction: SyncDirection.download);
|
result = await WebDAVService.instance.syncData(direction: SyncDirection.download);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ class BackupService {
|
|||||||
|
|
||||||
// ─── 共享导出逻辑 ─────────────────────────────────────
|
// ─── 共享导出逻辑 ─────────────────────────────────────
|
||||||
|
|
||||||
/// 收集所有表数据和图片,构建 ZIP 字节
|
/// 收集所有表数据和图片,构建 ZIP 文件
|
||||||
Future<_ExportData> _buildExportData() async {
|
Future<_ExportData> _buildExportData() async {
|
||||||
|
// 阶段1:主线程收集数据(DB 查询、SharedPreferences 需主线程)
|
||||||
final db = await DatabaseHelper.instance.database;
|
final db = await DatabaseHelper.instance.database;
|
||||||
|
|
||||||
final movies = await db.query('movies');
|
final movies = await db.query('movies');
|
||||||
@@ -109,69 +110,25 @@ class BackupService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建 ZIP(逐文件写入磁盘,避免全部加载到内存)
|
// 阶段2:后台 isolate 执行 JSON 编码 + ZIP 压缩(避免阻塞主线程动画)
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final tempZipPath = path.join(tempDir.path, 'mooknote_backup_temp.zip');
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
final encoder = ZipFileEncoder();
|
|
||||||
encoder.create(tempZipPath);
|
|
||||||
|
|
||||||
try {
|
final result = await compute(_buildZipInIsolate, _ZipComputeParams(
|
||||||
// data.json
|
backupData: backupData,
|
||||||
final jsonString = const JsonEncoder.withIndent(' ').convert(backupData);
|
imagePaths: imagePaths.toList(),
|
||||||
final jsonBytes = Uint8List.fromList(utf8.encode(jsonString));
|
tempDirPath: tempDir.path,
|
||||||
final dataFile = File(path.join(tempDir.path, 'mooknote_data.json'));
|
appDirPath: appDir.path,
|
||||||
await dataFile.writeAsBytes(jsonBytes);
|
));
|
||||||
encoder.addFile(dataFile, 'data.json');
|
|
||||||
await dataFile.delete();
|
|
||||||
|
|
||||||
int imageCount = 0;
|
return _ExportData(
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
zipPath: result.zipPath,
|
||||||
final imagesRoot = path.join(appDir.path, 'images');
|
movieCount: movies.length,
|
||||||
|
bookCount: books.length,
|
||||||
for (final imagePath in imagePaths) {
|
noteCount: notes.length,
|
||||||
final file = File(imagePath);
|
imageCount: result.imageCount,
|
||||||
if (await file.exists()) {
|
epubCount: result.epubCount,
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 手动导出 ─────────────────────────────────────────
|
// ─── 手动导出 ─────────────────────────────────────────
|
||||||
@@ -912,3 +869,83 @@ class ImportResult {
|
|||||||
return stats!.entries.map((e) => '${e.key}: ${e.value}').join(',');
|
return stats!.entries.map((e) => '${e.key}: ${e.value}').join(',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── compute isolate 参数和函数 ──────────────────────────
|
||||||
|
|
||||||
|
class _ZipComputeParams {
|
||||||
|
final Map<String, dynamic> backupData;
|
||||||
|
final List<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user