Files
sm-emby-share/lib/shared/widgets/latest_media_row.dart
T

195 lines
6.9 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/material.dart';
import '../../core/contracts/library.dart';
import '../theme/app_theme.dart';
import 'hover_scrollable_row.dart';
import 'media_row_metrics.dart';
import 'media_poster_card.dart';
class LatestMediaRow extends StatefulWidget {
final String title;
final String? subtitle;
final List<EmbyRawItem> items;
final String? Function(EmbyRawItem) imageUrlBuilder;
final String? Function(EmbyRawItem)? preloadImageUrlBuilder;
final void Function(EmbyRawItem) onItemTap;
final VoidCallback? onSeeAll;
final int? totalCount;
final MediaCardVariant cardVariant;
final bool enableContextMenu;
final Map<String, bool>? favoriteMap;
final Map<String, bool>? playedMap;
final Map<String, bool>? favoriteLoadingMap;
final Map<String, bool>? playedLoadingMap;
final void Function(EmbyRawItem)? onAddFavorite;
final void Function(EmbyRawItem)? onMarkPlayed;
final void Function(EmbyRawItem)? onHideFromResume;
final Map<String, bool>? hideFromResumeLoadingMap;
final bool showUnplayedCountBadge;
final bool showHoverPlayIcon;
final String? activeItemId;
final String? fallbackImageUrl;
final Map<String, String>? imageHeaders;
final Color? titleColor;
const LatestMediaRow({
super.key,
required this.title,
this.subtitle,
required this.items,
required this.imageUrlBuilder,
this.preloadImageUrlBuilder,
required this.onItemTap,
this.onSeeAll,
this.totalCount,
this.cardVariant = MediaCardVariant.poster,
this.enableContextMenu = false,
this.favoriteMap,
this.playedMap,
this.favoriteLoadingMap,
this.playedLoadingMap,
this.onAddFavorite,
this.onMarkPlayed,
this.onHideFromResume,
this.hideFromResumeLoadingMap,
this.showUnplayedCountBadge = true,
this.showHoverPlayIcon = true,
this.activeItemId,
this.fallbackImageUrl,
this.imageHeaders,
this.titleColor,
});
@override
State<LatestMediaRow> createState() => _LatestMediaRowState();
}
class _LatestMediaRowState extends State<LatestMediaRow> {
@override
Widget build(BuildContext context) {
final compact = MediaRowMetrics.isCompact(context);
final rowHeight = MediaRowMetrics.rowHeight(
widget.cardVariant,
compact: compact,
);
final cardWidth = MediaRowMetrics.cardWidth(
widget.cardVariant,
compact: compact,
);
final theme = Theme.of(context);
final tokens = context.appTokens;
final titleColor = widget.titleColor;
final mutedColor = titleColor != null
? titleColor.withValues(alpha: 0.55)
: tokens.mutedForeground;
final seeAllColor = titleColor != null
? titleColor.withValues(alpha: 0.78)
: theme.colorScheme.onSurface.withValues(alpha: 0.82);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 10),
child: Row(
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: titleColor,
),
),
),
if (widget.subtitle != null) ...[
const SizedBox(width: 8),
Text(
'· ${widget.subtitle}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: mutedColor,
),
),
],
],
),
),
if (widget.onSeeAll != null)
TextButton(
onPressed: widget.onSeeAll,
style: TextButton.styleFrom(
foregroundColor: seeAllColor,
padding: const EdgeInsets.symmetric(horizontal: 8),
visualDensity: VisualDensity.compact,
),
child: Text(
widget.totalCount != null && widget.totalCount! > 0
? '查看全部 ${widget.totalCount}'
: '查看全部',
),
),
],
),
),
SizedBox(
height: rowHeight,
child: HoverScrollableRow(
builder: (_, controller) => ListView.separated(
controller: controller,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 24),
itemCount: widget.items.length,
separatorBuilder: (_, index) => const SizedBox(width: 14),
itemBuilder: (_, index) {
final item = widget.items[index];
final isActive = widget.activeItemId == item.Id;
return MediaPosterCard(
item: item,
imageUrl: widget.imageUrlBuilder(item),
onTap: () => widget.onItemTap(item),
width: cardWidth,
variant: widget.cardVariant,
showHoverPlayIcon: widget.showHoverPlayIcon,
enableContextMenu: widget.enableContextMenu,
isFavoriteOverride:
widget.favoriteMap?[item.Id] ?? item.UserData?.IsFavorite,
isPlayedOverride:
widget.playedMap?[item.Id] ?? item.UserData?.Played,
favoriteLoading: widget.favoriteLoadingMap?[item.Id] ?? false,
playedLoading: widget.playedLoadingMap?[item.Id] ?? false,
onAddFavorite: widget.onAddFavorite != null
? () => widget.onAddFavorite!(item)
: null,
onMarkPlayed: widget.onMarkPlayed != null
? () => widget.onMarkPlayed!(item)
: null,
onHideFromResume: widget.onHideFromResume != null
? () => widget.onHideFromResume!(item)
: null,
hideFromResumeLoading:
widget.hideFromResumeLoadingMap?[item.Id] ?? false,
showUnplayedCountBadge: widget.showUnplayedCountBadge,
isActive: isActive,
fallbackImageUrl: widget.fallbackImageUrl,
preloadImageUrl: widget.preloadImageUrlBuilder?.call(item),
imageHeaders: widget.imageHeaders,
textColor: widget.titleColor,
);
},
),
),
),
],
);
}
}