优化代码结构,修复部分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(''' // 创建新表
CREATE TABLE notes ( await txn.execute('''
id TEXT PRIMARY KEY, CREATE TABLE notes (
title TEXT DEFAULT '', id TEXT PRIMARY KEY,
content TEXT NOT NULL, title TEXT DEFAULT '',
content_type TEXT DEFAULT 'markdown', content TEXT NOT NULL,
tags TEXT, content_type TEXT DEFAULT 'markdown',
created_at TEXT NOT NULL, tags TEXT,
updated_at TEXT NOT NULL created_at TEXT NOT NULL,
) updated_at TEXT NOT NULL
'''); )
''');
// 迁移旧数据将title字段恢复
for (final row in oldData) { // 迁移旧数据将title字段恢复
try { for (final row in oldData) {
final now = DateTime.now().toIso8601String(); try {
final title = row['title']?.toString() ?? ''; final now = DateTime.now().toIso8601String();
final content = row['content']?.toString() ?? ''; final title = row['title']?.toString() ?? '';
final content = row['content']?.toString() ?? '';
await db.insert('notes', {
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), await txn.insert('notes', {
'title': title, 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
'content': content, 'title': title,
'content_type': 'markdown', 'content': content,
'tags': row['tags'] ?? '', 'content_type': 'markdown',
'created_at': row['created_at']?.toString() ?? now, 'tags': row['tags'] ?? '',
'updated_at': row['updated_at']?.toString() ?? now, 'created_at': row['created_at']?.toString() ?? now,
}); 'updated_at': row['updated_at']?.toString() ?? now,
} catch (e) { });
debugPrint('[DB] 迁移笔记记录失败: $e'); } catch (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(''' // 创建新表
CREATE TABLE books ( await txn.execute('''
id TEXT PRIMARY KEY, CREATE TABLE books (
title TEXT NOT NULL, id TEXT PRIMARY KEY,
cover_path TEXT, title TEXT NOT NULL,
authors TEXT, cover_path TEXT,
alternate_titles TEXT, authors TEXT,
publisher TEXT, alternate_titles TEXT,
genres TEXT, publisher TEXT,
summary TEXT, genres TEXT,
rating REAL, summary TEXT,
status TEXT NOT NULL, rating REAL,
created_at TEXT NOT NULL, status TEXT NOT NULL,
updated_at TEXT NOT NULL, created_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0 updated_at TEXT NOT NULL,
) is_deleted INTEGER DEFAULT 0
'''); )
''');
// 迁移旧数据
for (final row in oldData) { // 迁移旧数据
try { for (final row in oldData) {
final now = DateTime.now().toIso8601String(); try {
await db.insert('books', { final now = DateTime.now().toIso8601String();
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), await txn.insert('books', {
'title': row['title']?.toString() ?? '', 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
'cover_path': row['cover'], 'title': row['title']?.toString() ?? '',
'authors': row['author'] != null ? '["${row['author']}"]' : '[]', 'cover_path': row['cover'],
'alternate_titles': '[]', 'authors': row['author'] != null ? '["${row['author']}"]' : '[]',
'publisher': null, 'alternate_titles': '[]',
'genres': '[]', 'publisher': null,
'summary': row['note'], 'genres': '[]',
'rating': row['rating'], 'summary': row['note'],
'status': row['status'] ?? 'want_to_read', 'rating': row['rating'],
'created_at': now, 'status': row['status'] ?? 'want_to_read',
'updated_at': now, 'created_at': now,
'is_deleted': 0, 'updated_at': now,
}); 'is_deleted': 0,
} catch (e) { });
debugPrint('[DB] 迁移书籍记录失败: $e'); } catch (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 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
)
''');
// 迁移旧数据(尽可能保留) // 删除旧表
for (final row in oldData) { await txn.execute('DROP TABLE IF EXISTS movies');
try {
await db.insert('movies', { // 创建新表
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), await txn.execute('''
'title': row['title']?.toString() ?? '', CREATE TABLE movies (
'poster_path': row['poster_path'], id TEXT PRIMARY KEY,
'release_date': null, title TEXT NOT NULL,
'directors': '[]', poster_path TEXT,
'writers': '[]', release_date TEXT,
'actors': '[]', directors TEXT,
'genres': '[]', writers TEXT,
'alternate_titles': '[]', actors TEXT,
'summary': row['note'], genres TEXT,
'rating': row['rating'], alternate_titles TEXT,
'status': row['status'] ?? 'want_to_watch', summary TEXT,
'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(), rating REAL,
'updated_at': DateTime.now().toIso8601String(), status TEXT NOT NULL,
'is_deleted': row['is_deleted'] ?? 0, category TEXT NOT NULL DEFAULT 'movie',
}); created_at TEXT NOT NULL,
} catch (e) { updated_at TEXT NOT NULL,
debugPrint('[DB] 迁移影视记录失败: $e'); 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');
}
} }
} });
} }
// 创建数据库表 // 创建数据库表

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);