From 8bdb92132999dcfbaf7bffbaf462e010fa73d5d9 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sun, 28 Jun 2026 04:46:04 +0800 Subject: [PATCH] 1 --- lib/pages/epub_reader/reader_screen.dart | 13 +- .../markdown_reader/md_reader_tab_page.dart | 11 +- lib/pages/profile_page.dart | 232 ++++++++++++++---- lib/utils/sync/backup_service.dart | 55 +++++ 4 files changed, 253 insertions(+), 58 deletions(-) diff --git a/lib/pages/epub_reader/reader_screen.dart b/lib/pages/epub_reader/reader_screen.dart index ad7b8ee..27549d1 100644 --- a/lib/pages/epub_reader/reader_screen.dart +++ b/lib/pages/epub_reader/reader_screen.dart @@ -12,6 +12,7 @@ import '../../utils/epub/reader_settings.dart'; import '../../utils/epub/reader_models.dart'; import '../../utils/epub/volume_control_service.dart'; import '../../utils/epub/reader_dao.dart'; +import '../../utils/toast_util.dart'; import 'book_session.dart'; import 'reader_renderer.dart'; import 'control_panel.dart'; @@ -346,11 +347,7 @@ class _ReaderScreenState extends State if (existing != null) { // 删除已有书签 await _readerDao.deleteBookmark(existing['id'] as int); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('已移除书签'), duration: Duration(seconds: 1)), - ); - } + if (mounted) ToastUtil.show(context, '已移除书签'); } else { // 添加书签 final chapterTitle = bookSession.spine.isNotEmpty && @@ -374,11 +371,7 @@ class _ReaderScreenState extends State 'created_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(), }); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('已添加书签'), duration: Duration(seconds: 1)), - ); - } + if (mounted) ToastUtil.show(context, '已添加书签'); } await _loadBookmarks(); } diff --git a/lib/pages/markdown_reader/md_reader_tab_page.dart b/lib/pages/markdown_reader/md_reader_tab_page.dart index 01905aa..a57b829 100644 --- a/lib/pages/markdown_reader/md_reader_tab_page.dart +++ b/lib/pages/markdown_reader/md_reader_tab_page.dart @@ -39,7 +39,7 @@ class _MdReaderTabPageState extends State { Future _init() async { final saved = UserPrefs().lastMdFolder; - if (saved != null && saved.isNotEmpty) { + if (saved != null && saved.isNotEmpty && saved.length > 3) { final dir = Directory(saved); if (await dir.exists()) { _rootPath = saved; @@ -239,8 +239,15 @@ class _MdReaderTabPageState extends State { } } catch (e) { if (mounted) { + final isPermissionError = e is FileSystemException && e.osError?.errorCode == 13; + if (isPermissionError) { + // 清除无效的保存路径 + await UserPrefs().setLastMdFolder(''); + _rootPath = null; + _currentPath = null; + } setState(() { - _error = '读取失败: $e'; + _error = isPermissionError ? '权限不足,请重新选择目录' : '读取失败: $e'; _isLoading = false; }); } diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index e5f80e1..c646def 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -11,6 +11,7 @@ import '../providers/app_provider.dart'; import '../utils/user_prefs.dart'; import '../utils/theme/app_theme.dart'; import '../utils/toast_util.dart'; +import '../utils/database_helper.dart'; import 'recycle_bin_page.dart'; import 'sync/backup_page.dart'; import '../widgets/fade_in_local_image.dart'; @@ -989,7 +990,7 @@ class _SettingsPageState extends State { _buildActionItem( icon: Icons.cleaning_services_outlined, title: '清除缓存数据', - subtitle: '清理未在数据库中引用的图片文件', + subtitle: '清理未在数据库中引用的文件', onTap: () => _showClearCacheDialog(context), ), Divider( @@ -1306,7 +1307,8 @@ class _SettingsPageState extends State { final colors = Theme.of(context).colorScheme; final provider = context.watch(); final currentIndex = provider.colorSchemeIndex; - final label = currentIndex == -1 ? '莫奈取色' : AppTheme.colorSchemeNames[currentIndex]; + final label = + currentIndex == -1 ? '莫奈取色' : AppTheme.colorSchemeNames[currentIndex]; return InkWell( onTap: () => _showColorSchemePicker(), child: Container( @@ -1319,8 +1321,12 @@ class _SettingsPageState extends State { decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), - child: Icon(currentIndex == -1 ? Icons.auto_awesome : Icons.palette_outlined, - color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + child: Icon( + currentIndex == -1 + ? Icons.auto_awesome + : Icons.palette_outlined, + color: colors.onSurface.withValues(alpha: 0.6), + size: 18)), const SizedBox(width: 12), Expanded( child: Column( @@ -1445,13 +1451,17 @@ class _SettingsPageState extends State { ); } - Widget _buildMonetOption(AppProvider provider, ColorScheme colors, BuildContext ctx) { + Widget _buildMonetOption( + AppProvider provider, ColorScheme colors, BuildContext ctx) { final selected = provider.colorSchemeIndex == -1; final monetColor = AppTheme.monetColor; final available = monetColor != null; return GestureDetector( onTap: available - ? () { provider.setColorScheme(-1); Navigator.pop(ctx); } + ? () { + provider.setColorScheme(-1); + Navigator.pop(ctx); + } : null, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), @@ -1463,34 +1473,51 @@ class _SettingsPageState extends State { border: Border.all( color: selected ? (monetColor ?? colors.primary) - : available ? colors.outlineVariant : colors.outlineVariant.withValues(alpha: 0.5), + : available + ? colors.outlineVariant + : colors.outlineVariant.withValues(alpha: 0.5), width: selected ? 1.5 : 0.5), ), child: Row( children: [ Container( - width: 20, height: 20, + width: 20, + height: 20, decoration: BoxDecoration( shape: BoxShape.circle, gradient: monetColor != null - ? LinearGradient(colors: [monetColor, monetColor.withValues(alpha: 0.6)]) + ? LinearGradient( + colors: [monetColor, monetColor.withValues(alpha: 0.6)]) : null, color: available ? null : colors.outlineVariant, ), - child: available ? null : Icon(Icons.auto_awesome, size: 12, color: colors.onSurface.withValues(alpha: 0.3)), + child: available + ? null + : Icon(Icons.auto_awesome, + size: 12, color: colors.onSurface.withValues(alpha: 0.3)), ), const SizedBox(width: 10), Expanded( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('莫奈取色', style: TextStyle( - fontSize: 13, - fontWeight: selected ? FontWeight.w600 : FontWeight.w500, - color: available ? colors.onSurface : colors.onSurface.withValues(alpha: 0.35))), - Text(available ? '从系统壁纸自动提取配色' : '此设备不支持', - style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))), - ]), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('莫奈取色', + style: TextStyle( + fontSize: 13, + fontWeight: + selected ? FontWeight.w600 : FontWeight.w500, + color: available + ? colors.onSurface + : colors.onSurface.withValues(alpha: 0.35))), + Text(available ? '从系统壁纸自动提取配色' : '此设备不支持', + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.35))), + ]), ), - if (selected) Icon(Icons.check_circle, size: 18, color: monetColor ?? colors.primary), + if (selected) + Icon(Icons.check_circle, + size: 18, color: monetColor ?? colors.primary), ], ), ), @@ -1839,17 +1866,23 @@ class _SettingsPageState extends State { final dbImagePaths = await _getAllDbImagePaths(appProvider); final deletedImages = await _cleanImageDirectory(dbImagePaths); - // 2. 清理临时目录缓存(分享海报、备份ZIP等) + // 2. 清理孤立的 epub_books 目录 + final deletedEpubs = await _cleanOrphanedEpubBooks(appProvider); + + // 3. 清理临时目录缓存(分享海报、备份ZIP等) final deletedTemp = await _cleanTempDirectory(); + // 4. 清理空文件夹 + final deletedEmptyDirs = await _cleanEmptyDirectories(); + Navigator.pop(context); if (context.mounted) { - final total = deletedImages + deletedTemp; + final total = deletedImages + deletedEpubs + deletedTemp + deletedEmptyDirs; if (total == 0) { ToastUtil.show(context, '没有需要清理的缓存'); } else { ToastUtil.show( - context, '已清理 $deletedImages 个孤立图片,$deletedTemp 个临时文件'); + context, '已清理 $deletedImages 个孤立图片,$deletedEpubs 个孤立电子书,$deletedTemp 个临时文件,$deletedEmptyDirs 个空文件夹'); } } } catch (e) { @@ -1902,6 +1935,36 @@ class _SettingsPageState extends State { return deletedCount; } + /// 清理 epub_books 中孤立的目录(数据库中不存在的) + Future _cleanOrphanedEpubBooks(AppProvider provider) async { + int deletedCount = 0; + try { + // 收集数据库中所有 reader_books 的 bookId + final db = await DatabaseHelper.instance.database; + final rows = await db.query('reader_books', columns: ['id']); + final dbIds = rows.map((r) => r['id'] as String).toSet(); + + final appDir = await getApplicationDocumentsDirectory(); + final epubDir = Directory('${appDir.path}/epub_books'); + if (!await epubDir.exists()) return 0; + + await for (final entity in epubDir.list(followLinks: false)) { + if (entity is Directory) { + final dirName = path.basename(entity.path); + if (!dbIds.contains(dirName)) { + try { + await entity.delete(recursive: true); + deletedCount++; + } catch (_) {} + } + } + } + } catch (e) { + debugPrint('清理 epub_books 目录失败: $e'); + } + return deletedCount; + } + Future _cleanTempDirectory() async { int deletedCount = 0; final now = DateTime.now(); @@ -1953,6 +2016,48 @@ class _SettingsPageState extends State { return deletedCount; } + + /// 清理 images、epub_books、cache 下的空文件夹 + Future _cleanEmptyDirectories() async { + int deletedCount = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final cacheDir = await getApplicationCacheDirectory(); + final dirs = [ + Directory('${appDir.path}/images'), + Directory('${appDir.path}/epub_books'), + cacheDir, + ]; + for (final dir in dirs) { + if (!await dir.exists()) continue; + deletedCount += await _removeEmptyDirsRecursive(dir); + } + } catch (e) { + debugPrint('清理空文件夹失败: $e'); + } + return deletedCount; + } + + /// 递归删除空子文件夹(自底向上),不删除根目录本身 + Future _removeEmptyDirsRecursive(Directory dir) async { + int count = 0; + try { + final children = await dir.list(followLinks: false).toList(); + for (final child in children) { + if (child is Directory) { + count += await _removeEmptyDirsRecursive(child); + final remaining = await child.list(followLinks: false).toList(); + if (remaining.isEmpty) { + try { + await child.delete(); + count++; + } catch (_) {} + } + } + } + } catch (_) {} + return count; + } } // ─── 主界面设置 ─── @@ -2478,63 +2583,98 @@ class _SidebarSettingsPageState extends State { body: ListView( children: [ _buildSectionHeader('信息模块'), - _buildSwitchItem(Icons.calendar_today, '热力图', '显示创作活跃度热力图', - _showHeatmap, (v) async { + _buildSwitchItem( + Icons.calendar_today, '热力图', '显示创作活跃度热力图', _showHeatmap, + (v) async { await _userPrefs.setShowSidebarHeatmap(v); setState(() => _showHeatmap = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.schedule, '最近添加', '显示最近添加的记录', - _showRecent, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.schedule, '最近添加', '显示最近添加的记录', _showRecent, + (v) async { await _userPrefs.setShowSidebarRecent(v); setState(() => _showRecent = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.favorite_border, '统计', '与应用相遇的天数和数据概览', - _showEncounter, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.favorite_border, '统计', '与应用相遇的天数和数据概览', _showEncounter, + (v) async { await _userPrefs.setShowSidebarEncounter(v); setState(() => _showEncounter = v); }), _buildSectionHeader('快捷功能'), - _buildSwitchItem(Icons.explore_outlined, '漫步', '随机发现内容', - _showStroll, (v) async { + _buildSwitchItem(Icons.explore_outlined, '漫步', '随机发现内容', _showStroll, + (v) async { await _userPrefs.setShowSidebarStroll(v); setState(() => _showStroll = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.calendar_month_outlined, '书影日历', '按日历查看记录', - _showCalendar, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.calendar_month_outlined, '书影日历', '按日历查看记录', _showCalendar, + (v) async { await _userPrefs.setShowSidebarCalendar(v); setState(() => _showCalendar = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.people_outline, '角色信息', '管理影视和书籍中的角色', - _showPerson, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.people_outline, '角色信息', '管理影视和书籍中的角色', _showPerson, + (v) async { await _userPrefs.setShowSidebarPerson(v); setState(() => _showPerson = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.label_outline, '标签管理', '管理所有标签', - _showTags, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem(Icons.label_outline, '标签管理', '管理所有标签', _showTags, + (v) async { await _userPrefs.setShowSidebarTags(v); setState(() => _showTags = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), _buildSwitchItem(Icons.description_outlined, 'MD阅读', 'Markdown 文件阅读器', _showMdReader, (v) async { await _userPrefs.setShowSidebarMdReader(v); setState(() => _showMdReader = v); }), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.auto_stories_outlined, 'EPUB阅读', 'EPUB 电子书阅读器', - _showEpub, (v) async { + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildSwitchItem( + Icons.auto_stories_outlined, 'EPUB阅读', 'EPUB 电子书阅读器', _showEpub, + (v) async { await _userPrefs.setShowSidebarEpub(v); setState(() => _showEpub = v); }), Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Text('关闭后对应功能将从侧边栏中隐藏。', - style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))), + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.3))), ), ], ), diff --git a/lib/utils/sync/backup_service.dart b/lib/utils/sync/backup_service.dart index c6a40c0..84fb2ba 100644 --- a/lib/utils/sync/backup_service.dart +++ b/lib/utils/sync/backup_service.dart @@ -31,6 +31,9 @@ class BackupService { final bookReviews = await db.query('book_reviews'); final bookExcerpts = await db.query('book_excerpts'); final tags = await db.query('tags'); + final readerBooks = await db.query('reader_books'); + final bookAnnotations = await db.query('book_annotations'); + final notePlus = await db.query('note_plus'); // 收集图片路径 final imagePaths = {}; @@ -58,6 +61,10 @@ class BackupService { } } } + for (final rb in readerBooks) { + final p = rb['cover_path'] as String?; + if (p != null && p.isNotEmpty) imagePaths.add(p); + } final userPrefs = UserPrefs(); final userInfo = { @@ -85,6 +92,9 @@ class BackupService { 'book_reviews': bookReviews, 'book_excerpts': bookExcerpts, 'tags': tags, + 'reader_books': readerBooks, + 'book_annotations': bookAnnotations, + 'note_plus': notePlus, }, }; @@ -304,15 +314,21 @@ class BackupService { final bookReviewsCols = await _getTableColumns(db, 'book_reviews'); final bookExcerptsCols = await _getTableColumns(db, 'book_excerpts'); final tagsCols = await _getTableColumns(db, 'tags'); + final readerBooksCols = await _getTableColumns(db, 'reader_books'); + final bookAnnotationsCols = await _getTableColumns(db, 'book_annotations'); + final notePlusCols = await _getTableColumns(db, 'note_plus'); await db.transaction((txn) async { await txn.delete('movie_reviews'); await txn.delete('movie_posters'); await txn.delete('book_reviews'); await txn.delete('book_excerpts'); + await txn.delete('book_annotations'); await txn.delete('movies'); await txn.delete('books'); await txn.delete('notes'); + await txn.delete('reader_books'); + await txn.delete('note_plus'); await txn.delete('tags'); if (data.containsKey('movies')) { @@ -350,6 +366,21 @@ class BackupService { await txn.insert('book_excerpts', _convertToDbMapSafe(e, bookExcerptsCols)); } } + if (data.containsKey('reader_books')) { + for (final rb in data['reader_books'] as List) { + await txn.insert('reader_books', _updateImagePath(_convertToDbMapSafe(rb, readerBooksCols), 'cover_path', imagePathMap)); + } + } + if (data.containsKey('book_annotations')) { + for (final a in data['book_annotations'] as List) { + await txn.insert('book_annotations', _convertToDbMapSafe(a, bookAnnotationsCols)); + } + } + if (data.containsKey('note_plus')) { + for (final np in data['note_plus'] as List) { + await txn.insert('note_plus', _convertToDbMapSafe(np, notePlusCols)); + } + } if (data.containsKey('tags')) { for (final t in data['tags'] as List) { final map = _convertToDbMapSafe(t, tagsCols); @@ -421,15 +452,21 @@ class BackupService { final bookReviewsCols = await _getTableColumns(db, 'book_reviews'); final bookExcerptsCols = await _getTableColumns(db, 'book_excerpts'); final tagsCols = await _getTableColumns(db, 'tags'); + final readerBooksCols = await _getTableColumns(db, 'reader_books'); + final bookAnnotationsCols = await _getTableColumns(db, 'book_annotations'); + final notePlusCols = await _getTableColumns(db, 'note_plus'); await db.transaction((txn) async { await txn.delete('movie_reviews'); await txn.delete('movie_posters'); await txn.delete('book_reviews'); await txn.delete('book_excerpts'); + await txn.delete('book_annotations'); await txn.delete('movies'); await txn.delete('books'); await txn.delete('notes'); + await txn.delete('reader_books'); + await txn.delete('note_plus'); await txn.delete('tags'); // 修复: 之前漏删 tags 表 if (data.containsKey('movies')) { @@ -467,6 +504,21 @@ class BackupService { await txn.insert('book_excerpts', _convertToDbMapSafe(e, bookExcerptsCols)); } } + if (data.containsKey('reader_books')) { + for (final rb in data['reader_books'] as List) { + await txn.insert('reader_books', _updateImagePath(_convertToDbMapSafe(rb, readerBooksCols), 'cover_path', imagePathMap)); + } + } + if (data.containsKey('book_annotations')) { + for (final a in data['book_annotations'] as List) { + await txn.insert('book_annotations', _convertToDbMapSafe(a, bookAnnotationsCols)); + } + } + if (data.containsKey('note_plus')) { + for (final np in data['note_plus'] as List) { + await txn.insert('note_plus', _convertToDbMapSafe(np, notePlusCols)); + } + } if (data.containsKey('tags')) { for (final t in data['tags'] as List) { final map = _convertToDbMapSafe(t, tagsCols); @@ -560,6 +612,9 @@ class BackupService { if (data.containsKey('book_reviews')) stats['书评'] = (data['book_reviews'] as List).length; if (data.containsKey('book_excerpts')) stats['书摘'] = (data['book_excerpts'] as List).length; if (data.containsKey('tags')) stats['标签'] = (data['tags'] as List).length; + if (data.containsKey('reader_books')) stats['阅读'] = (data['reader_books'] as List).length; + if (data.containsKey('book_annotations')) stats['批注'] = (data['book_annotations'] as List).length; + if (data.containsKey('note_plus')) stats['高级笔记'] = (data['note_plus'] as List).length; if (imageCount > 0) stats['图片'] = imageCount; return stats; }