// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; const int _columns = 2; const double _columnGap = 10; const double _cardGap = 10; const double _coverAspectRatio = 5 / 6; const double _cardPadding = 8; const double _coverTitleGap = 6; const double _lineGap = 2; const double _viewsIconSize = 10; const double _viewsFontSize = 9; /// 首页浏览态使用的双列剧目网格。 class HomeDramaGrid extends StatelessWidget { const HomeDramaGrid({ super.key, required this.l10n, required this.dramas, required this.onOpen, }); final AppLocalizations l10n; final List dramas; final ValueChanged onOpen; @override Widget build(BuildContext context) { return Padding( key: const Key('home-popular-grid'), padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _column(0)), const SizedBox(width: _columnGap), Expanded(child: _column(1)), ], ), ); } Widget _column(int start) { final children = []; for (var index = start; index < dramas.length; index += _columns) { final drama = dramas[index]; children.add( HomeDramaCard(l10n: l10n, drama: drama, onTap: () => onOpen(drama)), ); if (index + _columns < dramas.length) { children.add(const SizedBox(height: _cardGap)); } } return Column(children: children); } } /// 首页剧卡:封面、角标、标题、题材、集数与热度。 class HomeDramaCard extends StatelessWidget { const HomeDramaCard({ super.key, required this.l10n, required this.drama, required this.onTap, }); final AppLocalizations l10n; final Drama drama; final VoidCallback onTap; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), boxShadow: const [AppTheme.cardShadow], ), child: Material( color: AppColors.transparent, borderRadius: BorderRadius.circular(AppDimens.cardRadius), clipBehavior: Clip.antiAlias, child: InkWell( key: Key('home-drama-card-${drama.id}'), onTap: onTap, child: Padding( padding: const EdgeInsets.all(_cardPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _cover(), const SizedBox(height: _coverTitleGap), _title(context), const SizedBox(height: _lineGap), _genre(context), const SizedBox(height: _lineGap), _metadata(), ], ), ), ), ), ); } Widget _cover() { return ClipRRect( key: Key('home-drama-cover-${drama.id}'), borderRadius: BorderRadius.circular(AppDimens.coverRadius), child: Stack( children: [ RemoteImage( drama.posterUrl, width: double.infinity, fit: BoxFit.fitWidth, fallbackAspectRatio: _coverAspectRatio, ), Positioned(top: 8, left: 8, child: _tagBadge()), Positioned(left: 8, bottom: 8, child: _viewsBadge()), Positioned(right: 8, bottom: 8, child: _episodeBadge()), ], ), ); } bool get _isFree { return drama.freeEpisodes > 0 || drama.episodes.any((episode) => episode.isFree); } Widget _tagBadge() { final label = _isFree ? l10n.free : l10n.originalTag; return Container( key: Key('home-drama-tag-${drama.id}'), padding: const EdgeInsets.symmetric( horizontal: AppDimens.spaceSm, vertical: 3, ), decoration: BoxDecoration( color: AppColors.badgePink, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( label, style: const TextStyle( color: AppColors.badgePinkText, fontSize: AppDimens.cardMeta, fontWeight: FontWeight.w800, ), ), ); } Widget _episodeBadge() { return Container( key: Key('home-drama-episodes-badge-${drama.id}'), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.badgeBeige, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( l10n.allEpisodesBadge(drama.episodeCount), style: const TextStyle( color: AppColors.onSurface, fontSize: 9, fontWeight: FontWeight.w700, ), ), ); } /// 播放次数角标:深色半透明胶囊 + 白色小播放图标。 Widget _viewsBadge() { return Container( key: Key('home-drama-views-${drama.id}'), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.playerOverlayPill, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.play_arrow_rounded, color: AppColors.onPrimary, size: _viewsIconSize, ), const SizedBox(width: 2), Text( formatInteractionCount(drama.viewCount), style: const TextStyle( color: AppColors.onPrimary, fontSize: _viewsFontSize, fontWeight: FontWeight.w700, ), ), ], ), ); } Widget _title(BuildContext context) { return Text( drama.localizedTitle(Localizations.localeOf(context).languageCode), key: Key('home-drama-title-${drama.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: AppDimens.cardTitle, fontWeight: FontWeight.w800, ), ); } Widget _genre(BuildContext context) { final labels = drama.localizedTypeLabels( Localizations.localeOf(context).languageCode, ); final genre = labels.isEmpty ? (drama.genre.isEmpty ? drama.category : drama.genre) : labels.join(' / '); return Text( genre.isEmpty ? l10n.videoPending : genre, key: Key('home-drama-genre-${drama.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.cardMeta, ), ); } Widget _metadata() { return Text( l10n.episodeCount(drama.episodeCount), key: Key('home-drama-episode-count-${drama.id}'), style: const TextStyle( color: AppColors.subtle, fontSize: AppDimens.cardViews, ), ); } }