// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/config/app_config.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 '../../../l10n/app_localizations.dart'; const int _columns = 2; const double _columnGap = 8; const double _cardGap = 14; const double _gridHorizontalPadding = 12; const double _coverAspectRatio = 3 / 4; const double _coverRadius = 10; const double _titleGap = 8; const double _tagsGap = 6; const double _titleFontSize = 15; const double _tagChipRadius = 6; const double _tagChipFontSize = 11; const double _heatFontSize = 9; const double _heatIconSize = 11; const int _maxTags = 3; const Color _heatBackground = Color(0x99000000); const Color _tagChipBackground = Color(0xFFF4F1EE); final RegExp _tagSplitter = RegExp(r'[,/、·]'); /// 剧场/分类页共用的双列剧目网格。 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: _gridHorizontalPadding), 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 GestureDetector( key: Key('home-drama-card-${drama.id}'), behavior: HitTestBehavior.opaque, onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _cover(), const SizedBox(height: _titleGap), _title(), const SizedBox(height: _tagsGap), _tags(), ], ), ); } Widget _cover() { return ClipRRect( key: Key('home-drama-cover-${drama.id}'), borderRadius: BorderRadius.circular(_coverRadius), child: AspectRatio( aspectRatio: _coverAspectRatio, child: Stack( fit: StackFit.expand, children: [ _DiscoverCoverImage(path: drama.posterUrl), Positioned(left: 8, bottom: 8, child: _heatBadge()), ], ), ), ); } Widget _heatBadge() { return Container( key: Key('drama-heat-${drama.id}'), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: _heatBackground, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.local_fire_department_rounded, color: AppColors.onPrimary, size: _heatIconSize, ), const SizedBox(width: 2), Text( l10n.heatLabel(formatInteractionCount(drama.viewCount)), style: const TextStyle( color: AppColors.onPrimary, fontSize: _heatFontSize, fontWeight: FontWeight.w600, ), ), ], ), ); } Widget _title() { return Text( drama.title, key: Key('home-drama-title-${drama.id}'), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: _titleFontSize, fontWeight: FontWeight.w700, height: 1.25, ), ); } Widget _tags() { final tags = _tagValues(); if (tags.isEmpty) return const SizedBox.shrink(); return Wrap( spacing: 6, runSpacing: 4, children: [ for (var index = 0; index < tags.length; index++) Container( key: Key('drama-tag-chip-${drama.id}-$index'), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _tagChipBackground, borderRadius: BorderRadius.circular(_tagChipRadius), ), child: Text( tags[index], style: const TextStyle( color: AppColors.muted, fontSize: _tagChipFontSize, ), ), ), ], ); } List _tagValues() { final result = []; for (final value in [drama.genre, drama.category]) { for (final part in value.split(_tagSplitter)) { final tag = part.trim(); if (tag.isEmpty || result.contains(tag)) continue; result.add(tag); if (result.length == _maxTags) return result; } } return result; } } class _DiscoverCoverImage extends StatelessWidget { const _DiscoverCoverImage({required this.path}); final String path; @override Widget build(BuildContext context) { final url = AppConfig.mediaUrl(path); if (url.isEmpty) return _placeholder(); return Image.network( url, width: double.infinity, height: double.infinity, fit: BoxFit.cover, loadingBuilder: (context, child, progress) { if (progress == null) return child; return _placeholder(); }, errorBuilder: (_, _, _) => _placeholder(), ); } Widget _placeholder() { return const ColoredBox( color: AppColors.card, child: Center( child: Icon( Icons.image_not_supported_outlined, color: AppColors.muted, size: 32, ), ), ); } }