feat: group television resources by season

This commit is contained in:
ngfchl
2026-07-18 14:10:44 +08:00
parent 73262cd71c
commit f1ab8a9a3b
+119 -62
View File
@@ -2030,11 +2030,30 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
}
Widget _resourceList(BuildContext context, ShadColorScheme cs) {
final isSeries = widget.work.primary.mediaKind == TMDBMediaKind.tv;
final episodesBySeason = <int, List<MediaLibraryItem>>{};
if (isSeries) {
for (final resource in widget.work.resources) {
final parsed = ParsedMediaName.parse(resource.file.name);
(episodesBySeason[parsed.season ?? 1] ??= []).add(resource);
}
for (final values in episodesBySeason.values) {
values.sort((a, b) {
final aEpisode = ParsedMediaName.parse(a.file.name).episode ?? 9999;
final bEpisode = ParsedMediaName.parse(b.file.name).episode ?? 9999;
return aEpisode == bEpisode
? a.file.name.compareTo(b.file.name)
: aEpisode.compareTo(bEpisode);
});
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.work.resources.length > 1
isSeries
? '剧集 (${widget.work.resources.length} 集)'
: widget.work.resources.length > 1
? '资源版本 (${widget.work.resources.length})'
: '媒体资源',
style: TextStyle(
@@ -2044,69 +2063,101 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
),
),
const SizedBox(height: 8),
...widget.work.resources.map((resource) {
final selected = resource.id == _resource.id;
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: ShadContextMenuRegion(
items: [
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.play, size: 16),
onPressed: () => widget.onPlay(resource),
child: const Text('播放'),
if (isSeries && episodesBySeason.isNotEmpty)
for (final entry in episodesBySeason.entries) ...[
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Text(
'${entry.key}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: cs.foreground,
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.monitorPlay, size: 16),
onPressed: () => widget.onExternalPlay(resource),
child: const Text('外部播放器'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.download, size: 16),
onPressed: () => widget.onDownload(resource),
child: const Text('下载'),
),
],
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _resource = resource),
borderRadius: BorderRadius.circular(6),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: selected
? cs.primary.withValues(alpha: 0.10)
: cs.card,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: selected ? cs.primary : cs.border,
),
),
...entry.value.map(
(resource) => _resourceTile(
resource,
cs,
episode: ParsedMediaName.parse(resource.file.name).episode,
),
),
]
else
...widget.work.resources.map(
(resource) => _resourceTile(resource, cs),
),
],
);
}
Widget _resourceTile(
MediaLibraryItem resource,
ShadColorScheme cs, {
int? episode,
}) {
final selected = resource.id == _resource.id;
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: ShadContextMenuRegion(
items: [
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.play, size: 16),
onPressed: () => widget.onPlay(resource),
child: const Text('播放'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.monitorPlay, size: 16),
onPressed: () => widget.onExternalPlay(resource),
child: const Text('外部播放器'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.download, size: 16),
onPressed: () => widget.onDownload(resource),
child: const Text('下载'),
),
],
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _resource = resource),
borderRadius: BorderRadius.circular(6),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: selected ? cs.primary.withValues(alpha: 0.10) : cs.card,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: selected ? cs.primary : cs.border),
),
child: Row(
children: [
if (episode != null)
SizedBox(
width: 38,
child: Text(
'E${episode.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: cs.primary,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
resource.file.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: cs.foreground,
),
),
),
if (selected)
Icon(
Icons.check_circle_rounded,
size: 16,
color: cs.primary,
),
],
Text(
resource.file.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: cs.foreground,
),
),
const SizedBox(height: 4),
Text(
@@ -2121,12 +2172,18 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
],
),
),
),
if (selected)
Icon(
Icons.check_circle_rounded,
size: 16,
color: cs.primary,
),
],
),
),
);
}),
],
),
),
),
);
}
}