import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import '../../../core/contracts/library.dart'; import '../../../shared/theme/app_theme.dart'; import '../../../shared/utils/emby_ticks.dart'; import '../../../shared/widgets/app_card.dart'; import '../../../shared/widgets/card_progress_bar.dart'; import '../../../shared/widgets/metadata_chip.dart'; import '../../../shared/widgets/smart_image.dart'; class AggregatedMediaCard extends StatefulWidget { final EmbyRawItem item; final String? imageUrl; final String? fallbackImageUrl; final Map? imageHeaders; final String title; final String? subtitle; final String serverName; final int serverSourceCount; final String? timeLabel; final String? networkName; final String? networkLogoUrl; final bool showProgress; final VoidCallback? onTap; final VoidCallback? onServerSourcesTap; final List? contextMenuItems; const AggregatedMediaCard({ super.key, required this.item, required this.imageUrl, required this.title, required this.serverName, this.serverSourceCount = 1, this.fallbackImageUrl, this.imageHeaders, this.subtitle, this.timeLabel, this.networkName, this.networkLogoUrl, this.showProgress = false, this.onTap, this.onServerSourcesTap, this.contextMenuItems, }) : assert(serverSourceCount > 0); static const double height = 84; static const double _thumbWidth = 124; @override State createState() => _AggregatedMediaCardState(); } class _AggregatedMediaCardState extends State { bool _hovered = false; @override Widget build(BuildContext context) { final theme = Theme.of(context); final tokens = context.appTokens; final radius = tokens.radiusCard; final progress = widget.showProgress ? playbackProgress(widget.item) : 0.0; final remaining = widget.showProgress ? formatRemainingLabel(widget.item) : null; return AppCard( onTap: widget.onTap, contextMenuItems: widget.contextMenuItems, radius: radius, padding: EdgeInsets.zero, onHoverChanged: (h) => setState(() => _hovered = h), child: SizedBox( height: AggregatedMediaCard.height, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _thumbnail(radius, progress), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(12, 8, 10, 8), child: _info(theme, tokens, remaining), ), ), ], ), ), ); } Widget _thumbnail(double radius, double progress) { return SizedBox( width: AggregatedMediaCard._thumbWidth, child: Stack( fit: StackFit.expand, children: [ SmartImage( url: widget.imageUrl, fallbackUrls: widget.fallbackImageUrl != null ? [widget.fallbackImageUrl!] : const [], httpHeaders: widget.imageHeaders, fit: BoxFit.cover, borderRadius: 0, ), if (_hovered) Container( color: Colors.black.withValues(alpha: 0.32), child: const Center( child: Icon( Icons.play_arrow_rounded, size: 30, color: Colors.white, ), ), ), if (progress > 0) CardProgressBar(progress: progress), ], ), ); } Widget _networkBadge(AppTokens tokens) { if (widget.networkLogoUrl != null) { return SizedBox( height: 16, child: SmartImage( url: widget.networkLogoUrl, fit: BoxFit.contain, borderRadius: 0, ), ); } return MetadataChip( label: widget.networkName!, dense: true, foreground: tokens.mutedForeground, ); } Widget _serverBadge() { final hasMultipleSources = widget.serverSourceCount > 1; if (!hasMultipleSources || widget.onServerSourcesTap == null) { return MetadataChip(label: widget.serverName, dense: true); } final label = '${widget.serverName} ${widget.serverSourceCount}服'; return Tooltip( message: '选择服务器并查看各自的播放进度', child: Semantics( button: true, label: '$label,选择其他服务器', child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.onServerSourcesTap, child: MetadataChip( label: label, icon: Icons.keyboard_arrow_down_rounded, dense: true, ), ), ), ), ); } Widget _info(ThemeData theme, AppTokens tokens, String? remaining) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), if (widget.subtitle != null) ...[ const SizedBox(height: 2), Text( widget.subtitle!, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: tokens.mutedForeground, ), ), ], ], ), Row( children: [ _serverBadge(), if (widget.networkName != null && widget.networkName!.isNotEmpty) ...[ const SizedBox(width: 6), _networkBadge(tokens), ], if (remaining != null) ...[ const SizedBox(width: 8), Flexible( child: Text( remaining, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( color: tokens.mutedForeground, fontWeight: FontWeight.w600, ), ), ), ] else if (widget.timeLabel != null) ...[ const SizedBox(width: 8), Flexible( child: Text( widget.timeLabel!, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( color: tokens.mutedForeground, ), ), ), ], ], ), ], ); } }