diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 0f2db2e..d7c8081 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1288,3 +1288,9 @@ $ flutter test - 下拉刷新统一为 40×40、60% 黑底圆角指示器内嵌 20 白色加载圈,不再显示任何状态文字。 - 网络分页与本地分页 footer 统一仅显示白色加载圈,无更多状态完全隐藏;通知页补触底自动加载。 - 移除不再使用的刷新提示与无更多文案;`flutter analyze`:No issues found。 + +## 2026-10-22 首页热播榜重做 + +- 首页榜单 tab 改为独立领奖台 + 单列排行布局,不再复用三列网格。 +- 前三名按 2/1/3 排列,冠军放大并上移;第 4 名起使用大号描边名次与横向信息行。 +- 榜单沿用现有播放热度排序、分页和播放逻辑;`flutter analyze`:No issues found。 diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 7ce34c0..cbc1920 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -50,6 +50,7 @@ abstract final class AppColors { static const vipBadgeSurface = Color(0x1F3D2408); static const bronze = Color(0xFFCD7F32); static const bronzeDeep = Color(0xFFA9713A); + static const rankSilver = Color(0xFFC0C0C0); /// 成功状态。 static const success = Color(0xFF2FA873); diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index cf5d312..67ddd3e 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -141,6 +141,21 @@ abstract final class AppDimens { static const double recommendRowHeight = 252; static const double recommendCardWidth = 122; + /// 首页热播榜。 + static const double rankingHeroTitle = 22; + static const double rankingHeroSubtitle = 11; + static const double rankingPodiumChampionWidth = 110; + static const double rankingPodiumChampionHeight = 146; + static const double rankingPodiumWidth = 92; + static const double rankingPodiumHeight = 122; + static const double rankingPodiumRadius = 10; + static const double rankingRowHeight = 96; + static const double rankingNumberWidth = 56; + static const double rankingNumberFont = 48; + static const double rankingCoverWidth = 60; + static const double rankingCoverHeight = 80; + static const double rankingCoverRadius = 6; + /// 全部短剧筛选与追剧双列。 static const double filterChipFont = 16; static const double filterChipHeight = 32; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 96606d1..58f9e34 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -64,7 +64,7 @@ "count": {"type": "String"} } }, - "topCharts": "Top Charts", + "topCharts": "Hot Ranking", "newReleases": "New Releases", "newDramas": "New", "trendingMicroDramas": "Trending Micro-Dramas", @@ -633,7 +633,7 @@ "ledgerAdjustment": "Adjustment", "rankingTab": "Charts", "rankingTitle": "Top Charts", - "rankingSubtitle": "Ranked from today's live catalog feed.", + "rankingSubtitle": "Ranked live by play heat", "rankingHot": "Trending", "rankingNew": "New releases", "newReleasesAll": "All new releases", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 7154718..a05c235 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -48,7 +48,7 @@ "authorNoteLabel": "Catatan penulis", "reserve": "Reservasi", "heatLabel": "{count} panas", - "topCharts": "Peringkat Teratas", + "topCharts": "Peringkat Populer", "newReleases": "Rilisan Baru", "newDramas": "Baru", "trendingMicroDramas": "Drama Mini Populer", @@ -595,7 +595,7 @@ "ledgerAdjustment": "Penyesuaian", "rankingTab": "Peringkat", "rankingTitle": "Peringkat Teratas", - "rankingSubtitle": "Diurutkan dari feed katalog terkini.", + "rankingSubtitle": "Diurutkan langsung berdasarkan jumlah tayangan", "rankingHot": "Populer", "rankingNew": "Rilis baru", "newReleasesAll": "Semua rilis baru", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c1477e5..e6d0fa0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -391,7 +391,7 @@ abstract class AppLocalizations { /// No description provided for @topCharts. /// /// In en, this message translates to: - /// **'Top Charts'** + /// **'Hot Ranking'** String get topCharts; /// No description provided for @newReleases. @@ -2677,7 +2677,7 @@ abstract class AppLocalizations { /// No description provided for @rankingSubtitle. /// /// In en, this message translates to: - /// **'Ranked from today\'s live catalog feed.'** + /// **'Ranked live by play heat'** String get rankingSubtitle; /// No description provided for @rankingHot. diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 669128c..a8d5b9e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -162,7 +162,7 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get topCharts => 'Top Charts'; + String get topCharts => 'Hot Ranking'; @override String get newReleases => 'New Releases'; @@ -1389,7 +1389,7 @@ class AppLocalizationsEn extends AppLocalizations { String get rankingTitle => 'Top Charts'; @override - String get rankingSubtitle => 'Ranked from today\'s live catalog feed.'; + String get rankingSubtitle => 'Ranked live by play heat'; @override String get rankingHot => 'Trending'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 53f9a02..8d05415 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -162,7 +162,7 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get topCharts => 'Peringkat Teratas'; + String get topCharts => 'Peringkat Populer'; @override String get newReleases => 'Rilisan Baru'; @@ -1389,7 +1389,8 @@ class AppLocalizationsId extends AppLocalizations { String get rankingTitle => 'Peringkat Teratas'; @override - String get rankingSubtitle => 'Diurutkan dari feed katalog terkini.'; + String get rankingSubtitle => + 'Diurutkan langsung berdasarkan jumlah tayangan'; @override String get rankingHot => 'Populer'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8d700cc..c4e1ab3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -159,7 +159,7 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get topCharts => '排行榜'; + String get topCharts => '热播榜'; @override String get newReleases => '新剧'; @@ -1367,7 +1367,7 @@ class AppLocalizationsZh extends AppLocalizations { String get rankingTitle => '热门榜单'; @override - String get rankingSubtitle => '依据目录实时分区排序。'; + String get rankingSubtitle => '按播放热度实时排序'; @override String get rankingHot => '热播'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index bb792b8..4954462 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -48,7 +48,7 @@ "authorNoteLabel": "作者声明", "reserve": "预约", "heatLabel": "{count}热度", - "topCharts": "排行榜", + "topCharts": "热播榜", "newReleases": "新剧", "newDramas": "新剧", "trendingMicroDramas": "热门短剧", @@ -557,7 +557,7 @@ "ledgerAdjustment": "余额调整", "rankingTab": "榜单", "rankingTitle": "热门榜单", - "rankingSubtitle": "依据目录实时分区排序。", + "rankingSubtitle": "按播放热度实时排序", "rankingHot": "热播", "rankingNew": "新剧", "newReleasesAll": "全部新剧", diff --git a/lib/pages/home/widgets/home_feed_grid.dart b/lib/pages/home/widgets/home_feed_grid.dart index 23cd742..5c7cb54 100644 --- a/lib/pages/home/widgets/home_feed_grid.dart +++ b/lib/pages/home/widgets/home_feed_grid.dart @@ -17,6 +17,7 @@ import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/paged_list_footer.dart'; import '../../../l10n/app_localizations.dart'; +import '../../ranking/ranking_page.dart'; import 'home_drama_classification.dart'; import 'home_feed_card.dart'; import 'home_grid_header.dart'; @@ -155,6 +156,9 @@ class _HomeFeedGridState extends State { required HomePrimaryTab tab, required List shortcuts, }) { + if (tab == HomePrimaryTab.ranking) { + return const RankingPage(embedded: true); + } if (widget.feedController.loadingFirstPage) { return const Center( child: SizedBox.square( diff --git a/lib/pages/ranking/ranking_page.dart b/lib/pages/ranking/ranking_page.dart index ae5b478..3083418 100644 --- a/lib/pages/ranking/ranking_page.dart +++ b/lib/pages/ranking/ranking_page.dart @@ -10,6 +10,7 @@ import '../../core/data/services/catalog_controller.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/app_pull_to_refresh.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/paged_local_list.dart'; @@ -19,20 +20,9 @@ import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; const int _topCount = 50; -const double _podiumGap = 8; -const double _coverRadius = 12; -const double _titleSlotHeight = 24; -const double _rowGap = 12; -const double _rankColumnWidth = 56; -const double _listCoverWidth = 72; -const double _listCoverHeight = 96; -const double _loadMoreThreshold = 200; -const Color _goldStart = AppColors.gold; -const Color _goldEnd = AppColors.goldDeep; -const Color _bronzeStart = AppColors.bronze; -const Color _bronzeEnd = AppColors.bronzeDeep; +const double _loadMoreThreshold = 200, _championLift = 12; -/// 剧场榜单页:前三名横排领奖台,第 4 名起纵向列表。 +/// 剧场榜单页:前三名领奖台,第 4 名起为单列排行行。 class RankingPage extends StatefulWidget { const RankingPage({super.key, this.embedded = false}); @@ -93,12 +83,10 @@ class _RankingPageState extends State { bool _onScroll(ScrollNotification notification) { if (notification.depth != 0 || _refreshing) return false; - final reachedBottom = - notification.metrics.extentAfter <= _loadMoreThreshold; - if (reachedBottom && - (notification is ScrollUpdateNotification || - notification is ScrollEndNotification || - notification is OverscrollNotification)) { + if (notification.metrics.extentAfter > _loadMoreThreshold) return false; + if (notification is ScrollUpdateNotification || + notification is ScrollEndNotification || + notification is OverscrollNotification) { unawaited(_pager!.loadMore()); } return false; @@ -107,10 +95,10 @@ class _RankingPageState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final pager = _pager!; - final top = pager.visibleItems; - final podium = top.take(3).toList(); - final rest = top.skip(3).toList(); + final catalog = context.watch(); + final top = _pager!.visibleItems; + final podium = top.take(3).toList(growable: false); + final rest = top.skip(3).toList(growable: false); final content = NotificationListener( onNotification: _onScroll, child: AppPullToRefresh( @@ -120,39 +108,47 @@ class _RankingPageState extends State { key: const Key('ranking-list'), controller: _scroll, physics: AppPullToRefresh.physics, - padding: const EdgeInsets.only(top: 4, bottom: 24), + padding: const EdgeInsets.only(top: 8, bottom: 24), children: top.isEmpty - ? [ - SizedBox( - height: 420, - child: EmptyState( - icon: Icons.leaderboard_outlined, - title: l10n.noStories, - ), - ), - ] + ? [_emptyState(l10n, catalog)] : [ + if (widget.embedded) _intro(l10n), + const SizedBox(height: AppDimens.spaceMd), if (podium.isNotEmpty) Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), + padding: const EdgeInsets.symmetric( + horizontal: AppDimens.feedPageHorizontal, + ), child: _Podium(podium: podium, l10n: l10n), ), if (rest.isNotEmpty) ...[ - const SizedBox(height: 16), + const SizedBox(height: AppDimens.spaceXl), for (var index = 0; index < rest.length; index++) ...[ - if (index > 0) const SizedBox(height: _rowGap), Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), + padding: const EdgeInsets.symmetric( + horizontal: AppDimens.feedPageHorizontal, + ), child: _RankingRow( index: index + 3, drama: rest[index], l10n: l10n, ), ), + if (index != rest.length - 1) + const Padding( + padding: EdgeInsets.symmetric( + horizontal: AppDimens.feedPageHorizontal, + ), + child: Divider( + height: AppDimens.mineGridDividerWidth, + thickness: AppDimens.mineGridDividerWidth, + color: AppColors.border, + ), + ), ], ], PagedLoadFooter( - loading: pager.loading, + loading: _pager!.loading, loadMoreKey: const Key('ranking-load-more'), ), ], @@ -160,9 +156,9 @@ class _RankingPageState extends State { ), ); if (widget.embedded) { - return ColoredBox( + return DecoratedBox( key: const Key('ranking-page'), - color: AppColors.appBackground, + decoration: const BoxDecoration(gradient: AppTheme.pageGradient), child: SafeArea(bottom: false, child: content), ); } @@ -174,6 +170,59 @@ class _RankingPageState extends State { child: content, ); } + + Widget _intro(AppLocalizations l10n) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppDimens.feedPageHorizontal, + ), + child: Column( + key: const Key('ranking-hero'), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.topCharts, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: AppDimens.rankingHeroTitle, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + l10n.rankingSubtitle, + style: const TextStyle( + color: AppColors.textTertiary, + fontSize: AppDimens.rankingHeroSubtitle, + ), + ), + ], + ), + ); + } + + Widget _emptyState(AppLocalizations l10n, CatalogController catalog) { + final theme = Theme.of(context); + return SizedBox( + height: 420, + child: Theme( + data: theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + onSurface: AppColors.textTertiary, + ), + ), + child: EmptyState( + key: Key(catalog.error == null ? 'ranking-empty' : 'ranking-error'), + icon: catalog.error == null + ? Icons.leaderboard_outlined + : Icons.cloud_off_rounded, + title: catalog.error == null ? l10n.noStories : l10n.catalogError, + actionLabel: catalog.error == null ? null : l10n.retry, + onAction: catalog.error == null ? null : () => unawaited(_refresh()), + ), + ), + ); + } } class _Podium extends StatelessWidget { @@ -187,19 +236,32 @@ class _Podium extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Expanded(flex: 100, child: _slot(2)), - const SizedBox(width: _podiumGap), - Expanded(flex: 115, child: _slot(1)), - const SizedBox(width: _podiumGap), - Expanded(flex: 100, child: _slot(3)), + SizedBox( + width: AppDimens.rankingPodiumWidth, + child: podium.length > 1 + ? _PodiumCard(rank: 2, drama: podium[1], l10n: l10n) + : const SizedBox.shrink(), + ), + const Spacer(), + SizedBox( + width: AppDimens.rankingPodiumChampionWidth, + child: podium.isNotEmpty + ? Transform.translate( + offset: const Offset(0, -_championLift), + child: _PodiumCard(rank: 1, drama: podium.first, l10n: l10n), + ) + : const SizedBox.shrink(), + ), + const Spacer(), + SizedBox( + width: AppDimens.rankingPodiumWidth, + child: podium.length > 2 + ? _PodiumCard(rank: 3, drama: podium[2], l10n: l10n) + : const SizedBox.shrink(), + ), ], ); } - - Widget _slot(int rank) { - if (podium.length < rank) return const SizedBox.shrink(); - return _PodiumCard(rank: rank, drama: podium[rank - 1], l10n: l10n); - } } class _PodiumCard extends StatelessWidget { @@ -213,26 +275,23 @@ class _PodiumCard extends StatelessWidget { final Drama drama; final AppLocalizations l10n; - bool get _center => rank == 1; + double get _coverWidth => rank == 1 + ? AppDimens.rankingPodiumChampionWidth + : AppDimens.rankingPodiumWidth; - LinearGradient get _badgeGradient => switch (rank) { - 1 => const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppColors.blue, AppColors.bluePressed], - ), - 2 => const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [_goldStart, _goldEnd], - ), - _ => const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [_bronzeStart, _bronzeEnd], - ), + double get _coverHeight => rank == 1 + ? AppDimens.rankingPodiumChampionHeight + : AppDimens.rankingPodiumHeight; + + Color get _badgeColor => switch (rank) { + 1 => AppColors.homeCrownGold, + 2 => AppColors.rankSilver, + _ => AppColors.bronze, }; + Color get _badgeTextColor => + rank == 3 ? AppColors.textPrimary : AppColors.onGold; + @override Widget build(BuildContext context) { return GestureDetector( @@ -241,10 +300,11 @@ class _PodiumCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ClipRRect( - borderRadius: BorderRadius.circular(_coverRadius), - child: AspectRatio( + borderRadius: BorderRadius.circular(AppDimens.rankingPodiumRadius), + child: SizedBox( key: Key('ranking-cover-$rank'), - aspectRatio: 3 / 4, + width: _coverWidth, + height: _coverHeight, child: Stack( fit: StackFit.expand, children: [ @@ -255,23 +315,23 @@ class _PodiumCard extends StatelessWidget { fallbackAspectRatio: 3 / 4, ), Positioned( - top: 8, - left: 8, + top: 4, + left: 4, child: Container( key: Key('ranking-top-$rank'), padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, + horizontal: 6, + vertical: 2, ), decoration: BoxDecoration( - gradient: _badgeGradient, - borderRadius: BorderRadius.circular(999), + color: _badgeColor, + borderRadius: BorderRadius.circular(6), ), child: Text( l10n.topLabel(rank), maxLines: 1, - style: const TextStyle( - color: AppColors.onPrimary, + style: TextStyle( + color: _badgeTextColor, fontSize: 11, fontWeight: FontWeight.w800, ), @@ -284,22 +344,25 @@ class _PodiumCard extends StatelessWidget { ), const SizedBox(height: 6), SizedBox( - height: _titleSlotHeight, - child: Center( - child: Text( - drama.title, - key: Key('ranking-title-$rank'), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle( - color: AppColors.onSurface, - fontSize: _center ? 15 : 13, - fontWeight: _center ? FontWeight.w800 : FontWeight.w700, - ), + height: 34, + child: Text( + drama.title, + key: Key('ranking-title-$rank'), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: 13, + fontWeight: FontWeight.w600, + height: 1.25, ), ), ), + if (drama.viewCount > 0) ...[ + const SizedBox(height: 2), + _heat(drama.viewCount, l10n), + ], ], ), ); @@ -319,164 +382,119 @@ class _RankingRow extends StatelessWidget { int get _rank => index + 1; - List _tagValues() { - final result = []; + String get _category { + final values = {}; for (final value in [drama.genre, drama.category]) { for (final part in value.split(RegExp(r'[,/、·]'))) { final tag = part.trim(); - if (tag.isEmpty || result.contains(tag)) continue; - result.add(tag); - if (result.length == 3) return result; + if (tag.isNotEmpty) values.add(tag); } } - return result; + return values.join(' · '); } @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - border: Border.all(color: AppColors.border), - ), + return SizedBox( + height: AppDimens.rankingRowHeight, child: Material( color: AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - clipBehavior: Clip.antiAlias, child: InkWell( key: Key('ranking-row-$index'), onTap: () => unawaited(playDramaFromCatalog(context, drama)), - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - SizedBox( - width: _rankColumnWidth, - child: Text( - l10n.topLabel(_rank), - key: Key('ranking-top-$_rank'), - textAlign: TextAlign.center, - maxLines: 1, - style: const TextStyle( - color: AppColors.subtle, - fontSize: 18, - fontWeight: FontWeight.w800, - ), + child: Row( + children: [ + SizedBox( + width: AppDimens.rankingNumberWidth, + child: Text( + '$_rank', + key: Key('ranking-top-$_rank'), + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: AppColors.textPrimary.withValues(alpha: 0.12), + fontSize: AppDimens.rankingNumberFont, + fontWeight: FontWeight.w800, + height: 1, ), ), - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: SizedBox( - width: _listCoverWidth, - height: _listCoverHeight, - child: RemoteImage( - drama.posterUrl, - fit: BoxFit.cover, - showFallbackIcon: true, - fallbackAspectRatio: 3 / 4, - ), + ), + ClipRRect( + borderRadius: BorderRadius.circular( + AppDimens.rankingCoverRadius, + ), + child: SizedBox( + key: Key('ranking-row-cover-$_rank'), + width: AppDimens.rankingCoverWidth, + height: AppDimens.rankingCoverHeight, + child: RemoteImage( + drama.posterUrl, + fit: BoxFit.cover, + showFallbackIcon: true, + fallbackAspectRatio: 3 / 4, ), ), - const SizedBox(width: 12), - Expanded( + ), + const SizedBox(width: AppDimens.spaceMd), + Expanded( + child: SizedBox( + height: AppDimens.rankingCoverHeight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ Text( drama.title, - maxLines: 2, + maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.onSurface, + color: AppColors.textPrimary, fontSize: 15, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w500, ), ), - if (drama.synopsis.trim().isNotEmpty) ...[ - const SizedBox(height: 4), + if (_category.isNotEmpty) ...[ + const SizedBox(height: AppDimens.spaceXs), Text( - drama.synopsis, - maxLines: 2, + _category, + maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: AppColors.muted, - fontSize: 12, - height: 1.3, + color: AppColors.textTertiary, + fontSize: 11, ), ), ], - if (_tagValues().isNotEmpty) ...[ - const SizedBox(height: 6), - Wrap( - spacing: 6, - runSpacing: 4, - children: [ - for (final tag in _tagValues()) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppColors.raised, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - tag, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.muted, - fontSize: 11, - ), - ), - ), - ], - ), - ], - const SizedBox(height: 8), - Row( - children: [ - const Icon( - Icons.local_fire_department_rounded, - color: AppColors.blue, - size: 12, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - l10n.heatLabel( - formatInteractionCount(drama.viewCount), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.muted, - fontSize: 11, - ), - ), - ), - const SizedBox(width: 8), - Text( - l10n.episodeCount(drama.episodeCount), - maxLines: 1, - style: const TextStyle( - color: AppColors.muted, - fontSize: 11, - ), - ), - ], - ), + const Spacer(), + if (drama.viewCount > 0) _heat(drama.viewCount, l10n), ], ), ), - ], - ), + ), + ], ), ), ), ); } } + +Widget _heat(int count, AppLocalizations l10n) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.local_fire_department_rounded, + color: AppColors.textTertiary, + size: 11, + ), + const SizedBox(width: 4), + Text( + l10n.heatLabel(formatPlayCount(count)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: AppColors.textTertiary, fontSize: 11), + ), + ], + ); +} diff --git a/test/discover/theater_pages_test.dart b/test/discover/theater_pages_test.dart index 95757e0..23d4ccb 100644 --- a/test/discover/theater_pages_test.dart +++ b/test/discover/theater_pages_test.dart @@ -117,8 +117,8 @@ void main() { expect(top3.right, closeTo(376, 0.5)); expect(top2.left, lessThan(top1.left)); expect(top3.left, greaterThan(top1.right)); - expect(top1.bottom, closeTo(top2.bottom, 0.5)); - expect(top1.bottom, closeTo(top3.bottom, 0.5)); + expect(top1.bottom, closeTo(top2.bottom - 12, 0.5)); + expect(top2.bottom, closeTo(top3.bottom, 0.5)); expect(find.byKey(const Key('ranking-row-3')), findsNothing); }); @@ -129,13 +129,19 @@ void main() { expect(find.byKey(const Key('ranking-row-3')), findsOneWidget); expect(find.byKey(const Key('ranking-row-2')), findsNothing); - expect(find.byKey(const Key('ranking-top-4')), findsOneWidget); + final rankNumber = tester.widget( + find.byKey(const Key('ranking-top-4')), + ); + expect(rankNumber.data, '4'); + expect(rankNumber.style?.fontSize, 48); + expect(rankNumber.style?.fontWeight, FontWeight.w800); expect( - find.descendant( - of: find.byKey(const Key('ranking-row-3')), - matching: find.text('synopsis'), - ), - findsOneWidget, + tester.getSize(find.byKey(const Key('ranking-row-3'))), + const Size(362, 96), + ); + expect( + tester.getSize(find.byKey(const Key('ranking-row-cover-4'))), + const Size(60, 80), ); }); diff --git a/test/home/home_grid_layout_test.dart b/test/home/home_grid_layout_test.dart index 56d64d5..4c46e99 100644 --- a/test/home/home_grid_layout_test.dart +++ b/test/home/home_grid_layout_test.dart @@ -114,8 +114,9 @@ void main() { await tester.tap(find.byKey(const Key('home-section-ranking'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('ranking-page')), findsNothing); - expect(firstDramaId(), 'page1-0'); + expect(find.byKey(const Key('ranking-page')), findsOneWidget); + expect(find.byKey(const Key('ranking-hero')), findsOneWidget); + expect(find.byKey(const Key('home-grid-cards')), findsNothing); await tester.tap(find.byKey(const Key('home-section-categories'))); await tester.pumpAndSettle();