// Author: fruit.com.cn import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import '../../home/widgets/home_drama_classification.dart'; const double _seriesFont = 19; const double _tagFont = 13; const double _synopsisFont = 13; const double _watchAllHeight = 44; const double _actionGap = 28; const double _actionIcon = 32; const double _actionWidth = 64; const List _textShadows = [ Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)), ]; final RegExp _tagSplitter = RegExp(r'[,/、·]'); class HomeFeedInfoPanel extends StatefulWidget { const HomeFeedInfoPanel({ super.key, required this.drama, required this.episode, required this.onDetail, required this.onOpenFullPlayer, }); final Drama drama; final Episode episode; final VoidCallback onDetail; final VoidCallback onOpenFullPlayer; @override State createState() => _HomeFeedInfoPanelState(); } class _HomeFeedInfoPanelState extends State { bool _synopsisExpanded = false; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final dramaTitle = localizedDramaTitle(l10n, widget.drama); final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim(); final genres = _genreValues(Localizations.localeOf(context).languageCode); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( key: const Key('home-feed-series-name'), behavior: HitTestBehavior.opaque, onTap: widget.onOpenFullPlayer, child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( dramaTitle, key: const Key('home-feed-series-title'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textPrimary, fontSize: _seriesFont, fontWeight: FontWeight.w600, shadows: _textShadows, ), ), ), const SizedBox(width: 4), const Icon( Icons.chevron_right_rounded, size: 20, color: AppColors.textPrimary, ), ], ), ), const SizedBox(height: 8), _tagMetaRow(l10n, genres), if (synopsis.isNotEmpty) ...[ const SizedBox(height: 8), _synopsis(l10n, synopsis), ], const SizedBox(height: 12), _watchAll(context, l10n), ], ); } Widget _tagMetaRow(AppLocalizations l10n, List genres) { return Row( key: const Key('home-feed-tags'), children: [ if (isHotDrama(widget.drama)) ...[ Container( key: const Key('home-feed-hot-badge'), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.error, borderRadius: BorderRadius.circular(3), ), child: Text( l10n.homeBadgeHot, style: const TextStyle( color: AppColors.textPrimary, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 8), ], Flexible( fit: FlexFit.loose, child: Text( genres.join(' '), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.overlayLabel, fontSize: _tagFont, ), ), ), const SizedBox(width: 8), Flexible( child: Text( l10n.favoriteEpisodeProgress( widget.episode.number, widget.drama.episodeCount, ), key: const Key('home-feed-episode-progress'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.overlayLabel, fontSize: _tagFont, ), ), ), ], ); } Widget _synopsis(AppLocalizations l10n, String synopsis) { const style = TextStyle( color: AppColors.overlaySecondary, fontSize: _synopsisFont, height: 1.35, shadows: _textShadows, ); return LayoutBuilder( builder: (context, constraints) { final painter = TextPainter( text: TextSpan(text: synopsis, style: style), maxLines: 2, textDirection: Directionality.of(context), textScaler: MediaQuery.textScalerOf(context), )..layout(maxWidth: constraints.maxWidth); final overflows = painter.didExceedMaxLines; if (!overflows) { return Text( synopsis, key: const Key('home-feed-synopsis'), style: style, ); } if (_synopsisExpanded) { return _expandedSynopsis(constraints.maxWidth, synopsis, style, l10n); } return Stack( children: [ Padding( padding: const EdgeInsets.only(right: 40), child: Text( synopsis, key: const Key('home-feed-synopsis'), maxLines: 2, overflow: TextOverflow.ellipsis, style: style, ), ), Positioned( right: 0, bottom: 0, child: _synopsisToggle( key: const Key('home-feed-synopsis-more'), label: l10n.expandSynopsis, ), ), ], ); }, ); } Widget _expandedSynopsis( double maxWidth, String synopsis, TextStyle style, AppLocalizations l10n, ) { return SizedBox( width: maxWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(synopsis, key: const Key('home-feed-synopsis'), style: style), Align( alignment: Alignment.centerRight, child: _synopsisToggle( key: const Key('home-feed-synopsis-more'), label: l10n.showLess, ), ), ], ), ); } Widget _synopsisToggle({required Key key, required String label}) { return GestureDetector( key: key, onTap: () => setState(() => _synopsisExpanded = !_synopsisExpanded), child: Text( label, style: const TextStyle( color: AppColors.textPrimary, fontSize: _synopsisFont, fontWeight: FontWeight.w600, ), ), ); } Widget _watchAll(BuildContext context, AppLocalizations l10n) { final screenWidth = MediaQuery.sizeOf(context).width; return LayoutBuilder( builder: (context, constraints) { final buttonWidth = math.min(screenWidth * 0.7, constraints.maxWidth); return Align( alignment: Alignment.centerLeft, child: SizedBox( width: buttonWidth, height: _watchAllHeight, child: DecoratedBox( key: const Key('home-feed-full-series-bg'), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.actionRed, AppColors.blue], ), borderRadius: BorderRadius.circular(8), ), child: Material( color: AppColors.transparent, child: InkWell( key: const Key('home-feed-full-series'), onTap: widget.onOpenFullPlayer, borderRadius: BorderRadius.circular(8), child: Center( child: Text( l10n.homeWatchAll, style: const TextStyle( color: AppColors.textPrimary, fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ), ), ), ); }, ); } List _genreValues(String languageCode) { final localized = widget.drama.localizedTypeLabels(languageCode); if (localized.isNotEmpty) return localized.take(3).toList(growable: false); final result = []; for (final value in [widget.drama.genre, widget.drama.category]) { for (final part in value.split(_tagSplitter)) { final tag = part.trim(); if (tag.isEmpty || result.contains(tag)) continue; result.add(tag); } } return result.take(3).toList(growable: false); } } class HomeFeedActionRail extends StatelessWidget { const HomeFeedActionRail({ super.key, required this.saved, required this.liked, required this.onSave, required this.onLike, required this.onEpisodes, this.favoriteCount, this.likeCount, }); final bool saved; final bool liked; final int? favoriteCount; final int? likeCount; final VoidCallback onSave; final VoidCallback onLike; final VoidCallback onEpisodes; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return SizedBox( width: _actionWidth, child: Column( mainAxisSize: MainAxisSize.min, children: [ _action( key: const Key('rail-save'), icon: saved ? Icons.bookmark_rounded : Icons.bookmark_border_rounded, label: favoriteCount == null ? l10n.favorite : formatInteractionCount(favoriteCount!), labelKey: const Key('rail-save-count'), onTap: onSave, ), const SizedBox(height: _actionGap), _action( key: const Key('rail-like'), icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded, iconColor: liked ? AppColors.doubleTapHeartRed : AppColors.textPrimary, label: likeCount == null ? l10n.like : formatInteractionCount(likeCount!), labelKey: const Key('rail-like-count'), onTap: onLike, ), const SizedBox(height: _actionGap), _action( key: const Key('rail-episodes'), icon: Icons.layers_rounded, label: l10n.episodes, labelKey: const Key('rail-episodes-label'), labelFontSize: 11, onTap: onEpisodes, ), ], ), ); } Widget _action({ required Key key, required IconData icon, required String label, required Key labelKey, required VoidCallback onTap, Color iconColor = AppColors.textPrimary, double labelFontSize = 12, }) { return GestureDetector( key: key, behavior: HitTestBehavior.opaque, onTap: onTap, child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: _actionIcon, color: iconColor, shadows: const [ Shadow( color: AppColors.shadowSoft, blurRadius: 4, offset: Offset(0, 1), ), ], ), const SizedBox(height: 2), Text( label, key: labelKey, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( color: AppColors.textPrimary, fontSize: labelFontSize, fontWeight: FontWeight.w500, height: 1, shadows: _textShadows, ), ), ], ), ); } }