diff --git a/lib/pages/home/widgets/home_feed_grid.dart b/lib/pages/home/widgets/home_feed_grid.dart index 8f99c3e..5fbce27 100644 --- a/lib/pages/home/widgets/home_feed_grid.dart +++ b/lib/pages/home/widgets/home_feed_grid.dart @@ -14,6 +14,7 @@ import '../../../core/data/services/home_feed_controller.dart'; import '../../../core/data/services/wallet_controller.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/drama_grid_skeleton.dart'; import '../../../core/widgets/empty_state.dart'; @@ -81,17 +82,16 @@ class _HomeFeedGridState extends State { final auth = context.watch(); final wallet = context.watch(); final shortcuts = _categoryShortcuts(categories, l10n); - final tags = _availableTags(l10n); - final activeTag = - tags.any((tag) => tag.id == _selectedTags[_selectedPrimary]) - ? _selectedTags[_selectedPrimary] - : null; final walletBadge = auth.isSignedIn && wallet.snapshot != null ? wallet.balance : null; - return ColoredBox( + final ranking = _selectedPrimary == HomePrimaryTab.ranking; + return DecoratedBox( key: const Key('home-grid'), - color: AppColors.appBackground, + decoration: BoxDecoration( + color: ranking ? null : AppColors.appBackground, + gradient: ranking ? AppTheme.pageGradient : null, + ), child: SafeArea( bottom: false, child: Column( @@ -100,12 +100,8 @@ class _HomeFeedGridState extends State { HomeGridHeader( callbacks: widget.callbacks, categories: shortcuts, - tags: tags, selectedPrimary: _selectedPrimary, - selectedTag: activeTag, onPrimarySelected: _selectPrimary, - onTagSelected: (tag) => - setState(() => _selectedTags[_selectedPrimary] = tag), walletBadge: walletBadge, ), Expanded( @@ -322,17 +318,6 @@ class _HomeFeedGridState extends State { return null; } - List _availableTags(AppLocalizations l10n) { - // UI 先完整展示筛选维度;后续接口补齐题材字段时只替换数据来源。 - return [ - HomeGridTagOption(id: 'hot', label: l10n.homeTagHot), - HomeGridTagOption(id: 'uplifting', label: l10n.homeTagUplifting), - HomeGridTagOption(id: 'sweet', label: l10n.homeTagSweet), - HomeGridTagOption(id: 'faceSlap', label: l10n.homeTagFaceSlap), - HomeGridTagOption(id: 'angst', label: l10n.homeTagAngst), - ]; - } - List _dramasFor( HomePrimaryTab tab, String? tag, diff --git a/lib/pages/home/widgets/home_grid_header.dart b/lib/pages/home/widgets/home_grid_header.dart index 4c6ef5e..131dcc4 100644 --- a/lib/pages/home/widgets/home_grid_header.dart +++ b/lib/pages/home/widgets/home_grid_header.dart @@ -28,13 +28,6 @@ class HomeGridCategoryShortcut { final HomePrimaryTab tab; } -class HomeGridTagOption { - const HomeGridTagOption({required this.id, required this.label}); - - final String id; - final String label; -} - class HomeGridCallbacks { const HomeGridCallbacks({ required this.onSearch, @@ -54,21 +47,15 @@ class HomeGridHeader extends StatelessWidget { super.key, required this.callbacks, required this.categories, - required this.tags, required this.selectedPrimary, - required this.selectedTag, required this.onPrimarySelected, - required this.onTagSelected, this.walletBadge, }); final HomeGridCallbacks callbacks; final List categories; - final List tags; final HomePrimaryTab selectedPrimary; - final String? selectedTag; final ValueChanged onPrimarySelected; - final ValueChanged onTagSelected; final int? walletBadge; @override @@ -81,10 +68,7 @@ class HomeGridHeader extends StatelessWidget { children: [ _searchRow(l10n), _primaryRow(l10n), - if (selectedPrimary == HomePrimaryTab.featured) - _tagRow(l10n) - else - const SizedBox(height: AppDimens.homeSecondaryGridGap), + const SizedBox(height: AppDimens.homeSecondaryGridGap), ], ); } @@ -323,119 +307,4 @@ class HomeGridHeader extends StatelessWidget { ), ); } - - Widget _tagRow(AppLocalizations l10n) { - return Padding( - padding: const EdgeInsets.fromLTRB( - AppDimens.feedPageHorizontal, - AppDimens.homePrimarySecondaryGap, - AppDimens.feedPageHorizontal, - AppDimens.homeSecondaryGridGap, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - key: const Key('home-secondary-tags'), - scrollDirection: Axis.horizontal, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - for (final tag in tags) ...[ - _tagChip(tag), - const SizedBox(width: AppDimens.homeSecondaryTagGap), - ], - ], - ), - ), - ), - const SizedBox(width: AppDimens.homeSecondaryFilterGap), - Tooltip( - message: l10n.filter, - child: SizedBox.square( - dimension: AppDimens.homeSecondaryFilterVisualSize, - child: Material( - color: AppColors.transparent, - child: InkWell( - key: const Key('home-grid-filter'), - onTap: callbacks.onOpenFilter, - child: Center( - child: SizedBox.square( - key: const Key('home-secondary-filter-visual'), - dimension: AppDimens.homeSecondaryFilterVisualSize, - child: const Center( - child: CustomPaint( - key: Key('home-secondary-filter-icon'), - size: Size.square(AppDimens.homeSecondaryFilterIcon), - painter: _ThreeLineFilterPainter(), - ), - ), - ), - ), - ), - ), - ), - ), - ], - ), - ); - } - - Widget _tagChip(HomeGridTagOption tag) { - final selected = selectedTag == tag.id; - final label = Text( - tag.label, - style: TextStyle( - color: selected ? AppColors.navSelected : AppColors.textTertiary, - fontSize: AppDimens.homeSecondaryTagFont, - fontWeight: FontWeight.w600, - letterSpacing: 0, - ), - ); - return InkWell( - key: Key('home-tag-${tag.id}'), - borderRadius: BorderRadius.circular(AppDimens.pillRadius), - onTap: () => onTagSelected(selected ? null : tag.id), - child: AnimatedContainer( - key: Key('home-tag-animated-${tag.id}'), - duration: AppDimens.homeSecondaryTagAnimationDuration, - curve: Curves.easeOut, - height: AppDimens.homeSecondaryTagHeight, - padding: const EdgeInsets.symmetric( - horizontal: AppDimens.homeSecondaryTagHorizontalPadding, - ), - alignment: Alignment.center, - decoration: BoxDecoration( - color: selected ? AppColors.contentSurface : AppColors.transparent, - borderRadius: BorderRadius.circular(AppDimens.homeSecondaryTagRadius), - ), - child: label, - ), - ); - } -} - -class _ThreeLineFilterPainter extends CustomPainter { - const _ThreeLineFilterPainter(); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = AppColors.textFilterIcon - ..strokeWidth = AppDimens.homeSecondaryFilterStroke - ..strokeCap = StrokeCap.butt; - for (final y in [ - size.height * 0.25, - size.height * 0.5, - size.height * 0.75, - ]) { - canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); - } - } - - @override - bool shouldRepaint(_ThreeLineFilterPainter oldDelegate) => false; } diff --git a/lib/pages/ranking/ranking_page.dart b/lib/pages/ranking/ranking_page.dart index 3083418..105cd02 100644 --- a/lib/pages/ranking/ranking_page.dart +++ b/lib/pages/ranking/ranking_page.dart @@ -10,7 +10,6 @@ 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'; @@ -20,7 +19,12 @@ import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; const int _topCount = 50; -const double _loadMoreThreshold = 200, _championLift = 12; +const double _loadMoreThreshold = 200; +const double _championLift = 12; +const double _podiumGap = 8; +const double _podiumHorizontal = 8; +const double _championScale = 1.18; +const double _coverAspect = 146 / 110; /// 剧场榜单页:前三名领奖台,第 4 名起为单列排行行。 class RankingPage extends StatefulWidget { @@ -117,7 +121,7 @@ class _RankingPageState extends State { if (podium.isNotEmpty) Padding( padding: const EdgeInsets.symmetric( - horizontal: AppDimens.feedPageHorizontal, + horizontal: _podiumHorizontal, ), child: _Podium(podium: podium, l10n: l10n), ), @@ -156,11 +160,7 @@ class _RankingPageState extends State { ), ); if (widget.embedded) { - return DecoratedBox( - key: const Key('ranking-page'), - decoration: const BoxDecoration(gradient: AppTheme.pageGradient), - child: SafeArea(bottom: false, child: content), - ); + return KeyedSubtree(key: const Key('ranking-page'), child: content); } return PlainPageScaffold( pageKey: const Key('ranking-page'), @@ -233,33 +233,55 @@ class _Podium extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - 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(), - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + final available = constraints.maxWidth - _podiumGap * 2; + final side = available / (2 + _championScale); + final champion = side * _championScale; + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: side, + child: podium.length > 1 + ? _PodiumCard( + rank: 2, + drama: podium[1], + l10n: l10n, + coverWidth: side, + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: _podiumGap), + SizedBox( + width: champion, + child: podium.isNotEmpty + ? Transform.translate( + offset: const Offset(0, -_championLift), + child: _PodiumCard( + rank: 1, + drama: podium.first, + l10n: l10n, + coverWidth: champion, + ), + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: _podiumGap), + SizedBox( + width: side, + child: podium.length > 2 + ? _PodiumCard( + rank: 3, + drama: podium[2], + l10n: l10n, + coverWidth: side, + ) + : const SizedBox.shrink(), + ), + ], + ); + }, ); } } @@ -269,19 +291,15 @@ class _PodiumCard extends StatelessWidget { required this.rank, required this.drama, required this.l10n, + required this.coverWidth, }); final int rank; final Drama drama; final AppLocalizations l10n; + final double coverWidth; - double get _coverWidth => rank == 1 - ? AppDimens.rankingPodiumChampionWidth - : AppDimens.rankingPodiumWidth; - - double get _coverHeight => rank == 1 - ? AppDimens.rankingPodiumChampionHeight - : AppDimens.rankingPodiumHeight; + double get _coverHeight => coverWidth * _coverAspect; Color get _badgeColor => switch (rank) { 1 => AppColors.homeCrownGold, @@ -303,7 +321,7 @@ class _PodiumCard extends StatelessWidget { borderRadius: BorderRadius.circular(AppDimens.rankingPodiumRadius), child: SizedBox( key: Key('ranking-cover-$rank'), - width: _coverWidth, + width: coverWidth, height: _coverHeight, child: Stack( fit: StackFit.expand, diff --git a/test/discover/theater_pages_test.dart b/test/discover/theater_pages_test.dart index 23d4ccb..5bac72d 100644 --- a/test/discover/theater_pages_test.dart +++ b/test/discover/theater_pages_test.dart @@ -113,8 +113,10 @@ void main() { final top3 = tester.getRect(find.byKey(const Key('ranking-cover-3'))); expect(top1.width, greaterThan(top2.width)); expect(top3.width, closeTo(top2.width, 0.5)); - expect(top2.left, closeTo(14, 0.5)); - expect(top3.right, closeTo(376, 0.5)); + expect(top2.left, closeTo(8, 0.5)); + expect(top3.right, closeTo(382, 0.5)); + expect(top1.left - top2.right, lessThanOrEqualTo(10)); + expect(top2.width, greaterThan(100)); expect(top2.left, lessThan(top1.left)); expect(top3.left, greaterThan(top1.right)); expect(top1.bottom, closeTo(top2.bottom - 12, 0.5)); diff --git a/test/home/home_grid_layout_test.dart b/test/home/home_grid_layout_test.dart index 4c46e99..54509ac 100644 --- a/test/home/home_grid_layout_test.dart +++ b/test/home/home_grid_layout_test.dart @@ -12,6 +12,7 @@ import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; +import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; @@ -55,18 +56,7 @@ void main() { .map((key) => tester.getCenter(find.byKey(key)).dx) .toList(); expect(sectionCenters, orderedEquals(sectionCenters.toList()..sort())); - - final tagKeys = [ - const Key('home-tag-hot'), - const Key('home-tag-uplifting'), - const Key('home-tag-sweet'), - const Key('home-tag-faceSlap'), - const Key('home-tag-angst'), - ]; - final tagCenters = tagKeys - .map((key) => tester.getCenter(find.byKey(key)).dx) - .toList(); - expect(tagCenters, orderedEquals(tagCenters.toList()..sort())); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); expect(find.text('Featured'), findsOneWidget); expect(find.text('New'), findsOneWidget); expect(find.text('Categories'), findsOneWidget); @@ -117,6 +107,11 @@ void main() { 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); + final home = tester.widget(find.byKey(const Key('home-grid'))); + expect( + (home.decoration as BoxDecoration).gradient, + AppTheme.pageGradient, + ); await tester.tap(find.byKey(const Key('home-section-categories'))); await tester.pumpAndSettle(); @@ -158,7 +153,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('home-section-featured'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); final afterScrollable = find .descendant( @@ -176,7 +171,7 @@ void main() { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); - expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); await tester.drag( find.byKey(const Key('home-primary-page-view')), const Offset(-320, 0), @@ -195,7 +190,7 @@ void main() { const Offset(320, 0), ); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); }); testWidgets('should_load_next_page_without_expanding_page_size', ( diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 5bd0f70..901e7bf 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -93,44 +93,8 @@ void main() { expect(find.byKey(const Key('home-section-ranking')), findsOneWidget); expect(find.byKey(const Key('home-section-category-古装')), findsOneWidget); expect(find.byKey(const Key('home-section-category-社群')), findsOneWidget); - expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); - expect(find.byKey(const Key('home-grid-filter')), findsOneWidget); - expect( - tester.getSize(find.byKey(const Key('home-grid-filter'))), - const Size.square(AppDimens.homeSecondaryFilterVisualSize), - ); - expect( - tester.getSize(find.byKey(const Key('home-secondary-filter-visual'))), - const Size.square(AppDimens.homeSecondaryFilterVisualSize), - ); - expect( - tester.getCenter(find.byKey(const Key('home-grid-filter'))).dy, - closeTo(tester.getCenter(find.byKey(const Key('home-tag-hot'))).dy, 0.1), - ); - expect( - tester.getRect(find.byKey(const Key('home-grid-filter'))).right, - closeTo( - tester.getRect(find.byKey(const Key('home-grid'))).right - - AppDimens.feedPageHorizontal, - 0.1, - ), - ); - final filterIcon = tester.widget( - find.byKey(const Key('home-secondary-filter-icon')), - ); - expect( - filterIcon.size, - const Size.square(AppDimens.homeSecondaryFilterIcon), - ); - final filterMaterial = tester.widget( - find - .ancestor( - of: find.byKey(const Key('home-grid-filter')), - matching: find.byType(Material), - ) - .first, - ); - expect(filterMaterial.color, AppColors.transparent); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); + expect(find.byKey(const Key('home-grid-filter')), findsNothing); final searchBox = find.descendant( of: find.byKey(const Key('home-search-entry')), matching: find.byType(Container), @@ -169,28 +133,6 @@ void main() { ), findsNothing, ); - final secondaryTag = tester.widget( - find.descendant( - of: find.byKey(const Key('home-tag-hot')), - matching: find.text('Hot'), - ), - ); - expect(secondaryTag.style?.fontSize, AppDimens.homeSecondaryTagFont); - expect(secondaryTag.style?.fontWeight, FontWeight.w600); - expect(secondaryTag.style?.color, AppColors.textTertiary); - final unselectedTagChip = tester.widget( - find.byKey(const Key('home-tag-animated-hot')), - ); - expect( - (unselectedTagChip.decoration! as BoxDecoration).color, - AppColors.transparent, - ); - expect( - unselectedTagChip.padding, - const EdgeInsets.symmetric( - horizontal: AppDimens.homeSecondaryTagHorizontalPadding, - ), - ); final primaryStart = tester.getTopLeft(find.text('Featured')); final pageStart = tester.getTopLeft(searchBox); expect(primaryStart.dx, pageStart.dx); @@ -198,41 +140,6 @@ void main() { tester.getTopLeft(find.text('New')).dx - tester.getBottomRight(find.text('Featured')).dx; expect(primaryGap, closeTo(AppDimens.homePrimaryCategoryGap, 0.1)); - final tagStart = tester.getTopLeft(find.byKey(const Key('home-tag-hot'))); - expect(tagStart.dx, pageStart.dx); - expect( - tester.getSize(find.byKey(const Key('home-tag-hot'))).height, - AppDimens.homeSecondaryTagHeight, - ); - final tagGap = - tester.getTopLeft(find.byKey(const Key('home-tag-uplifting'))).dx - - tester.getBottomRight(find.byKey(const Key('home-tag-hot'))).dx; - expect(tagGap, closeTo(AppDimens.homeSecondaryTagGap, 0.1)); - await tester.tap(find.byKey(const Key('home-tag-hot'))); - await tester.pump(); - final selectedTagChip = tester.widget( - find.byKey(const Key('home-tag-animated-hot')), - ); - expect( - (selectedTagChip.decoration! as BoxDecoration).color, - AppColors.contentSurface, - ); - expect( - (selectedTagChip.decoration! as BoxDecoration).borderRadius, - BorderRadius.circular(AppDimens.homeSecondaryTagRadius), - ); - expect( - tester.getSize(find.byKey(const Key('home-tag-hot'))).height, - AppDimens.homeSecondaryTagHeight, - ); - final selectedTag = tester.widget( - find.descendant( - of: find.byKey(const Key('home-tag-hot')), - matching: find.text('Hot'), - ), - ); - expect(selectedTag.style?.color, AppColors.navSelected); - expect(selectedTag.style?.fontWeight, FontWeight.w600); expect(find.byType(PageView), findsNothing); expect(tester.takeException(), isNull); }); @@ -351,13 +258,11 @@ void main() { ); await tester.pumpAndSettle(); - expect( - tester.widget(find.byKey(const Key('home-feed-title'))).data, - 'Feed Drama', - ); final seriesTitle = tester.widget( find.byKey(const Key('home-feed-series-title')), ); + expect(seriesTitle.data, 'Feed Drama'); + expect(find.text('Episode 2'), findsNothing); expect(seriesTitle.style?.fontSize, 19); expect(seriesTitle.style?.fontWeight, FontWeight.w600); expect(seriesTitle.style?.color, AppColors.textPrimary); @@ -726,7 +631,7 @@ void main() { await gesture.moveBy(const Offset(120, 0)); await tester.pump(); expect(scrubVisibility.last, isTrue); - expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(find.byKey(const Key('home-feed-series-title')), findsOneWidget); expect(find.byKey(const Key('rail-save')), findsOneWidget); expect( tester @@ -824,7 +729,7 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); expect(scrubVisibility.last, isFalse); - expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(find.byKey(const Key('home-feed-series-title')), findsOneWidget); expect( tester .widget( diff --git a/test/widget_test.dart b/test/widget_test.dart index 28f8aa5..b8c4d3d 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -204,7 +204,7 @@ void main() { expect(find.byKey(const Key('home-grid')), findsOneWidget); expect(find.byKey(const Key('home-grid-cards')), findsOneWidget); expect(find.byKey(const Key('home-search-entry')), findsOneWidget); - expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); expect(tester.takeException(), isNull); });