From a8842fe91d2789c71d5ac74d0dda5394aa6ea1ce Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Wed, 15 Jul 2026 18:29:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=A4=A7=E6=A6=82?= =?UTF-8?q?=E5=86=99=E5=A5=BD=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/epub_reader/epub_library_page.dart | 109 ++++++++---------- lib/pages/home/home_page.dart | 111 +++++++++++++++++-- 2 files changed, 147 insertions(+), 73 deletions(-) diff --git a/lib/pages/epub_reader/epub_library_page.dart b/lib/pages/epub_reader/epub_library_page.dart index 97df5c2..3e61989 100644 --- a/lib/pages/epub_reader/epub_library_page.dart +++ b/lib/pages/epub_reader/epub_library_page.dart @@ -393,7 +393,7 @@ class _EpubLibraryPageState extends State { Widget _buildListView(ColorScheme colors) { return ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 100), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), itemCount: _filteredBooks.length, itemBuilder: (context, index) { final book = _filteredBooks[index]; @@ -420,74 +420,53 @@ class _EpubLibraryPageState extends State { onTap: () => _openBook(book), onLongPress: () => _deleteBook(book), child: Container( - margin: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: colors.outlineVariant, width: 0.5), + borderRadius: BorderRadius.circular(12), ), - child: Column( + child: Row( children: [ - Row( - children: [ - // 封面 - SizedBox( - width: 56, height: 80, - child: _buildCover(coverPath, colors), - ), - const SizedBox(width: 14), - // 信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), - if (author.isNotEmpty) ...[ - const SizedBox(height: 4), - Text(author, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), - ], - const SizedBox(height: 8), - // 状态标签 + 百分比 - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: statusColor.withValues(alpha: 0.25), width: 0.5), - ), - child: Text(statusLabel, - style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: statusColor)), - ), - if (progress > 0 && progress < 1.0) ...[ - const SizedBox(width: 6), - Text('${(progress * 100).toInt()}%', - style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.5))), - ], - ], - ), - ], - ), - ), - ], - ), - // 进度条 - if (progress > 0) ...[ - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular(1.5), - child: LinearProgressIndicator( - value: progress, - minHeight: 3, - backgroundColor: colors.outlineVariant.withValues(alpha: 0.3), - valueColor: AlwaysStoppedAnimation(statusColor), - ), + // 封面 + Container( + width: 48, height: 64, + decoration: BoxDecoration( + color: colors.outlineVariant, + borderRadius: BorderRadius.circular(6), ), - ], + clipBehavior: Clip.antiAlias, + child: _buildCover(coverPath, colors), + ), + const SizedBox(width: 12), + // 信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), + if (author.isNotEmpty) ...[ + const SizedBox(height: 3), + Text(author, maxLines: 1, overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))), + ], + const SizedBox(height: 6), + // 状态标签 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text(statusLabel, + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: statusColor)), + ), + ], + ), + ), + const SizedBox(width: 8), + Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.2), size: 20), ], ), ), @@ -506,10 +485,10 @@ class _EpubLibraryPageState extends State { } return Container( decoration: BoxDecoration( - color: colors.surfaceContainerHighest, + color: colors.outlineVariant, borderRadius: BorderRadius.circular(6), ), - child: Icon(Icons.auto_stories_outlined, size: 24, + child: Icon(Icons.auto_stories_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)), ); } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 2abf5b3..bd79021 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -3905,7 +3905,10 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { selected: provider.selectedMovie?.id == m.id, statusLabel: label, statusColor: color, + rating: m.rating, onTap: () => provider.selectMovie(m), + onEdit: () => provider.selectMovie(m), + onDelete: () => _deleteItem(context, 'movie', m.id, m.title), ); }, ); @@ -3926,14 +3929,17 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { final (label, color) = _bookStatus(b.status); return _CompactListItem( title: b.title, - subtitle: b.authors.isNotEmpty ? b.authors.join(', ') : null, + subtitle: _bookSubtitle(b), imagePath: b.coverPath, accentColor: const Color(0xFF16A34A), icon: Icons.menu_book_outlined, selected: provider.selectedBook?.id == b.id, statusLabel: label, statusColor: color, + rating: b.rating, onTap: () => provider.selectBook(b), + onEdit: () => provider.selectBook(b), + onDelete: () => _deleteItem(context, 'book', b.id, b.title), ); }, ); @@ -3977,14 +3983,17 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { final (label, color) = _gameStatus(g.status); return _CompactListItem( title: g.title, - subtitle: g.platforms.isNotEmpty ? g.platforms.join(', ') : null, + subtitle: _gameSubtitle(g), imagePath: g.coverPath, accentColor: const Color(0xFFEA580C), icon: Icons.sports_esports_outlined, selected: provider.selectedGame?.id == g.id, statusLabel: label, statusColor: color, + rating: g.rating, onTap: () => provider.selectGame(g), + onEdit: () => provider.selectGame(g), + onDelete: () => _deleteItem(context, 'game', g.id, g.title), ); }, ); @@ -3993,10 +4002,18 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { } String? _movieSubtitle(Movie m) { - final parts = []; - if (m.directors.isNotEmpty) parts.add(m.directors.join(', ')); - if ((m.rating ?? 0) > 0) parts.add('⭐ ${m.rating}'); - return parts.isNotEmpty ? parts.join(' · ') : null; + if (m.directors.isEmpty) return null; + return m.directors.join(', '); + } + + String? _bookSubtitle(Book b) { + if (b.authors.isEmpty) return null; + return b.authors.join(', '); + } + + String? _gameSubtitle(Game g) { + if (g.platforms.isEmpty) return null; + return g.platforms.join(', '); } (String, Color) _movieStatus(String status) => switch (status) { @@ -4022,6 +4039,30 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { _ => ('通关', const Color(0xFF16A34A)), }; + Future _deleteItem(BuildContext context, String type, String id, String title) async { + final provider = context.read(); + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(ctx).colorScheme.surface, + title: const Text('确认删除'), + content: Text('确定要删除「$title」吗?'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), + TextButton(onPressed: () => Navigator.pop(ctx, true), + child: Text('删除', style: TextStyle(color: Theme.of(ctx).colorScheme.error))), + ], + ), + ); + if (confirmed == true && context.mounted) { + switch (type) { + case 'movie': await provider.removeMovie(id); + case 'book': await provider.removeBook(id); + case 'game': await provider.removeGame(id); + } + } + } + // ─── 搜索结果 ────────────────────────────────────────── Widget _buildSearchResults(BuildContext context) { @@ -4057,6 +4098,7 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { selected: provider.selectedMovie?.id == m.id, statusLabel: label, statusColor: color, + rating: m.rating, onTap: () { provider.setMainTabIndex(0); provider.selectMovie(m); @@ -4070,13 +4112,14 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { final (label, color) = _bookStatus(b.status); return _CompactListItem( title: b.title, - subtitle: b.authors.isNotEmpty ? b.authors.join(', ') : null, + subtitle: _bookSubtitle(b), imagePath: b.coverPath, accentColor: const Color(0xFF16A34A), icon: Icons.menu_book_outlined, selected: provider.selectedBook?.id == b.id, statusLabel: label, statusColor: color, + rating: b.rating, onTap: () { provider.setMainTabIndex(1); provider.selectBook(b); @@ -4105,13 +4148,14 @@ class _DesktopListPanelState extends State<_DesktopListPanel> { final (label, color) = _gameStatus(g.status); return _CompactListItem( title: g.title, - subtitle: g.platforms.isNotEmpty ? g.platforms.join(', ') : null, + subtitle: _gameSubtitle(g), imagePath: g.coverPath, accentColor: const Color(0xFFEA580C), icon: Icons.sports_esports_outlined, selected: provider.selectedGame?.id == g.id, statusLabel: label, statusColor: color, + rating: g.rating, onTap: () { provider.setMainTabIndex(3); provider.selectGame(g); @@ -4176,6 +4220,9 @@ class _CompactListItem extends StatelessWidget { final VoidCallback onTap; final String? statusLabel; // 状态文字,如"已看""想看" final Color? statusColor; // 状态颜色 + final double? rating; + final VoidCallback? onEdit; + final VoidCallback? onDelete; const _CompactListItem({ required this.title, @@ -4187,6 +4234,9 @@ class _CompactListItem extends StatelessWidget { required this.onTap, this.statusLabel, this.statusColor, + this.rating, + this.onEdit, + this.onDelete, }); @override @@ -4205,6 +4255,7 @@ class _CompactListItem extends StatelessWidget { child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), + onSecondaryTapUp: (onEdit != null || onDelete != null) ? (details) => _showContextMenu(context, details) : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), child: Row( @@ -4256,6 +4307,13 @@ class _CompactListItem extends StatelessWidget { ], ), ), + // 评分 + if (rating != null && rating! > 0) ...[ + const SizedBox(width: 4), + const Icon(Icons.star, size: 12, color: Color(0xFFFFB800)), + const SizedBox(width: 1), + Text(rating!.toStringAsFixed(1), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.6))), + ], // 状态标签 if (statusLabel != null) ...[ const SizedBox(width: 6), @@ -4275,6 +4333,43 @@ class _CompactListItem extends StatelessWidget { ), ); } + + void _showContextMenu(BuildContext context, TapUpDetails details) { + final colors = Theme.of(context).colorScheme; + final renderBox = context.findRenderObject() as RenderBox; + final position = renderBox.localToGlobal(details.localPosition); + + showMenu( + context: context, + position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx + 1, position.dy + 1), + items: [ + if (onEdit != null) + PopupMenuItem( + value: 'edit', + height: 36, + child: Row(children: [ + Icon(Icons.edit_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 8), + const Text('编辑', style: TextStyle(fontSize: 13)), + ]), + ), + if (onDelete != null) + PopupMenuItem( + value: 'delete', + height: 36, + child: Row(children: [ + Icon(Icons.delete_outline, size: 16, color: colors.error), + const SizedBox(width: 8), + Text('删除', style: TextStyle(color: colors.error, fontSize: 13)), + ]), + ), + ], + ).then((value) { + if (value == null || !context.mounted) return; + if (value == 'edit') onEdit?.call(); + if (value == 'delete') onDelete?.call(); + }); + } } // ─── 桌面端笔记列表项 ──────────────────────────────────────