195 lines
6.9 KiB
Dart
195 lines
6.9 KiB
Dart
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,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|