generated from dellevin/template
优化代码结构,修复部分bug
This commit is contained in:
@@ -159,17 +159,7 @@ class DatabaseHelper {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
// v16: 确保 cover_offset 列存在(v15 的数据库可能缺少此列)
|
||||
if (oldVersion < 16) {
|
||||
final movieCols = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
if (!movieCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE movies ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
final bookCols = await db.rawQuery('PRAGMA table_info(books)');
|
||||
if (!bookCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
// v16: 已合并到 v15(cover_offset 列的添加逻辑相同,v15 的 PRAGMA 检查已确保幂等)
|
||||
if (oldVersion < 17) {
|
||||
await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
@@ -248,9 +238,7 @@ class DatabaseHelper {
|
||||
if (oldVersion < 26) {
|
||||
await _upgradeBooksTableV26(db);
|
||||
}
|
||||
if (oldVersion < 27) {
|
||||
await _upgradeBooksTableV27(db);
|
||||
}
|
||||
// v27: 已合并到 v26(start_date/finish_date 的添加逻辑相同,v26 的 PRAGMA 检查已确保幂等)
|
||||
if (oldVersion < 28) {
|
||||
// 移除 Note Plus 功能,删除 note_plus 表
|
||||
await db.execute('DROP TABLE IF EXISTS note_plus');
|
||||
@@ -341,20 +329,6 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V27(添加阅读始末日期字段)
|
||||
Future<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(添加阅读始末日期字段)
|
||||
Future<void> _upgradeBooksTableV26(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||
@@ -567,153 +541,159 @@ class DatabaseHelper {
|
||||
|
||||
/// 升级notes表到V4
|
||||
Future<void> _upgradeNotesTableV4(Database db) async {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('notes');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS notes');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
content_type TEXT DEFAULT 'markdown',
|
||||
tags TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据(将title字段恢复)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
final title = row['title']?.toString() ?? '';
|
||||
final content = row['content']?.toString() ?? '';
|
||||
|
||||
await db.insert('notes', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': title,
|
||||
'content': content,
|
||||
'content_type': 'markdown',
|
||||
'tags': row['tags'] ?? '',
|
||||
'created_at': row['created_at']?.toString() ?? now,
|
||||
'updated_at': row['updated_at']?.toString() ?? now,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移笔记记录失败: $e');
|
||||
await db.transaction((txn) async {
|
||||
// 备份旧数据
|
||||
final oldData = await txn.query('notes');
|
||||
|
||||
// 删除旧表
|
||||
await txn.execute('DROP TABLE IF EXISTS notes');
|
||||
|
||||
// 创建新表
|
||||
await txn.execute('''
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
content_type TEXT DEFAULT 'markdown',
|
||||
tags TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据(将title字段恢复)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
final title = row['title']?.toString() ?? '';
|
||||
final content = row['content']?.toString() ?? '';
|
||||
|
||||
await txn.insert('notes', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': title,
|
||||
'content': content,
|
||||
'content_type': 'markdown',
|
||||
'tags': row['tags'] ?? '',
|
||||
'created_at': row['created_at']?.toString() ?? now,
|
||||
'updated_at': row['updated_at']?.toString() ?? now,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移笔记记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 升级books表到V3
|
||||
Future<void> _upgradeBooksTableV3(Database db) async {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('books');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS books');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
cover_path TEXT,
|
||||
authors TEXT,
|
||||
alternate_titles TEXT,
|
||||
publisher TEXT,
|
||||
genres TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await db.insert('books', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'cover_path': row['cover'],
|
||||
'authors': row['author'] != null ? '["${row['author']}"]' : '[]',
|
||||
'alternate_titles': '[]',
|
||||
'publisher': null,
|
||||
'genres': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_read',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
'is_deleted': 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移书籍记录失败: $e');
|
||||
await db.transaction((txn) async {
|
||||
// 备份旧数据
|
||||
final oldData = await txn.query('books');
|
||||
|
||||
// 删除旧表
|
||||
await txn.execute('DROP TABLE IF EXISTS books');
|
||||
|
||||
// 创建新表
|
||||
await txn.execute('''
|
||||
CREATE TABLE books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
cover_path TEXT,
|
||||
authors TEXT,
|
||||
alternate_titles TEXT,
|
||||
publisher TEXT,
|
||||
genres TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await txn.insert('books', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'cover_path': row['cover'],
|
||||
'authors': row['author'] != null ? '["${row['author']}"]' : '[]',
|
||||
'alternate_titles': '[]',
|
||||
'publisher': null,
|
||||
'genres': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_read',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
'is_deleted': 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移书籍记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 升级movies表到V2
|
||||
Future<void> _upgradeMoviesTableV2(Database db) async {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('movies');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS movies');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE movies (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
poster_path TEXT,
|
||||
release_date TEXT,
|
||||
directors TEXT,
|
||||
writers TEXT,
|
||||
actors TEXT,
|
||||
genres TEXT,
|
||||
alternate_titles TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
await db.transaction((txn) async {
|
||||
// 备份旧数据
|
||||
final oldData = await txn.query('movies');
|
||||
|
||||
// 迁移旧数据(尽可能保留)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
await db.insert('movies', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'poster_path': row['poster_path'],
|
||||
'release_date': null,
|
||||
'directors': '[]',
|
||||
'writers': '[]',
|
||||
'actors': '[]',
|
||||
'genres': '[]',
|
||||
'alternate_titles': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_watch',
|
||||
'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
'is_deleted': row['is_deleted'] ?? 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移影视记录失败: $e');
|
||||
// 删除旧表
|
||||
await txn.execute('DROP TABLE IF EXISTS movies');
|
||||
|
||||
// 创建新表
|
||||
await txn.execute('''
|
||||
CREATE TABLE movies (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
poster_path TEXT,
|
||||
release_date TEXT,
|
||||
directors TEXT,
|
||||
writers TEXT,
|
||||
actors TEXT,
|
||||
genres TEXT,
|
||||
alternate_titles TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据(尽可能保留)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
await txn.insert('movies', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'poster_path': row['poster_path'],
|
||||
'release_date': null,
|
||||
'directors': '[]',
|
||||
'writers': '[]',
|
||||
'actors': '[]',
|
||||
'genres': '[]',
|
||||
'alternate_titles': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_watch',
|
||||
'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
'is_deleted': row['is_deleted'] ?? 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移影视记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建数据库表
|
||||
|
||||
@@ -144,6 +144,9 @@ class GameDao {
|
||||
// 彻底删除游戏
|
||||
Future<int> permanentDeleteGame(String id) => _wrap('permanentDeleteGame', () async {
|
||||
final db = await _dbHelper.database;
|
||||
// 清理子记录,防止孤儿数据
|
||||
await db.delete('game_reviews', where: 'game_id = ?', whereArgs: [id]);
|
||||
await db.delete('game_screenshots', where: 'game_id = ?', whereArgs: [id]);
|
||||
return await db.delete(
|
||||
'games',
|
||||
where: 'id = ?',
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'utils/theme/app_theme.dart';
|
||||
import 'utils/app_router.dart';
|
||||
import 'utils/user_prefs.dart';
|
||||
import 'services/changelog_service.dart';
|
||||
import 'services/sync/auto_backup_service.dart';
|
||||
import 'services/usage_stats_service.dart';
|
||||
import 'providers/app_provider.dart';
|
||||
|
||||
@@ -42,24 +41,13 @@ Future<void> _bootstrap(AppProvider appProvider) async {
|
||||
await appProvider.initDatabase();
|
||||
} catch (e) {
|
||||
debugPrint('[Startup] 数据库初始化失败: $e');
|
||||
appProvider.markDbInitFailed();
|
||||
}
|
||||
appProvider.initMainTabIndex();
|
||||
|
||||
unawaited(_initAutoBackup());
|
||||
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 {
|
||||
try {
|
||||
await UsageStatsService.instance.start();
|
||||
|
||||
@@ -1374,14 +1374,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBook(widget.book.id);
|
||||
if (!mounted) return;
|
||||
if (!mounted || !context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
if (widget.embedded) {
|
||||
context.read<AppProvider>().selectBook(null);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
ToastUtil.show(context, '已删除');
|
||||
if (mounted && context.mounted) {
|
||||
ToastUtil.show(context, '已删除');
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.error,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -157,12 +158,14 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
_halfCard('书名', _titleController.text, Icons.book_outlined, required: true,
|
||||
onTap: () async {
|
||||
final r = await TextInputPanel.show(context: context, title: '书名', initialValue: _titleController.text, hint: '请输入书名');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _titleController.text = r);
|
||||
},
|
||||
),
|
||||
_halfCard('别名', _alternateTitles.isEmpty ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('、')}', Icons.alternate_email_outlined,
|
||||
onTap: () async {
|
||||
final r = await GenreSelectorPage.show(context: context, title: '添加别名', existingTags: [], initialSelected: _alternateTitles, hint: '输入别名');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _alternateTitles = r);
|
||||
},
|
||||
),
|
||||
@@ -173,6 +176,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
final provider = context.read<AppProvider>();
|
||||
final data = provider.books.map((b) => b.authors).toList();
|
||||
final r = await GenreSelectorPage.show(context: context, title: '选择作者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _authors, hint: '如:余华、莫言');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _authors = r);
|
||||
},
|
||||
),
|
||||
@@ -181,12 +185,14 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
final provider = context.read<AppProvider>();
|
||||
final data = provider.books.map((b) => b.translators).toList();
|
||||
final r = await GenreSelectorPage.show(context: context, title: '选择译者', existingTagsFuture: compute(_collectUnique, data), initialSelected: _translators, hint: '如:李继宏、许钧');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _translators = r);
|
||||
},
|
||||
),
|
||||
_halfCard('出版社', _publisherController.text, Icons.business_outlined,
|
||||
onTap: () async {
|
||||
final r = await TextInputPanel.show(context: context, title: '出版社', initialValue: _publisherController.text, hint: '请输入出版社');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _publisherController.text = r);
|
||||
},
|
||||
),
|
||||
@@ -198,12 +204,14 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
final tags = await provider.getTags('book_genre', excludeHidden: true);
|
||||
if (!mounted) return;
|
||||
final r = await GenreSelectorPage.show(context: context, title: '选择类型', existingTags: tags.map((t) => t['name'] as String).toList(), initialSelected: _genres, hint: '如:小说、历史、传记');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _genres = r);
|
||||
},
|
||||
),
|
||||
_halfCard('ISBN', _isbnController.text, Icons.qr_code_outlined,
|
||||
onTap: () async {
|
||||
final r = await TextInputPanel.show(context: context, title: 'ISBN', initialValue: _isbnController.text, hint: '请输入ISBN编号');
|
||||
if (!mounted) return;
|
||||
if (r != null) setState(() => _isbnController.text = r);
|
||||
},
|
||||
),
|
||||
@@ -425,10 +433,11 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
final picked = await _picker.pickImage(source: ImageSource.gallery, maxWidth: 800, maxHeight: 1200, imageQuality: 85);
|
||||
if (picked != null) {
|
||||
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final bookId = widget.book?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final bookId = widget.book?.id ?? const Uuid().v4();
|
||||
final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
await File(picked.path).copy(targetPath);
|
||||
if (!mounted) return;
|
||||
setState(() => _coverPath = targetPath);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -492,10 +501,11 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
|
||||
|
||||
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final bookId = widget.book?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final bookId = widget.book?.id ?? const Uuid().v4();
|
||||
final targetPath = await ImagePathHelper.instance.getBookCoverPath(bookId, fileName);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
await File(targetPath).writeAsBytes(response.bodyBytes);
|
||||
if (!mounted) return;
|
||||
setState(() => _coverPath = targetPath);
|
||||
} catch (e) {
|
||||
debugPrint('封面下载失败: $e');
|
||||
@@ -556,21 +566,25 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
|
||||
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)));
|
||||
if (!mounted) return;
|
||||
if (picked != null) setState(() => _publishDate = picked);
|
||||
}
|
||||
|
||||
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)));
|
||||
if (!mounted) return;
|
||||
if (picked != null) setState(() => _startDate = picked);
|
||||
}
|
||||
|
||||
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)));
|
||||
if (!mounted) return;
|
||||
if (picked != null) setState(() => _finishDate = picked);
|
||||
}
|
||||
|
||||
Future<void> _editSummary() async {
|
||||
final result = await Navigator.push<String>(context, MaterialPageRoute(builder: (_) => _SummaryEditorPage(initialText: _summaryController.text)));
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _summaryController.text = result);
|
||||
}
|
||||
|
||||
@@ -619,7 +633,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (widget.book == null) {
|
||||
final newBookId = now.millisecondsSinceEpoch.toString();
|
||||
final newBookId = const Uuid().v4();
|
||||
String? finalCoverPath;
|
||||
if (_coverPath != null && _coverPath!.isNotEmpty) {
|
||||
finalCoverPath = await _moveCoverToNewId(_coverPath!, newBookId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
|
||||
@@ -313,7 +314,7 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
|
||||
if (widget.review == null) {
|
||||
final newReview = BookReview(
|
||||
id: now.millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
bookId: widget.bookId,
|
||||
content: _contentController.text.trim(),
|
||||
reviewer: _reviewerController.text.trim(),
|
||||
|
||||
@@ -295,7 +295,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
ElevatedButton(onPressed: () async { await context.read<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,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
|
||||
child: const Text('删除'),
|
||||
|
||||
@@ -960,7 +960,7 @@ class _GameDetailPageState extends State<GameDetailPage> {
|
||||
onPressed: () async {
|
||||
final provider = context.read<AppProvider>();
|
||||
await provider.removeGame(widget.game.id);
|
||||
if (!mounted) return;
|
||||
if (!mounted || !context.mounted) return;
|
||||
if (widget.embedded) {
|
||||
Navigator.of(context).pop();
|
||||
provider.selectGame(null);
|
||||
@@ -969,7 +969,7 @@ class _GameDetailPageState extends State<GameDetailPage> {
|
||||
navigator.pop();
|
||||
navigator.pop();
|
||||
}
|
||||
if (mounted) {
|
||||
if (mounted && context.mounted) {
|
||||
ToastUtil.show(context, '已删除');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -185,6 +186,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
initialValue: _titleController.text,
|
||||
hint: '请输入游戏名称',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _titleController.text = result);
|
||||
},
|
||||
),
|
||||
@@ -210,6 +212,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
initialSelected: _platforms,
|
||||
hint: '如:PS5、Switch、Steam',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _platforms = result);
|
||||
},
|
||||
),
|
||||
@@ -235,6 +238,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
initialSelected: _versions,
|
||||
hint: '如:标准版、豪华版',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _versions = result);
|
||||
},
|
||||
),
|
||||
@@ -269,6 +273,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
initialSelected: _genres,
|
||||
hint: '如:RPG、动作、冒险',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _genres = result);
|
||||
},
|
||||
),
|
||||
@@ -305,6 +310,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
initialSelected: _purchasePlatforms,
|
||||
hint: '如:Steam、eShop、PlayStation Store',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _purchasePlatforms = result);
|
||||
},
|
||||
),
|
||||
@@ -344,6 +350,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
hint: '如:298元、49.99美元',
|
||||
keyboardType: TextInputType.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _purchasePriceController.text = result);
|
||||
},
|
||||
),
|
||||
@@ -496,6 +503,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
builder: (_) => _SummaryEditorPage(initialText: _summaryController.text),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) {
|
||||
setState(() => _summaryController.text = result);
|
||||
}
|
||||
@@ -753,10 +761,11 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
|
||||
if (pickedFile != null) {
|
||||
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final gameId = widget.game?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final gameId = widget.game?.id ?? const Uuid().v4();
|
||||
final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
await File(pickedFile.path).copy(targetPath);
|
||||
if (!mounted) return;
|
||||
setState(() => _coverPath = targetPath);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -919,11 +928,12 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
|
||||
|
||||
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final gameId = widget.game?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final gameId = widget.game?.id ?? const Uuid().v4();
|
||||
final targetPath = await ImagePathHelper.instance.getGameCoverPath(gameId, fileName);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
await File(targetPath).writeAsBytes(response.bodyBytes);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _coverPath = targetPath);
|
||||
} catch (e) {
|
||||
debugPrint('封面下载失败: $e');
|
||||
@@ -1009,6 +1019,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
lastDate: DateTime.now().add(const Duration(days: 365 * 5)),
|
||||
builder: (context, child) => child!,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (picked != null) {
|
||||
setState(() => _purchaseDate = picked);
|
||||
}
|
||||
@@ -1073,7 +1084,7 @@ class _GameFormPageState extends State<GameFormPage> {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (widget.game == null) {
|
||||
final newGameId = now.millisecondsSinceEpoch.toString();
|
||||
final newGameId = const Uuid().v4();
|
||||
String? finalCoverPath;
|
||||
if (_coverPath != null && _coverPath!.isNotEmpty) {
|
||||
finalCoverPath = await _moveCoverToNewId(_coverPath!, newGameId);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
|
||||
@@ -235,7 +236,7 @@ class _GameReviewFormPageState extends State<GameReviewFormPage> {
|
||||
final now = DateTime.now();
|
||||
if (widget.review == null) {
|
||||
final newReview = GameReview(
|
||||
id: now.millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
gameId: widget.gameId,
|
||||
content: _contentController.text.trim(),
|
||||
reviewer: _reviewerController.text.trim(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -230,7 +231,7 @@ class _GameScreenshotsPageState extends State<GameScreenshotsPage> {
|
||||
await File(pickedFile.path).copy(targetPath);
|
||||
|
||||
final newScreenshot = GameScreenshot(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
gameId: widget.game.id,
|
||||
screenshotPath: targetPath,
|
||||
createdAt: DateTime.now(),
|
||||
@@ -317,7 +318,7 @@ class _GameScreenshotsPageState extends State<GameScreenshotsPage> {
|
||||
await File(targetPath).writeAsBytes(response.bodyBytes);
|
||||
|
||||
final newScreenshot = GameScreenshot(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
gameId: widget.game.id,
|
||||
screenshotPath: targetPath,
|
||||
createdAt: DateTime.now(),
|
||||
|
||||
@@ -446,8 +446,9 @@ class _GameTabPageState extends State<GameTabPage> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeGame(game.id);
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
_loadFirst();
|
||||
if (mounted) _loadFirst();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
|
||||
@@ -45,12 +45,19 @@ class _MainContentPageState extends State<MainContentPage> {
|
||||
}
|
||||
|
||||
void _loadTabSettings() {
|
||||
setState(() {
|
||||
_showMovieTab = _userPrefs.showMovieTab;
|
||||
_showBookTab = _userPrefs.showBookTab;
|
||||
_showNoteTab = _userPrefs.showNoteTab;
|
||||
_showGameTab = _userPrefs.showGameTab;
|
||||
});
|
||||
final newMovie = _userPrefs.showMovieTab;
|
||||
final newBook = _userPrefs.showBookTab;
|
||||
final newNote = _userPrefs.showNoteTab;
|
||||
final newGame = _userPrefs.showGameTab;
|
||||
if (newMovie != _showMovieTab || newBook != _showBookTab ||
|
||||
newNote != _showNoteTab || newGame != _showGameTab) {
|
||||
setState(() {
|
||||
_showMovieTab = newMovie;
|
||||
_showBookTab = newBook;
|
||||
_showNoteTab = newNote;
|
||||
_showGameTab = newGame;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1189,7 +1189,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
onPressed: () async {
|
||||
final provider = context.read<AppProvider>();
|
||||
await provider.removeMovie(widget.movie.id);
|
||||
if (!mounted) return;
|
||||
if (!mounted || !context.mounted) return;
|
||||
if (widget.embedded) {
|
||||
Navigator.of(context).pop(); // close dialog
|
||||
provider.selectMovie(null);
|
||||
@@ -1198,7 +1198,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
navigator.pop();
|
||||
navigator.pop();
|
||||
}
|
||||
if (mounted) {
|
||||
if (mounted && context.mounted) {
|
||||
ToastUtil.show(context, '已删除');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -256,6 +257,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
arguments: url,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
// 处理返回的影视信息
|
||||
if (result != null && result is Map<String, dynamic>) {
|
||||
_fillMovieInfo(result);
|
||||
@@ -353,7 +355,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
|
||||
|
||||
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final movieId = widget.movie?.id ?? const Uuid().v4();
|
||||
final targetPath = await ImagePathHelper.instance.getMoviePosterPath(movieId, fileName);
|
||||
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
|
||||
await File(targetPath).writeAsBytes(response.bodyBytes);
|
||||
@@ -436,6 +438,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialValue: _titleController.text,
|
||||
hint: '请输入影视名称',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _titleController.text = result);
|
||||
},
|
||||
),
|
||||
@@ -458,6 +461,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialSelected: _alternateTitles,
|
||||
hint: '输入别名',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _alternateTitles = result);
|
||||
},
|
||||
),
|
||||
@@ -483,6 +487,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialSelected: _directors,
|
||||
hint: '如:张艺谋、李安',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _directors = result);
|
||||
},
|
||||
),
|
||||
@@ -506,6 +511,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialSelected: _writers,
|
||||
hint: '如:刘慈欣、王家卫',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _writers = result);
|
||||
},
|
||||
),
|
||||
@@ -531,6 +537,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialSelected: _actors,
|
||||
hint: '如:梁朝伟、周星驰',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _actors = result);
|
||||
},
|
||||
),
|
||||
@@ -556,6 +563,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
initialSelected: _genres,
|
||||
hint: '如:剧情、科幻、悬疑',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) setState(() => _genres = result);
|
||||
},
|
||||
),
|
||||
@@ -734,6 +742,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
builder: (_) => _SummaryEditorPage(initialText: _summaryController.text),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null) {
|
||||
setState(() => _summaryController.text = result);
|
||||
}
|
||||
@@ -1144,7 +1153,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
|
||||
// 如果是编辑模式,使用现有影视ID;如果是新建模式,使用临时ID(保存时会替换)
|
||||
final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final movieId = widget.movie?.id ?? const Uuid().v4();
|
||||
|
||||
// 保存到新的路径结构: images/movies/{movieId}/{fileName}
|
||||
final targetPath = await ImagePathHelper.instance.getMoviePosterPath(
|
||||
@@ -1155,6 +1164,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
|
||||
await File(pickedFile.path).copy(targetPath);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _posterPath = targetPath);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1237,6 +1247,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
builder: (context, child) => child!,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (picked != null) {
|
||||
setState(() => _releaseDate = picked);
|
||||
}
|
||||
@@ -1252,6 +1263,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
builder: (context, child) => child!,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (picked != null) {
|
||||
setState(() => _watchDate = picked);
|
||||
}
|
||||
@@ -1321,7 +1333,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
|
||||
if (widget.movie == null) {
|
||||
// 生成新的影视ID
|
||||
final newMovieId = now.millisecondsSinceEpoch.toString();
|
||||
final newMovieId = const Uuid().v4();
|
||||
|
||||
// 如果有海报,需要移动到正确的ID目录
|
||||
String? finalPosterPath;
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -286,7 +287,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
await File(pickedFile.path).copy(targetPath);
|
||||
|
||||
final newPoster = MoviePoster(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
movieId: widget.movie.id,
|
||||
posterPath: targetPath,
|
||||
createdAt: DateTime.now(),
|
||||
@@ -423,7 +424,7 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
|
||||
await File(targetPath).writeAsBytes(response.bodyBytes);
|
||||
|
||||
final newPoster = MoviePoster(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
movieId: widget.movie.id,
|
||||
posterPath: targetPath,
|
||||
createdAt: DateTime.now(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
|
||||
@@ -307,7 +308,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
|
||||
if (widget.review == null) {
|
||||
final newReview = MovieReview(
|
||||
id: now.millisecondsSinceEpoch.toString(),
|
||||
id: const Uuid().v4(),
|
||||
movieId: widget.movieId,
|
||||
content: _contentController.text.trim(),
|
||||
reviewer: _reviewerController.text.trim(),
|
||||
|
||||
@@ -447,8 +447,9 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMovie(movie.id);
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
_loadFirst();
|
||||
if (mounted) _loadFirst();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -97,7 +98,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
await context.read<AppProvider>().updateNote(updatedNote);
|
||||
_savedNote = updatedNote;
|
||||
} else {
|
||||
final noteId = now.millisecondsSinceEpoch.toString();
|
||||
final noteId = const Uuid().v4();
|
||||
List<String> finalImages = [];
|
||||
if (_images.isNotEmpty) {
|
||||
final oldNoteId = _tempNoteId ?? noteId;
|
||||
@@ -701,7 +702,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
await context.read<AppProvider>().updateNote(updatedNote);
|
||||
} else {
|
||||
// 添加新笔记
|
||||
final noteId = now.millisecondsSinceEpoch.toString();
|
||||
final noteId = const Uuid().v4();
|
||||
|
||||
// 如果有图片,需要移动到正确的ID目录
|
||||
List<String> finalImages = [];
|
||||
@@ -799,7 +800,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
noteId = widget.note!.id;
|
||||
} else {
|
||||
// 新建模式:使用已存在的临时ID或生成新的
|
||||
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
noteId = _tempNoteId ?? const Uuid().v4();
|
||||
_tempNoteId = noteId;
|
||||
}
|
||||
|
||||
@@ -810,10 +811,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
await File(image.path).copy(targetPath);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _images.add(targetPath));
|
||||
}
|
||||
} catch (e) {
|
||||
ToastUtil.show(context, '选择图片失败: $e');
|
||||
if (mounted) ToastUtil.show(context, '选择图片失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
TextButton(onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
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,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../services/sync/backup_service.dart';
|
||||
import '../../services/sync/auto_backup_service.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
|
||||
/// 本地备份页面
|
||||
@@ -16,27 +15,6 @@ class BackupPage extends StatefulWidget {
|
||||
class _BackupPageState extends State<BackupPage> {
|
||||
bool _isExporting = 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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -46,16 +24,9 @@ class _BackupPageState extends State<BackupPage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('本地备份'),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
// 自动备份开关 - 紧凑一行
|
||||
_buildAutoBackupSection(colors),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 手动备份
|
||||
_buildSectionTitle(colors, '手动备份'),
|
||||
const SizedBox(height: 10),
|
||||
@@ -492,74 +463,4 @@ class _BackupPageState extends State<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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,19 +101,58 @@ class AppProvider extends ChangeNotifier {
|
||||
String? _lastEditedItemId;
|
||||
String? get lastEditedItemId => _lastEditedItemId;
|
||||
|
||||
// 数据库初始化失败标志
|
||||
bool _dbInitFailed = false;
|
||||
bool get dbInitFailed => _dbInitFailed;
|
||||
|
||||
/// 标记数据库初始化失败(供 UI 提示重试)
|
||||
void markDbInitFailed() {
|
||||
_dbInitFailed = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 重试数据库初始化
|
||||
Future<void> retryInitDatabase() async {
|
||||
_dbInitFailed = false;
|
||||
notifyListeners();
|
||||
await initDatabase();
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
Future<void> initDatabase() async {
|
||||
debugPrint('[AppProvider] initDatabase');
|
||||
final results = await Future.wait([
|
||||
_movieDao.getAllMovies(),
|
||||
_bookDao.getAllBooks(),
|
||||
_noteDao.getAllNotes(),
|
||||
_gameDao.getAllGames(),
|
||||
]);
|
||||
_movies = results[0] as List<Movie>;
|
||||
_books = results[1] as List<Book>;
|
||||
_notes = results[2] as List<Note>;
|
||||
_games = results[3] as List<Game>;
|
||||
// 独立加载每个 DAO,避免一个失败导致全部中断
|
||||
try {
|
||||
_movies = await _movieDao.getAllMovies();
|
||||
} catch (e) {
|
||||
debugPrint('[AppProvider] 加载影视数据失败: $e');
|
||||
}
|
||||
try {
|
||||
_books = await _bookDao.getAllBooks();
|
||||
} catch (e) {
|
||||
debugPrint('[AppProvider] 加载书籍数据失败: $e');
|
||||
}
|
||||
try {
|
||||
_notes = await _noteDao.getAllNotes();
|
||||
} catch (e) {
|
||||
debugPrint('[AppProvider] 加载笔记数据失败: $e');
|
||||
}
|
||||
try {
|
||||
_games = await _gameDao.getAllGames();
|
||||
} catch (e) {
|
||||
debugPrint('[AppProvider] 加载游戏数据失败: $e');
|
||||
}
|
||||
// 检查是否全部失败
|
||||
if (_movies.isEmpty && _books.isEmpty && _notes.isEmpty && _games.isEmpty) {
|
||||
// 可能是初始化全部失败(非空数据库场景下不合理),标记以便 UI 提示
|
||||
try {
|
||||
final db = await DatabaseHelper.instance.database;
|
||||
final count = (await db.rawQuery('SELECT COUNT(*) as cnt FROM movies')).first['cnt'] as int;
|
||||
if (count > 0) _dbInitFailed = true;
|
||||
} catch (_) {
|
||||
_dbInitFailed = true;
|
||||
}
|
||||
}
|
||||
debugPrint('[AppProvider] 本地数据: movies=${_movies.length}, books=${_books.length}, notes=${_notes.length}, games=${_games.length}');
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -387,12 +426,15 @@ class AppProvider extends ChangeNotifier {
|
||||
// 添加影视记录
|
||||
Future<void> addMovie(Movie movie) async {
|
||||
await _movieDao.insertMovie(movie);
|
||||
await loadMovies();
|
||||
_movies.add(movie);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateMovie(Movie movie) async {
|
||||
await _movieDao.updateMovie(movie);
|
||||
await loadMovies();
|
||||
final idx = _movies.indexWhere((m) => m.id == movie.id);
|
||||
if (idx != -1) _movies[idx] = movie;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 仅更新封面偏移量(不触发全量刷新)
|
||||
@@ -417,52 +459,65 @@ class AppProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> removeMovie(String id) async {
|
||||
await _movieDao.deleteMovie(id);
|
||||
await loadMovies();
|
||||
_movies.removeWhere((m) => m.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addBook(Book book) async {
|
||||
await _bookDao.insertBook(book);
|
||||
await loadBooks();
|
||||
_books.add(book);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateBook(Book book) async {
|
||||
await _bookDao.updateBook(book);
|
||||
await loadBooks();
|
||||
final idx = _books.indexWhere((b) => b.id == book.id);
|
||||
if (idx != -1) _books[idx] = book;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeBook(String id) async {
|
||||
await _bookDao.deleteBook(id);
|
||||
await loadBooks();
|
||||
_books.removeWhere((b) => b.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addNote(Note note) async {
|
||||
await _noteDao.insertNote(note);
|
||||
await loadNotes();
|
||||
_notes.add(note);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateNote(Note note) async {
|
||||
await _noteDao.updateNote(note);
|
||||
await loadNotes();
|
||||
final idx = _notes.indexWhere((n) => n.id == note.id);
|
||||
if (idx != -1) _notes[idx] = note;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeNote(String id) async {
|
||||
await _noteDao.deleteNote(id);
|
||||
await loadNotes();
|
||||
_notes.removeWhere((n) => n.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addGame(Game game) async {
|
||||
await _gameDao.insertGame(game);
|
||||
await loadGames();
|
||||
_games.add(game);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateGame(Game game) async {
|
||||
await _gameDao.updateGame(game);
|
||||
await loadGames();
|
||||
final idx = _games.indexWhere((g) => g.id == game.id);
|
||||
if (idx != -1) _games[idx] = game;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeGame(String id) async {
|
||||
await _gameDao.deleteGame(id);
|
||||
await loadGames();
|
||||
_games.removeWhere((g) => g.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 仅更新游戏封面偏移量(不触发全量刷新)
|
||||
@@ -477,7 +532,10 @@ class AppProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> toggleNotePin(String id, bool isPinned) async {
|
||||
await _noteDao.togglePin(id, isPinned);
|
||||
await loadNotes();
|
||||
final idx = _notes.indexWhere((n) => n.id == id);
|
||||
if (idx != -1) {
|
||||
_notes[idx] = _notes[idx].copyWith(isPinned: isPinned);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -722,29 +780,59 @@ class AppProvider extends ChangeNotifier {
|
||||
final deletedBookExcerpts = await getDeletedBookExcerpts();
|
||||
final deletedGameReviews = await getDeletedGameReviews();
|
||||
|
||||
for (final movie in deletedMovies) {
|
||||
await permanentDeleteMovie(movie.id);
|
||||
// 先收集需要删除图片的 ID,再在事务中批量删除数据库记录
|
||||
final movieIds = deletedMovies.map((m) => m.id).toList();
|
||||
final bookIds = deletedBooks.map((b) => b.id).toList();
|
||||
final noteIds = deletedNotes.map((n) => n.id).toList();
|
||||
final gameIds = deletedGames.map((g) => g.id).toList();
|
||||
|
||||
// 事务内批量删除数据库记录,保证原子性
|
||||
final db = await DatabaseHelper.instance.database;
|
||||
await db.transaction((txn) async {
|
||||
for (final id in movieIds) {
|
||||
await txn.delete('movie_reviews', where: 'movie_id = ?', whereArgs: [id]);
|
||||
await txn.delete('movie_posters', where: 'movie_id = ?', whereArgs: [id]);
|
||||
await txn.delete('movies', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
for (final id in bookIds) {
|
||||
await txn.delete('book_reviews', where: 'book_id = ?', whereArgs: [id]);
|
||||
await txn.delete('book_excerpts', where: 'book_id = ?', whereArgs: [id]);
|
||||
await txn.delete('books', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
for (final id in noteIds) {
|
||||
await txn.delete('notes', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
for (final id in gameIds) {
|
||||
await txn.delete('game_reviews', where: 'game_id = ?', whereArgs: [id]);
|
||||
await txn.delete('game_screenshots', where: 'game_id = ?', whereArgs: [id]);
|
||||
await txn.delete('games', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
for (final review in deletedMovieReviews) {
|
||||
await txn.delete('movie_reviews', where: 'id = ?', whereArgs: [review.id]);
|
||||
}
|
||||
for (final review in deletedBookReviews) {
|
||||
await txn.delete('book_reviews', where: 'id = ?', whereArgs: [review.id]);
|
||||
}
|
||||
for (final excerpt in deletedBookExcerpts) {
|
||||
await txn.delete('book_excerpts', where: 'id = ?', whereArgs: [excerpt.id]);
|
||||
}
|
||||
for (final review in deletedGameReviews) {
|
||||
await txn.delete('game_reviews', where: 'id = ?', whereArgs: [review.id]);
|
||||
}
|
||||
});
|
||||
|
||||
// 事务成功后,清理关联的图片文件(文件删除失败不影响数据一致性)
|
||||
for (final id in movieIds) {
|
||||
await ImagePathHelper.instance.deleteMovieImages(id);
|
||||
}
|
||||
for (final book in deletedBooks) {
|
||||
await permanentDeleteBook(book.id);
|
||||
for (final id in bookIds) {
|
||||
await ImagePathHelper.instance.deleteBookImages(id);
|
||||
}
|
||||
for (final note in deletedNotes) {
|
||||
await permanentDeleteNote(note.id);
|
||||
for (final id in noteIds) {
|
||||
await ImagePathHelper.instance.deleteNoteImages(id);
|
||||
}
|
||||
for (final game in deletedGames) {
|
||||
await permanentDeleteGame(game.id);
|
||||
}
|
||||
for (final review in deletedMovieReviews) {
|
||||
await _reviewDao.permanentDeleteReview(review.id);
|
||||
}
|
||||
for (final review in deletedBookReviews) {
|
||||
await _bookReviewDao.permanentDeleteReview(review.id);
|
||||
}
|
||||
for (final excerpt in deletedBookExcerpts) {
|
||||
await _bookExcerptDao.permanentDeleteExcerpt(excerpt.id);
|
||||
}
|
||||
for (final review in deletedGameReviews) {
|
||||
await _gameReviewDao.permanentDeleteReview(review.id);
|
||||
for (final id in gameIds) {
|
||||
await ImagePathHelper.instance.deleteGameImages(id);
|
||||
}
|
||||
|
||||
await loadMovies();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../utils/user_prefs.dart';
|
||||
import '../utils/server_config.dart';
|
||||
|
||||
@@ -87,7 +88,7 @@ class UsageStatsService with WidgetsBindingObserver {
|
||||
final info = await deviceInfo.linuxInfo;
|
||||
rawId = '${info.name}-${info.id}';
|
||||
} else {
|
||||
rawId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
rawId = const Uuid().v4();
|
||||
}
|
||||
|
||||
final bytes = utf8.encode(rawId);
|
||||
|
||||
Reference in New Issue
Block a user