From 2fdcd4551d1482dab7c3d615315b7da4b0f8cd95 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Mon, 6 Jul 2026 02:49:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/database_helper.dart | 306 +++++++++---------- lib/data/game/game_dao.dart | 3 + lib/main.dart | 14 +- lib/pages/book/book_detail_page.dart | 6 +- lib/pages/book/book_form_page.dart | 20 +- lib/pages/book/book_review_form_page.dart | 3 +- lib/pages/book/book_tab_page.dart | 2 +- lib/pages/game/game_detail_page.dart | 4 +- lib/pages/game/game_form_page.dart | 17 +- lib/pages/game/game_review_form_page.dart | 3 +- lib/pages/game/game_screenshots_page.dart | 5 +- lib/pages/game/game_tab_page.dart | 3 +- lib/pages/home/main_content_page.dart | 19 +- lib/pages/movies/movie_detail_page.dart | 4 +- lib/pages/movies/movie_form_page.dart | 18 +- lib/pages/movies/movie_posters_page.dart | 5 +- lib/pages/movies/movie_review_form_page.dart | 3 +- lib/pages/movies/movie_tab_page.dart | 3 +- lib/pages/note/note_form_page.dart | 10 +- lib/pages/note/note_tab_page.dart | 2 +- lib/pages/sync/backup_page.dart | 101 +----- lib/providers/app_provider.dart | 174 ++++++++--- lib/services/usage_stats_service.dart | 3 +- 23 files changed, 372 insertions(+), 356 deletions(-) diff --git a/lib/data/database_helper.dart b/lib/data/database_helper.dart index 454c0fd..335d62e 100644 --- a/lib/data/database_helper.dart +++ b/lib/data/database_helper.dart @@ -159,17 +159,7 @@ class DatabaseHelper { await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0'); } } - // v16: 确保 cover_offset 列存在(v15 的数据库可能缺少此列) - if (oldVersion < 16) { - final movieCols = await db.rawQuery('PRAGMA table_info(movies)'); - if (!movieCols.any((col) => col['name'] == 'cover_offset')) { - await db.execute('ALTER TABLE movies ADD COLUMN cover_offset REAL DEFAULT 0'); - } - final bookCols = await db.rawQuery('PRAGMA table_info(books)'); - if (!bookCols.any((col) => col['name'] == 'cover_offset')) { - await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0'); - } - } + // v16: 已合并到 v15(cover_offset 列的添加逻辑相同,v15 的 PRAGMA 检查已确保幂等) if (oldVersion < 17) { await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0'); } @@ -248,9 +238,7 @@ class DatabaseHelper { if (oldVersion < 26) { await _upgradeBooksTableV26(db); } - if (oldVersion < 27) { - await _upgradeBooksTableV27(db); - } + // v27: 已合并到 v26(start_date/finish_date 的添加逻辑相同,v26 的 PRAGMA 检查已确保幂等) if (oldVersion < 28) { // 移除 Note Plus 功能,删除 note_plus 表 await db.execute('DROP TABLE IF EXISTS note_plus'); @@ -341,20 +329,6 @@ class DatabaseHelper { } } - /// 升级books表到V27(添加阅读始末日期字段) - Future _upgradeBooksTableV27(Database db) async { - final columns = await db.rawQuery('PRAGMA table_info(books)'); - final hasStartDate = columns.any((col) => col['name'] == 'start_date'); - final hasFinishDate = columns.any((col) => col['name'] == 'finish_date'); - - if (!hasStartDate) { - await db.execute('ALTER TABLE books ADD COLUMN start_date TEXT'); - } - if (!hasFinishDate) { - await db.execute('ALTER TABLE books ADD COLUMN finish_date TEXT'); - } - } - /// 升级books表到V26(添加阅读始末日期字段) Future _upgradeBooksTableV26(Database db) async { final columns = await db.rawQuery('PRAGMA table_info(books)'); @@ -567,153 +541,159 @@ class DatabaseHelper { /// 升级notes表到V4 Future _upgradeNotesTableV4(Database db) async { - // 备份旧数据 - final oldData = await db.query('notes'); - - // 删除旧表 - await db.execute('DROP TABLE IF EXISTS notes'); - - // 创建新表 - await db.execute(''' - CREATE TABLE notes ( - id TEXT PRIMARY KEY, - title TEXT DEFAULT '', - content TEXT NOT NULL, - content_type TEXT DEFAULT 'markdown', - tags TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - '''); - - // 迁移旧数据(将title字段恢复) - for (final row in oldData) { - try { - final now = DateTime.now().toIso8601String(); - final title = row['title']?.toString() ?? ''; - final content = row['content']?.toString() ?? ''; - - await db.insert('notes', { - 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), - 'title': title, - 'content': content, - 'content_type': 'markdown', - 'tags': row['tags'] ?? '', - 'created_at': row['created_at']?.toString() ?? now, - 'updated_at': row['updated_at']?.toString() ?? now, - }); - } catch (e) { - debugPrint('[DB] 迁移笔记记录失败: $e'); + await db.transaction((txn) async { + // 备份旧数据 + final oldData = await txn.query('notes'); + + // 删除旧表 + await txn.execute('DROP TABLE IF EXISTS notes'); + + // 创建新表 + await txn.execute(''' + CREATE TABLE notes ( + id TEXT PRIMARY KEY, + title TEXT DEFAULT '', + content TEXT NOT NULL, + content_type TEXT DEFAULT 'markdown', + tags TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + '''); + + // 迁移旧数据(将title字段恢复) + for (final row in oldData) { + try { + final now = DateTime.now().toIso8601String(); + final title = row['title']?.toString() ?? ''; + final content = row['content']?.toString() ?? ''; + + await txn.insert('notes', { + 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), + 'title': title, + 'content': content, + 'content_type': 'markdown', + 'tags': row['tags'] ?? '', + 'created_at': row['created_at']?.toString() ?? now, + 'updated_at': row['updated_at']?.toString() ?? now, + }); + } catch (e) { + debugPrint('[DB] 迁移笔记记录失败: $e'); + } } - } + }); } /// 升级books表到V3 Future _upgradeBooksTableV3(Database db) async { - // 备份旧数据 - final oldData = await db.query('books'); - - // 删除旧表 - await db.execute('DROP TABLE IF EXISTS books'); - - // 创建新表 - await db.execute(''' - CREATE TABLE books ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - cover_path TEXT, - authors TEXT, - alternate_titles TEXT, - publisher TEXT, - genres TEXT, - summary TEXT, - rating REAL, - status TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - is_deleted INTEGER DEFAULT 0 - ) - '''); - - // 迁移旧数据 - for (final row in oldData) { - try { - final now = DateTime.now().toIso8601String(); - await db.insert('books', { - 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), - 'title': row['title']?.toString() ?? '', - 'cover_path': row['cover'], - 'authors': row['author'] != null ? '["${row['author']}"]' : '[]', - 'alternate_titles': '[]', - 'publisher': null, - 'genres': '[]', - 'summary': row['note'], - 'rating': row['rating'], - 'status': row['status'] ?? 'want_to_read', - 'created_at': now, - 'updated_at': now, - 'is_deleted': 0, - }); - } catch (e) { - debugPrint('[DB] 迁移书籍记录失败: $e'); + await db.transaction((txn) async { + // 备份旧数据 + final oldData = await txn.query('books'); + + // 删除旧表 + await txn.execute('DROP TABLE IF EXISTS books'); + + // 创建新表 + await txn.execute(''' + CREATE TABLE books ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + cover_path TEXT, + authors TEXT, + alternate_titles TEXT, + publisher TEXT, + genres TEXT, + summary TEXT, + rating REAL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + is_deleted INTEGER DEFAULT 0 + ) + '''); + + // 迁移旧数据 + for (final row in oldData) { + try { + final now = DateTime.now().toIso8601String(); + await txn.insert('books', { + 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), + 'title': row['title']?.toString() ?? '', + 'cover_path': row['cover'], + 'authors': row['author'] != null ? '["${row['author']}"]' : '[]', + 'alternate_titles': '[]', + 'publisher': null, + 'genres': '[]', + 'summary': row['note'], + 'rating': row['rating'], + 'status': row['status'] ?? 'want_to_read', + 'created_at': now, + 'updated_at': now, + 'is_deleted': 0, + }); + } catch (e) { + debugPrint('[DB] 迁移书籍记录失败: $e'); + } } - } + }); } /// 升级movies表到V2 Future _upgradeMoviesTableV2(Database db) async { - // 备份旧数据 - final oldData = await db.query('movies'); - - // 删除旧表 - await db.execute('DROP TABLE IF EXISTS movies'); - - // 创建新表 - await db.execute(''' - CREATE TABLE movies ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - poster_path TEXT, - release_date TEXT, - directors TEXT, - writers TEXT, - actors TEXT, - genres TEXT, - alternate_titles TEXT, - 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 - ) - '''); + await db.transaction((txn) async { + // 备份旧数据 + final oldData = await txn.query('movies'); - // 迁移旧数据(尽可能保留) - for (final row in oldData) { - try { - await db.insert('movies', { - 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), - 'title': row['title']?.toString() ?? '', - 'poster_path': row['poster_path'], - 'release_date': null, - 'directors': '[]', - 'writers': '[]', - 'actors': '[]', - 'genres': '[]', - 'alternate_titles': '[]', - 'summary': row['note'], - 'rating': row['rating'], - 'status': row['status'] ?? 'want_to_watch', - 'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(), - 'updated_at': DateTime.now().toIso8601String(), - 'is_deleted': row['is_deleted'] ?? 0, - }); - } catch (e) { - debugPrint('[DB] 迁移影视记录失败: $e'); + // 删除旧表 + await txn.execute('DROP TABLE IF EXISTS movies'); + + // 创建新表 + await txn.execute(''' + CREATE TABLE movies ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + poster_path TEXT, + release_date TEXT, + directors TEXT, + writers TEXT, + actors TEXT, + genres TEXT, + alternate_titles TEXT, + 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 { + await txn.insert('movies', { + 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), + 'title': row['title']?.toString() ?? '', + 'poster_path': row['poster_path'], + 'release_date': null, + 'directors': '[]', + 'writers': '[]', + 'actors': '[]', + 'genres': '[]', + 'alternate_titles': '[]', + 'summary': row['note'], + 'rating': row['rating'], + 'status': row['status'] ?? 'want_to_watch', + 'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(), + 'updated_at': DateTime.now().toIso8601String(), + 'is_deleted': row['is_deleted'] ?? 0, + }); + } catch (e) { + debugPrint('[DB] 迁移影视记录失败: $e'); + } } - } + }); } // 创建数据库表 diff --git a/lib/data/game/game_dao.dart b/lib/data/game/game_dao.dart index 3a3fe9c..208c02e 100644 --- a/lib/data/game/game_dao.dart +++ b/lib/data/game/game_dao.dart @@ -144,6 +144,9 @@ class GameDao { // 彻底删除游戏 Future permanentDeleteGame(String id) => _wrap('permanentDeleteGame', () async { final db = await _dbHelper.database; + // 清理子记录,防止孤儿数据 + await db.delete('game_reviews', where: 'game_id = ?', whereArgs: [id]); + await db.delete('game_screenshots', where: 'game_id = ?', whereArgs: [id]); return await db.delete( 'games', where: 'id = ?', diff --git a/lib/main.dart b/lib/main.dart index b426cba..ded2644 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,6 @@ import 'utils/theme/app_theme.dart'; import 'utils/app_router.dart'; import 'utils/user_prefs.dart'; import 'services/changelog_service.dart'; -import 'services/sync/auto_backup_service.dart'; import 'services/usage_stats_service.dart'; import 'providers/app_provider.dart'; @@ -42,24 +41,13 @@ Future _bootstrap(AppProvider appProvider) async { await appProvider.initDatabase(); } catch (e) { debugPrint('[Startup] 数据库初始化失败: $e'); + appProvider.markDbInitFailed(); } appProvider.initMainTabIndex(); - unawaited(_initAutoBackup()); unawaited(_initUsageStats()); } -Future _initAutoBackup() async { - try { - final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled(); - if (isLocalAutoBackupEnabled) { - await AutoBackupService.instance.start(); - } - } catch (e) { - debugPrint('初始化自动备份失败: $e'); - } -} - Future _initUsageStats() async { try { await UsageStatsService.instance.start(); diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 09361e0..629498e 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -1374,14 +1374,16 @@ class _BookDetailPageState extends State { ElevatedButton( onPressed: () async { await context.read().removeBook(widget.book.id); - if (!mounted) return; + if (!mounted || !context.mounted) return; Navigator.pop(context); if (widget.embedded) { context.read().selectBook(null); } else { Navigator.pop(context); } - ToastUtil.show(context, '已删除'); + if (mounted && context.mounted) { + ToastUtil.show(context, '已删除'); + } }, style: ElevatedButton.styleFrom( backgroundColor: colors.error, diff --git a/lib/pages/book/book_form_page.dart b/lib/pages/book/book_form_page.dart index 32a4171..859c6c8 100644 --- a/lib/pages/book/book_form_page.dart +++ b/lib/pages/book/book_form_page.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -157,12 +158,14 @@ class _BookFormPageState extends State { _halfCard('书名', _titleController.text, Icons.book_outlined, required: true, onTap: () async { final r = await TextInputPanel.show(context: context, title: '书名', initialValue: _titleController.text, hint: '请输入书名'); + if (!mounted) return; if (r != null) setState(() => _titleController.text = r); }, ), _halfCard('别名', _alternateTitles.isEmpty ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('、')}', Icons.alternate_email_outlined, onTap: () async { final r = await GenreSelectorPage.show(context: context, title: '添加别名', existingTags: [], initialSelected: _alternateTitles, hint: '输入别名'); + if (!mounted) return; if (r != null) setState(() => _alternateTitles = r); }, ), @@ -173,6 +176,7 @@ class _BookFormPageState extends State { final provider = context.read(); final data = provider.books.map((b) => b.authors).toList(); final r = await GenreSelectorPage.show(context: context, title: '选择作者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _authors, hint: '如:余华、莫言'); + if (!mounted) return; if (r != null) setState(() => _authors = r); }, ), @@ -181,12 +185,14 @@ class _BookFormPageState extends State { final provider = context.read(); final data = provider.books.map((b) => b.translators).toList(); final r = await GenreSelectorPage.show(context: context, title: '选择译者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _translators, hint: '如:李继宏、许钧'); + if (!mounted) return; if (r != null) setState(() => _translators = r); }, ), _halfCard('出版社', _publisherController.text, Icons.business_outlined, onTap: () async { final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社'); + if (!mounted) return; if (r != null) setState(() => _publisherController.text = r); }, ), @@ -198,12 +204,14 @@ class _BookFormPageState extends State { final tags = await provider.getTags('book_genre', excludeHidden: true); if (!mounted) return; final r = await GenreSelectorPage.show(context: context, title: '选择类型', existingTags: tags.map((t) => t['name'] as String).toList(), initialSelected: _genres, hint: '如:小说、历史、传记'); + if (!mounted) return; if (r != null) setState(() => _genres = r); }, ), _halfCard('ISBN', _isbnController.text, Icons.qr_code_outlined, onTap: () async { final r = await TextInputPanel.show(context: context, title: 'ISBN', initialValue: _isbnController.text, hint: '请输入ISBN编号'); + if (!mounted) return; if (r != null) setState(() => _isbnController.text = r); }, ), @@ -425,10 +433,11 @@ class _BookFormPageState extends State { final picked = await _picker.pickImage(source: ImageSource.gallery, maxWidth: 800, maxHeight: 1200, imageQuality: 85); if (picked != null) { final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final bookId = widget.book?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final bookId = widget.book?.id ?? const Uuid().v4(); final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await File(picked.path).copy(targetPath); + if (!mounted) return; setState(() => _coverPath = targetPath); } } catch (e) { @@ -492,10 +501,11 @@ class _BookFormPageState extends State { if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final bookId = widget.book?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final bookId = widget.book?.id ?? const Uuid().v4(); final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await File(targetPath).writeAsBytes(response.bodyBytes); + if (!mounted) return; setState(() => _coverPath = targetPath); } catch (e) { debugPrint('封面下载失败: $e'); @@ -556,21 +566,25 @@ class _BookFormPageState extends State { Future _selectPublishDate() async { final picked = await showDatePicker(context: context, initialDate: _publishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); + if (!mounted) return; if (picked != null) setState(() => _publishDate = picked); } Future _selectStartDate() async { final picked = await showDatePicker(context: context, initialDate: _startDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); + if (!mounted) return; if (picked != null) setState(() => _startDate = picked); } Future _selectFinishDate() async { final picked = await showDatePicker(context: context, initialDate: _finishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); + if (!mounted) return; if (picked != null) setState(() => _finishDate = picked); } Future _editSummary() async { final result = await Navigator.push(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text))); + if (!mounted) return; if (result != null) setState(() => _summaryController.text = result); } @@ -619,7 +633,7 @@ class _BookFormPageState extends State { final now = DateTime.now(); if (widget.book == null) { - final newBookId = now.millisecondsSinceEpoch.toString(); + final newBookId = const Uuid().v4(); String? finalCoverPath; if (_coverPath != null && _coverPath!.isNotEmpty) { finalCoverPath = await _moveCoverToNewId(_coverPath!, newBookId); diff --git a/lib/pages/book/book_review_form_page.dart b/lib/pages/book/book_review_form_page.dart index 13be280..6c99f7e 100644 --- a/lib/pages/book/book_review_form_page.dart +++ b/lib/pages/book/book_review_form_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; @@ -313,7 +314,7 @@ class _BookReviewFormPageState extends State { if (widget.review == null) { final newReview = BookReview( - id: now.millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), bookId: widget.bookId, content: _contentController.text.trim(), reviewer: _reviewerController.text.trim(), diff --git a/lib/pages/book/book_tab_page.dart b/lib/pages/book/book_tab_page.dart index 210cd0e..270ccf2 100644 --- a/lib/pages/book/book_tab_page.dart +++ b/lib/pages/book/book_tab_page.dart @@ -295,7 +295,7 @@ class _BookTabPageState extends State { style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), - ElevatedButton(onPressed: () async { await context.read().removeBook(book.id); Navigator.pop(ctx); _loadFirst(); }, + ElevatedButton(onPressed: () async { await context.read().removeBook(book.id); if (!ctx.mounted) return; Navigator.pop(ctx); if (mounted) _loadFirst(); }, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), child: const Text('删除'), diff --git a/lib/pages/game/game_detail_page.dart b/lib/pages/game/game_detail_page.dart index 8c385e5..dd47df8 100644 --- a/lib/pages/game/game_detail_page.dart +++ b/lib/pages/game/game_detail_page.dart @@ -960,7 +960,7 @@ class _GameDetailPageState extends State { onPressed: () async { final provider = context.read(); await provider.removeGame(widget.game.id); - if (!mounted) return; + if (!mounted || !context.mounted) return; if (widget.embedded) { Navigator.of(context).pop(); provider.selectGame(null); @@ -969,7 +969,7 @@ class _GameDetailPageState extends State { navigator.pop(); navigator.pop(); } - if (mounted) { + if (mounted && context.mounted) { ToastUtil.show(context, '已删除'); } }, diff --git a/lib/pages/game/game_form_page.dart b/lib/pages/game/game_form_page.dart index 0ae19e9..d44d60f 100644 --- a/lib/pages/game/game_form_page.dart +++ b/lib/pages/game/game_form_page.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -185,6 +186,7 @@ class _GameFormPageState extends State { initialValue: _titleController.text, hint: '请输入游戏名称', ); + if (!mounted) return; if (result != null) setState(() => _titleController.text = result); }, ), @@ -210,6 +212,7 @@ class _GameFormPageState extends State { initialSelected: _platforms, hint: '如:PS5、Switch、Steam', ); + if (!mounted) return; if (result != null) setState(() => _platforms = result); }, ), @@ -235,6 +238,7 @@ class _GameFormPageState extends State { initialSelected: _versions, hint: '如:标准版、豪华版', ); + if (!mounted) return; if (result != null) setState(() => _versions = result); }, ), @@ -269,6 +273,7 @@ class _GameFormPageState extends State { initialSelected: _genres, hint: '如:RPG、动作、冒险', ); + if (!mounted) return; if (result != null) setState(() => _genres = result); }, ), @@ -305,6 +310,7 @@ class _GameFormPageState extends State { initialSelected: _purchasePlatforms, hint: '如:Steam、eShop、PlayStation Store', ); + if (!mounted) return; if (result != null) setState(() => _purchasePlatforms = result); }, ), @@ -344,6 +350,7 @@ class _GameFormPageState extends State { hint: '如:298元、49.99美元', keyboardType: TextInputType.text, ); + if (!mounted) return; if (result != null) setState(() => _purchasePriceController.text = result); }, ), @@ -496,6 +503,7 @@ class _GameFormPageState extends State { builder: (_) => _SummaryEditorPage(initialText: _summaryController.text), ), ); + if (!mounted) return; if (result != null) { setState(() => _summaryController.text = result); } @@ -753,10 +761,11 @@ class _GameFormPageState extends State { if (pickedFile != null) { final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final gameId = widget.game?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final gameId = widget.game?.id ?? const Uuid().v4(); final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await File(pickedFile.path).copy(targetPath); + if (!mounted) return; setState(() => _coverPath = targetPath); } } catch (e) { @@ -919,11 +928,12 @@ class _GameFormPageState extends State { if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final gameId = widget.game?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final gameId = widget.game?.id ?? const Uuid().v4(); final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await File(targetPath).writeAsBytes(response.bodyBytes); + if (!mounted) return; setState(() => _coverPath = targetPath); } catch (e) { debugPrint('封面下载失败: $e'); @@ -1009,6 +1019,7 @@ class _GameFormPageState extends State { lastDate: DateTime.now().add(const Duration(days: 365 * 5)), builder: (context, child) => child!, ); + if (!mounted) return; if (picked != null) { setState(() => _purchaseDate = picked); } @@ -1073,7 +1084,7 @@ class _GameFormPageState extends State { final now = DateTime.now(); if (widget.game == null) { - final newGameId = now.millisecondsSinceEpoch.toString(); + final newGameId = const Uuid().v4(); String? finalCoverPath; if (_coverPath != null && _coverPath!.isNotEmpty) { finalCoverPath = await _moveCoverToNewId(_coverPath!, newGameId); diff --git a/lib/pages/game/game_review_form_page.dart b/lib/pages/game/game_review_form_page.dart index a51e14e..9a4ae45 100644 --- a/lib/pages/game/game_review_form_page.dart +++ b/lib/pages/game/game_review_form_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; @@ -235,7 +236,7 @@ class _GameReviewFormPageState extends State { final now = DateTime.now(); if (widget.review == null) { final newReview = GameReview( - id: now.millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), gameId: widget.gameId, content: _contentController.text.trim(), reviewer: _reviewerController.text.trim(), diff --git a/lib/pages/game/game_screenshots_page.dart b/lib/pages/game/game_screenshots_page.dart index 8244118..d1a1314 100644 --- a/lib/pages/game/game_screenshots_page.dart +++ b/lib/pages/game/game_screenshots_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:http/http.dart' as http; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -230,7 +231,7 @@ class _GameScreenshotsPageState extends State { await File(pickedFile.path).copy(targetPath); final newScreenshot = GameScreenshot( - id: DateTime.now().millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), gameId: widget.game.id, screenshotPath: targetPath, createdAt: DateTime.now(), @@ -317,7 +318,7 @@ class _GameScreenshotsPageState extends State { await File(targetPath).writeAsBytes(response.bodyBytes); final newScreenshot = GameScreenshot( - id: DateTime.now().millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), gameId: widget.game.id, screenshotPath: targetPath, createdAt: DateTime.now(), diff --git a/lib/pages/game/game_tab_page.dart b/lib/pages/game/game_tab_page.dart index bfd2b26..c20b35a 100644 --- a/lib/pages/game/game_tab_page.dart +++ b/lib/pages/game/game_tab_page.dart @@ -446,8 +446,9 @@ class _GameTabPageState extends State { ElevatedButton( onPressed: () async { await context.read().removeGame(game.id); + if (!ctx.mounted) return; Navigator.pop(ctx); - _loadFirst(); + if (mounted) _loadFirst(); }, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), diff --git a/lib/pages/home/main_content_page.dart b/lib/pages/home/main_content_page.dart index 46ff3d4..342a6fe 100644 --- a/lib/pages/home/main_content_page.dart +++ b/lib/pages/home/main_content_page.dart @@ -45,12 +45,19 @@ class _MainContentPageState extends State { } void _loadTabSettings() { - setState(() { - _showMovieTab = _userPrefs.showMovieTab; - _showBookTab = _userPrefs.showBookTab; - _showNoteTab = _userPrefs.showNoteTab; - _showGameTab = _userPrefs.showGameTab; - }); + final newMovie = _userPrefs.showMovieTab; + final newBook = _userPrefs.showBookTab; + final newNote = _userPrefs.showNoteTab; + final newGame = _userPrefs.showGameTab; + if (newMovie != _showMovieTab || newBook != _showBookTab || + newNote != _showNoteTab || newGame != _showGameTab) { + setState(() { + _showMovieTab = newMovie; + _showBookTab = newBook; + _showNoteTab = newNote; + _showGameTab = newGame; + }); + } } @override diff --git a/lib/pages/movies/movie_detail_page.dart b/lib/pages/movies/movie_detail_page.dart index 6cc1535..66826c0 100644 --- a/lib/pages/movies/movie_detail_page.dart +++ b/lib/pages/movies/movie_detail_page.dart @@ -1189,7 +1189,7 @@ class _MovieDetailPageState extends State { onPressed: () async { final provider = context.read(); await provider.removeMovie(widget.movie.id); - if (!mounted) return; + if (!mounted || !context.mounted) return; if (widget.embedded) { Navigator.of(context).pop(); // close dialog provider.selectMovie(null); @@ -1198,7 +1198,7 @@ class _MovieDetailPageState extends State { navigator.pop(); navigator.pop(); } - if (mounted) { + if (mounted && context.mounted) { ToastUtil.show(context, '已删除'); } }, diff --git a/lib/pages/movies/movie_form_page.dart b/lib/pages/movies/movie_form_page.dart index bb47262..c1145d5 100644 --- a/lib/pages/movies/movie_form_page.dart +++ b/lib/pages/movies/movie_form_page.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -256,6 +257,7 @@ class _MovieFormPageState extends State { arguments: url, ); + if (!mounted) return; // 处理返回的影视信息 if (result != null && result is Map) { _fillMovieInfo(result); @@ -353,7 +355,7 @@ class _MovieFormPageState extends State { if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final movieId = widget.movie?.id ?? const Uuid().v4(); final targetPath = await ImagePathHelper.instance.getMoviePosterPath(movieId, fileName); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await File(targetPath).writeAsBytes(response.bodyBytes); @@ -436,6 +438,7 @@ class _MovieFormPageState extends State { initialValue: _titleController.text, hint: '请输入影视名称', ); + if (!mounted) return; if (result != null) setState(() => _titleController.text = result); }, ), @@ -458,6 +461,7 @@ class _MovieFormPageState extends State { initialSelected: _alternateTitles, hint: '输入别名', ); + if (!mounted) return; if (result != null) setState(() => _alternateTitles = result); }, ), @@ -483,6 +487,7 @@ class _MovieFormPageState extends State { initialSelected: _directors, hint: '如:张艺谋、李安', ); + if (!mounted) return; if (result != null) setState(() => _directors = result); }, ), @@ -506,6 +511,7 @@ class _MovieFormPageState extends State { initialSelected: _writers, hint: '如:刘慈欣、王家卫', ); + if (!mounted) return; if (result != null) setState(() => _writers = result); }, ), @@ -531,6 +537,7 @@ class _MovieFormPageState extends State { initialSelected: _actors, hint: '如:梁朝伟、周星驰', ); + if (!mounted) return; if (result != null) setState(() => _actors = result); }, ), @@ -556,6 +563,7 @@ class _MovieFormPageState extends State { initialSelected: _genres, hint: '如:剧情、科幻、悬疑', ); + if (!mounted) return; if (result != null) setState(() => _genres = result); }, ), @@ -734,6 +742,7 @@ class _MovieFormPageState extends State { builder: (_) => _SummaryEditorPage(initialText: _summaryController.text), ), ); + if (!mounted) return; if (result != null) { setState(() => _summaryController.text = result); } @@ -1144,7 +1153,7 @@ class _MovieFormPageState extends State { final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; // 如果是编辑模式,使用现有影视ID;如果是新建模式,使用临时ID(保存时会替换) - final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); + final movieId = widget.movie?.id ?? const Uuid().v4(); // 保存到新的路径结构: images/movies/{movieId}/{fileName} final targetPath = await ImagePathHelper.instance.getMoviePosterPath( @@ -1155,6 +1164,7 @@ class _MovieFormPageState extends State { await File(pickedFile.path).copy(targetPath); + if (!mounted) return; setState(() => _posterPath = targetPath); } } catch (e) { @@ -1237,6 +1247,7 @@ class _MovieFormPageState extends State { builder: (context, child) => child!, ); + if (!mounted) return; if (picked != null) { setState(() => _releaseDate = picked); } @@ -1252,6 +1263,7 @@ class _MovieFormPageState extends State { builder: (context, child) => child!, ); + if (!mounted) return; if (picked != null) { setState(() => _watchDate = picked); } @@ -1321,7 +1333,7 @@ class _MovieFormPageState extends State { if (widget.movie == null) { // 生成新的影视ID - final newMovieId = now.millisecondsSinceEpoch.toString(); + final newMovieId = const Uuid().v4(); // 如果有海报,需要移动到正确的ID目录 String? finalPosterPath; diff --git a/lib/pages/movies/movie_posters_page.dart b/lib/pages/movies/movie_posters_page.dart index 2d05290..20ee35d 100644 --- a/lib/pages/movies/movie_posters_page.dart +++ b/lib/pages/movies/movie_posters_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:http/http.dart' as http; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -286,7 +287,7 @@ class _MoviePostersPageState extends State { await File(pickedFile.path).copy(targetPath); final newPoster = MoviePoster( - id: DateTime.now().millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), movieId: widget.movie.id, posterPath: targetPath, createdAt: DateTime.now(), @@ -423,7 +424,7 @@ class _MoviePostersPageState extends State { await File(targetPath).writeAsBytes(response.bodyBytes); final newPoster = MoviePoster( - id: DateTime.now().millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), movieId: widget.movie.id, posterPath: targetPath, createdAt: DateTime.now(), diff --git a/lib/pages/movies/movie_review_form_page.dart b/lib/pages/movies/movie_review_form_page.dart index f7273bf..f32fd30 100644 --- a/lib/pages/movies/movie_review_form_page.dart +++ b/lib/pages/movies/movie_review_form_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../widgets/fade_in_local_image.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; @@ -307,7 +308,7 @@ class _MovieReviewFormPageState extends State { if (widget.review == null) { final newReview = MovieReview( - id: now.millisecondsSinceEpoch.toString(), + id: const Uuid().v4(), movieId: widget.movieId, content: _contentController.text.trim(), reviewer: _reviewerController.text.trim(), diff --git a/lib/pages/movies/movie_tab_page.dart b/lib/pages/movies/movie_tab_page.dart index cdb3875..7bc74ab 100644 --- a/lib/pages/movies/movie_tab_page.dart +++ b/lib/pages/movies/movie_tab_page.dart @@ -447,8 +447,9 @@ class _MovieTabPageState extends State { ElevatedButton( onPressed: () async { await context.read().removeMovie(movie.id); + if (!ctx.mounted) return; Navigator.pop(ctx); - _loadFirst(); + if (mounted) _loadFirst(); }, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), diff --git a/lib/pages/note/note_form_page.dart b/lib/pages/note/note_form_page.dart index e99176b..1103e7b 100644 --- a/lib/pages/note/note_form_page.dart +++ b/lib/pages/note/note_form_page.dart @@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as p; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import '../../providers/app_provider.dart'; +import 'package:uuid/uuid.dart'; import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; @@ -97,7 +98,7 @@ class _NoteFormPageState extends State { await context.read().updateNote(updatedNote); _savedNote = updatedNote; } else { - final noteId = now.millisecondsSinceEpoch.toString(); + final noteId = const Uuid().v4(); List finalImages = []; if (_images.isNotEmpty) { final oldNoteId = _tempNoteId ?? noteId; @@ -701,7 +702,7 @@ class _NoteFormPageState extends State { await context.read().updateNote(updatedNote); } else { // 添加新笔记 - final noteId = now.millisecondsSinceEpoch.toString(); + final noteId = const Uuid().v4(); // 如果有图片,需要移动到正确的ID目录 List finalImages = []; @@ -799,7 +800,7 @@ class _NoteFormPageState extends State { noteId = widget.note!.id; } else { // 新建模式:使用已存在的临时ID或生成新的 - noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString(); + noteId = _tempNoteId ?? const Uuid().v4(); _tempNoteId = noteId; } @@ -810,10 +811,11 @@ class _NoteFormPageState extends State { await File(image.path).copy(targetPath); + if (!mounted) return; setState(() => _images.add(targetPath)); } } catch (e) { - ToastUtil.show(context, '选择图片失败: $e'); + if (mounted) ToastUtil.show(context, '选择图片失败: $e'); } } diff --git a/lib/pages/note/note_tab_page.dart b/lib/pages/note/note_tab_page.dart index 2974c5c..6c15b0a 100644 --- a/lib/pages/note/note_tab_page.dart +++ b/lib/pages/note/note_tab_page.dart @@ -392,7 +392,7 @@ class _NoteTabPageState extends State { TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), ElevatedButton( - onPressed: () async { await context.read().removeNote(note.id); Navigator.pop(ctx); _loadFirst(); }, + onPressed: () async { await context.read().removeNote(note.id); if (!ctx.mounted) return; Navigator.pop(ctx); if (mounted) _loadFirst(); }, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), diff --git a/lib/pages/sync/backup_page.dart b/lib/pages/sync/backup_page.dart index 9685cfc..7528591 100644 --- a/lib/pages/sync/backup_page.dart +++ b/lib/pages/sync/backup_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../services/sync/backup_service.dart'; -import '../../services/sync/auto_backup_service.dart'; import '../../utils/toast_util.dart'; /// 本地备份页面 @@ -16,27 +15,6 @@ class BackupPage extends StatefulWidget { class _BackupPageState extends State { bool _isExporting = false; bool _isImporting = false; - bool _autoBackupEnabled = false; - bool _isLoading = true; - String? _backupDirPath; - - @override - void initState() { - super.initState(); - _loadAutoBackupStatus(); - } - - Future _loadAutoBackupStatus() async { - final enabled = await AutoBackupService.instance.getEnabled(); - final dirPath = await AutoBackupService.instance.getBackupDirectoryPath(); - if (mounted) { - setState(() { - _autoBackupEnabled = enabled; - _backupDirPath = dirPath; - _isLoading = false; - }); - } - } @override Widget build(BuildContext context) { @@ -46,16 +24,9 @@ class _BackupPageState extends State { appBar: AppBar( title: const Text('本地备份'), ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : ListView( + body: ListView( padding: const EdgeInsets.all(20), children: [ - // 自动备份开关 - 紧凑一行 - _buildAutoBackupSection(colors), - - const SizedBox(height: 20), - // 手动备份 _buildSectionTitle(colors, '手动备份'), const SizedBox(height: 10), @@ -492,74 +463,4 @@ class _BackupPageState extends State { } } } - - /// 构建自动备份区域 - 紧凑一行 - Widget _buildAutoBackupSection(ColorScheme colors) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: colors.outline, width: 0.5), - ), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10), - ), - child: Icon(Icons.schedule, - size: 18, color: colors.onSurface.withValues(alpha: 0.6)), - ), - const SizedBox(width: 10), - Expanded( - child: _backupDirPath != null && _autoBackupEnabled - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '自动本地备份', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface), - ), - const SizedBox(height: 2), - Text( - _backupDirPath!, - style: - TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ) - : Text( - '自动本地备份', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface), - ), - ), - Switch( - value: _autoBackupEnabled, - onChanged: (value) async { - setState(() => _autoBackupEnabled = value); - await AutoBackupService.instance.setEnabled(value); - if (value) { - ToastUtil.show(context, '自动备份已开启'); - } else { - ToastUtil.show(context, '自动备份已关闭'); - } - await _loadAutoBackupStatus(); - }, - activeThumbColor: colors.primary, - activeTrackColor: colors.primary.withValues(alpha: 0.3), - inactiveThumbColor: colors.surface, - inactiveTrackColor: colors.outline, - ), - ], - ), - ); - } } diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index bdb2b52..e3e9fe0 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -101,19 +101,58 @@ class AppProvider extends ChangeNotifier { String? _lastEditedItemId; String? get lastEditedItemId => _lastEditedItemId; + // 数据库初始化失败标志 + bool _dbInitFailed = false; + bool get dbInitFailed => _dbInitFailed; + + /// 标记数据库初始化失败(供 UI 提示重试) + void markDbInitFailed() { + _dbInitFailed = true; + notifyListeners(); + } + + /// 重试数据库初始化 + Future retryInitDatabase() async { + _dbInitFailed = false; + notifyListeners(); + await initDatabase(); + } + // 初始化数据库 Future initDatabase() async { debugPrint('[AppProvider] initDatabase'); - final results = await Future.wait([ - _movieDao.getAllMovies(), - _bookDao.getAllBooks(), - _noteDao.getAllNotes(), - _gameDao.getAllGames(), - ]); - _movies = results[0] as List; - _books = results[1] as List; - _notes = results[2] as List; - _games = results[3] as List; + // 独立加载每个 DAO,避免一个失败导致全部中断 + try { + _movies = await _movieDao.getAllMovies(); + } catch (e) { + debugPrint('[AppProvider] 加载影视数据失败: $e'); + } + try { + _books = await _bookDao.getAllBooks(); + } catch (e) { + debugPrint('[AppProvider] 加载书籍数据失败: $e'); + } + try { + _notes = await _noteDao.getAllNotes(); + } catch (e) { + debugPrint('[AppProvider] 加载笔记数据失败: $e'); + } + try { + _games = await _gameDao.getAllGames(); + } catch (e) { + debugPrint('[AppProvider] 加载游戏数据失败: $e'); + } + // 检查是否全部失败 + if (_movies.isEmpty && _books.isEmpty && _notes.isEmpty && _games.isEmpty) { + // 可能是初始化全部失败(非空数据库场景下不合理),标记以便 UI 提示 + try { + final db = await DatabaseHelper.instance.database; + final count = (await db.rawQuery('SELECT COUNT(*) as cnt FROM movies')).first['cnt'] as int; + if (count > 0) _dbInitFailed = true; + } catch (_) { + _dbInitFailed = true; + } + } debugPrint('[AppProvider] 本地数据: movies=${_movies.length}, books=${_books.length}, notes=${_notes.length}, games=${_games.length}'); notifyListeners(); } @@ -387,12 +426,15 @@ class AppProvider extends ChangeNotifier { // 添加影视记录 Future addMovie(Movie movie) async { await _movieDao.insertMovie(movie); - await loadMovies(); + _movies.add(movie); + notifyListeners(); } Future updateMovie(Movie movie) async { await _movieDao.updateMovie(movie); - await loadMovies(); + final idx = _movies.indexWhere((m) => m.id == movie.id); + if (idx != -1) _movies[idx] = movie; + notifyListeners(); } /// 仅更新封面偏移量(不触发全量刷新) @@ -417,52 +459,65 @@ class AppProvider extends ChangeNotifier { Future removeMovie(String id) async { await _movieDao.deleteMovie(id); - await loadMovies(); + _movies.removeWhere((m) => m.id == id); + notifyListeners(); } Future addBook(Book book) async { await _bookDao.insertBook(book); - await loadBooks(); + _books.add(book); + notifyListeners(); } Future updateBook(Book book) async { await _bookDao.updateBook(book); - await loadBooks(); + final idx = _books.indexWhere((b) => b.id == book.id); + if (idx != -1) _books[idx] = book; + notifyListeners(); } Future removeBook(String id) async { await _bookDao.deleteBook(id); - await loadBooks(); + _books.removeWhere((b) => b.id == id); + notifyListeners(); } Future addNote(Note note) async { await _noteDao.insertNote(note); - await loadNotes(); + _notes.add(note); + notifyListeners(); } Future updateNote(Note note) async { await _noteDao.updateNote(note); - await loadNotes(); + final idx = _notes.indexWhere((n) => n.id == note.id); + if (idx != -1) _notes[idx] = note; + notifyListeners(); } Future removeNote(String id) async { await _noteDao.deleteNote(id); - await loadNotes(); + _notes.removeWhere((n) => n.id == id); + notifyListeners(); } Future addGame(Game game) async { await _gameDao.insertGame(game); - await loadGames(); + _games.add(game); + notifyListeners(); } Future updateGame(Game game) async { await _gameDao.updateGame(game); - await loadGames(); + final idx = _games.indexWhere((g) => g.id == game.id); + if (idx != -1) _games[idx] = game; + notifyListeners(); } Future removeGame(String id) async { await _gameDao.deleteGame(id); - await loadGames(); + _games.removeWhere((g) => g.id == id); + notifyListeners(); } /// 仅更新游戏封面偏移量(不触发全量刷新) @@ -477,7 +532,10 @@ class AppProvider extends ChangeNotifier { Future toggleNotePin(String id, bool isPinned) async { await _noteDao.togglePin(id, isPinned); - await loadNotes(); + final idx = _notes.indexWhere((n) => n.id == id); + if (idx != -1) { + _notes[idx] = _notes[idx].copyWith(isPinned: isPinned); + } notifyListeners(); } @@ -722,29 +780,59 @@ class AppProvider extends ChangeNotifier { final deletedBookExcerpts = await getDeletedBookExcerpts(); final deletedGameReviews = await getDeletedGameReviews(); - for (final movie in deletedMovies) { - await permanentDeleteMovie(movie.id); + // 先收集需要删除图片的 ID,再在事务中批量删除数据库记录 + final movieIds = deletedMovies.map((m) => m.id).toList(); + final bookIds = deletedBooks.map((b) => b.id).toList(); + final noteIds = deletedNotes.map((n) => n.id).toList(); + final gameIds = deletedGames.map((g) => g.id).toList(); + + // 事务内批量删除数据库记录,保证原子性 + final db = await DatabaseHelper.instance.database; + await db.transaction((txn) async { + for (final id in movieIds) { + await txn.delete('movie_reviews', where: 'movie_id = ?', whereArgs: [id]); + await txn.delete('movie_posters', where: 'movie_id = ?', whereArgs: [id]); + await txn.delete('movies', where: 'id = ?', whereArgs: [id]); + } + for (final id in bookIds) { + await txn.delete('book_reviews', where: 'book_id = ?', whereArgs: [id]); + await txn.delete('book_excerpts', where: 'book_id = ?', whereArgs: [id]); + await txn.delete('books', where: 'id = ?', whereArgs: [id]); + } + for (final id in noteIds) { + await txn.delete('notes', where: 'id = ?', whereArgs: [id]); + } + for (final id in gameIds) { + await txn.delete('game_reviews', where: 'game_id = ?', whereArgs: [id]); + await txn.delete('game_screenshots', where: 'game_id = ?', whereArgs: [id]); + await txn.delete('games', where: 'id = ?', whereArgs: [id]); + } + for (final review in deletedMovieReviews) { + await txn.delete('movie_reviews', where: 'id = ?', whereArgs: [review.id]); + } + for (final review in deletedBookReviews) { + await txn.delete('book_reviews', where: 'id = ?', whereArgs: [review.id]); + } + for (final excerpt in deletedBookExcerpts) { + await txn.delete('book_excerpts', where: 'id = ?', whereArgs: [excerpt.id]); + } + for (final review in deletedGameReviews) { + await txn.delete('game_reviews', where: 'id = ?', whereArgs: [review.id]); + } + }); + + // 事务成功后,清理关联的图片文件(文件删除失败不影响数据一致性) + for (final id in movieIds) { + await ImagePathHelper.instance.deleteMovieImages(id); } - for (final book in deletedBooks) { - await permanentDeleteBook(book.id); + for (final id in bookIds) { + await ImagePathHelper.instance.deleteBookImages(id); } - for (final note in deletedNotes) { - await permanentDeleteNote(note.id); + for (final id in noteIds) { + await ImagePathHelper.instance.deleteNoteImages(id); } - for (final game in deletedGames) { - await permanentDeleteGame(game.id); - } - for (final review in deletedMovieReviews) { - await _reviewDao.permanentDeleteReview(review.id); - } - for (final review in deletedBookReviews) { - await _bookReviewDao.permanentDeleteReview(review.id); - } - for (final excerpt in deletedBookExcerpts) { - await _bookExcerptDao.permanentDeleteExcerpt(excerpt.id); - } - for (final review in deletedGameReviews) { - await _gameReviewDao.permanentDeleteReview(review.id); + for (final id in gameIds) { + await ImagePathHelper.instance.deleteGameImages(id); } await loadMovies(); diff --git a/lib/services/usage_stats_service.dart b/lib/services/usage_stats_service.dart index 78dcdc0..eda18a4 100644 --- a/lib/services/usage_stats_service.dart +++ b/lib/services/usage_stats_service.dart @@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:uuid/uuid.dart'; import '../utils/user_prefs.dart'; import '../utils/server_config.dart'; @@ -87,7 +88,7 @@ class UsageStatsService with WidgetsBindingObserver { final info = await deviceInfo.linuxInfo; rawId = '${info.name}-${info.id}'; } else { - rawId = DateTime.now().millisecondsSinceEpoch.toString(); + rawId = const Uuid().v4(); } final bytes = utf8.encode(rawId);