Files
2026-07-14 11:11:36 +08:00

244 lines
7.0 KiB
Dart

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<String, String>? 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<FItem>? 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<AggregatedMediaCard> createState() => _AggregatedMediaCardState();
}
class _AggregatedMediaCardState extends State<AggregatedMediaCard> {
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,
),
),
),
],
],
),
],
);
}
}