generated from dellevin/template
代码优化,epub阅读标签
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -7,7 +8,7 @@ import '../models/data_models.dart';
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
static Database? _database;
|
||||
static bool _isReopening = false;
|
||||
static Completer<void>? _reopenCompleter;
|
||||
|
||||
DatabaseHelper._init();
|
||||
|
||||
@@ -19,24 +20,30 @@ class DatabaseHelper {
|
||||
|
||||
/// 重新打开数据库(用于 WebDAV 同步后)
|
||||
Future<void> reopenDatabase() async {
|
||||
// 防止并发重开
|
||||
if (_isReopening) return;
|
||||
_isReopening = true;
|
||||
// 如果已有重开在进行,等待它完成即可
|
||||
if (_reopenCompleter != null) {
|
||||
return _reopenCompleter!.future;
|
||||
}
|
||||
_reopenCompleter = Completer<void>();
|
||||
try {
|
||||
if (_database != null) {
|
||||
await _database!.close();
|
||||
_database = null;
|
||||
}
|
||||
_database = await _initDB('mooknote.db');
|
||||
_reopenCompleter!.complete();
|
||||
} catch (e) {
|
||||
_reopenCompleter!.completeError(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isReopening = false;
|
||||
_reopenCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Database> get database async {
|
||||
// 等待重开完成
|
||||
while (_isReopening) {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
// 如果正在重开,等待完成(无忙等待)
|
||||
if (_reopenCompleter != null) {
|
||||
await _reopenCompleter!.future;
|
||||
}
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDB('mooknote.db');
|
||||
@@ -49,7 +56,7 @@ class DatabaseHelper {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 24,
|
||||
version: 25,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
@@ -215,6 +222,13 @@ class DatabaseHelper {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN book_id TEXT DEFAULT ''");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 25) {
|
||||
// movies 添加 category 列(影视分类)
|
||||
final cols = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
if (!cols.any((col) => col['name'] == 'category')) {
|
||||
await db.execute("ALTER TABLE movies ADD COLUMN category TEXT NOT NULL DEFAULT 'movie'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V11(添加ISBN和出版时间字段)
|
||||
@@ -531,12 +545,13 @@ class DatabaseHelper {
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
|
||||
// 迁移旧数据(尽可能保留)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
@@ -585,6 +600,7 @@ class DatabaseHelper {
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
watch_date TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
||||
@@ -22,9 +22,9 @@ class EpubService {
|
||||
|
||||
// 复制 EPUB 到永久存储(FilePicker 临时文件会被清理)
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final booksDir = Directory(p.join(appDir.path, 'epub_books'));
|
||||
if (!await booksDir.exists()) await booksDir.create(recursive: true);
|
||||
final permanentPath = p.join(booksDir.path, '$bookId.epub');
|
||||
final bookDir = Directory(p.join(appDir.path, 'epub_books', bookId));
|
||||
if (!await bookDir.exists()) await bookDir.create(recursive: true);
|
||||
final permanentPath = p.join(bookDir.path, 'book.epub');
|
||||
await File(sourcePath).copy(permanentPath);
|
||||
|
||||
// 从永久副本解析
|
||||
@@ -97,9 +97,9 @@ class EpubService {
|
||||
final coverFile = File(p.join(extractDir, coverRelPath));
|
||||
if (!await coverFile.exists()) return null;
|
||||
|
||||
// 保存到应用文档目录
|
||||
// 保存到 epub_books/{bookId}/ 目录下
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final coverDir = p.join(appDir.path, 'images', 'books', bookId);
|
||||
final coverDir = p.join(appDir.path, 'epub_books', bookId);
|
||||
await Directory(coverDir).create(recursive: true);
|
||||
final ext = p.extension(coverFile.path).toLowerCase();
|
||||
final destPath = p.join(coverDir, 'cover$ext');
|
||||
@@ -142,19 +142,11 @@ class EpubService {
|
||||
if (await dir.exists()) await dir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
|
||||
// 清理封面
|
||||
// 清理 epub_books/{bookId}/ 目录(epub + 封面)
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final coverDir = p.join(appDir.path, 'images', 'books', bookId);
|
||||
final dir = Directory(coverDir);
|
||||
if (await dir.exists()) await dir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
|
||||
// 清理永久 EPUB 文件
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final epubFile = File(p.join(appDir.path, 'epub_books', '$bookId.epub'));
|
||||
if (await epubFile.exists()) await epubFile.delete();
|
||||
final bookDir = Directory(p.join(appDir.path, 'epub_books', bookId));
|
||||
if (await bookDir.exists()) await bookDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
|
||||
// 软删除数据库记录
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'reader_scripts.dart';
|
||||
|
||||
/// 阅读器主题预设
|
||||
class ReaderThemePreset {
|
||||
final String name;
|
||||
final Color surface;
|
||||
final Color onSurface;
|
||||
final bool isDark;
|
||||
|
||||
const ReaderThemePreset({
|
||||
required this.name,
|
||||
required this.surface,
|
||||
required this.onSurface,
|
||||
this.isDark = false,
|
||||
});
|
||||
}
|
||||
|
||||
class ReaderThemePresets {
|
||||
static const List<ReaderThemePreset> presets = [
|
||||
ReaderThemePreset(name: '跟随App', surface: Colors.white, onSurface: Colors.black),
|
||||
ReaderThemePreset(name: '纯白', surface: Color(0xFFFFFFFF), onSurface: Color(0xFF1A1A1A)),
|
||||
ReaderThemePreset(name: '护眼', surface: Color(0xFFF4ECD8), onSurface: Color(0xFF5B4636)),
|
||||
ReaderThemePreset(name: '抹茶', surface: Color(0xFFF6FBF5), onSurface: Color(0xFF2E3E2E)),
|
||||
ReaderThemePreset(name: '樱花', surface: Color(0xFFFFF8F8), onSurface: Color(0xFF4A2030)),
|
||||
ReaderThemePreset(name: '午夜蓝', surface: Color(0xFFF7F9FC), onSurface: Color(0xFF1A2A3A)),
|
||||
ReaderThemePreset(name: '深色', surface: Color(0xFF191919), onSurface: Color(0xFFD4D4D4), isDark: true),
|
||||
ReaderThemePreset(name: '深色护眼', surface: Color(0xFF1C1A18), onSurface: Color(0xFFC8B8A0), isDark: true),
|
||||
ReaderThemePreset(name: '咖啡', surface: Color(0xFFFCF8F3), onSurface: Color(0xFF3E2E1E)),
|
||||
];
|
||||
}
|
||||
|
||||
class EpubTheme {
|
||||
final double zoom;
|
||||
final bool shouldOverrideTextColor;
|
||||
|
||||
@@ -133,4 +133,32 @@ class ReaderDao {
|
||||
final db = await _db.database;
|
||||
return db.delete('book_annotations', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
// ─── bookmarks ──────────────────────────────────────────────────
|
||||
|
||||
/// 获取某本书的所有书签
|
||||
Future<List<Map<String, dynamic>>> getBookmarksByBookId(String bookId) async {
|
||||
final db = await _db.database;
|
||||
return db.query(
|
||||
'book_annotations',
|
||||
where: 'book_id = ? AND type = ?',
|
||||
whereArgs: [bookId, 'bookmark'],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
}
|
||||
|
||||
/// 插入书签
|
||||
Future<int> insertBookmark(Map<String, dynamic> bookmark) async {
|
||||
final db = await _db.database;
|
||||
return db.insert('book_annotations', {
|
||||
...bookmark,
|
||||
'type': 'bookmark',
|
||||
});
|
||||
}
|
||||
|
||||
/// 删除书签
|
||||
Future<int> deleteBookmark(int id) async {
|
||||
final db = await _db.database;
|
||||
return db.delete('book_annotations', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@ class ReaderSettings {
|
||||
/// When true, volume up/down keys turn pages in the reader.
|
||||
final bool volumeKeyTurnsPage;
|
||||
|
||||
/// Reader theme preset index (0 = follow app, 1-8 = presets, 9 = custom).
|
||||
final int themeIndex;
|
||||
|
||||
/// Custom background color (ARGB int), used when themeIndex == 9.
|
||||
final int customBgColor;
|
||||
|
||||
/// Custom text color (ARGB int), used when themeIndex == 9.
|
||||
final int customTextColor;
|
||||
|
||||
const ReaderSettings({
|
||||
this.zoom = 1.0,
|
||||
this.followAppTheme = true,
|
||||
@@ -40,6 +49,9 @@ class ReaderSettings {
|
||||
this.fontFileName,
|
||||
this.overrideFontFamily = false,
|
||||
this.volumeKeyTurnsPage = false,
|
||||
this.themeIndex = 0,
|
||||
this.customBgColor = 0xFFFFFFFF,
|
||||
this.customTextColor = 0xFF1A1A1A,
|
||||
});
|
||||
|
||||
// Sentinel: lets copyWith(fontFileName: null) mean "set to null" rather than
|
||||
@@ -58,6 +70,9 @@ class ReaderSettings {
|
||||
Object? fontFileName = _kUnset,
|
||||
bool? overrideFontFamily,
|
||||
bool? volumeKeyTurnsPage,
|
||||
int? themeIndex,
|
||||
int? customBgColor,
|
||||
int? customTextColor,
|
||||
}) {
|
||||
return ReaderSettings(
|
||||
zoom: zoom ?? this.zoom,
|
||||
@@ -73,21 +88,78 @@ class ReaderSettings {
|
||||
: fontFileName as String?,
|
||||
overrideFontFamily: overrideFontFamily ?? this.overrideFontFamily,
|
||||
volumeKeyTurnsPage: volumeKeyTurnsPage ?? this.volumeKeyTurnsPage,
|
||||
themeIndex: themeIndex ?? this.themeIndex,
|
||||
customBgColor: customBgColor ?? this.customBgColor,
|
||||
customTextColor: customTextColor ?? this.customTextColor,
|
||||
);
|
||||
}
|
||||
|
||||
EpubTheme toEpubTheme(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
ColorScheme colorScheme;
|
||||
bool shouldOverride = true;
|
||||
|
||||
if (themeIndex == 0) {
|
||||
// 跟随 App 主题
|
||||
colorScheme = Theme.of(context).colorScheme;
|
||||
} else {
|
||||
Color bg;
|
||||
Color text;
|
||||
bool isDark;
|
||||
|
||||
if (themeIndex == 9) {
|
||||
// 自定义颜色
|
||||
bg = Color(customBgColor);
|
||||
text = Color(customTextColor);
|
||||
isDark = ThemeData.estimateBrightnessForColor(bg) == Brightness.dark;
|
||||
} else if (themeIndex >= 1 &&
|
||||
themeIndex <= ReaderThemePresets.presets.length) {
|
||||
final preset = ReaderThemePresets.presets[themeIndex];
|
||||
bg = preset.surface;
|
||||
text = preset.onSurface;
|
||||
isDark = preset.isDark;
|
||||
} else {
|
||||
colorScheme = Theme.of(context).colorScheme;
|
||||
return EpubTheme(
|
||||
zoom: zoom,
|
||||
shouldOverrideTextColor: true,
|
||||
colorScheme: colorScheme,
|
||||
padding: EdgeInsets.only(
|
||||
top: marginTop, bottom: marginBottom,
|
||||
left: marginLeft, right: marginRight,
|
||||
),
|
||||
fontFileName: fontFileName,
|
||||
overrideFontFamily: overrideFontFamily,
|
||||
);
|
||||
}
|
||||
|
||||
colorScheme = ColorScheme(
|
||||
brightness: isDark ? Brightness.dark : Brightness.light,
|
||||
primary: text,
|
||||
onPrimary: bg,
|
||||
secondary: text,
|
||||
onSecondary: bg,
|
||||
error: const Color(0xFFDC2626),
|
||||
onError: bg,
|
||||
surface: bg,
|
||||
onSurface: text,
|
||||
surfaceContainerHighest: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFF0F0F0),
|
||||
surfaceContainerHigh: isDark ? const Color(0xFF222222) : const Color(0xFFFAFAFA),
|
||||
surfaceContainer: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F5),
|
||||
surfaceContainerLow: isDark ? const Color(0xFF1A1A1A) : const Color(0xFFFAFAFA),
|
||||
outline: isDark ? const Color(0xFF444444) : const Color(0xFFCCCCCC),
|
||||
outlineVariant: isDark ? const Color(0xFF333333) : const Color(0xFFE5E5E5),
|
||||
onSurfaceVariant: isDark ? const Color(0xFFAAAAAA) : const Color(0xFF666666),
|
||||
primaryContainer: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFF0F0F0),
|
||||
);
|
||||
}
|
||||
|
||||
return EpubTheme(
|
||||
zoom: zoom,
|
||||
shouldOverrideTextColor: true,
|
||||
shouldOverrideTextColor: shouldOverride,
|
||||
colorScheme: colorScheme,
|
||||
padding: EdgeInsets.only(
|
||||
top: marginTop,
|
||||
bottom: marginBottom,
|
||||
left: marginLeft,
|
||||
right: marginRight,
|
||||
top: marginTop, bottom: marginBottom,
|
||||
left: marginLeft, right: marginRight,
|
||||
),
|
||||
fontFileName: fontFileName,
|
||||
overrideFontFamily: overrideFontFamily,
|
||||
@@ -115,6 +187,9 @@ class ReaderSettings {
|
||||
}
|
||||
await prefs.setBool('${_kPrefix}overrideFontFamily', overrideFontFamily);
|
||||
await prefs.setBool('${_kPrefix}volumeKeyTurnsPage', volumeKeyTurnsPage);
|
||||
await prefs.setInt('${_kPrefix}themeIndex', themeIndex);
|
||||
await prefs.setInt('${_kPrefix}customBgColor', customBgColor);
|
||||
await prefs.setInt('${_kPrefix}customTextColor', customTextColor);
|
||||
}
|
||||
|
||||
static Future<ReaderSettings> load() async {
|
||||
|
||||
@@ -100,4 +100,36 @@ class MovieReviewDao {
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 获取所有已删除的影评
|
||||
Future<List<MovieReview>> getDeletedReviews() => _wrap('getDeletedReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'is_deleted = 1',
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 恢复已删除的影评
|
||||
Future<void> restoreReview(String id) => _wrap('restoreReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'movie_reviews',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 彻底删除影评
|
||||
Future<void> permanentDeleteReview(String id) => _wrap('permanentDeleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'movie_reviews',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,35 +105,54 @@ class AutoBackupService {
|
||||
/// 获取备份目录(下载目录/mooknote)
|
||||
Future<Directory?> _getBackupDirectory() async {
|
||||
try {
|
||||
// 尝试获取下载目录
|
||||
Directory? downloadDir;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
// Android: 使用外部存储的下载目录
|
||||
// 优先级 1: 官方 API 获取下载目录
|
||||
try {
|
||||
final dirs = await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads,
|
||||
);
|
||||
if (dirs != null && dirs.isNotEmpty) {
|
||||
return Directory('${dirs.first.path}/$_backupDirName');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 优先级 2: 标准路径直接拼
|
||||
final standardPath = '/storage/emulated/0/Download/$_backupDirName';
|
||||
final standardDir = Directory(standardPath);
|
||||
if (await standardDir.parent.exists()) {
|
||||
return standardDir;
|
||||
}
|
||||
|
||||
// 优先级 3: 从外部存储路径推导(旧逻辑兜底)
|
||||
final externalDir = await getExternalStorageDirectory();
|
||||
if (externalDir != null) {
|
||||
// 通常路径是 /storage/emulated/0/Android/data/.../files
|
||||
// 我们需要找到真正的下载目录
|
||||
final path = externalDir.path;
|
||||
final downloadPath = path.replaceAll(
|
||||
'/Android/data/${externalDir.uri.pathSegments[externalDir.uri.pathSegments.length - 3]}/files',
|
||||
'/Download',
|
||||
);
|
||||
downloadDir = Directory('$downloadPath/$_backupDirName');
|
||||
final segments = externalDir.uri.pathSegments;
|
||||
if (segments.length >= 3) {
|
||||
final pkg = segments[segments.length - 3];
|
||||
final downloadPath = externalDir.path.replaceAll(
|
||||
'/Android/data/$pkg/files',
|
||||
'/Download',
|
||||
);
|
||||
return Directory('$downloadPath/$_backupDirName');
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级 4: 降级到 app 内部目录
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
return Directory('${appDir.path}/$_backupDirName');
|
||||
|
||||
} else if (Platform.isIOS) {
|
||||
// iOS: 使用文档目录
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
downloadDir = Directory('${docDir.path}/$_backupDirName');
|
||||
return Directory('${docDir.path}/$_backupDirName');
|
||||
} else {
|
||||
// 桌面端: 使用下载目录
|
||||
// 桌面端
|
||||
final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||
if (home != null) {
|
||||
downloadDir = Directory('$home/Downloads/$_backupDirName');
|
||||
return Directory('$home/Downloads/$_backupDirName');
|
||||
}
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
return Directory('${docDir.path}/$_backupDirName');
|
||||
}
|
||||
|
||||
return downloadDir;
|
||||
} catch (e) {
|
||||
debugPrint('AutoBackup: 获取备份目录失败 - $e');
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -89,57 +88,73 @@ class BackupService {
|
||||
},
|
||||
};
|
||||
|
||||
// 创建 ZIP
|
||||
final archive = Archive();
|
||||
final jsonString = const JsonEncoder.withIndent(' ').convert(backupData);
|
||||
final jsonBytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
archive.addFile(ArchiveFile('data.json', jsonBytes.length, jsonBytes));
|
||||
// 创建 ZIP(逐文件写入磁盘,避免全部加载到内存)
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempZipPath = path.join(tempDir.path, 'mooknote_backup_temp.zip');
|
||||
final encoder = ZipFileEncoder();
|
||||
encoder.create(tempZipPath);
|
||||
|
||||
int imageCount = 0;
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final imagesRoot = path.join(appDir.path, 'images');
|
||||
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();
|
||||
|
||||
for (final imagePath in imagePaths) {
|
||||
final file = File(imagePath);
|
||||
if (await file.exists()) {
|
||||
final bytes = await file.readAsBytes();
|
||||
String relativePath;
|
||||
if (imagePath.startsWith(imagesRoot)) {
|
||||
relativePath = imagePath.substring(imagesRoot.length + 1);
|
||||
} else {
|
||||
relativePath = path.basename(imagePath);
|
||||
}
|
||||
archive.addFile(ArchiveFile('images/$relativePath', bytes.length, bytes));
|
||||
imageCount++;
|
||||
}
|
||||
}
|
||||
int imageCount = 0;
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final imagesRoot = path.join(appDir.path, 'images');
|
||||
|
||||
// 收集 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 bytes = await entity.readAsBytes();
|
||||
final relativePath = entity.path.substring(epubRoot.length + 1);
|
||||
archive.addFile(ArchiveFile('epub_books/$relativePath', bytes.length, bytes));
|
||||
epubCount++;
|
||||
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 文件
|
||||
final zipFile = File(tempZipPath);
|
||||
final zipBytes = await zipFile.readAsBytes();
|
||||
await zipFile.delete();
|
||||
|
||||
return _ExportData(
|
||||
zipBytes: Uint8List.fromList(zipBytes),
|
||||
movieCount: movies.length,
|
||||
bookCount: books.length,
|
||||
noteCount: notes.length,
|
||||
imageCount: imageCount,
|
||||
epubCount: epubCount,
|
||||
);
|
||||
} catch (e) {
|
||||
encoder.close();
|
||||
try { await File(tempZipPath).delete(); } catch (_) {}
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final zipBytes = ZipEncoder().encode(archive);
|
||||
if (zipBytes == null) throw Exception('压缩备份文件失败');
|
||||
|
||||
return _ExportData(
|
||||
zipBytes: Uint8List.fromList(zipBytes),
|
||||
movieCount: movies.length,
|
||||
bookCount: books.length,
|
||||
noteCount: notes.length,
|
||||
imageCount: imageCount,
|
||||
epubCount: epubCount,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 手动导出 ─────────────────────────────────────────
|
||||
|
||||
@@ -311,11 +311,16 @@ class WebDAVService {
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_lastSyncKey, DateTime.now().toIso8601String());
|
||||
|
||||
// 仅在上传成功或下载成功时记录同步时间
|
||||
final bool anySuccess = uploadedFiles > 0 || downloadedFiles > 0;
|
||||
if (anySuccess) {
|
||||
await prefs.setString(_lastSyncKey, DateTime.now().toIso8601String());
|
||||
}
|
||||
|
||||
return SyncResult(
|
||||
success: true,
|
||||
message: '同步完成',
|
||||
success: anySuccess,
|
||||
message: anySuccess ? '同步完成' : '同步未完成,未传输任何数据',
|
||||
lastSyncTime: DateTime.now(),
|
||||
uploadedFiles: uploadedFiles,
|
||||
downloadedFiles: downloadedFiles,
|
||||
|
||||
@@ -127,6 +127,44 @@ class AppTheme {
|
||||
selectedLabelStyle: TextStyle(fontFamily: _fontFamily, fontSize: 11, fontWeight: _medium),
|
||||
unselectedLabelStyle: TextStyle(fontFamily: _fontFamily, fontSize: 11, fontWeight: _regular, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
headlineLarge: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 32, fontWeight: _semibold,
|
||||
color: scheme.onSurface, letterSpacing: 0, height: 1.2,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 24, fontWeight: _semibold,
|
||||
color: scheme.onSurface, letterSpacing: 0, height: 1.3,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 20, fontWeight: _semibold,
|
||||
color: scheme.onSurface, letterSpacing: 0, height: 1.4,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 16, fontWeight: _regular,
|
||||
color: scheme.onSurface, height: 1.6,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 15, fontWeight: _regular,
|
||||
color: scheme.onSurface, height: 1.5,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 13, fontWeight: _regular,
|
||||
color: scheme.onSurfaceVariant, height: 1.5,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 14, fontWeight: _medium,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 12, fontWeight: _medium,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: _fontFamily, fontSize: 11, fontWeight: _medium,
|
||||
color: scheme.onSurfaceVariant, letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user