windows版本初步完成

This commit is contained in:
DelLevin-Home
2026-07-13 19:39:01 +08:00
parent 121cbf8b0b
commit 6c30026e32
37 changed files with 5583 additions and 624 deletions

View File

@@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
import '../../data/database_helper.dart';
import '../../utils/user_prefs.dart';
import '../../utils/image_path_helper.dart';
/// 数据备份服务 - 支持导出和导入数据(包含图片)
class BackupService {
@@ -17,6 +18,11 @@ class BackupService {
BackupService._init();
/// 获取应用数据根目录(统一路径)
Future<String> _getAppDir() async {
return await ImagePathHelper.getAppDir();
}
// ─── 共享导出逻辑 ─────────────────────────────────────
/// 收集所有表数据和图片,构建 ZIP 文件
@@ -112,13 +118,25 @@ class BackupService {
// 阶段2后台 isolate 执行 JSON 编码 + ZIP 压缩(避免阻塞主线程动画)
final tempDir = await getTemporaryDirectory();
final appDir = await getApplicationDocumentsDirectory();
final appDirPath = await _getAppDir();
// DEBUG: 诊断 Windows 导出图片缺失问题
final imagesRootPath = path.join(appDirPath, 'images');
debugPrint('[BackupService] DEBUG appDirPath=$appDirPath');
debugPrint('[BackupService] DEBUG imagesRoot=$imagesRootPath');
debugPrint('[BackupService] DEBUG imagePaths count=${imagePaths.length}');
for (final ip in imagePaths) {
final f = File(ip);
final exists = f.existsSync();
final match = ip.startsWith(imagesRootPath);
debugPrint('[BackupService] DEBUG path=$ip exists=$exists startsWithImagesRoot=$match');
}
final result = await compute(_buildZipInIsolate, _ZipComputeParams(
backupData: backupData,
imagePaths: imagePaths.toList(),
tempDirPath: tempDir.path,
appDirPath: appDir.path,
appDirPath: appDirPath,
));
return _ExportData(
@@ -144,19 +162,17 @@ class BackupService {
String? finalPath;
try {
// FilePicker.saveFile 需要 bytes这里必须读入内存
final zipBytes = await zipFile.readAsBytes();
final outputPath = await FilePicker.platform.saveFile(
dialogTitle: '保存备份文件',
fileName: fileName,
type: FileType.custom,
allowedExtensions: ['zip'],
bytes: zipBytes,
);
if (outputPath == null) {
await zipFile.delete();
return ExportResult.cancelled();
}
await zipFile.copy(outputPath);
finalPath = outputPath;
} catch (e) {
// FilePicker 不可用时,复制到临时目录
@@ -239,8 +255,8 @@ class BackupService {
backupData = jsonDecode(utf8.decode(dataFile.content as List<int>)) as Map<String, dynamic>;
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(path.join(appDir.path, 'images'));
final appDirPath = await _getAppDir();
final imagesDir = Directory(path.join(appDirPath, 'images'));
if (!await imagesDir.exists()) await imagesDir.create(recursive: true);
for (final archiveFile in archive) {
@@ -254,7 +270,7 @@ class BackupService {
imageCount++;
} else if (archiveFile.name.startsWith('epub_books/')) {
final relativePath = archiveFile.name.substring(12);
final epubDir = Directory(path.join(appDir.path, 'epub_books'));
final epubDir = Directory(path.join(appDirPath, 'epub_books'));
if (!await epubDir.exists()) await epubDir.create(recursive: true);
final outputFile = File(path.join(epubDir.path, relativePath));
if (!await outputFile.parent.exists()) await outputFile.parent.create(recursive: true);
@@ -401,8 +417,8 @@ class BackupService {
final epubFileMap = <String, String>{};
int imageCount = 0;
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(path.join(appDir.path, 'images'));
final appDirPath = await _getAppDir();
final imagesDir = Directory(path.join(appDirPath, 'images'));
if (!await imagesDir.exists()) await imagesDir.create(recursive: true);
for (final archiveFile in archive) {
@@ -415,7 +431,7 @@ class BackupService {
imageCount++;
} else if (archiveFile.name.startsWith('epub_books/')) {
final relativePath = archiveFile.name.substring(12);
final epubDir = Directory(path.join(appDir.path, 'epub_books'));
final epubDir = Directory(path.join(appDirPath, 'epub_books'));
if (!await epubDir.exists()) await epubDir.create(recursive: true);
final outputFile = File(path.join(epubDir.path, relativePath));
if (!await outputFile.parent.exists()) await outputFile.parent.create(recursive: true);
@@ -627,12 +643,11 @@ class BackupService {
/// 将绝对路径转为 images/ 下的相对路径(用于 imagePathMap key
String _toRelativePath(String absolutePath) {
// 统一为正斜杠,避免 Windows 反斜杠与 zip 内正斜杠不匹配
final normalized = absolutePath.replaceAll('\\', '/');
// 尝试提取 images/ 后面的部分
final idx = absolutePath.indexOf('/images/');
if (idx >= 0) return absolutePath.substring(idx + 8); // skip '/images/'
// Windows 路径
final winIdx = absolutePath.indexOf('\\images\\');
if (winIdx >= 0) return absolutePath.substring(winIdx + 8);
final idx = normalized.indexOf('/images/');
if (idx >= 0) return normalized.substring(idx + 8); // skip '/images/'
return path.basename(absolutePath);
}
@@ -683,10 +698,9 @@ class BackupService {
/// 从绝对路径中提取 epub_books/ 下的相对路径
String? _toEpubRelativePath(String absolutePath) {
final idx = absolutePath.indexOf('/epub_books/');
if (idx >= 0) return absolutePath.substring(idx + 13); // skip '/epub_books/'
final winIdx = absolutePath.indexOf('\\epub_books\\');
if (winIdx >= 0) return absolutePath.substring(winIdx + 13);
final normalized = absolutePath.replaceAll('\\', '/');
final idx = normalized.indexOf('/epub_books/');
if (idx >= 0) return normalized.substring(idx + 13); // skip '/epub_books/'
return null;
}
@@ -910,14 +924,16 @@ _ZipComputeResult _buildZipInIsolate(_ZipComputeParams params) {
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()) {
// 统一用 /images/ 子串匹配提取相对路径,兼容旧路径(路径前缀可能不含 mooknote 子目录)
final normalized = imagePath.replaceAll('\\', '/');
final idx = normalized.indexOf('/images/');
String relativePath;
if (imagePath.startsWith(imagesRoot)) {
relativePath = imagePath.substring(imagesRoot.length + 1);
if (idx >= 0) {
relativePath = normalized.substring(idx + 8); // skip '/images/'
} else {
relativePath = path.basename(imagePath);
}