优化代码结构,修复部分bug

This commit is contained in:
DelLevin-Home
2026-07-06 02:49:46 +08:00
parent 4a7296f00f
commit 2fdcd4551d
23 changed files with 372 additions and 356 deletions

View File

@@ -159,17 +159,7 @@ class DatabaseHelper {
await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0'); await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0');
} }
} }
// v16: 确保 cover_offset 列存在v15 的数据库可能缺少此列 // v16: 已合并到 v15cover_offset 列的添加逻辑相同v15 的 PRAGMA 检查已确保幂等
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');
}
}
if (oldVersion < 17) { if (oldVersion < 17) {
await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0'); await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0');
} }
@@ -248,9 +238,7 @@ class DatabaseHelper {
if (oldVersion < 26) { if (oldVersion < 26) {
await _upgradeBooksTableV26(db); await _upgradeBooksTableV26(db);
} }
if (oldVersion < 27) { // v27: 已合并到 v26start_date/finish_date 的添加逻辑相同v26 的 PRAGMA 检查已确保幂等)
await _upgradeBooksTableV27(db);
}
if (oldVersion < 28) { if (oldVersion < 28) {
// 移除 Note Plus 功能,删除 note_plus 表 // 移除 Note Plus 功能,删除 note_plus 表
await db.execute('DROP TABLE IF EXISTS note_plus'); await db.execute('DROP TABLE IF EXISTS note_plus');
@@ -341,20 +329,6 @@ class DatabaseHelper {
} }
} }
/// 升级books表到V27添加阅读始末日期字段
Future<void> _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添加阅读始末日期字段 /// 升级books表到V26添加阅读始末日期字段
Future<void> _upgradeBooksTableV26(Database db) async { Future<void> _upgradeBooksTableV26(Database db) async {
final columns = await db.rawQuery('PRAGMA table_info(books)'); final columns = await db.rawQuery('PRAGMA table_info(books)');
@@ -567,153 +541,159 @@ class DatabaseHelper {
/// 升级notes表到V4 /// 升级notes表到V4
Future<void> _upgradeNotesTableV4(Database db) async { Future<void> _upgradeNotesTableV4(Database db) async {
// 备份旧数据 await db.transaction((txn) async {
final oldData = await db.query('notes'); // 备份旧数据
final oldData = await txn.query('notes');
// 删除旧表 // 删除旧表
await db.execute('DROP TABLE IF EXISTS notes'); await txn.execute('DROP TABLE IF EXISTS notes');
// 创建新表 // 创建新表
await db.execute(''' await txn.execute('''
CREATE TABLE notes ( CREATE TABLE notes (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
title TEXT DEFAULT '', title TEXT DEFAULT '',
content TEXT NOT NULL, content TEXT NOT NULL,
content_type TEXT DEFAULT 'markdown', content_type TEXT DEFAULT 'markdown',
tags TEXT, tags TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL updated_at TEXT NOT NULL
) )
'''); ''');
// 迁移旧数据将title字段恢复 // 迁移旧数据将title字段恢复
for (final row in oldData) { for (final row in oldData) {
try { try {
final now = DateTime.now().toIso8601String(); final now = DateTime.now().toIso8601String();
final title = row['title']?.toString() ?? ''; final title = row['title']?.toString() ?? '';
final content = row['content']?.toString() ?? ''; final content = row['content']?.toString() ?? '';
await db.insert('notes', { await txn.insert('notes', {
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
'title': title, 'title': title,
'content': content, 'content': content,
'content_type': 'markdown', 'content_type': 'markdown',
'tags': row['tags'] ?? '', 'tags': row['tags'] ?? '',
'created_at': row['created_at']?.toString() ?? now, 'created_at': row['created_at']?.toString() ?? now,
'updated_at': row['updated_at']?.toString() ?? now, 'updated_at': row['updated_at']?.toString() ?? now,
}); });
} catch (e) { } catch (e) {
debugPrint('[DB] 迁移笔记记录失败: $e'); debugPrint('[DB] 迁移笔记记录失败: $e');
}
} }
} });
} }
/// 升级books表到V3 /// 升级books表到V3
Future<void> _upgradeBooksTableV3(Database db) async { Future<void> _upgradeBooksTableV3(Database db) async {
// 备份旧数据 await db.transaction((txn) async {
final oldData = await db.query('books'); // 备份旧数据
final oldData = await txn.query('books');
// 删除旧表 // 删除旧表
await db.execute('DROP TABLE IF EXISTS books'); await txn.execute('DROP TABLE IF EXISTS books');
// 创建新表 // 创建新表
await db.execute(''' await txn.execute('''
CREATE TABLE books ( CREATE TABLE books (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
cover_path TEXT, cover_path TEXT,
authors TEXT, authors TEXT,
alternate_titles TEXT, alternate_titles TEXT,
publisher TEXT, publisher TEXT,
genres TEXT, genres TEXT,
summary TEXT, summary TEXT,
rating REAL, rating REAL,
status TEXT NOT NULL, status TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0 is_deleted INTEGER DEFAULT 0
) )
'''); ''');
// 迁移旧数据 // 迁移旧数据
for (final row in oldData) { for (final row in oldData) {
try { try {
final now = DateTime.now().toIso8601String(); final now = DateTime.now().toIso8601String();
await db.insert('books', { await txn.insert('books', {
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
'title': row['title']?.toString() ?? '', 'title': row['title']?.toString() ?? '',
'cover_path': row['cover'], 'cover_path': row['cover'],
'authors': row['author'] != null ? '["${row['author']}"]' : '[]', 'authors': row['author'] != null ? '["${row['author']}"]' : '[]',
'alternate_titles': '[]', 'alternate_titles': '[]',
'publisher': null, 'publisher': null,
'genres': '[]', 'genres': '[]',
'summary': row['note'], 'summary': row['note'],
'rating': row['rating'], 'rating': row['rating'],
'status': row['status'] ?? 'want_to_read', 'status': row['status'] ?? 'want_to_read',
'created_at': now, 'created_at': now,
'updated_at': now, 'updated_at': now,
'is_deleted': 0, 'is_deleted': 0,
}); });
} catch (e) { } catch (e) {
debugPrint('[DB] 迁移书籍记录失败: $e'); debugPrint('[DB] 迁移书籍记录失败: $e');
}
} }
} });
} }
/// 升级movies表到V2 /// 升级movies表到V2
Future<void> _upgradeMoviesTableV2(Database db) async { Future<void> _upgradeMoviesTableV2(Database db) async {
// 备份旧数据 await db.transaction((txn) async {
final oldData = await db.query('movies'); // 备份旧数据
final oldData = await txn.query('movies');
// 删除旧表 // 删除旧表
await db.execute('DROP TABLE IF EXISTS movies'); await txn.execute('DROP TABLE IF EXISTS movies');
// 创建新表 // 创建新表
await db.execute(''' await txn.execute('''
CREATE TABLE movies ( CREATE TABLE movies (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
poster_path TEXT, poster_path TEXT,
release_date TEXT, release_date TEXT,
directors TEXT, directors TEXT,
writers TEXT, writers TEXT,
actors TEXT, actors TEXT,
genres TEXT, genres TEXT,
alternate_titles TEXT, alternate_titles TEXT,
summary TEXT, summary TEXT,
rating REAL, rating REAL,
status TEXT NOT NULL, status TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'movie', category TEXT NOT NULL DEFAULT 'movie',
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0 is_deleted INTEGER DEFAULT 0
) )
'''); ''');
// 迁移旧数据(尽可能保留) // 迁移旧数据(尽可能保留)
for (final row in oldData) { for (final row in oldData) {
try { try {
await db.insert('movies', { await txn.insert('movies', {
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
'title': row['title']?.toString() ?? '', 'title': row['title']?.toString() ?? '',
'poster_path': row['poster_path'], 'poster_path': row['poster_path'],
'release_date': null, 'release_date': null,
'directors': '[]', 'directors': '[]',
'writers': '[]', 'writers': '[]',
'actors': '[]', 'actors': '[]',
'genres': '[]', 'genres': '[]',
'alternate_titles': '[]', 'alternate_titles': '[]',
'summary': row['note'], 'summary': row['note'],
'rating': row['rating'], 'rating': row['rating'],
'status': row['status'] ?? 'want_to_watch', 'status': row['status'] ?? 'want_to_watch',
'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(), 'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(),
'is_deleted': row['is_deleted'] ?? 0, 'is_deleted': row['is_deleted'] ?? 0,
}); });
} catch (e) { } catch (e) {
debugPrint('[DB] 迁移影视记录失败: $e'); debugPrint('[DB] 迁移影视记录失败: $e');
}
} }
} });
} }
// 创建数据库表 // 创建数据库表

View File

@@ -144,6 +144,9 @@ class GameDao {
// 彻底删除游戏 // 彻底删除游戏
Future<int> permanentDeleteGame(String id) => _wrap('permanentDeleteGame', () async { Future<int> permanentDeleteGame(String id) => _wrap('permanentDeleteGame', () async {
final db = await _dbHelper.database; 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( return await db.delete(
'games', 'games',
where: 'id = ?', where: 'id = ?',

View File

@@ -14,7 +14,6 @@ import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart'; import 'utils/app_router.dart';
import 'utils/user_prefs.dart'; import 'utils/user_prefs.dart';
import 'services/changelog_service.dart'; import 'services/changelog_service.dart';
import 'services/sync/auto_backup_service.dart';
import 'services/usage_stats_service.dart'; import 'services/usage_stats_service.dart';
import 'providers/app_provider.dart'; import 'providers/app_provider.dart';
@@ -42,24 +41,13 @@ Future<void> _bootstrap(AppProvider appProvider) async {
await appProvider.initDatabase(); await appProvider.initDatabase();
} catch (e) { } catch (e) {
debugPrint('[Startup] 数据库初始化失败: $e'); debugPrint('[Startup] 数据库初始化失败: $e');
appProvider.markDbInitFailed();
} }
appProvider.initMainTabIndex(); appProvider.initMainTabIndex();
unawaited(_initAutoBackup());
unawaited(_initUsageStats()); unawaited(_initUsageStats());
} }
Future<void> _initAutoBackup() async {
try {
final isLocalAutoBackupEnabled = await AutoBackupService.instance.getEnabled();
if (isLocalAutoBackupEnabled) {
await AutoBackupService.instance.start();
}
} catch (e) {
debugPrint('初始化自动备份失败: $e');
}
}
Future<void> _initUsageStats() async { Future<void> _initUsageStats() async {
try { try {
await UsageStatsService.instance.start(); await UsageStatsService.instance.start();

View File

@@ -1374,14 +1374,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
await context.read<AppProvider>().removeBook(widget.book.id); await context.read<AppProvider>().removeBook(widget.book.id);
if (!mounted) return; if (!mounted || !context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
if (widget.embedded) { if (widget.embedded) {
context.read<AppProvider>().selectBook(null); context.read<AppProvider>().selectBook(null);
} else { } else {
Navigator.pop(context); Navigator.pop(context);
} }
ToastUtil.show(context, '已删除'); if (mounted && context.mounted) {
ToastUtil.show(context, '已删除');
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: colors.error, backgroundColor: colors.error,

View File

@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -157,12 +158,14 @@ class _BookFormPageState extends State<BookFormPage> {
_halfCard('书名', _titleController.text, Icons.book_outlined, required: true, _halfCard('书名', _titleController.text, Icons.book_outlined, required: true,
onTap: () async { onTap: () async {
final r = await TextInputPanel.show(context: context, title: '书名', initialValue: _titleController.text, hint: '请输入书名'); final r = await TextInputPanel.show(context: context, title: '书名', initialValue: _titleController.text, hint: '请输入书名');
if (!mounted) return;
if (r != null) setState(() => _titleController.text = r); if (r != null) setState(() => _titleController.text = r);
}, },
), ),
_halfCard('别名', _alternateTitles.isEmpty ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('')}', Icons.alternate_email_outlined, _halfCard('别名', _alternateTitles.isEmpty ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('')}', Icons.alternate_email_outlined,
onTap: () async { onTap: () async {
final r = await GenreSelectorPage.show(context: context, title: '添加别名', existingTags: [], initialSelected: _alternateTitles, hint: '输入别名'); final r = await GenreSelectorPage.show(context: context, title: '添加别名', existingTags: [], initialSelected: _alternateTitles, hint: '输入别名');
if (!mounted) return;
if (r != null) setState(() => _alternateTitles = r); if (r != null) setState(() => _alternateTitles = r);
}, },
), ),
@@ -173,6 +176,7 @@ class _BookFormPageState extends State<BookFormPage> {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
final data = provider.books.map((b) => b.authors).toList(); final data = provider.books.map((b) => b.authors).toList();
final r = await GenreSelectorPage.show(context: context, title: '选择作者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _authors, hint: '如:余华、莫言'); final r = await GenreSelectorPage.show(context: context, title: '选择作者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _authors, hint: '如:余华、莫言');
if (!mounted) return;
if (r != null) setState(() => _authors = r); if (r != null) setState(() => _authors = r);
}, },
), ),
@@ -181,12 +185,14 @@ class _BookFormPageState extends State<BookFormPage> {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
final data = provider.books.map((b) => b.translators).toList(); final data = provider.books.map((b) => b.translators).toList();
final r = await GenreSelectorPage.show(context: context, title: '选择译者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _translators, hint: '如:李继宏、许钧'); final r = await GenreSelectorPage.show(context: context, title: '选择译者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _translators, hint: '如:李继宏、许钧');
if (!mounted) return;
if (r != null) setState(() => _translators = r); if (r != null) setState(() => _translators = r);
}, },
), ),
_halfCard('出版社', _publisherController.text, Icons.business_outlined, _halfCard('出版社', _publisherController.text, Icons.business_outlined,
onTap: () async { onTap: () async {
final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社'); final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社');
if (!mounted) return;
if (r != null) setState(() => _publisherController.text = r); if (r != null) setState(() => _publisherController.text = r);
}, },
), ),
@@ -198,12 +204,14 @@ class _BookFormPageState extends State<BookFormPage> {
final tags = await provider.getTags('book_genre', excludeHidden: true); final tags = await provider.getTags('book_genre', excludeHidden: true);
if (!mounted) return; if (!mounted) return;
final r = await GenreSelectorPage.show(context: context, title: '选择类型', existingTags: tags.map((t) => t['name'] as String).toList(), initialSelected: _genres, hint: '如:小说、历史、传记'); 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); if (r != null) setState(() => _genres = r);
}, },
), ),
_halfCard('ISBN', _isbnController.text, Icons.qr_code_outlined, _halfCard('ISBN', _isbnController.text, Icons.qr_code_outlined,
onTap: () async { onTap: () async {
final r = await TextInputPanel.show(context: context, title: 'ISBN', initialValue: _isbnController.text, hint: '请输入ISBN编号'); final r = await TextInputPanel.show(context: context, title: 'ISBN', initialValue: _isbnController.text, hint: '请输入ISBN编号');
if (!mounted) return;
if (r != null) setState(() => _isbnController.text = r); if (r != null) setState(() => _isbnController.text = r);
}, },
), ),
@@ -425,10 +433,11 @@ class _BookFormPageState extends State<BookFormPage> {
final picked = await _picker.pickImage(source: ImageSource.gallery, maxWidth: 800, maxHeight: 1200, imageQuality: 85); final picked = await _picker.pickImage(source: ImageSource.gallery, maxWidth: 800, maxHeight: 1200, imageQuality: 85);
if (picked != null) { if (picked != null) {
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; 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); final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(picked.path).copy(targetPath); await File(picked.path).copy(targetPath);
if (!mounted) return;
setState(() => _coverPath = targetPath); setState(() => _coverPath = targetPath);
} }
} catch (e) { } catch (e) {
@@ -492,10 +501,11 @@ class _BookFormPageState extends State<BookFormPage> {
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; 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); final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(targetPath).writeAsBytes(response.bodyBytes); await File(targetPath).writeAsBytes(response.bodyBytes);
if (!mounted) return;
setState(() => _coverPath = targetPath); setState(() => _coverPath = targetPath);
} catch (e) { } catch (e) {
debugPrint('封面下载失败: $e'); debugPrint('封面下载失败: $e');
@@ -556,21 +566,25 @@ class _BookFormPageState extends State<BookFormPage> {
Future<void> _selectPublishDate() async { Future<void> _selectPublishDate() async {
final picked = await showDatePicker(context: context, initialDate: _publishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); 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); if (picked != null) setState(() => _publishDate = picked);
} }
Future<void> _selectStartDate() async { Future<void> _selectStartDate() async {
final picked = await showDatePicker(context: context, initialDate: _startDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); 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); if (picked != null) setState(() => _startDate = picked);
} }
Future<void> _selectFinishDate() async { Future<void> _selectFinishDate() async {
final picked = await showDatePicker(context: context, initialDate: _finishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); 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); if (picked != null) setState(() => _finishDate = picked);
} }
Future<void> _editSummary() async { Future<void> _editSummary() async {
final result = await Navigator.push<String>(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text))); final result = await Navigator.push<String>(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text)));
if (!mounted) return;
if (result != null) setState(() => _summaryController.text = result); if (result != null) setState(() => _summaryController.text = result);
} }
@@ -619,7 +633,7 @@ class _BookFormPageState extends State<BookFormPage> {
final now = DateTime.now(); final now = DateTime.now();
if (widget.book == null) { if (widget.book == null) {
final newBookId = now.millisecondsSinceEpoch.toString(); final newBookId = const Uuid().v4();
String? finalCoverPath; String? finalCoverPath;
if (_coverPath != null && _coverPath!.isNotEmpty) { if (_coverPath != null && _coverPath!.isNotEmpty) {
finalCoverPath = await _moveCoverToNewId(_coverPath!, newBookId); finalCoverPath = await _moveCoverToNewId(_coverPath!, newBookId);

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
@@ -313,7 +314,7 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
if (widget.review == null) { if (widget.review == null) {
final newReview = BookReview( final newReview = BookReview(
id: now.millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
bookId: widget.bookId, bookId: widget.bookId,
content: _contentController.text.trim(), content: _contentController.text.trim(),
reviewer: _reviewerController.text.trim(), reviewer: _reviewerController.text.trim(),

View File

@@ -295,7 +295,7 @@ class _BookTabPageState extends State<BookTabPage> {
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
ElevatedButton(onPressed: () async { await context.read<AppProvider>().removeBook(book.id); Navigator.pop(ctx); _loadFirst(); }, ElevatedButton(onPressed: () async { await context.read<AppProvider>().removeBook(book.id); if (!ctx.mounted) return; Navigator.pop(ctx); if (mounted) _loadFirst(); },
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, 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)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
child: const Text('删除'), child: const Text('删除'),

View File

@@ -960,7 +960,7 @@ class _GameDetailPageState extends State<GameDetailPage> {
onPressed: () async { onPressed: () async {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
await provider.removeGame(widget.game.id); await provider.removeGame(widget.game.id);
if (!mounted) return; if (!mounted || !context.mounted) return;
if (widget.embedded) { if (widget.embedded) {
Navigator.of(context).pop(); Navigator.of(context).pop();
provider.selectGame(null); provider.selectGame(null);
@@ -969,7 +969,7 @@ class _GameDetailPageState extends State<GameDetailPage> {
navigator.pop(); navigator.pop();
navigator.pop(); navigator.pop();
} }
if (mounted) { if (mounted && context.mounted) {
ToastUtil.show(context, '已删除'); ToastUtil.show(context, '已删除');
} }
}, },

View File

@@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -185,6 +186,7 @@ class _GameFormPageState extends State<GameFormPage> {
initialValue: _titleController.text, initialValue: _titleController.text,
hint: '请输入游戏名称', hint: '请输入游戏名称',
); );
if (!mounted) return;
if (result != null) setState(() => _titleController.text = result); if (result != null) setState(() => _titleController.text = result);
}, },
), ),
@@ -210,6 +212,7 @@ class _GameFormPageState extends State<GameFormPage> {
initialSelected: _platforms, initialSelected: _platforms,
hint: 'PS5、Switch、Steam', hint: 'PS5、Switch、Steam',
); );
if (!mounted) return;
if (result != null) setState(() => _platforms = result); if (result != null) setState(() => _platforms = result);
}, },
), ),
@@ -235,6 +238,7 @@ class _GameFormPageState extends State<GameFormPage> {
initialSelected: _versions, initialSelected: _versions,
hint: '如:标准版、豪华版', hint: '如:标准版、豪华版',
); );
if (!mounted) return;
if (result != null) setState(() => _versions = result); if (result != null) setState(() => _versions = result);
}, },
), ),
@@ -269,6 +273,7 @@ class _GameFormPageState extends State<GameFormPage> {
initialSelected: _genres, initialSelected: _genres,
hint: 'RPG、动作、冒险', hint: 'RPG、动作、冒险',
); );
if (!mounted) return;
if (result != null) setState(() => _genres = result); if (result != null) setState(() => _genres = result);
}, },
), ),
@@ -305,6 +310,7 @@ class _GameFormPageState extends State<GameFormPage> {
initialSelected: _purchasePlatforms, initialSelected: _purchasePlatforms,
hint: 'Steam、eShop、PlayStation Store', hint: 'Steam、eShop、PlayStation Store',
); );
if (!mounted) return;
if (result != null) setState(() => _purchasePlatforms = result); if (result != null) setState(() => _purchasePlatforms = result);
}, },
), ),
@@ -344,6 +350,7 @@ class _GameFormPageState extends State<GameFormPage> {
hint: '298元、49.99美元', hint: '298元、49.99美元',
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
); );
if (!mounted) return;
if (result != null) setState(() => _purchasePriceController.text = result); if (result != null) setState(() => _purchasePriceController.text = result);
}, },
), ),
@@ -496,6 +503,7 @@ class _GameFormPageState extends State<GameFormPage> {
builder: (_) => _SummaryEditorPage(initialText: _summaryController.text), builder: (_) => _SummaryEditorPage(initialText: _summaryController.text),
), ),
); );
if (!mounted) return;
if (result != null) { if (result != null) {
setState(() => _summaryController.text = result); setState(() => _summaryController.text = result);
} }
@@ -753,10 +761,11 @@ class _GameFormPageState extends State<GameFormPage> {
if (pickedFile != null) { if (pickedFile != null) {
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; 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); final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(pickedFile.path).copy(targetPath); await File(pickedFile.path).copy(targetPath);
if (!mounted) return;
setState(() => _coverPath = targetPath); setState(() => _coverPath = targetPath);
} }
} catch (e) { } catch (e) {
@@ -919,11 +928,12 @@ class _GameFormPageState extends State<GameFormPage> {
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; 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); final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(targetPath).writeAsBytes(response.bodyBytes); await File(targetPath).writeAsBytes(response.bodyBytes);
if (!mounted) return;
setState(() => _coverPath = targetPath); setState(() => _coverPath = targetPath);
} catch (e) { } catch (e) {
debugPrint('封面下载失败: $e'); debugPrint('封面下载失败: $e');
@@ -1009,6 +1019,7 @@ class _GameFormPageState extends State<GameFormPage> {
lastDate: DateTime.now().add(const Duration(days: 365 * 5)), lastDate: DateTime.now().add(const Duration(days: 365 * 5)),
builder: (context, child) => child!, builder: (context, child) => child!,
); );
if (!mounted) return;
if (picked != null) { if (picked != null) {
setState(() => _purchaseDate = picked); setState(() => _purchaseDate = picked);
} }
@@ -1073,7 +1084,7 @@ class _GameFormPageState extends State<GameFormPage> {
final now = DateTime.now(); final now = DateTime.now();
if (widget.game == null) { if (widget.game == null) {
final newGameId = now.millisecondsSinceEpoch.toString(); final newGameId = const Uuid().v4();
String? finalCoverPath; String? finalCoverPath;
if (_coverPath != null && _coverPath!.isNotEmpty) { if (_coverPath != null && _coverPath!.isNotEmpty) {
finalCoverPath = await _moveCoverToNewId(_coverPath!, newGameId); finalCoverPath = await _moveCoverToNewId(_coverPath!, newGameId);

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
@@ -235,7 +236,7 @@ class _GameReviewFormPageState extends State<GameReviewFormPage> {
final now = DateTime.now(); final now = DateTime.now();
if (widget.review == null) { if (widget.review == null) {
final newReview = GameReview( final newReview = GameReview(
id: now.millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
gameId: widget.gameId, gameId: widget.gameId,
content: _contentController.text.trim(), content: _contentController.text.trim(),
reviewer: _reviewerController.text.trim(), reviewer: _reviewerController.text.trim(),

View File

@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -230,7 +231,7 @@ class _GameScreenshotsPageState extends State<GameScreenshotsPage> {
await File(pickedFile.path).copy(targetPath); await File(pickedFile.path).copy(targetPath);
final newScreenshot = GameScreenshot( final newScreenshot = GameScreenshot(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
gameId: widget.game.id, gameId: widget.game.id,
screenshotPath: targetPath, screenshotPath: targetPath,
createdAt: DateTime.now(), createdAt: DateTime.now(),
@@ -317,7 +318,7 @@ class _GameScreenshotsPageState extends State<GameScreenshotsPage> {
await File(targetPath).writeAsBytes(response.bodyBytes); await File(targetPath).writeAsBytes(response.bodyBytes);
final newScreenshot = GameScreenshot( final newScreenshot = GameScreenshot(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
gameId: widget.game.id, gameId: widget.game.id,
screenshotPath: targetPath, screenshotPath: targetPath,
createdAt: DateTime.now(), createdAt: DateTime.now(),

View File

@@ -446,8 +446,9 @@ class _GameTabPageState extends State<GameTabPage> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
await context.read<AppProvider>().removeGame(game.id); await context.read<AppProvider>().removeGame(game.id);
if (!ctx.mounted) return;
Navigator.pop(ctx); Navigator.pop(ctx);
_loadFirst(); if (mounted) _loadFirst();
}, },
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),

View File

@@ -45,12 +45,19 @@ class _MainContentPageState extends State<MainContentPage> {
} }
void _loadTabSettings() { void _loadTabSettings() {
setState(() { final newMovie = _userPrefs.showMovieTab;
_showMovieTab = _userPrefs.showMovieTab; final newBook = _userPrefs.showBookTab;
_showBookTab = _userPrefs.showBookTab; final newNote = _userPrefs.showNoteTab;
_showNoteTab = _userPrefs.showNoteTab; final newGame = _userPrefs.showGameTab;
_showGameTab = _userPrefs.showGameTab; if (newMovie != _showMovieTab || newBook != _showBookTab ||
}); newNote != _showNoteTab || newGame != _showGameTab) {
setState(() {
_showMovieTab = newMovie;
_showBookTab = newBook;
_showNoteTab = newNote;
_showGameTab = newGame;
});
}
} }
@override @override

View File

@@ -1189,7 +1189,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
onPressed: () async { onPressed: () async {
final provider = context.read<AppProvider>(); final provider = context.read<AppProvider>();
await provider.removeMovie(widget.movie.id); await provider.removeMovie(widget.movie.id);
if (!mounted) return; if (!mounted || !context.mounted) return;
if (widget.embedded) { if (widget.embedded) {
Navigator.of(context).pop(); // close dialog Navigator.of(context).pop(); // close dialog
provider.selectMovie(null); provider.selectMovie(null);
@@ -1198,7 +1198,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
navigator.pop(); navigator.pop();
navigator.pop(); navigator.pop();
} }
if (mounted) { if (mounted && context.mounted) {
ToastUtil.show(context, '已删除'); ToastUtil.show(context, '已删除');
} }
}, },

View File

@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -256,6 +257,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
arguments: url, arguments: url,
); );
if (!mounted) return;
// 处理返回的影视信息 // 处理返回的影视信息
if (result != null && result is Map<String, dynamic>) { if (result != null && result is Map<String, dynamic>) {
_fillMovieInfo(result); _fillMovieInfo(result);
@@ -353,7 +355,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大'); if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; 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); final targetPath = await ImagePathHelper.instance.getMoviePosterPath(movieId, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath)); await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(targetPath).writeAsBytes(response.bodyBytes); await File(targetPath).writeAsBytes(response.bodyBytes);
@@ -436,6 +438,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialValue: _titleController.text, initialValue: _titleController.text,
hint: '请输入影视名称', hint: '请输入影视名称',
); );
if (!mounted) return;
if (result != null) setState(() => _titleController.text = result); if (result != null) setState(() => _titleController.text = result);
}, },
), ),
@@ -458,6 +461,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialSelected: _alternateTitles, initialSelected: _alternateTitles,
hint: '输入别名', hint: '输入别名',
); );
if (!mounted) return;
if (result != null) setState(() => _alternateTitles = result); if (result != null) setState(() => _alternateTitles = result);
}, },
), ),
@@ -483,6 +487,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialSelected: _directors, initialSelected: _directors,
hint: '如:张艺谋、李安', hint: '如:张艺谋、李安',
); );
if (!mounted) return;
if (result != null) setState(() => _directors = result); if (result != null) setState(() => _directors = result);
}, },
), ),
@@ -506,6 +511,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialSelected: _writers, initialSelected: _writers,
hint: '如:刘慈欣、王家卫', hint: '如:刘慈欣、王家卫',
); );
if (!mounted) return;
if (result != null) setState(() => _writers = result); if (result != null) setState(() => _writers = result);
}, },
), ),
@@ -531,6 +537,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialSelected: _actors, initialSelected: _actors,
hint: '如:梁朝伟、周星驰', hint: '如:梁朝伟、周星驰',
); );
if (!mounted) return;
if (result != null) setState(() => _actors = result); if (result != null) setState(() => _actors = result);
}, },
), ),
@@ -556,6 +563,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
initialSelected: _genres, initialSelected: _genres,
hint: '如:剧情、科幻、悬疑', hint: '如:剧情、科幻、悬疑',
); );
if (!mounted) return;
if (result != null) setState(() => _genres = result); if (result != null) setState(() => _genres = result);
}, },
), ),
@@ -734,6 +742,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
builder: (_) => _SummaryEditorPage(initialText: _summaryController.text), builder: (_) => _SummaryEditorPage(initialText: _summaryController.text),
), ),
); );
if (!mounted) return;
if (result != null) { if (result != null) {
setState(() => _summaryController.text = result); setState(() => _summaryController.text = result);
} }
@@ -1144,7 +1153,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 如果是编辑模式使用现有影视ID如果是新建模式使用临时ID保存时会替换 // 如果是编辑模式使用现有影视ID如果是新建模式使用临时ID保存时会替换
final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString(); final movieId = widget.movie?.id ?? const Uuid().v4();
// 保存到新的路径结构: images/movies/{movieId}/{fileName} // 保存到新的路径结构: images/movies/{movieId}/{fileName}
final targetPath = await ImagePathHelper.instance.getMoviePosterPath( final targetPath = await ImagePathHelper.instance.getMoviePosterPath(
@@ -1155,6 +1164,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
await File(pickedFile.path).copy(targetPath); await File(pickedFile.path).copy(targetPath);
if (!mounted) return;
setState(() => _posterPath = targetPath); setState(() => _posterPath = targetPath);
} }
} catch (e) { } catch (e) {
@@ -1237,6 +1247,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
builder: (context, child) => child!, builder: (context, child) => child!,
); );
if (!mounted) return;
if (picked != null) { if (picked != null) {
setState(() => _releaseDate = picked); setState(() => _releaseDate = picked);
} }
@@ -1252,6 +1263,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
builder: (context, child) => child!, builder: (context, child) => child!,
); );
if (!mounted) return;
if (picked != null) { if (picked != null) {
setState(() => _watchDate = picked); setState(() => _watchDate = picked);
} }
@@ -1321,7 +1333,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
if (widget.movie == null) { if (widget.movie == null) {
// 生成新的影视ID // 生成新的影视ID
final newMovieId = now.millisecondsSinceEpoch.toString(); final newMovieId = const Uuid().v4();
// 如果有海报需要移动到正确的ID目录 // 如果有海报需要移动到正确的ID目录
String? finalPosterPath; String? finalPosterPath;

View File

@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -286,7 +287,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
await File(pickedFile.path).copy(targetPath); await File(pickedFile.path).copy(targetPath);
final newPoster = MoviePoster( final newPoster = MoviePoster(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
movieId: widget.movie.id, movieId: widget.movie.id,
posterPath: targetPath, posterPath: targetPath,
createdAt: DateTime.now(), createdAt: DateTime.now(),
@@ -423,7 +424,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
await File(targetPath).writeAsBytes(response.bodyBytes); await File(targetPath).writeAsBytes(response.bodyBytes);
final newPoster = MoviePoster( final newPoster = MoviePoster(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
movieId: widget.movie.id, movieId: widget.movie.id,
posterPath: targetPath, posterPath: targetPath,
createdAt: DateTime.now(), createdAt: DateTime.now(),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart'; import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
@@ -307,7 +308,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
if (widget.review == null) { if (widget.review == null) {
final newReview = MovieReview( final newReview = MovieReview(
id: now.millisecondsSinceEpoch.toString(), id: const Uuid().v4(),
movieId: widget.movieId, movieId: widget.movieId,
content: _contentController.text.trim(), content: _contentController.text.trim(),
reviewer: _reviewerController.text.trim(), reviewer: _reviewerController.text.trim(),

View File

@@ -447,8 +447,9 @@ class _MovieTabPageState extends State<MovieTabPage> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
await context.read<AppProvider>().removeMovie(movie.id); await context.read<AppProvider>().removeMovie(movie.id);
if (!ctx.mounted) return;
Navigator.pop(ctx); Navigator.pop(ctx);
_loadFirst(); if (mounted) _loadFirst();
}, },
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),

View File

@@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart'; import '../../models/data_models.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart'; import '../../utils/image_path_helper.dart';
@@ -97,7 +98,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
await context.read<AppProvider>().updateNote(updatedNote); await context.read<AppProvider>().updateNote(updatedNote);
_savedNote = updatedNote; _savedNote = updatedNote;
} else { } else {
final noteId = now.millisecondsSinceEpoch.toString(); final noteId = const Uuid().v4();
List<String> finalImages = []; List<String> finalImages = [];
if (_images.isNotEmpty) { if (_images.isNotEmpty) {
final oldNoteId = _tempNoteId ?? noteId; final oldNoteId = _tempNoteId ?? noteId;
@@ -701,7 +702,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
await context.read<AppProvider>().updateNote(updatedNote); await context.read<AppProvider>().updateNote(updatedNote);
} else { } else {
// 添加新笔记 // 添加新笔记
final noteId = now.millisecondsSinceEpoch.toString(); final noteId = const Uuid().v4();
// 如果有图片需要移动到正确的ID目录 // 如果有图片需要移动到正确的ID目录
List<String> finalImages = []; List<String> finalImages = [];
@@ -799,7 +800,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
noteId = widget.note!.id; noteId = widget.note!.id;
} else { } else {
// 新建模式使用已存在的临时ID或生成新的 // 新建模式使用已存在的临时ID或生成新的
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString(); noteId = _tempNoteId ?? const Uuid().v4();
_tempNoteId = noteId; _tempNoteId = noteId;
} }
@@ -810,10 +811,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
await File(image.path).copy(targetPath); await File(image.path).copy(targetPath);
if (!mounted) return;
setState(() => _images.add(targetPath)); setState(() => _images.add(targetPath));
} }
} catch (e) { } catch (e) {
ToastUtil.show(context, '选择图片失败: $e'); if (mounted) ToastUtil.show(context, '选择图片失败: $e');
} }
} }

View File

@@ -392,7 +392,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
TextButton(onPressed: () => Navigator.pop(ctx), TextButton(onPressed: () => Navigator.pop(ctx),
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
ElevatedButton( ElevatedButton(
onPressed: () async { await context.read<AppProvider>().removeNote(note.id); Navigator.pop(ctx); _loadFirst(); }, onPressed: () async { await context.read<AppProvider>().removeNote(note.id); if (!ctx.mounted) return; Navigator.pop(ctx); if (mounted) _loadFirst(); },
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../../services/sync/backup_service.dart'; import '../../services/sync/backup_service.dart';
import '../../services/sync/auto_backup_service.dart';
import '../../utils/toast_util.dart'; import '../../utils/toast_util.dart';
/// 本地备份页面 /// 本地备份页面
@@ -16,27 +15,6 @@ class BackupPage extends StatefulWidget {
class _BackupPageState extends State<BackupPage> { class _BackupPageState extends State<BackupPage> {
bool _isExporting = false; bool _isExporting = false;
bool _isImporting = false; bool _isImporting = false;
bool _autoBackupEnabled = false;
bool _isLoading = true;
String? _backupDirPath;
@override
void initState() {
super.initState();
_loadAutoBackupStatus();
}
Future<void> _loadAutoBackupStatus() async {
final enabled = await AutoBackupService.instance.getEnabled();
final dirPath = await AutoBackupService.instance.getBackupDirectoryPath();
if (mounted) {
setState(() {
_autoBackupEnabled = enabled;
_backupDirPath = dirPath;
_isLoading = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -46,16 +24,9 @@ class _BackupPageState extends State<BackupPage> {
appBar: AppBar( appBar: AppBar(
title: const Text('本地备份'), title: const Text('本地备份'),
), ),
body: _isLoading body: ListView(
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
children: [ children: [
// 自动备份开关 - 紧凑一行
_buildAutoBackupSection(colors),
const SizedBox(height: 20),
// 手动备份 // 手动备份
_buildSectionTitle(colors, '手动备份'), _buildSectionTitle(colors, '手动备份'),
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -492,74 +463,4 @@ class _BackupPageState extends State<BackupPage> {
} }
} }
} }
/// 构建自动备份区域 - 紧凑一行
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,
),
],
),
);
}
} }

View File

@@ -101,19 +101,58 @@ class AppProvider extends ChangeNotifier {
String? _lastEditedItemId; String? _lastEditedItemId;
String? get lastEditedItemId => _lastEditedItemId; String? get lastEditedItemId => _lastEditedItemId;
// 数据库初始化失败标志
bool _dbInitFailed = false;
bool get dbInitFailed => _dbInitFailed;
/// 标记数据库初始化失败(供 UI 提示重试)
void markDbInitFailed() {
_dbInitFailed = true;
notifyListeners();
}
/// 重试数据库初始化
Future<void> retryInitDatabase() async {
_dbInitFailed = false;
notifyListeners();
await initDatabase();
}
// 初始化数据库 // 初始化数据库
Future<void> initDatabase() async { Future<void> initDatabase() async {
debugPrint('[AppProvider] initDatabase'); debugPrint('[AppProvider] initDatabase');
final results = await Future.wait([ // 独立加载每个 DAO避免一个失败导致全部中断
_movieDao.getAllMovies(), try {
_bookDao.getAllBooks(), _movies = await _movieDao.getAllMovies();
_noteDao.getAllNotes(), } catch (e) {
_gameDao.getAllGames(), debugPrint('[AppProvider] 加载影视数据失败: $e');
]); }
_movies = results[0] as List<Movie>; try {
_books = results[1] as List<Book>; _books = await _bookDao.getAllBooks();
_notes = results[2] as List<Note>; } catch (e) {
_games = results[3] as List<Game>; 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}'); debugPrint('[AppProvider] 本地数据: movies=${_movies.length}, books=${_books.length}, notes=${_notes.length}, games=${_games.length}');
notifyListeners(); notifyListeners();
} }
@@ -387,12 +426,15 @@ class AppProvider extends ChangeNotifier {
// 添加影视记录 // 添加影视记录
Future<void> addMovie(Movie movie) async { Future<void> addMovie(Movie movie) async {
await _movieDao.insertMovie(movie); await _movieDao.insertMovie(movie);
await loadMovies(); _movies.add(movie);
notifyListeners();
} }
Future<void> updateMovie(Movie movie) async { Future<void> updateMovie(Movie movie) async {
await _movieDao.updateMovie(movie); 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<void> removeMovie(String id) async { Future<void> removeMovie(String id) async {
await _movieDao.deleteMovie(id); await _movieDao.deleteMovie(id);
await loadMovies(); _movies.removeWhere((m) => m.id == id);
notifyListeners();
} }
Future<void> addBook(Book book) async { Future<void> addBook(Book book) async {
await _bookDao.insertBook(book); await _bookDao.insertBook(book);
await loadBooks(); _books.add(book);
notifyListeners();
} }
Future<void> updateBook(Book book) async { Future<void> updateBook(Book book) async {
await _bookDao.updateBook(book); await _bookDao.updateBook(book);
await loadBooks(); final idx = _books.indexWhere((b) => b.id == book.id);
if (idx != -1) _books[idx] = book;
notifyListeners();
} }
Future<void> removeBook(String id) async { Future<void> removeBook(String id) async {
await _bookDao.deleteBook(id); await _bookDao.deleteBook(id);
await loadBooks(); _books.removeWhere((b) => b.id == id);
notifyListeners();
} }
Future<void> addNote(Note note) async { Future<void> addNote(Note note) async {
await _noteDao.insertNote(note); await _noteDao.insertNote(note);
await loadNotes(); _notes.add(note);
notifyListeners();
} }
Future<void> updateNote(Note note) async { Future<void> updateNote(Note note) async {
await _noteDao.updateNote(note); await _noteDao.updateNote(note);
await loadNotes(); final idx = _notes.indexWhere((n) => n.id == note.id);
if (idx != -1) _notes[idx] = note;
notifyListeners();
} }
Future<void> removeNote(String id) async { Future<void> removeNote(String id) async {
await _noteDao.deleteNote(id); await _noteDao.deleteNote(id);
await loadNotes(); _notes.removeWhere((n) => n.id == id);
notifyListeners();
} }
Future<void> addGame(Game game) async { Future<void> addGame(Game game) async {
await _gameDao.insertGame(game); await _gameDao.insertGame(game);
await loadGames(); _games.add(game);
notifyListeners();
} }
Future<void> updateGame(Game game) async { Future<void> updateGame(Game game) async {
await _gameDao.updateGame(game); await _gameDao.updateGame(game);
await loadGames(); final idx = _games.indexWhere((g) => g.id == game.id);
if (idx != -1) _games[idx] = game;
notifyListeners();
} }
Future<void> removeGame(String id) async { Future<void> removeGame(String id) async {
await _gameDao.deleteGame(id); await _gameDao.deleteGame(id);
await loadGames(); _games.removeWhere((g) => g.id == id);
notifyListeners();
} }
/// 仅更新游戏封面偏移量(不触发全量刷新) /// 仅更新游戏封面偏移量(不触发全量刷新)
@@ -477,7 +532,10 @@ class AppProvider extends ChangeNotifier {
Future<void> toggleNotePin(String id, bool isPinned) async { Future<void> toggleNotePin(String id, bool isPinned) async {
await _noteDao.togglePin(id, isPinned); 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(); notifyListeners();
} }
@@ -722,29 +780,59 @@ class AppProvider extends ChangeNotifier {
final deletedBookExcerpts = await getDeletedBookExcerpts(); final deletedBookExcerpts = await getDeletedBookExcerpts();
final deletedGameReviews = await getDeletedGameReviews(); final deletedGameReviews = await getDeletedGameReviews();
for (final movie in deletedMovies) { // 先收集需要删除图片的 ID再在事务中批量删除数据库记录
await permanentDeleteMovie(movie.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) { for (final id in bookIds) {
await permanentDeleteBook(book.id); await ImagePathHelper.instance.deleteBookImages(id);
} }
for (final note in deletedNotes) { for (final id in noteIds) {
await permanentDeleteNote(note.id); await ImagePathHelper.instance.deleteNoteImages(id);
} }
for (final game in deletedGames) { for (final id in gameIds) {
await permanentDeleteGame(game.id); await ImagePathHelper.instance.deleteGameImages(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);
} }
await loadMovies(); await loadMovies();

View File

@@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:uuid/uuid.dart';
import '../utils/user_prefs.dart'; import '../utils/user_prefs.dart';
import '../utils/server_config.dart'; import '../utils/server_config.dart';
@@ -87,7 +88,7 @@ class UsageStatsService with WidgetsBindingObserver {
final info = await deviceInfo.linuxInfo; final info = await deviceInfo.linuxInfo;
rawId = '${info.name}-${info.id}'; rawId = '${info.name}-${info.id}';
} else { } else {
rawId = DateTime.now().millisecondsSinceEpoch.toString(); rawId = const Uuid().v4();
} }
final bytes = utf8.encode(rawId); final bytes = utf8.encode(rawId);