桌面端大概写好了

This commit is contained in:
DelLevin-Home
2026-07-15 18:29:03 +08:00
parent 1fcbfb6874
commit a8842fe91d
2 changed files with 147 additions and 73 deletions

View File

@@ -393,7 +393,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
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<EpubLibraryPage> {
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<EpubLibraryPage> {
}
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)),
);
}

View File

@@ -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 = <String>[];
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<void> _deleteItem(BuildContext context, String type, String id, String title) async {
final provider = context.read<AppProvider>();
final confirmed = await showDialog<bool>(
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<String>(
context: context,
position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx + 1, position.dy + 1),
items: [
if (onEdit != null)
PopupMenuItem<String>(
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<String>(
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();
});
}
}
// ─── 桌面端笔记列表项 ──────────────────────────────────────