diff --git a/lib/pages/profile/settings_page.dart b/lib/pages/profile/settings_page.dart index 2a6f4a1..a02c710 100644 --- a/lib/pages/profile/settings_page.dart +++ b/lib/pages/profile/settings_page.dart @@ -11,6 +11,7 @@ import '../../utils/user_prefs.dart'; import '../../utils/theme/app_theme.dart'; import '../../utils/toast_util.dart'; import '../../data/database_helper.dart'; +import '../../services/sync/cache_cleaner.dart'; import '../online_search/enhanced_search_settings_page.dart'; import '../settings/legal_page.dart'; import 'app_icon_picker_page.dart'; @@ -1035,30 +1036,13 @@ class _SettingsPageState extends State { context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator())); - final appProvider = context.read(); - - // 1. 清理未引用的图片文件 - final dbImagePaths = await _getAllDbImagePaths(appProvider); - final deletedImages = await _cleanImageDirectory(dbImagePaths); - - // 2. 清理孤立的 epub_books 目录 - final deletedEpubs = await _cleanOrphanedEpubBooks(appProvider); - - // 3. 清理临时目录缓存(分享海报、备份ZIP等) - final deletedTemp = await _cleanTempDirectory(); - - // 4. 清理空文件夹 - final deletedEmptyDirs = await _cleanEmptyDirectories(); - + final result = await CacheCleaner.instance.clean(context.read()); Navigator.pop(context); if (context.mounted) { - final total = - deletedImages + deletedEpubs + deletedTemp + deletedEmptyDirs; - if (total == 0) { + if (result.total == 0) { ToastUtil.show(context, '没有需要清理的缓存'); } else { - ToastUtil.show(context, - '已清理 $deletedImages 个孤立图片,$deletedEpubs 个孤立电子书,$deletedTemp 个临时文件,$deletedEmptyDirs 个空文件夹'); + ToastUtil.show(context, result.description); } } } catch (e) { @@ -1096,77 +1080,6 @@ class _SettingsPageState extends State { return paths; } - Future _cleanImageDirectory(Set dbImagePaths) async { - int deletedCount = 0; - try { - final appDir = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${appDir.path}/images'); - if (!await imagesDir.exists()) return 0; - await for (final entity - in imagesDir.list(recursive: true, followLinks: false)) { - if (entity is File && - !dbImagePaths.contains(entity.path) && - !path.basename(entity.path).startsWith('avatar')) { - try { - await entity.delete(); - deletedCount++; - } catch (_) {} - } - } - } catch (e) { - debugPrint('清理图片目录失败: $e'); - } - return deletedCount; - } - - /// 清理 epub_books 中孤立的目录(数据库中不存在的) - Future _cleanOrphanedEpubBooks(AppProvider provider) async { - int deletedCount = 0; - try { - final db = await DatabaseHelper.instance.database; - // 收集数据库中所有引用的 epub_books 子目录名(包括软删除的) - final rows = await db.query('reader_books', - columns: ['id', 'file_path', 'cover_path', 'is_deleted']); - final usedDirs = {}; - for (final r in rows) { - // 只收集未删除的记录对应的目录 - final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; - if (isDeleted) continue; - final id = r['id'] as String?; - if (id != null && id.isNotEmpty) usedDirs.add(id); - _collectEpubDirName(r['file_path'] as String?, usedDirs); - _collectEpubDirName(r['cover_path'] as String?, usedDirs); - } - - // 检查 /data/user/0/top.iletter.mooknote/app_flutter/epub_books 路径 - final appDir = await getApplicationDocumentsDirectory(); - final possiblePaths = [ - '${appDir.path}/epub_books', - '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', - ]; - - for (final epubPath in possiblePaths) { - final epubDir = Directory(epubPath); - if (!await epubDir.exists()) continue; - - await for (final entity in epubDir.list(followLinks: false)) { - if (entity is Directory) { - final dirName = path.basename(entity.path); - if (!usedDirs.contains(dirName)) { - try { - await entity.delete(recursive: true); - deletedCount++; - } catch (_) {} - } - } - } - } - } catch (e) { - debugPrint('清理 epub_books 目录失败: $e'); - } - return deletedCount; - } - /// 从绝对路径中提取 epub_books/ 下的目录名 void _collectEpubDirName(String? pathStr, Set dirs) { if (pathStr == null || pathStr.isEmpty) return; @@ -1178,100 +1091,6 @@ class _SettingsPageState extends State { dirs.add(slashIdx >= 0 ? rest.substring(0, slashIdx) : rest); } - Future _cleanTempDirectory() async { - int deletedCount = 0; - final now = DateTime.now(); - - // 1. 清理临时目录中的分享海报(保留备份ZIP) - try { - final tempDir = await getTemporaryDirectory(); - if (await tempDir.exists()) { - await for (final entity in tempDir.list(followLinks: false)) { - if (entity is File) { - final name = path.basename(entity.path); - if (name.startsWith('book_poster_') || - name.startsWith('movie_poster_') || - name.startsWith('note_share_') || - name.startsWith('mooknote_download') || - name.startsWith('mooknote_bidir')) { - try { - final stat = await entity.stat(); - if (now.difference(stat.modified).inHours >= 1) { - await entity.delete(); - deletedCount++; - } - } catch (_) {} - } - } - } - } - } catch (e) { - debugPrint('清理临时目录失败: $e'); - } - - // 2. 清理应用缓存目录 (/data/user/0/{package}/cache/) - try { - final cacheDir = await getApplicationCacheDirectory(); - if (await cacheDir.exists()) { - await for (final entity - in cacheDir.list(recursive: true, followLinks: false)) { - if (entity is File) { - try { - await entity.delete(); - deletedCount++; - } catch (_) {} - } - } - } - } catch (e) { - debugPrint('清理缓存目录失败: $e'); - } - - 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; - } - // ─── 扫描方法(只统计不删除) ────────────────────────────────────────────── /// 返回 (文件数, 总字节数) diff --git a/lib/pages/sync/backup_page.dart b/lib/pages/sync/backup_page.dart index 7528591..9b2d5bf 100644 --- a/lib/pages/sync/backup_page.dart +++ b/lib/pages/sync/backup_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../services/sync/backup_service.dart'; +import '../../services/sync/cache_cleaner.dart'; import '../../utils/toast_util.dart'; /// 本地备份页面 @@ -334,6 +335,9 @@ class _BackupPageState extends State { setState(() => _isExporting = true); try { + // 先清理缓存 + await CacheCleaner.instance.clean(context.read()); + final result = await BackupService.instance.exportDataWithImages(); if (!mounted) return; @@ -431,6 +435,9 @@ class _BackupPageState extends State { setState(() => _isImporting = true); try { + // 先清理缓存 + await CacheCleaner.instance.clean(context.read()); + final result = await BackupService.instance.importData(); if (!mounted) return; diff --git a/lib/pages/sync/webdav_sync_page.dart b/lib/pages/sync/webdav_sync_page.dart index 39ce11d..8a70674 100644 --- a/lib/pages/sync/webdav_sync_page.dart +++ b/lib/pages/sync/webdav_sync_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../utils/toast_util.dart'; import '../../services/sync/webdav_service.dart'; +import '../../services/sync/cache_cleaner.dart'; import '../../providers/app_provider.dart'; /// WebDAV 备份页面 @@ -126,6 +127,11 @@ class _WebDAVSyncPageState extends State { setState(() => _isLoading = true); try { + // 先清理缓存 + setState(() => _syncStep = '正在清理缓存...'); + await Future.delayed(Duration.zero); + await CacheCleaner.instance.clean(context.read()); + SyncResult result; if (_syncDirection == SyncDirection.upload) { diff --git a/lib/services/sync/cache_cleaner.dart b/lib/services/sync/cache_cleaner.dart new file mode 100644 index 0000000..06c216a --- /dev/null +++ b/lib/services/sync/cache_cleaner.dart @@ -0,0 +1,238 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; +import '../../providers/app_provider.dart'; +import '../../data/database_helper.dart'; + +/// 缓存清理服务 +class CacheCleaner { + CacheCleaner._(); + static final CacheCleaner instance = CacheCleaner._(); + + /// 执行完整缓存清理,返回各分类删除数量 + Future clean(AppProvider provider) async { + final dbImagePaths = await _getAllDbImagePaths(provider); + final deletedImages = await _cleanImageDirectory(dbImagePaths); + final deletedEpubs = await _cleanOrphanedEpubBooks(provider); + final deletedTemp = await _cleanTempDirectory(); + final deletedEmptyDirs = await _cleanEmptyDirectories(); + return CacheCleanResult( + images: deletedImages, + epubs: deletedEpubs, + temp: deletedTemp, + emptyDirs: deletedEmptyDirs, + ); + } + + Future> _getAllDbImagePaths(AppProvider provider) async { + final paths = {}; + for (final movie in provider.movies) { + if (movie.posterPath?.isNotEmpty == true) paths.add(movie.posterPath!); + } + for (final book in provider.books) { + if (book.coverPath?.isNotEmpty == true) paths.add(book.coverPath!); + } + for (final note in provider.notes) { + for (final p in note.images) { + if (p.isNotEmpty) paths.add(p); + } + } + for (final movieId in provider.movies.map((m) => m.id)) { + for (final poster in await provider.getMoviePosters(movieId)) { + if (poster.posterPath.isNotEmpty) paths.add(poster.posterPath); + } + } + for (final game in provider.games) { + if (game.coverPath?.isNotEmpty == true) paths.add(game.coverPath!); + } + for (final gameId in provider.games.map((g) => g.id)) { + for (final screenshot in await provider.getGameScreenshots(gameId)) { + if (screenshot.screenshotPath.isNotEmpty) paths.add(screenshot.screenshotPath); + } + } + return paths; + } + + Future _cleanImageDirectory(Set dbImagePaths) async { + int deletedCount = 0; + try { + final appDir = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${appDir.path}/images'); + if (!await imagesDir.exists()) return 0; + await for (final entity in imagesDir.list(recursive: true, followLinks: false)) { + if (entity is File && + !dbImagePaths.contains(entity.path) && + !path.basename(entity.path).startsWith('avatar')) { + try { + await entity.delete(); + deletedCount++; + } catch (_) {} + } + } + } catch (e) { + debugPrint('清理图片目录失败: $e'); + } + return deletedCount; + } + + Future _cleanOrphanedEpubBooks(AppProvider provider) async { + int deletedCount = 0; + try { + final db = await DatabaseHelper.instance.database; + final rows = await db.query('reader_books', columns: ['id', 'file_path', 'cover_path', 'is_deleted']); + final usedDirs = {}; + for (final r in rows) { + final isDeleted = r['is_deleted'] == 1 || r['is_deleted'] == true; + if (isDeleted) continue; + final id = r['id'] as String?; + if (id != null && id.isNotEmpty) usedDirs.add(id); + _collectEpubDirName(r['file_path'] as String?, usedDirs); + _collectEpubDirName(r['cover_path'] as String?, usedDirs); + } + + final appDir = await getApplicationDocumentsDirectory(); + final possiblePaths = [ + '${appDir.path}/epub_books', + '/data/user/0/top.iletter.mooknote/app_flutter/epub_books', + ]; + + for (final epubPath in possiblePaths) { + final epubDir = Directory(epubPath); + if (!await epubDir.exists()) continue; + await for (final entity in epubDir.list(followLinks: false)) { + if (entity is Directory) { + final dirName = path.basename(entity.path); + if (!usedDirs.contains(dirName)) { + try { + await entity.delete(recursive: true); + deletedCount++; + } catch (_) {} + } + } + } + } + } catch (e) { + debugPrint('清理 epub_books 目录失败: $e'); + } + return deletedCount; + } + + void _collectEpubDirName(String? pathStr, Set dirs) { + if (pathStr == null || pathStr.isEmpty) return; + final marker = '/epub_books/'; + final idx = pathStr.indexOf(marker); + if (idx < 0) return; + final rest = pathStr.substring(idx + marker.length); + final slashIdx = rest.indexOf('/'); + dirs.add(slashIdx >= 0 ? rest.substring(0, slashIdx) : rest); + } + + Future _cleanTempDirectory() async { + int deletedCount = 0; + final now = DateTime.now(); + + try { + final tempDir = await getTemporaryDirectory(); + if (await tempDir.exists()) { + await for (final entity in tempDir.list(followLinks: false)) { + if (entity is File) { + final name = path.basename(entity.path); + if (name.startsWith('book_poster_') || + name.startsWith('movie_poster_') || + name.startsWith('note_share_') || + name.startsWith('mooknote_download') || + name.startsWith('mooknote_bidir')) { + try { + final stat = await entity.stat(); + if (now.difference(stat.modified).inHours >= 1) { + await entity.delete(); + deletedCount++; + } + } catch (_) {} + } + } + } + } + } catch (e) { + debugPrint('清理临时目录失败: $e'); + } + + try { + final cacheDir = await getApplicationCacheDirectory(); + if (await cacheDir.exists()) { + await for (final entity in cacheDir.list(recursive: true, followLinks: false)) { + if (entity is File) { + try { + await entity.delete(); + deletedCount++; + } catch (_) {} + } + } + } + } catch (e) { + debugPrint('清理缓存目录失败: $e'); + } + + return deletedCount; + } + + 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; + } +} + +class CacheCleanResult { + final int images; + final int epubs; + final int temp; + final int emptyDirs; + + const CacheCleanResult({ + required this.images, + required this.epubs, + required this.temp, + required this.emptyDirs, + }); + + int get total => images + epubs + temp + emptyDirs; + + String get description => + '已清理 $images 个孤立图片,$epubs 个孤立电子书,$temp 个临时文件,$emptyDirs 个空文件夹'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 964efd5..121048f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mooknote description: "app for tracking movies, books, and notes" publish_to: 'none' -version: 0.2.5 +version: 0.2.6 environment: sdk: ^3.5.0