diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 890e1f0..a766d09 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -123,3 +123,29 @@ Running Gradle task 'assembleDebug'... 26.2s 解读:静态分析零告警,559 项全量测试通过,debug APK 构建成功; `pubspec.lock` 已还原,无依赖变更,未安装或操作真机。 + +## 2026-10-21 K98.1 收藏与观看记录直接进入播放页 + +- 已从最新 `origin/develop` 创建 `fix/k98.1-play-from-lists`。 +- 收藏卡片已改为调用 `playDramaFromCatalog`,不再进入详情页。 +- 观看记录卡片已按 `progress.episodeId` 直达播放页对应集;episodeId 为空时从第 1 集起播。 +- 详情未加载完成的观看记录仍保持原禁用、加载和重试行为。 +- 已补收藏及观看记录直达 PlayerPage 的路由断言。 + +门禁原始输出: + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test --no-pub +01:09 +561: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 28.7s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:静态分析零告警,561 项全量测试通过,debug APK 构建成功; +`pubspec.lock` 已还原,无依赖变更,未安装或操作真机。 diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 2676569..3f68c21 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -19,7 +19,7 @@ import '../../core/widgets/empty_state.dart'; import '../../core/widgets/remote_image.dart'; import '../../core/widgets/subpage_header.dart'; import '../../l10n/app_localizations.dart'; -import '../drama_detail/drama_detail_view.dart'; +import '../player/open_player.dart'; import 'favorites_binding.dart'; const double _pageHorizontal = 14; @@ -194,9 +194,7 @@ class _FavoritesBody extends StatelessWidget { } Future _open(BuildContext context, Drama drama) async { - await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => DramaDetailPage(drama: drama)), - ); + await playDramaFromCatalog(context, drama); } Future _remove(BuildContext context, String dramaId) async { diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index 157c48b..5daa2b8 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../core/data/models/drama.dart'; +import '../../core/data/models/watch_record.dart'; import '../../core/data/repositories/catalog_repository.dart'; import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/auth_controller.dart'; @@ -19,7 +19,7 @@ import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/subpage_header.dart'; import '../../l10n/app_localizations.dart'; -import '../drama_detail/drama_detail_view.dart'; +import '../player/open_player.dart'; import 'watch_records_binding.dart'; import 'widgets/watch_record_card.dart'; @@ -138,7 +138,7 @@ class _WatchRecordsBodyState extends State<_WatchRecordsBody> { record: record, onRetry: () => controller.retryDetail(record.dramaId), onRemove: () => _remove(record.dramaId), - onOpen: record.drama == null ? null : () => _open(record.drama!), + onOpen: record.drama == null ? null : () => _open(record), ), ], ], @@ -191,9 +191,12 @@ class _WatchRecordsBodyState extends State<_WatchRecordsBody> { ); } - Future _open(Drama drama) async { - await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => DramaDetailPage(drama: drama)), + Future _open(WatchRecord record) async { + final episodeId = record.progress.episodeId; + await playDramaFromCatalog( + context, + record.drama!, + episodeId: episodeId.isEmpty ? null : episodeId, ); } diff --git a/test/library/favorites_page_test.dart b/test/library/favorites_page_test.dart index b286d81..2091e85 100644 --- a/test/library/favorites_page_test.dart +++ b/test/library/favorites_page_test.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.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/core/data/services/auth_controller.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart'; import 'package:fruitshorts/core/data/services/favorite_controller.dart'; @@ -14,13 +17,20 @@ import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/widgets/app_pull_to_refresh.dart'; import 'package:fruitshorts/core/widgets/empty_state.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart'; import 'package:fruitshorts/pages/favorites/favorites_page.dart'; +import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:provider/provider.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_http_adapter.dart'; +import '../support/fake_catalog.dart'; +import '../support/fake_category.dart'; import '../support/fake_haptics.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/fake_notice_repository.dart'; +import '../support/widget_player.dart'; Drama _card(String id) => Drama( id: id, @@ -152,6 +162,43 @@ void main() { expect(repository.listCalls, 0); }, ); + + testWidgets('should_open_player_when_favorite_card_is_tapped', ( + tester, + ) async { + await _auth(signedIn: true); + final catalog = FakeCatalogRepository(); + final drama = catalog.dramas.first; + + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: InMemoryWatchHistoryRepository(), + favoriteRepository: InMemoryFavoriteRepository( + saved: {drama.id}, + cards: [drama], + ), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + noticeRepository: FakeNoticeRepository(), + haptics: FakeHaptics(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-mine'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('mine-watchlist'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(Key('favorite-card-${drama.id}'))); + await tester.pumpAndSettle(); + + expect(find.byType(PlayerPage), findsOneWidget); + expect(find.byType(DramaDetailPage), findsNothing); + }); } Future _auth({required bool signedIn}) async { diff --git a/test/watch/watch_records_page_test.dart b/test/watch/watch_records_page_test.dart index 90bd1a0..4d4cca0 100644 --- a/test/watch/watch_records_page_test.dart +++ b/test/watch/watch_records_page_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/feed_page.dart'; import 'package:fruitshorts/core/data/models/home_feed.dart'; @@ -9,6 +10,8 @@ import 'package:fruitshorts/core/data/models/watch_progress.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.dart'; import 'package:fruitshorts/core/data/repositories/catalog_repository.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/core/data/services/auth_controller.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart'; @@ -17,14 +20,20 @@ import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/widgets/app_pull_to_refresh.dart'; import 'package:fruitshorts/core/widgets/empty_state.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 'package:fruitshorts/pages/watch_records/watch_records_page.dart'; import 'package:provider/provider.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_http_adapter.dart'; import '../support/fake_haptics.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/fake_notice_repository.dart'; +import '../support/widget_player.dart'; void main() { setUp(() { @@ -180,6 +189,53 @@ void main() { expect(find.text('账号不存在,请先注册'), findsOneWidget); expect(find.byKey(const Key('watch-records-retry')), findsOneWidget); }); + + testWidgets('should_open_player_at_recorded_episode_when_card_is_tapped', ( + tester, + ) async { + await _auth(signedIn: true); + final catalog = FakeCatalogRepository(); + final drama = catalog.dramas.first; + final episode = drama.episodes[2]; + final history = InMemoryWatchHistoryRepository( + seed: [ + WatchProgress( + dramaId: drama.id, + episodeId: episode.id, + position: const Duration(seconds: 12), + duration: const Duration(seconds: 60), + updatedAt: DateTime(2026, 10, 17, 12), + ), + ], + ); + + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: history, + favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + noticeRepository: FakeNoticeRepository(), + haptics: FakeHaptics(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-mine'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('mine-history'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(Key('watch-record-${drama.id}'))); + await tester.pumpAndSettle(); + + final player = tester.widget(find.byType(PlayerPage)); + expect(player.initialIndex, 2); + expect(find.byType(DramaDetailPage), findsNothing); + }); } WatchProgress _progress(String dramaId) => WatchProgress(