// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/data/models/home_feed.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; import '../ranking_logic.dart'; const int _topLimit = 5; const double _rowHeight = 68; const double _posterWidth = 40; const double _posterHeight = 54; const double _badgeSize = 24; /// 发现页内嵌榜单区块:热播/新剧切换 + 前 5 名。 class RankingSection extends StatefulWidget { const RankingSection({ super.key, required this.sections, required this.fallback, required this.onOpen, }); final List sections; final List fallback; final ValueChanged onOpen; @override State createState() => _RankingSectionState(); } class _RankingSectionState extends State { late final RankingLogic logic = RankingLogic( sections: widget.sections, fallback: widget.fallback, ); @override void didUpdateWidget(covariant RankingSection oldWidget) { super.didUpdateWidget(oldWidget); if (!identical(oldWidget.sections, widget.sections) || !identical(oldWidget.fallback, widget.fallback)) { logic.updateSources(sections: widget.sections, fallback: widget.fallback); } } @override void dispose() { logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) => ListenableBuilder( listenable: logic, builder: (context, _) => _content(context), ); Widget _content(BuildContext context) { final l10n = AppLocalizations.of(context)!; final entries = logic.entries.take(_topLimit).toList(); return Container( key: const Key('discover-ranking-section'), margin: const EdgeInsets.fromLTRB(14, 16, 14, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.border), ), child: Column( children: [ _header(l10n), const SizedBox(height: 8), if (entries.isEmpty) EmptyState( key: const Key('ranking-empty'), icon: Icons.leaderboard_outlined, title: l10n.rankingEmptyTitle, subtitle: l10n.rankingEmptyBody, compact: true, ) else for (final entry in entries) _row(entry, l10n), ], ), ); } Widget _header(AppLocalizations l10n) { return Row( children: [ const Icon( Icons.local_fire_department_rounded, color: AppColors.blue, size: 18, ), const SizedBox(width: 6), Expanded( child: Text( l10n.rankingTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: 13, fontWeight: FontWeight.w800, ), ), ), _boardChip( key: const Key('ranking-tab-hot'), label: l10n.rankingHot, board: RankingBoard.hot, ), const SizedBox(width: 6), _boardChip( key: const Key('ranking-tab-new'), label: l10n.rankingNew, board: RankingBoard.newReleases, ), ], ); } Widget _boardChip({ required Key key, required String label, required RankingBoard board, }) { final selected = logic.board == board; return Material( key: key, color: selected ? AppColors.blue : AppColors.card, borderRadius: BorderRadius.circular(999), clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => logic.select(board), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), child: Text( label, style: TextStyle( color: selected ? AppColors.onPrimary : AppColors.onSurface, fontSize: 9, fontWeight: FontWeight.w700, ), ), ), ), ); } Widget _row(RankingEntry entry, AppLocalizations l10n) { return _RankRow( entry: entry, subtitle: _subtitle(entry.drama, l10n), onTap: () => widget.onOpen(entry.drama), ); } String _subtitle(Drama drama, AppLocalizations l10n) { final genre = drama.genre.isNotEmpty ? drama.genre : drama.category; final count = l10n.episodeCount(drama.episodeCount); return genre.isEmpty ? count : '$genre · $count'; } } class _RankRow extends StatelessWidget { const _RankRow({ required this.entry, required this.subtitle, required this.onTap, }); final RankingEntry entry; final String subtitle; final VoidCallback onTap; @override Widget build(BuildContext context) { final drama = entry.drama; return Material( key: Key('ranking-entry-${drama.id}'), color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: SizedBox(height: _rowHeight, child: _content(drama)), ), ); } Widget _content(Drama drama) { return Row( children: [ _rankBadge(entry.rank), const SizedBox(width: 8), ClipRRect( borderRadius: BorderRadius.circular(7), child: RemoteImage( drama.posterUrl, width: _posterWidth, height: _posterHeight, ), ), const SizedBox(width: 10), Expanded(child: _text(drama)), const Icon( Icons.chevron_right_rounded, color: AppColors.subtle, size: 18, ), ], ); } Widget _text(Drama drama) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( drama.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: 11, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 3), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: AppColors.subtle, fontSize: 9), ), ], ); } Widget _rankBadge(int rank) { final top = rank <= 3; return Container( width: _badgeSize, height: _badgeSize, alignment: Alignment.center, decoration: BoxDecoration( color: top ? AppColors.gold.withValues(alpha: 0.16) : AppColors.raised, borderRadius: BorderRadius.circular(7), ), child: Text( '$rank', style: TextStyle( color: top ? AppColors.gold : AppColors.muted, fontSize: 11, fontWeight: FontWeight.w900, ), ), ); } }