import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../../../shared/constants/breakpoints.dart'; import '../../../shared/theme/app_theme.dart'; import '../../../shared/widgets/app_dialog.dart'; import '../../../shared/widgets/smart_image.dart'; class DetailHeroPanel extends StatelessWidget { final String title; final String? logoUrl; final String? posterUrl; final Map? imageHeaders; final double? rating; final List metaParts; final List genres; final String? overview; final String? tagline; final Widget? actions; const DetailHeroPanel({ super.key, required this.title, this.logoUrl, this.posterUrl, this.imageHeaders, this.rating, this.metaParts = const [], this.genres = const [], this.overview, this.tagline, this.actions, }); bool get _hasOverview => (overview?.trim().isNotEmpty ?? false) || (tagline?.trim().isNotEmpty ?? false); @override Widget build(BuildContext context) { final compact = MediaQuery.sizeOf(context).width < Breakpoints.detail; final titleBlock = _HeroTitle( title: title, logoUrl: logoUrl, posterUrl: posterUrl, imageHeaders: imageHeaders, compact: compact, ); final pills = _MetaPills( rating: rating, metaParts: metaParts, genres: genres, ); if (!compact && _hasOverview) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 380), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ titleBlock, if (actions != null) ...[const SizedBox(height: 24), actions!], ], ), ), const SizedBox(width: 48), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [ pills, const SizedBox(height: 16), _Overview(overview: overview, tagline: tagline), ], ), ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ titleBlock, const SizedBox(height: 12), pills, if (actions != null) ...[const SizedBox(height: 20), actions!], if (_hasOverview) ...[ const SizedBox(height: 20), _Overview(overview: overview, tagline: tagline), ], ], ); } } class _HeroTitle extends StatefulWidget { final String title; final String? logoUrl; final String? posterUrl; final Map? imageHeaders; final bool compact; const _HeroTitle({ required this.title, required this.logoUrl, required this.posterUrl, required this.imageHeaders, required this.compact, }); @override State<_HeroTitle> createState() => _HeroTitleState(); } class _HeroTitleState extends State<_HeroTitle> { bool _logoFailed = false; @override void didUpdateWidget(covariant _HeroTitle oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.logoUrl != widget.logoUrl) _logoFailed = false; } @override Widget build(BuildContext context) { final logo = widget.logoUrl; if (logo != null && logo.isNotEmpty && !_logoFailed) { return ConstrainedBox( constraints: BoxConstraints( maxWidth: widget.compact ? 240 : 400, maxHeight: widget.compact ? 80 : 120, ), child: CachedNetworkImage( imageUrl: logo, httpHeaders: widget.imageHeaders, fit: BoxFit.contain, alignment: Alignment.bottomLeft, memCacheWidth: widget.compact ? 720 : 1200, fadeInDuration: const Duration(milliseconds: 300), placeholder: (_, _) => _TitleText(title: widget.title), errorWidget: (_, _, _) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_logoFailed) { setState(() => _logoFailed = true); } }); return _TitleText(title: widget.title); }, ), ); } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.posterUrl != null && widget.posterUrl!.isNotEmpty) ...[ SizedBox( width: widget.compact ? 104 : 130, child: AspectRatio( aspectRatio: 2 / 3, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.5), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: SmartImage( url: widget.posterUrl, httpHeaders: widget.imageHeaders, fallbackIcon: Icons.movie_outlined, borderRadius: 10, ), ), ), ), const SizedBox(height: 14), ], _TitleText(title: widget.title), ], ); } } class _TitleText extends StatelessWidget { final String title; const _TitleText({required this.title}); @override Widget build(BuildContext context) { return Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.w700, height: 1.15, ), ); } } class _MetaPills extends StatelessWidget { final double? rating; final List metaParts; final List genres; const _MetaPills({ required this.rating, required this.metaParts, required this.genres, }); @override Widget build(BuildContext context) { final tokens = context.appTokens; final r = rating; return Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (r != null && r > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: tokens.ratingColor.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.star_rounded, size: 14, color: Colors.black87), const SizedBox(width: 3), Text( r.toStringAsFixed(1), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Colors.black87, ), ), ], ), ), for (final part in [ ...metaParts, for (final g in genres.take(4)) if (g.isNotEmpty) g, ]) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.white.withValues(alpha: 0.15)), ), child: Text( part, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white.withValues(alpha: 0.85), ), ), ), ], ); } } class _Overview extends StatelessWidget { final String? overview; final String? tagline; const _Overview({required this.overview, required this.tagline}); void _showDialog(BuildContext context, String text) { showAppRawDialog( context, constraints: const BoxConstraints(maxWidth: 560, maxHeight: 480), builder: (ctx) => ColoredBox( color: const Color(0xFF1a1c22), child: Padding( padding: const EdgeInsets.fromLTRB(28, 28, 28, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '简介', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white.withValues(alpha: 0.9), ), ), const SizedBox(height: 16), Flexible( child: SingleChildScrollView( child: Text( text, style: TextStyle( fontSize: 14, height: 1.8, color: Colors.white.withValues(alpha: 0.82), ), ), ), ), const SizedBox(height: 16), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text( '关闭', style: TextStyle( color: Colors.white.withValues(alpha: 0.6), ), ), ), ), ], ), ), ), ); } @override Widget build(BuildContext context) { final body = overview?.trim(); final line = tagline?.trim(); if ((body == null || body.isEmpty) && (line == null || line.isEmpty)) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (line != null && line.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( line, style: TextStyle( fontSize: 15, fontStyle: FontStyle.italic, color: Colors.white.withValues(alpha: 0.6), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), if (body != null && body.isNotEmpty) GestureDetector( onTap: () => _showDialog(context, body), child: Text( body, style: TextStyle( fontSize: 14, height: 1.7, color: Colors.white.withValues(alpha: 0.82), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ); } }