代码优化2

This commit is contained in:
DelLevin-Home
2026-06-27 19:20:03 +08:00
parent bcff4f6c01
commit a1fe0a23ef
31 changed files with 285 additions and 162 deletions

View File

@@ -174,6 +174,9 @@ class BookDao {
// 彻底删除书籍
Future<int> permanentDeleteBook(String id) => _wrap('permanentDeleteBook', () async {
final db = await _dbHelper.database;
// 清理子记录,防止孤儿数据
await db.delete('book_reviews', where: 'book_id = ?', whereArgs: [id]);
await db.delete('book_excerpts', where: 'book_id = ?', whereArgs: [id]);
return await db.delete(
'books',
where: 'id = ?',

View File

@@ -67,7 +67,7 @@ class BookExcerptDao {
final db = await _dbHelper.database;
await db.update(
'book_excerpts',
{'is_deleted': 1},
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
where: 'id = ?',
whereArgs: [id],
);

View File

@@ -67,7 +67,7 @@ class BookReviewDao {
final db = await _dbHelper.database;
await db.update(
'book_reviews',
{'is_deleted': 1},
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
where: 'id = ?',
whereArgs: [id],
);

View File

@@ -7,6 +7,7 @@ import '../models/data_models.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
static bool _isReopening = false;
DatabaseHelper._init();
@@ -18,16 +19,25 @@ class DatabaseHelper {
/// 重新打开数据库(用于 WebDAV 同步后)
Future<void> reopenDatabase() async {
// 关闭现有连接
if (_database != null) {
await _database!.close();
_database = null;
// 防止并发重开
if (_isReopening) return;
_isReopening = true;
try {
if (_database != null) {
await _database!.close();
_database = null;
}
_database = await _initDB('mooknote.db');
} finally {
_isReopening = false;
}
// 重新初始化
_database = await _initDB('mooknote.db');
}
Future<Database> get database async {
// 等待重开完成
while (_isReopening) {
await Future.delayed(const Duration(milliseconds: 50));
}
if (_database != null) return _database!;
_database = await _initDB('mooknote.db');
return _database!;
@@ -157,7 +167,9 @@ class DatabaseHelper {
await db.execute("ALTER TABLE note_plus ADD COLUMN parent_id TEXT DEFAULT ''");
}
}
} catch (_) {}
} catch (e) {
debugPrint('Migration v20 failed: $e');
}
}
if (oldVersion < 21) {
try {
@@ -165,7 +177,9 @@ class DatabaseHelper {
if (!cols.any((col) => col['name'] == 'sort_index')) {
await db.execute("ALTER TABLE note_plus ADD COLUMN sort_index INTEGER DEFAULT 0");
}
} catch (_) {}
} catch (e) {
debugPrint('Migration v21 failed: $e');
}
}
if (oldVersion < 22) {
// 为笔记表添加置顶字段
@@ -285,7 +299,7 @@ class DatabaseHelper {
final movies = await db.query('movies',
where: 'genres IS NOT NULL AND genres != ?', whereArgs: ['[]']);
for (final row in movies) {
for (final genre in Movie.parseStringList(row['genres'])) {
for (final genre in parseStringListGeneric(row['genres'])) {
await insertTag(genre, 'movie_genre');
}
}
@@ -294,7 +308,7 @@ class DatabaseHelper {
final books = await db.query('books',
where: 'genres IS NOT NULL AND genres != ?', whereArgs: ['[]']);
for (final row in books) {
for (final genre in Movie.parseStringList(row['genres'])) {
for (final genre in parseStringListGeneric(row['genres'])) {
await insertTag(genre, 'book_genre');
}
}
@@ -304,7 +318,7 @@ class DatabaseHelper {
where: 'tags IS NOT NULL AND tags != ? AND tags != ?',
whereArgs: ['[]', '']);
for (final row in notes) {
for (final tag in Movie.parseStringList(row['tags'])) {
for (final tag in parseStringListGeneric(row['tags'])) {
await insertTag(tag, 'note_tag');
}
}

View File

@@ -253,6 +253,9 @@ class MovieDao {
// 彻底删除影视
Future<int> permanentDeleteMovie(String id) => _wrap('permanentDeleteMovie', () async {
final db = await _dbHelper.database;
// 清理子记录,防止孤儿数据
await db.delete('movie_reviews', where: 'movie_id = ?', whereArgs: [id]);
await db.delete('movie_posters', where: 'movie_id = ?', whereArgs: [id]);
return await db.delete(
'movies',
where: 'id = ?',

View File

@@ -61,7 +61,7 @@ class MovieReviewDao {
final db = await _dbHelper.database;
return await db.update(
'movie_reviews',
{'is_deleted': 1},
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
where: 'id = ?',
whereArgs: [id],
);

View File

@@ -44,16 +44,16 @@ class AutoBackupService {
/// 启动自动备份
Future<void> start() async {
if (_isRunning) return;
_isRunning = true;
// 立即执行一次备份
await _performBackup();
// 启动定时器
_timer = Timer.periodic(_backupInterval, (_) async {
await _performBackup();
});
_isRunning = true;
}
/// 停止自动备份

View File

@@ -114,6 +114,21 @@ class BackupService {
}
}
// 收集 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++;
}
}
}
final zipBytes = ZipEncoder().encode(archive);
if (zipBytes == null) throw Exception('压缩备份文件失败');
@@ -123,6 +138,7 @@ class BackupService {
bookCount: books.length,
noteCount: notes.length,
imageCount: imageCount,
epubCount: epubCount,
);
}
@@ -187,6 +203,7 @@ class BackupService {
bookCount: data.bookCount,
noteCount: data.noteCount,
imageCount: data.imageCount,
epubCount: data.epubCount,
);
} catch (e) {
return AutoBackupExportResult.error('导出失败: $e');
@@ -239,6 +256,13 @@ class BackupService {
// 用完整相对路径做 key避免不同目录下同名文件碰撞
imagePathMap[relativePath] = outputFile.path;
imageCount++;
} else if (archiveFile.name.startsWith('epub_books/')) {
final relativePath = archiveFile.name.substring(12);
final epubDir = Directory(path.join(appDir.path, '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);
await outputFile.writeAsBytes(archiveFile.content as List<int>);
}
}
} else {
@@ -354,6 +378,13 @@ class BackupService {
await outputFile.writeAsBytes(archiveFile.content as List<int>);
imagePathMap[relativePath] = outputFile.path;
imageCount++;
} else if (archiveFile.name.startsWith('epub_books/')) {
final relativePath = archiveFile.name.substring(12);
final epubDir = Directory(path.join(appDir.path, '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);
await outputFile.writeAsBytes(archiveFile.content as List<int>);
}
}
@@ -475,14 +506,21 @@ class BackupService {
return map;
}
/// 恢复 SharedPreferences保留当前 avatarPath因为已在上面用 imagePathMap 更新过
/// 恢复 SharedPreferences保留当前设备的同步和路径配置
Future<void> _restoreSharedPrefs(Map<String, dynamic> data) async {
final prefs = await SharedPreferences.getInstance();
// 这些键是设备特定的,不应从备份恢复
const skipKeys = {
'avatarPath',
'webdav_config',
'webdav_last_sync',
'webdav_auto_sync',
'webdav_auto_sync_interval',
};
for (final entry in data.entries) {
final key = entry.key;
final value = entry.value;
// avatarPath 已单独处理,跳过
if (key == 'avatarPath') continue;
if (skipKeys.contains(key)) continue;
if (value is String) {
await prefs.setString(key, value);
} else if (value is int) {
@@ -592,6 +630,7 @@ class _ExportData {
final int bookCount;
final int noteCount;
final int imageCount;
final int epubCount;
_ExportData({
required this.zipBytes,
@@ -599,6 +638,7 @@ class _ExportData {
required this.bookCount,
required this.noteCount,
required this.imageCount,
this.epubCount = 0,
});
}
@@ -612,6 +652,7 @@ class AutoBackupExportResult {
final int bookCount;
final int noteCount;
final int imageCount;
final int epubCount;
AutoBackupExportResult._({
required this.success,
@@ -621,6 +662,7 @@ class AutoBackupExportResult {
this.bookCount = 0,
this.noteCount = 0,
this.imageCount = 0,
this.epubCount = 0,
});
factory AutoBackupExportResult.success({
@@ -629,11 +671,13 @@ class AutoBackupExportResult {
required int bookCount,
required int noteCount,
required int imageCount,
int epubCount = 0,
}) {
return AutoBackupExportResult._(
success: true, zipBytes: zipBytes,
movieCount: movieCount, bookCount: bookCount,
noteCount: noteCount, imageCount: imageCount,
epubCount: epubCount,
);
}

View File

@@ -57,6 +57,7 @@ class WebDAVService {
Timer? _autoSyncTimer;
bool _isAutoSyncEnabled = false;
int _autoSyncIntervalMinutes = _defaultAutoSyncInterval;
bool _isSyncing = false;
/// 获取配置
Future<Map<String, String>?> getConfig() async {
@@ -195,8 +196,15 @@ class WebDAVService {
/// 同步数据 — 完整备份 zip 格式,与本地备份完全一致
Future<SyncResult> syncData({SyncDirection direction = SyncDirection.bidirectional}) async {
// 防止并发同步
if (_isSyncing) {
return SyncResult(success: false, message: '同步正在进行中,请稍后再试');
}
_isSyncing = true;
final config = await getConfig();
if (config == null) {
_isSyncing = false;
return SyncResult(success: false, message: '未配置 WebDAV');
}
@@ -255,27 +263,42 @@ class WebDAVService {
}
} else if (direction == SyncDirection.bidirectional) {
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'mooknote_bidir.zip'));
// 获取远程备份的修改时间
final remoteModTime = await _getRemoteFileModifiedTime(client, zipUrl, username, password);
final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip);
if (downloadSuccess && await tempZip.exists()) {
final bytes = await tempZip.readAsBytes();
final importResult = await BackupService.instance.restoreFromZipBytes(bytes);
await tempZip.delete();
// 获取上次同步时间
final syncPrefs = await SharedPreferences.getInstance();
final lastSyncStr = syncPrefs.getString(_lastSyncKey);
final lastSyncTime = lastSyncStr != null ? DateTime.tryParse(lastSyncStr) : null;
if (importResult.success) {
downloadedFiles = 1;
downloadedImages = importResult.stats?['图片'] ?? 0;
needReload = true;
debugPrint('[WebDAV] 远程备份已恢复: ${importResult.statsText}');
final bool remoteIsNewer = remoteModTime != null &&
(lastSyncTime == null || remoteModTime.isAfter(lastSyncTime));
if (remoteIsNewer) {
// 远程更新,下载并恢复
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'mooknote_bidir.zip'));
final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip);
if (downloadSuccess && await tempZip.exists()) {
final bytes = await tempZip.readAsBytes();
final importResult = await BackupService.instance.restoreFromZipBytes(bytes);
await tempZip.delete();
if (importResult.success) {
downloadedFiles = 1;
downloadedImages = importResult.stats?['图片'] ?? 0;
needReload = true;
debugPrint('[WebDAV] 远程备份较新,已恢复: ${importResult.statsText}');
}
} else {
try { await tempZip.delete(); } catch (_) {}
}
} else {
try { await tempZip.delete(); } catch (_) {}
debugPrint('[WebDAV] 服务器无备份,仅上传本地数据');
debugPrint('[WebDAV] 本地数据已是最新或远程无更新,跳过下载');
}
// 始终上传本地,确保远程最新
// 上传本地备份(无论是否下载,确保远程最新数据)
final exportResult = await BackupService.instance.exportDataForAutoBackup();
if (exportResult.success && exportResult.zipBytes != null) {
final uploadSuccess = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!);
@@ -305,6 +328,8 @@ class WebDAVService {
}
} catch (e) {
return SyncResult(success: false, message: '同步失败: $e');
} finally {
_isSyncing = false;
}
}
@@ -346,7 +371,11 @@ class WebDAVService {
Duration(minutes: _autoSyncIntervalMinutes),
(timer) async {
if (_isAutoSyncEnabled) {
await syncData(direction: SyncDirection.bidirectional);
try {
await syncData(direction: SyncDirection.bidirectional);
} catch (e) {
debugPrint('[WebDAV] 自动同步异常: $e');
}
}
},
);
@@ -456,6 +485,43 @@ class WebDAVService {
}
}
/// 获取远程文件的修改时间
Future<DateTime?> _getRemoteFileModifiedTime(
http.Client client,
String url,
String username,
String password,
) async {
try {
var request = http.Request('HEAD', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password);
var response = await client.send(request);
// 处理重定向
if (response.statusCode == 301 || response.statusCode == 302 ||
response.statusCode == 307 || response.statusCode == 308) {
final location = response.headers['location'];
if (location != null) {
request = http.Request('HEAD', Uri.parse(location));
request.headers['Authorization'] = _basicAuth(username, password);
response = await client.send(request);
}
}
if (response.statusCode == 200) {
final lastModified = response.headers['last-modified'];
if (lastModified != null) {
return HttpDate.parse(lastModified);
}
}
return null;
} catch (e) {
debugPrint('[WebDAV] 获取远程文件时间失败: $e');
return null;
}
}
/// Basic Auth 编码
String _basicAuth(String username, String password) {
final credentials = base64Encode(utf8.encode('$username:$password'));

View File

@@ -84,9 +84,8 @@ class AppTheme {
),
cardTheme: CardThemeData(
color: scheme.surface, elevation: 0,
shape: RoundedRectangleBorder(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
side: BorderSide(color: scheme.outlineVariant, width: 0.5),
),
margin: EdgeInsets.zero,
),

View File

@@ -22,7 +22,7 @@ class ToastUtil {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A).withOpacity(0.9),
color: const Color(0xFF1A1A1A).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(24),
),
child: Text(

View File

@@ -159,7 +159,7 @@ class UserPrefs {
List<String> get searchHistory => prefs.getStringList('searchHistory') ?? [];
Future<bool> setSearchHistory(List<String> value) => prefs.setStringList('searchHistory', value);
/// 添加搜索记录(最多 20 条)
/// 添加搜索记录(最多 50 条)
Future<void> addSearchHistory(String keyword) async {
final list = searchHistory;
list.remove(keyword);