From b796ff11b30ff6e68d2f2b6348c2838cab9ea213 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sun, 9 Aug 2026 02:20:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=9E=E6=94=B6=E7=AB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/character/book_character_dao.dart | 23 +++ lib/data/character/game_character_dao.dart | 23 +++ lib/data/character/movie_character_dao.dart | 23 +++ lib/pages/character/character_list_page.dart | 142 ++++++------------- lib/pages/settings/recycle_bin_page.dart | 106 +++++++++++--- lib/providers/app_provider.dart | 75 ++++++++++ 6 files changed, 275 insertions(+), 117 deletions(-) diff --git a/lib/data/character/book_character_dao.dart b/lib/data/character/book_character_dao.dart index 93ac124..f8a1b5f 100644 --- a/lib/data/character/book_character_dao.dart +++ b/lib/data/character/book_character_dao.dart @@ -67,6 +67,29 @@ class BookCharacterDao { ); }); + /// 恢复已删除的角色 + Future restore(String id) => _wrap('restore', () async { + final db = await _dbHelper.database; + return await db.update( + 'book_characters', + {'is_deleted': 0, 'updated_at': DateTime.now().toUtc().toIso8601String()}, + where: 'id = ?', + whereArgs: [id], + ); + }); + + /// 获取已删除的角色 + Future> getDeleted() => _wrap('getDeleted', () async { + final db = await _dbHelper.database; + final maps = await db.query( + 'book_characters', + where: 'is_deleted = ?', + whereArgs: [1], + orderBy: 'updated_at DESC', + ); + return maps.map((m) => BookCharacter.fromJson(m)).toList(); + }); + /// 获取书籍的角色数量 Future getCount(String bookId) => _wrap('getCount', () async { final db = await _dbHelper.database; diff --git a/lib/data/character/game_character_dao.dart b/lib/data/character/game_character_dao.dart index 07aa831..7278d41 100644 --- a/lib/data/character/game_character_dao.dart +++ b/lib/data/character/game_character_dao.dart @@ -67,6 +67,29 @@ class GameCharacterDao { ); }); + /// 恢复已删除的角色 + Future restore(String id) => _wrap('restore', () async { + final db = await _dbHelper.database; + return await db.update( + 'game_characters', + {'is_deleted': 0, 'updated_at': DateTime.now().toUtc().toIso8601String()}, + where: 'id = ?', + whereArgs: [id], + ); + }); + + /// 获取已删除的角色 + Future> getDeleted() => _wrap('getDeleted', () async { + final db = await _dbHelper.database; + final maps = await db.query( + 'game_characters', + where: 'is_deleted = ?', + whereArgs: [1], + orderBy: 'updated_at DESC', + ); + return maps.map((m) => GameCharacter.fromJson(m)).toList(); + }); + /// 获取游戏的角色数量 Future getCount(String gameId) => _wrap('getCount', () async { final db = await _dbHelper.database; diff --git a/lib/data/character/movie_character_dao.dart b/lib/data/character/movie_character_dao.dart index 1fcff6a..79e18fa 100644 --- a/lib/data/character/movie_character_dao.dart +++ b/lib/data/character/movie_character_dao.dart @@ -67,6 +67,29 @@ class MovieCharacterDao { ); }); + /// 恢复已删除的角色 + Future restore(String id) => _wrap('restore', () async { + final db = await _dbHelper.database; + return await db.update( + 'movie_characters', + {'is_deleted': 0, 'updated_at': DateTime.now().toUtc().toIso8601String()}, + where: 'id = ?', + whereArgs: [id], + ); + }); + + /// 获取已删除的角色 + Future> getDeleted() => _wrap('getDeleted', () async { + final db = await _dbHelper.database; + final maps = await db.query( + 'movie_characters', + where: 'is_deleted = ?', + whereArgs: [1], + orderBy: 'updated_at DESC', + ); + return maps.map((m) => MovieCharacter.fromJson(m)).toList(); + }); + /// 获取影视的角色数量 Future getCount(String movieId) => _wrap('getCount', () async { final db = await _dbHelper.database; diff --git a/lib/pages/character/character_list_page.dart b/lib/pages/character/character_list_page.dart index 364d696..8b5a7f1 100644 --- a/lib/pages/character/character_list_page.dart +++ b/lib/pages/character/character_list_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../models/data_models.dart'; import '../../providers/app_provider.dart'; -import '../../utils/image_path_helper.dart'; import '../../utils/toast_util.dart'; import '../../widgets/fade_in_local_image.dart'; import 'character_form_page.dart'; @@ -51,9 +50,6 @@ class _MovieCharactersPageState extends State { } Future _delete(MovieCharacter c) async { - if (c.imagePath != null && c.imagePath!.isNotEmpty) { - await ImagePathHelper.instance.deleteCharacterImages(c.id); - } await context.read().deleteMovieCharacter(c.id); _loadCharacters(); if (mounted) ToastUtil.show(context, '已删除'); @@ -116,9 +112,6 @@ class _BookCharactersPageState extends State { } Future _delete(BookCharacter c) async { - if (c.imagePath != null && c.imagePath!.isNotEmpty) { - await ImagePathHelper.instance.deleteCharacterImages(c.id); - } await context.read().deleteBookCharacter(c.id); _loadCharacters(); if (mounted) ToastUtil.show(context, '已删除'); @@ -181,9 +174,6 @@ class _GameCharactersPageState extends State { } Future _delete(GameCharacter c) async { - if (c.imagePath != null && c.imagePath!.isNotEmpty) { - await ImagePathHelper.instance.deleteCharacterImages(c.id); - } await context.read().deleteGameCharacter(c.id); _loadCharacters(); if (mounted) ToastUtil.show(context, '已删除'); @@ -247,7 +237,7 @@ class _CharacterListScaffold extends StatelessWidget { description: c.description as String?, imagePath: c.imagePath as String?, onTap: () => onTap(c), - onDelete: () => _confirmDelete(context, c), + onDelete: () => onDelete(c), ); }, ), @@ -276,41 +266,6 @@ class _CharacterListScaffold extends StatelessWidget { ), ); } - - void _confirmDelete(BuildContext context, dynamic c) { - final colors = Theme.of(context).colorScheme; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), - content: Text('确定要删除该角色吗?', - 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: () { - Navigator.pop(ctx); - onDelete(c); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.error, - foregroundColor: colors.onError, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: const Text('删除'), - ), - ], - actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ); - } } class _CharacterTile extends StatelessWidget { @@ -335,61 +290,48 @@ class _CharacterTile extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; - return Dismissible( - key: ValueKey(name + imagePath.toString()), - direction: DismissDirection.endToStart, - background: Container( - color: colors.error, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: Icon(Icons.delete_outline, color: colors.onError), - ), - confirmDismiss: (_) async { - _showDeleteDialog(context); - return false; - }, - child: ListTile( - onTap: onTap, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: _buildAvatar(colors), - title: Text(name, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (aliases.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text(aliases.join('、'), - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), - ), - if (tags.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 6), - child: Wrap( - spacing: 4, - runSpacing: 4, - children: tags.map((t) => Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: Text(t, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))), - )).toList(), - ), - ), - if (description != null && description!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text(description!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)), - ), - ], - ), - trailing: Icon(Icons.chevron_right, size: 18, color: colors.onSurface.withValues(alpha: 0.25)), + return ListTile( + onTap: onTap, + onLongPress: () => _showDeleteDialog(context), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: _buildAvatar(colors), + title: Text(name, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (aliases.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text(aliases.join('、'), + style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + ), + if (tags.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: tags.map((t) => Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: Text(t, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))), + )).toList(), + ), + ), + if (description != null && description!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text(description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)), + ), + ], ), + trailing: Icon(Icons.chevron_right, size: 18, color: colors.onSurface.withValues(alpha: 0.25)), ); } diff --git a/lib/pages/settings/recycle_bin_page.dart b/lib/pages/settings/recycle_bin_page.dart index 580ad44..653cba5 100644 --- a/lib/pages/settings/recycle_bin_page.dart +++ b/lib/pages/settings/recycle_bin_page.dart @@ -12,7 +12,7 @@ class RecycleBinPage extends StatefulWidget { State createState() => _RecycleBinPageState(); } -enum _ItemType { movie, book, note, game, movieReview, bookReview, bookExcerpt, gameReview, person } +enum _ItemType { movie, book, note, game, movieReview, bookReview, bookExcerpt, gameReview, person, movieCharacter, bookCharacter, gameCharacter } class _DeletedItem { final _ItemType type; @@ -94,6 +94,30 @@ class _DeletedItem { icon = Icons.person_outline, typeLabel = '人物'; + _DeletedItem.movieCharacter(MovieCharacter c) + : type = _ItemType.movieCharacter, + id = c.id, + title = c.name, + subtitle = '删除于 ${c.updatedAt.year}.${c.updatedAt.month.toString().padLeft(2, '0')}.${c.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.movie_outlined, + typeLabel = '影视角色'; + + _DeletedItem.bookCharacter(BookCharacter c) + : type = _ItemType.bookCharacter, + id = c.id, + title = c.name, + subtitle = '删除于 ${c.updatedAt.year}.${c.updatedAt.month.toString().padLeft(2, '0')}.${c.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.menu_book_outlined, + typeLabel = '书籍角色'; + + _DeletedItem.gameCharacter(GameCharacter c) + : type = _ItemType.gameCharacter, + id = c.id, + title = c.name, + subtitle = '删除于 ${c.updatedAt.year}.${c.updatedAt.month.toString().padLeft(2, '0')}.${c.updatedAt.day.toString().padLeft(2, '0')}', + icon = Icons.sports_esports_outlined, + typeLabel = '游戏角色'; + } class _RecycleBinPageState extends State { @@ -122,6 +146,9 @@ class _RecycleBinPageState extends State { final bookExcerpts = await provider.getDeletedBookExcerpts(); final gameReviews = await provider.getDeletedGameReviews(); final people = await provider.getDeletedPeople(); + final movieCharacters = await provider.getDeletedMovieCharacters(); + final bookCharacters = await provider.getDeletedBookCharacters(); + final gameCharacters = await provider.getDeletedGameCharacters(); if (!mounted) return; setState(() { _allItems = [ @@ -134,6 +161,9 @@ class _RecycleBinPageState extends State { for (final e in bookExcerpts) _DeletedItem.bookExcerpt(e), for (final r in gameReviews) _DeletedItem.gameReview(r), for (final p in people) _DeletedItem.person(p), + for (final c in movieCharacters) _DeletedItem.movieCharacter(c), + for (final c in bookCharacters) _DeletedItem.bookCharacter(c), + for (final c in gameCharacters) _DeletedItem.gameCharacter(c), ]; _isLoading = false; }); @@ -191,27 +221,54 @@ class _RecycleBinPageState extends State { Widget _buildFilterRow() { final colors = Theme.of(context).colorScheme; + final chips = [ + _filterChip('全部', null), + _filterChip('影视', _ItemType.movie), + _filterChip('书籍', _ItemType.book), + _filterChip('笔记', _ItemType.note), + _filterChip('游戏', _ItemType.game), + _filterChip('影评', _ItemType.movieReview), + _filterChip('书评', _ItemType.bookReview), + _filterChip('书摘', _ItemType.bookExcerpt), + _filterChip('游戏评价', _ItemType.gameReview), + _filterChip('人物', _ItemType.person), + _filterChip('影视角色', _ItemType.movieCharacter), + _filterChip('书籍角色', _ItemType.bookCharacter), + _filterChip('游戏角色', _ItemType.gameCharacter), + ]; return Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)), ), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _filterChip('全部', null), - _filterChip('影视', _ItemType.movie), - _filterChip('书籍', _ItemType.book), - _filterChip('笔记', _ItemType.note), - _filterChip('游戏', _ItemType.game), - _filterChip('影评', _ItemType.movieReview), - _filterChip('书评', _ItemType.bookReview), - _filterChip('书摘', _ItemType.bookExcerpt), - _filterChip('游戏评价', _ItemType.gameReview), - _filterChip('人物', _ItemType.person), - ], + child: ShaderMask( + shaderCallback: (Rect bounds) { + return const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color(0x00FFFFFF), + Color(0xFFFFFFFF), + Color(0xFFFFFFFF), + Color(0x00FFFFFF), + ], + stops: [0.0, 0.04, 0.96, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + for (var i = 0; i < chips.length; i++) ...[ + if (i > 0) const SizedBox(width: 8), + chips[i], + ], + ], + ), + ), ), ); } @@ -442,6 +499,15 @@ class _RecycleBinPageState extends State { case _ItemType.person: await provider.restorePerson(item.id); if (mounted) ToastUtil.show(context, '人物已恢复'); + case _ItemType.movieCharacter: + await provider.restoreMovieCharacter(item.id); + if (mounted) ToastUtil.show(context, '影视角色已恢复'); + case _ItemType.bookCharacter: + await provider.restoreBookCharacter(item.id); + if (mounted) ToastUtil.show(context, '书籍角色已恢复'); + case _ItemType.gameCharacter: + await provider.restoreGameCharacter(item.id); + if (mounted) ToastUtil.show(context, '游戏角色已恢复'); } _loadDeletedItems(); } @@ -469,6 +535,12 @@ class _RecycleBinPageState extends State { await provider.permanentDeleteGameReview(item.id); case _ItemType.person: await provider.permanentDeletePerson(item.id); + case _ItemType.movieCharacter: + await provider.permanentDeleteMovieCharacter(item.id); + case _ItemType.bookCharacter: + await provider.permanentDeleteBookCharacter(item.id); + case _ItemType.gameCharacter: + await provider.permanentDeleteGameCharacter(item.id); } _loadDeletedItems(); if (mounted) ToastUtil.show(context, '已彻底删除'); diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 8c8e94e..5fbe44e 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -962,6 +962,9 @@ class AppProvider extends ChangeNotifier { final deletedBookExcerpts = await getDeletedBookExcerpts(); final deletedGameReviews = await getDeletedGameReviews(); final deletedPeople = await getDeletedPeople(); + final deletedMovieCharacters = await getDeletedMovieCharacters(); + final deletedBookCharacters = await getDeletedBookCharacters(); + final deletedGameCharacters = await getDeletedGameCharacters(); // 先收集需要删除图片的 ID,再在事务中批量删除数据库记录 final movieIds = deletedMovies.map((m) => m.id).toList(); @@ -969,6 +972,9 @@ class AppProvider extends ChangeNotifier { final noteIds = deletedNotes.map((n) => n.id).toList(); final gameIds = deletedGames.map((g) => g.id).toList(); final personIds = deletedPeople.map((p) => p.id).toList(); + final movieCharacterIds = deletedMovieCharacters.map((c) => c.id).toList(); + final bookCharacterIds = deletedBookCharacters.map((c) => c.id).toList(); + final gameCharacterIds = deletedGameCharacters.map((c) => c.id).toList(); // 事务内批量删除数据库记录,保证原子性 final db = await DatabaseHelper.instance.database; @@ -1009,6 +1015,15 @@ class AppProvider extends ChangeNotifier { await txn.delete('game_people', where: 'person_id = ?', whereArgs: [id]); await txn.delete('people', where: 'id = ?', whereArgs: [id]); } + for (final id in movieCharacterIds) { + await txn.delete('movie_characters', where: 'id = ?', whereArgs: [id]); + } + for (final id in bookCharacterIds) { + await txn.delete('book_characters', where: 'id = ?', whereArgs: [id]); + } + for (final id in gameCharacterIds) { + await txn.delete('game_characters', where: 'id = ?', whereArgs: [id]); + } }); // 事务成功后,清理关联的图片文件(文件删除失败不影响数据一致性) @@ -1027,6 +1042,15 @@ class AppProvider extends ChangeNotifier { for (final id in personIds) { await ImagePathHelper.instance.deletePersonImages(id); } + for (final id in movieCharacterIds) { + await ImagePathHelper.instance.deleteCharacterImages(id); + } + for (final id in bookCharacterIds) { + await ImagePathHelper.instance.deleteCharacterImages(id); + } + for (final id in gameCharacterIds) { + await ImagePathHelper.instance.deleteCharacterImages(id); + } await loadMovies(); await loadBooks(); @@ -1482,6 +1506,23 @@ class AppProvider extends ChangeNotifier { notifyListeners(); } + /// 恢复已删除的影视角色 + Future restoreMovieCharacter(String id) async { + await _movieCharacterDao.restore(id); + notifyListeners(); + } + + /// 彻底删除影视角色 + Future permanentDeleteMovieCharacter(String id) async { + await ImagePathHelper.instance.deleteCharacterImages(id); + await _movieCharacterDao.permanentDelete(id); + } + + /// 获取已删除的影视角色 + Future> getDeletedMovieCharacters() async { + return await _movieCharacterDao.getDeleted(); + } + // ─── 书籍角色 ─── Future> getBookCharacters(String bookId) async { return await _bookCharacterDao.getByBookId(bookId); @@ -1506,6 +1547,23 @@ class AppProvider extends ChangeNotifier { notifyListeners(); } + /// 恢复已删除的书籍角色 + Future restoreBookCharacter(String id) async { + await _bookCharacterDao.restore(id); + notifyListeners(); + } + + /// 彻底删除书籍角色 + Future permanentDeleteBookCharacter(String id) async { + await ImagePathHelper.instance.deleteCharacterImages(id); + await _bookCharacterDao.permanentDelete(id); + } + + /// 获取已删除的书籍角色 + Future> getDeletedBookCharacters() async { + return await _bookCharacterDao.getDeleted(); + } + // ─── 游戏角色 ─── Future> getGameCharacters(String gameId) async { return await _gameCharacterDao.getByGameId(gameId); @@ -1529,4 +1587,21 @@ class AppProvider extends ChangeNotifier { await _gameCharacterDao.delete(id); notifyListeners(); } + + /// 恢复已删除的游戏角色 + Future restoreGameCharacter(String id) async { + await _gameCharacterDao.restore(id); + notifyListeners(); + } + + /// 彻底删除游戏角色 + Future permanentDeleteGameCharacter(String id) async { + await ImagePathHelper.instance.deleteCharacterImages(id); + await _gameCharacterDao.permanentDelete(id); + } + + /// 获取已删除的游戏角色 + Future> getDeletedGameCharacters() async { + return await _gameCharacterDao.getDeleted(); + } }