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

154 lines
4.8 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/material.dart';
import '../../core/contracts/script_widget.dart';
import '../theme/app_theme.dart';
import 'app_card.dart';
import 'metadata_chip.dart';
import 'smart_image.dart';
class ScriptVideoCard extends StatefulWidget {
final ScriptVideoItem item;
final double width;
final VoidCallback? onTap;
final Color? textColor;
const ScriptVideoCard({
super.key,
required this.item,
required this.width,
this.onTap,
this.textColor,
});
@override
State<ScriptVideoCard> createState() => _ScriptVideoCardState();
}
class _ScriptVideoCardState extends State<ScriptVideoCard> {
bool _isHovered = false;
String? get _imageUrl {
final imageCandidates = <String>[
widget.item.posterPath,
widget.item.coverUrl,
widget.item.detailPoster,
];
for (final imageCandidate in imageCandidates) {
if (imageCandidate.trim().isNotEmpty) return imageCandidate;
}
return null;
}
String? get _subtitle {
final subtitleParts = <String>[
if (widget.item.releaseDate.trim().isNotEmpty) widget.item.releaseDate,
if (widget.item.genreTitle.trim().isNotEmpty) widget.item.genreTitle,
];
return subtitleParts.isEmpty ? null : subtitleParts.join(' · ');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final tokens = context.appTokens;
final rating = widget.item.rating;
final title = widget.item.title.trim().isEmpty
? widget.item.id
: widget.item.title;
return SizedBox(
width: widget.width,
child: AppCard(
filled: false,
enableHover: false,
radius: tokens.radiusCard,
padding: EdgeInsets.zero,
onTap: widget.onTap,
onHoverChanged: (isHovered) {
if (_isHovered == isHovered) return;
setState(() => _isHovered = isHovered);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
AnimatedOpacity(
opacity: _isHovered ? 0.84 : 1,
duration: const Duration(milliseconds: 160),
child: SmartImage(
url: _imageUrl,
fallbackIcon: Icons.movie_outlined,
borderRadius: tokens.posterRadius,
),
),
Positioned.fill(
child: AnimatedOpacity(
opacity: _isHovered && widget.onTap != null ? 1 : 0,
duration: const Duration(milliseconds: 160),
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.30),
borderRadius: BorderRadius.circular(
tokens.posterRadius,
),
),
child: const Center(
child: Icon(
Icons.play_arrow_rounded,
size: 32,
color: Colors.white,
),
),
),
),
),
),
if (rating != null && rating > 0)
Positioned(
top: 7,
right: 7,
child: MetadataChip(
icon: Icons.star_rounded,
label: rating.toStringAsFixed(1),
foreground: tokens.ratingColor,
background: Colors.black.withValues(alpha: 0.68),
),
),
],
),
),
const SizedBox(height: 8),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: widget.textColor,
),
),
if (_subtitle case final subtitle?) ...[
const SizedBox(height: 2),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color:
widget.textColor?.withValues(alpha: 0.55) ??
tokens.mutedForeground,
),
),
],
],
),
),
);
}
}