diff --git a/lib/pages/following/following_favorites_grid.dart b/lib/pages/following/following_favorites_grid.dart index 92a712d..d98cb26 100644 --- a/lib/pages/following/following_favorites_grid.dart +++ b/lib/pages/following/following_favorites_grid.dart @@ -1,22 +1,18 @@ // Author: fruit.com.cn +// 职责:追剧收藏网格、分页和编辑选择,不承载顶部续播或重复区块标题。 import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../core/data/models/watch_record.dart'; import '../../core/data/services/favorite_controller.dart'; -import '../../core/data/services/watch_records_controller.dart'; import '../../core/formatting/error_text.dart'; -import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_dimens.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; import 'following_grid_card.dart'; import 'following_paged_grid.dart'; -import 'widgets/continue_watching_banner.dart'; /// 追剧编辑中的选中状态。 class FollowingFavoriteSelection { @@ -36,19 +32,15 @@ class FollowingFavoritesGrid extends StatelessWidget { const FollowingFavoritesGrid({ super.key, required this.controller, - required this.history, required this.selection, required this.scrollController, required this.onGoHome, - required this.onContinueRecord, }); final FavoriteController controller; - final WatchRecordsController history; final FollowingFavoriteSelection selection; final ScrollController scrollController; final VoidCallback onGoHome; - final ValueChanged onContinueRecord; @override Widget build(BuildContext context) { @@ -59,7 +51,7 @@ class FollowingFavoritesGrid extends StatelessWidget { valueListenable: selection.selected, builder: (context, picked, _) { return ListenableBuilder( - listenable: Listenable.merge([controller, history]), + listenable: controller, builder: (context, _) => _grid(context, isEditing, picked), ); }, @@ -89,7 +81,6 @@ class FollowingFavoritesGrid extends StatelessWidget { onRefresh: controller.load, onLoadMore: controller.loadMore, itemCount: controller.cards.length + extra, - topSlivers: _topSlivers(context), empty: EmptyState( key: const Key('following-favorites-empty'), icon: Icons.bookmark_border_rounded, @@ -99,68 +90,6 @@ class FollowingFavoritesGrid extends StatelessWidget { ); } - List _topSlivers(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final continueRecord = _continueRecord(); - return [ - if (continueRecord != null) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB( - AppDimens.feedPageHorizontal, - 4, - AppDimens.feedPageHorizontal, - 0, - ), - child: ContinueWatchingBanner( - record: continueRecord, - languageCode: Localizations.localeOf(context).languageCode, - onTap: () => onContinueRecord(continueRecord), - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB( - AppDimens.feedPageHorizontal, - AppDimens.followingSectionTopGap, - AppDimens.feedPageHorizontal, - AppDimens.followingSectionBottomGap, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - l10n.favoriteDramas, - style: const TextStyle( - color: AppColors.textPrimary, - fontSize: AppDimens.followingSectionTitleFont, - fontWeight: FontWeight.w400, - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - l10n.totalDramasCount(controller.total), - style: const TextStyle( - color: AppColors.textTabInactive, - fontSize: AppDimens.followingSectionCountFont, - ), - ), - ), - ], - ), - ), - ), - ]; - } - - /// 最近一条观看记录(records 已按最近观看排序,与观看历史页首条一致)。 - WatchRecord? _continueRecord() { - return history.records.isEmpty ? null : history.records.first; - } - Widget _item( BuildContext context, int index, diff --git a/lib/pages/following/following_page.dart b/lib/pages/following/following_page.dart index d099fb9..e595d8b 100644 --- a/lib/pages/following/following_page.dart +++ b/lib/pages/following/following_page.dart @@ -1,4 +1,5 @@ // Author: fruit.com.cn +// 职责:追剧收藏与观看历史切换、数据刷新、管理和续播入口。 import 'dart:async'; @@ -39,7 +40,7 @@ class FollowingPage extends StatefulWidget { final FavoriteController favoriteController; final int historyRequest; - /// 当前是否为首页选中的 tab;重新可见时刷新观看历史,保证横幅是最新进度。 + /// 当前是否为首页选中的 tab;重新可见时刷新观看历史,保证记录是最新进度。 final bool visible; @override @@ -157,7 +158,6 @@ class _FollowingPageState extends State { FollowingKeepAliveTab( child: FollowingFavoritesGrid( controller: widget.favoriteController, - history: _history, selection: FollowingFavoriteSelection( editing: _editing, selected: _selected, @@ -165,9 +165,6 @@ class _FollowingPageState extends State { ), scrollController: _favoriteScroll, onGoHome: widget.onGoHome, - onContinueRecord: (record) => unawaited( - _openHistory(context, _history, record), - ), ), ), FollowingKeepAliveTab( diff --git a/test/following/following_compact_layout_test.dart b/test/following/following_compact_layout_test.dart new file mode 100644 index 0000000..9de370c --- /dev/null +++ b/test/following/following_compact_layout_test.dart @@ -0,0 +1,143 @@ +// Author: fruit.com.cn +// 职责:验证追剧页移除顶部横幅/重复收藏标题后,卡片直达及历史续播能力。 +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/app.dart'; +import 'package:fruitshorts/core/data/models/watch_progress.dart'; +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/l10n/app_localizations.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +import '../support/fake_catalog.dart'; +import '../support/fake_category.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/widget_player.dart'; + +const _viewport = Size(390, 844); +const _tolerance = 0.1; +const _dramaId = 'the-signal-demo'; +const _banner = Key('following-continue-banner'); +const _page = Key('following-page'); +const _favoritesGrid = Key('following-favorites-grid'); +const _historyRecord = WatchProgress( + dramaId: _dramaId, + episodeId: 'the-signal-demo-3', + dramaTitle: 'Recent story', + episodeNumber: 3, + totalEpisodes: 8, + position: Duration(seconds: 25), + duration: Duration(seconds: 100), +); + +Future _pumpFollowing( + WidgetTester tester, { + bool hasFavorites = true, +}) async { + tester.view.physicalSize = _viewport; + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final catalog = FakeCatalogRepository(); + final cards = hasFavorites + ? catalog.dramas.take(1).toList() + : catalog.dramas.take(0).toList(); + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: InMemoryWatchHistoryRepository( + seed: [_historyRecord], + ), + favoriteRepository: InMemoryFavoriteRepository( + cards: cards, + saved: cards.map((drama) => drama.id).toSet(), + ), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-follow'))); + await tester.pumpAndSettle(); +} + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); + + testWidgets('should_hide_continue_banner_when_recent_history_exists', ( + tester, + ) async { + await _pumpFollowing(tester); + expect(find.byKey(_banner), findsNothing); + expect(find.byKey(const Key('following-card-$_dramaId')), findsOneWidget); + expect(find.byKey(const Key('following-add-card')), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + for (final hasFavorites in [true, false]) { + testWidgets( + 'should_omit_section_heading_and_count_when_favorites_are_$hasFavorites', + (tester) async { + await _pumpFollowing(tester, hasFavorites: hasFavorites); + final l10n = AppLocalizations.of(tester.element(find.byKey(_page)))!; + final grid = find.byKey(_favoritesGrid); + expect( + find.descendant(of: grid, matching: find.text(l10n.favoriteDramas)), + findsNothing, + ); + expect( + find.descendant( + of: grid, + matching: find.text(l10n.totalDramasCount(hasFavorites ? 1 : 0)), + ), + findsNothing, + ); + if (hasFavorites) { + final header = tester.getRect( + find.byKey(const Key('following-header')), + ); + final card = tester.getRect( + find.byKey(const Key('following-card-$_dramaId')), + ); + expect(card.top, closeTo(header.bottom, _tolerance)); + } else { + expect( + find.byKey(const Key('following-favorites-empty')), + findsOneWidget, + ); + } + expect(tester.takeException(), isNull); + }, + ); + } + + testWidgets('should_keep_history_continue_entry_when_history_tab_opens', ( + tester, + ) async { + await _pumpFollowing(tester); + await tester.tap(find.byKey(const Key('following-tab-history'))); + await tester.pumpAndSettle(); + expect( + find.byKey(const Key('following-history-card-$_dramaId')), + findsOneWidget, + ); + expect( + find.byKey(const Key('following-history-continue-$_dramaId')), + findsOneWidget, + ); + expect( + find.byKey(const Key('following-progress-$_dramaId')), + findsOneWidget, + ); + expect(find.byKey(_banner), findsNothing); + expect(tester.takeException(), isNull); + }); +}