// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; const int _columns = 2; const double _columnGap = 10; const double _cardGap = 10; /// 剧场/分类页共用的双列剧目网格。 class DramaGrid extends StatelessWidget { const DramaGrid({ 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-all-dramas-section'), padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( key: const Key('home-all-dramas-grid'), 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( DramaGridCard(l10n: l10n, drama: drama, onTap: () => onOpen(drama)), ); if (index + _columns < dramas.length) { children.add(const SizedBox(height: _cardGap)); } } return Column(children: children); } } /// 双列网格中的剧目卡片。 class DramaGridCard extends StatelessWidget { const DramaGridCard({ 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 InkWell( key: Key('home-drama-card-${drama.id}'), onTap: onTap, borderRadius: BorderRadius.circular(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _cover(), const SizedBox(height: 5), _title(), const SizedBox(height: 2), _genre(), const SizedBox(height: AppDimens.spaceXs), _metadata(), ], ), ); } Widget _cover() { final free = drama.freeEpisodes > 0 || drama.episodes.any((episode) => episode.isFree); return ClipRRect( key: Key('home-drama-cover-${drama.id}'), borderRadius: BorderRadius.circular(10), child: Stack( children: [ _DiscoverCoverImage(path: drama.posterUrl), if (free) Positioned(top: 6, left: 6, child: _freeBadge()), ], ), ); } Widget _freeBadge() { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.blue, borderRadius: BorderRadius.circular(6), ), child: Text( l10n.free, style: const TextStyle( color: AppColors.onPrimary, fontSize: AppDimens.cardMeta, fontWeight: FontWeight.w800, ), ), ); } Widget _title() { return Text( drama.title, key: Key('home-drama-title-${drama.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: AppDimens.previewSectionTitle, fontWeight: FontWeight.w800, ), ); } Widget _genre() { final genre = drama.genre.isEmpty ? drama.category : drama.genre; 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 Wrap( spacing: AppDimens.spaceSm, runSpacing: AppDimens.spaceXs, children: [ Text( l10n.episodeCount(drama.episodeCount), key: Key('home-drama-episode-count-${drama.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.subtle, fontSize: AppDimens.cardViews, ), ), Text( l10n.viewCountLabel(drama.viewCount), key: Key('home-drama-views-${drama.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.subtle, fontSize: AppDimens.cardViews, ), ), ], ); } } class _DiscoverCoverImage extends StatelessWidget { const _DiscoverCoverImage({required this.path}); final String path; @override Widget build(BuildContext context) { return RemoteImage( path, width: double.infinity, fit: BoxFit.fitWidth, showFallbackIcon: true, fallbackAspectRatio: 3 / 4, ); } }