generated from dellevin/template
桌面端大概写好了
This commit is contained in:
@@ -393,7 +393,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
|
|||||||
|
|
||||||
Widget _buildListView(ColorScheme colors) {
|
Widget _buildListView(ColorScheme colors) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 100),
|
||||||
itemCount: _filteredBooks.length,
|
itemCount: _filteredBooks.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final book = _filteredBooks[index];
|
final book = _filteredBooks[index];
|
||||||
@@ -420,74 +420,53 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
|
|||||||
onTap: () => _openBook(book),
|
onTap: () => _openBook(book),
|
||||||
onLongPress: () => _deleteBook(book),
|
onLongPress: () => _deleteBook(book),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors.surfaceContainerHigh,
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
// 封面
|
||||||
children: [
|
Container(
|
||||||
// 封面
|
width: 48, height: 64,
|
||||||
SizedBox(
|
decoration: BoxDecoration(
|
||||||
width: 56, height: 80,
|
color: colors.outlineVariant,
|
||||||
child: _buildCover(coverPath, colors),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors.surfaceContainerHighest,
|
color: colors.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(6),
|
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)),
|
color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3905,7 +3905,10 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
|
|||||||
selected: provider.selectedMovie?.id == m.id,
|
selected: provider.selectedMovie?.id == m.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: m.rating,
|
||||||
onTap: () => provider.selectMovie(m),
|
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);
|
final (label, color) = _bookStatus(b.status);
|
||||||
return _CompactListItem(
|
return _CompactListItem(
|
||||||
title: b.title,
|
title: b.title,
|
||||||
subtitle: b.authors.isNotEmpty ? b.authors.join(', ') : null,
|
subtitle: _bookSubtitle(b),
|
||||||
imagePath: b.coverPath,
|
imagePath: b.coverPath,
|
||||||
accentColor: const Color(0xFF16A34A),
|
accentColor: const Color(0xFF16A34A),
|
||||||
icon: Icons.menu_book_outlined,
|
icon: Icons.menu_book_outlined,
|
||||||
selected: provider.selectedBook?.id == b.id,
|
selected: provider.selectedBook?.id == b.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: b.rating,
|
||||||
onTap: () => provider.selectBook(b),
|
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);
|
final (label, color) = _gameStatus(g.status);
|
||||||
return _CompactListItem(
|
return _CompactListItem(
|
||||||
title: g.title,
|
title: g.title,
|
||||||
subtitle: g.platforms.isNotEmpty ? g.platforms.join(', ') : null,
|
subtitle: _gameSubtitle(g),
|
||||||
imagePath: g.coverPath,
|
imagePath: g.coverPath,
|
||||||
accentColor: const Color(0xFFEA580C),
|
accentColor: const Color(0xFFEA580C),
|
||||||
icon: Icons.sports_esports_outlined,
|
icon: Icons.sports_esports_outlined,
|
||||||
selected: provider.selectedGame?.id == g.id,
|
selected: provider.selectedGame?.id == g.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: g.rating,
|
||||||
onTap: () => provider.selectGame(g),
|
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) {
|
String? _movieSubtitle(Movie m) {
|
||||||
final parts = <String>[];
|
if (m.directors.isEmpty) return null;
|
||||||
if (m.directors.isNotEmpty) parts.add(m.directors.join(', '));
|
return m.directors.join(', ');
|
||||||
if ((m.rating ?? 0) > 0) parts.add('⭐ ${m.rating}');
|
}
|
||||||
return parts.isNotEmpty ? parts.join(' · ') : null;
|
|
||||||
|
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) {
|
(String, Color) _movieStatus(String status) => switch (status) {
|
||||||
@@ -4022,6 +4039,30 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
|
|||||||
_ => ('通关', const Color(0xFF16A34A)),
|
_ => ('通关', 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) {
|
Widget _buildSearchResults(BuildContext context) {
|
||||||
@@ -4057,6 +4098,7 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
|
|||||||
selected: provider.selectedMovie?.id == m.id,
|
selected: provider.selectedMovie?.id == m.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: m.rating,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
provider.setMainTabIndex(0);
|
provider.setMainTabIndex(0);
|
||||||
provider.selectMovie(m);
|
provider.selectMovie(m);
|
||||||
@@ -4070,13 +4112,14 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
|
|||||||
final (label, color) = _bookStatus(b.status);
|
final (label, color) = _bookStatus(b.status);
|
||||||
return _CompactListItem(
|
return _CompactListItem(
|
||||||
title: b.title,
|
title: b.title,
|
||||||
subtitle: b.authors.isNotEmpty ? b.authors.join(', ') : null,
|
subtitle: _bookSubtitle(b),
|
||||||
imagePath: b.coverPath,
|
imagePath: b.coverPath,
|
||||||
accentColor: const Color(0xFF16A34A),
|
accentColor: const Color(0xFF16A34A),
|
||||||
icon: Icons.menu_book_outlined,
|
icon: Icons.menu_book_outlined,
|
||||||
selected: provider.selectedBook?.id == b.id,
|
selected: provider.selectedBook?.id == b.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: b.rating,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
provider.setMainTabIndex(1);
|
provider.setMainTabIndex(1);
|
||||||
provider.selectBook(b);
|
provider.selectBook(b);
|
||||||
@@ -4105,13 +4148,14 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
|
|||||||
final (label, color) = _gameStatus(g.status);
|
final (label, color) = _gameStatus(g.status);
|
||||||
return _CompactListItem(
|
return _CompactListItem(
|
||||||
title: g.title,
|
title: g.title,
|
||||||
subtitle: g.platforms.isNotEmpty ? g.platforms.join(', ') : null,
|
subtitle: _gameSubtitle(g),
|
||||||
imagePath: g.coverPath,
|
imagePath: g.coverPath,
|
||||||
accentColor: const Color(0xFFEA580C),
|
accentColor: const Color(0xFFEA580C),
|
||||||
icon: Icons.sports_esports_outlined,
|
icon: Icons.sports_esports_outlined,
|
||||||
selected: provider.selectedGame?.id == g.id,
|
selected: provider.selectedGame?.id == g.id,
|
||||||
statusLabel: label,
|
statusLabel: label,
|
||||||
statusColor: color,
|
statusColor: color,
|
||||||
|
rating: g.rating,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
provider.setMainTabIndex(3);
|
provider.setMainTabIndex(3);
|
||||||
provider.selectGame(g);
|
provider.selectGame(g);
|
||||||
@@ -4176,6 +4220,9 @@ class _CompactListItem extends StatelessWidget {
|
|||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final String? statusLabel; // 状态文字,如"已看""想看"
|
final String? statusLabel; // 状态文字,如"已看""想看"
|
||||||
final Color? statusColor; // 状态颜色
|
final Color? statusColor; // 状态颜色
|
||||||
|
final double? rating;
|
||||||
|
final VoidCallback? onEdit;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
const _CompactListItem({
|
const _CompactListItem({
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -4187,6 +4234,9 @@ class _CompactListItem extends StatelessWidget {
|
|||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.statusLabel,
|
this.statusLabel,
|
||||||
this.statusColor,
|
this.statusColor,
|
||||||
|
this.rating,
|
||||||
|
this.onEdit,
|
||||||
|
this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -4205,6 +4255,7 @@ class _CompactListItem extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onSecondaryTapUp: (onEdit != null || onDelete != null) ? (details) => _showContextMenu(context, details) : null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||||
child: Row(
|
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) ...[
|
if (statusLabel != null) ...[
|
||||||
const SizedBox(width: 6),
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 桌面端笔记列表项 ──────────────────────────────────────
|
// ─── 桌面端笔记列表项 ──────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user