From fbfb9769a0ab0701d399af9b4e6346d57c350302 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 16:32:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(detail):=20=E9=80=89=E9=9B=86=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=AD=89=E5=AE=BD=E6=87=92=E5=8A=A0=E8=BD=BD=E7=BD=91?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/drama_detail/drama_detail_view.dart | 56 ++++++++--- test/drama_detail/drama_detail_view_test.dart | 99 ++++++++++++++++++- 2 files changed, 135 insertions(+), 20 deletions(-) diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart index 2cdfe14..952dda7 100644 --- a/lib/pages/drama_detail/drama_detail_view.dart +++ b/lib/pages/drama_detail/drama_detail_view.dart @@ -303,14 +303,41 @@ class _DramaDetailPageState extends State { ), ), const SizedBox(height: 8), - Wrap( - key: const Key('detail-episode-chips'), - spacing: 6, - runSpacing: 6, - children: [ - for (var index = 0; index < drama.episodes.length; index++) - _episodeChip(context, drama, index, continueIndex), - ], + LayoutBuilder( + builder: (context, constraints) { + final sixColumnsWidth = (constraints.maxWidth - 5 * 8) / 6; + final columns = sixColumnsWidth >= 40 ? 6 : 5; + final selectedIndex = continueIndex ?? 0; + final selectedNumber = drama.episodes.isEmpty + ? null + : drama.episodes[selectedIndex].number; + final ordered = [ + for (var index = 0; index < drama.episodes.length; index++) + (index: index, episode: drama.episodes[index]), + ]..sort((a, b) => a.episode.number.compareTo(b.episode.number)); + return GridView.builder( + key: const Key('detail-episode-chips'), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: ordered.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 1.35, + ), + itemBuilder: (context, position) { + final item = ordered[position]; + return _episodeChip( + context, + item.episode, + item.index, + selectedNumber, + ); + }, + ); + }, ), ], ), @@ -319,20 +346,17 @@ class _DramaDetailPageState extends State { Widget _episodeChip( BuildContext context, - Drama drama, + Episode episode, int index, - int? selectedIndex, + int? selectedNumber, ) { - final episode = drama.episodes[index]; - final selected = index == (selectedIndex ?? 0); + final selected = episode.number == selectedNumber; final locked = episode.locked || (!episode.isFree && index > 0); return InkWell( key: Key('detail-episode-chip-${episode.number}'), onTap: () => logic.openPlayer(context, index), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(9), child: Container( - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), alignment: Alignment.center, decoration: BoxDecoration( color: selected @@ -340,7 +364,7 @@ class _DramaDetailPageState extends State { : locked ? AppColors.secondary.withValues(alpha: 0.16) : const Color(0xFF29272B), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(9), ), child: Text( '${episode.number}', diff --git a/test/drama_detail/drama_detail_view_test.dart b/test/drama_detail/drama_detail_view_test.dart index 66352a7..ed939b3 100644 --- a/test/drama_detail/drama_detail_view_test.dart +++ b/test/drama_detail/drama_detail_view_test.dart @@ -5,20 +5,25 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; +import 'package:fruitshorts/core/data/repositories/comment_repository.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; +import 'package:fruitshorts/core/data/repositories/interaction_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/data/services/catalog_controller.dart'; import 'package:fruitshorts/core/data/services/favorite_controller.dart'; import 'package:fruitshorts/core/data/services/interaction_controller.dart'; +import 'package:fruitshorts/core/data/services/playback_session.dart'; import 'package:fruitshorts/core/platform/haptics.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart'; +import 'package:fruitshorts/pages/player/player_view.dart'; import '../support/fake_catalog.dart'; import '../support/fake_haptics.dart'; import '../support/fake_interaction_repositories.dart'; +import '../support/widget_player.dart'; void main() { testWidgets('should_render_preview_hero_cta_synopsis_and_episode_chips', ( @@ -78,6 +83,14 @@ void main() { await tester.pumpWidget(_host(_detailDrama())); await tester.pumpAndSettle(); + final grid = tester.widget( + find.byKey(const Key('detail-episode-chips')), + ); + expect( + (grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount) + .crossAxisCount, + 6, + ); await tester.drag( find.byKey(const Key('detail-scroll')), const Offset(0, -520), @@ -87,22 +100,96 @@ void main() { expect(find.byKey(const Key('detail-episode-chip-8')), findsOneWidget); expect(tester.takeException(), isNull); }); + + testWidgets('should_render_sorted_six_column_grid_and_open_original_index', ( + tester, + ) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(_host(_detailDrama(count: 80))); + await tester.pumpAndSettle(); + + final grid = tester.widget( + find.byKey(const Key('detail-episode-chips')), + ); + final delegate = + grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; + expect(delegate.crossAxisCount, 6); + expect(delegate.mainAxisSpacing, 8); + expect(delegate.crossAxisSpacing, 8); + expect(delegate.childAspectRatio, closeTo(1.35, 0.001)); + + final chip1 = find.byKey(const Key('detail-episode-chip-1')); + final chip2 = find.byKey(const Key('detail-episode-chip-2')); + final chip6 = find.byKey(const Key('detail-episode-chip-6')); + final chip7 = find.byKey(const Key('detail-episode-chip-7')); + expect(tester.getTopLeft(chip1).dy, tester.getTopLeft(chip6).dy); + expect( + tester.getTopLeft(chip7).dy, + greaterThan(tester.getTopLeft(chip1).dy), + ); + expect(tester.getTopLeft(chip1).dx, lessThan(tester.getTopLeft(chip2).dx)); + expect(tester.getSize(chip1), tester.getSize(chip2)); + + await tester.drag( + find.byKey(const Key('detail-scroll')), + const Offset(0, -420), + ); + await tester.pumpAndSettle(); + final chip12 = find.byKey(const Key('detail-episode-chip-12')); + expect(chip12, findsOneWidget); + await tester.tap(chip12); + await tester.pumpAndSettle(); + + final player = tester.widget(find.byType(PlayerPage)); + expect(player.initialIndex, 11); + }); + + testWidgets('should_sort_episode_chips_ascending_by_number', (tester) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(_host(_detailDrama(count: 80, descending: true))); + await tester.pumpAndSettle(); + + final chip1 = find.byKey(const Key('detail-episode-chip-1')); + final chip2 = find.byKey(const Key('detail-episode-chip-2')); + final chip6 = find.byKey(const Key('detail-episode-chip-6')); + final chip7 = find.byKey(const Key('detail-episode-chip-7')); + expect(tester.getTopLeft(chip1).dy, tester.getTopLeft(chip6).dy); + expect(tester.getTopLeft(chip1).dx, lessThan(tester.getTopLeft(chip2).dx)); + expect( + tester.getTopLeft(chip7).dy, + greaterThan(tester.getTopLeft(chip1).dy), + ); + expect(tester.getSize(chip1), tester.getSize(chip2)); + }); } Widget _host(Drama drama) { final catalog = FakeCatalogRepository(dramas: [drama]); + final interactions = FakeInteractionRepository(); return MultiProvider( providers: [ Provider.value( value: InMemoryWatchHistoryRepository(), ), Provider.value(value: FakePlaybackRepository()), + Provider.value(value: createWidgetPlayer), + Provider(create: (_) => PlaybackCoordinator()), + Provider.value(value: interactions), + Provider.value(value: FakeCommentRepository()), Provider.value(value: FakeHaptics()), ChangeNotifierProvider( create: (_) => FavoriteController(InMemoryFavoriteRepository()), ), ChangeNotifierProvider( - create: (_) => InteractionController(FakeInteractionRepository()), + create: (_) => InteractionController(interactions), ), ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()), ], @@ -116,7 +203,11 @@ Widget _host(Drama drama) { ); } -Drama _detailDrama() { +Drama _detailDrama({int count = 8, bool descending = false}) { + final numbers = List.generate( + count, + (index) => descending ? count - index : index + 1, + ); return Drama( id: 'detail-k25', title: 'Detail K25', @@ -126,11 +217,11 @@ Drama _detailDrama() { posterUrl: '/files/detail-k25.png', genre: 'Urban Romance', category: 'Romance', - totalEpisodes: 8, + totalEpisodes: count, viewCount: 4800000, detailLoaded: true, episodes: [ - for (var number = 1; number <= 8; number++) + for (final number in numbers) Episode( id: 'detail-k25-$number', number: number,