diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 049c0a1..cf32104 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1194,3 +1194,9 @@ $ flutter test - 推荐流拖动使用独立 opaque 手势与 pointer 兜底,end/cancel 同时恢复浮层和轨道。 - 推荐浮层改为 AnimatedOpacity + IgnorePointer,进度条节点始终留在树中;scrub setState 均加 mounted 守卫。 - `flutter test test/player/seek_scrub_visibility_test.dart`:6 项测试全部通过。 + +## 2026-10-22 推荐页播放跳转修正 + +- 标题、剧名行和观看全集统一通过 `playDramaFromCatalog` 打开非 `homeMode` 逐集播放页。 +- 仅含集数的剧集标题不再显示在标题上方,集数只保留下方进度文案。 +- `flutter analyze`:No issues found。 diff --git a/lib/pages/player/widgets/home_feed_recommend_panel.dart b/lib/pages/player/widgets/home_feed_recommend_panel.dart index 6ca74a0..3299d01 100644 --- a/lib/pages/player/widgets/home_feed_recommend_panel.dart +++ b/lib/pages/player/widgets/home_feed_recommend_panel.dart @@ -50,38 +50,44 @@ class _HomeFeedInfoPanelState extends State { final l10n = AppLocalizations.of(context)!; final dramaTitle = localizedDramaTitle(l10n, widget.drama); final episodeTitle = widget.episode.title.trim(); - final headline = episodeTitle.isEmpty ? dramaTitle : episodeTitle; + final headline = _headline(l10n, dramaTitle, episodeTitle); final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim(); final genres = _genreValues(); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - headline, - key: const Key('home-feed-title'), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.textPrimary, - fontSize: _headlineFont, - fontWeight: FontWeight.w700, - height: 1.2, - letterSpacing: 0, - shadows: _textShadows, + GestureDetector( + key: const Key('home-feed-title-open'), + behavior: HitTestBehavior.opaque, + onTap: widget.onOpenFullPlayer, + child: Text( + headline, + key: const Key('home-feed-title'), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: _headlineFont, + fontWeight: FontWeight.w700, + height: 1.2, + letterSpacing: 0, + shadows: _textShadows, + ), ), ), const SizedBox(height: 8), GestureDetector( key: const Key('home-feed-series-name'), behavior: HitTestBehavior.opaque, - onTap: widget.onDetail, + onTap: widget.onOpenFullPlayer, child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( dramaTitle, + key: const Key('home-feed-series-title'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -113,6 +119,23 @@ class _HomeFeedInfoPanelState extends State { ); } + String _headline( + AppLocalizations l10n, + String dramaTitle, + String episodeTitle, + ) { + if (episodeTitle.isEmpty) return dramaTitle; + final normalized = episodeTitle.toLowerCase().replaceAll(' ', ''); + final localizedEpisode = l10n + .episodeLabel(widget.episode.number) + .toLowerCase() + .replaceAll(' ', ''); + final episodeOnly = RegExp(r'^(第?\d+集|episode\d+)$').hasMatch(normalized); + return normalized == localizedEpisode || episodeOnly + ? dramaTitle + : episodeTitle; + } + Widget _tagMetaRow(AppLocalizations l10n, List genres) { return Row( key: const Key('home-feed-tags'), diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index 00a9023..9090b6a 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -171,11 +171,12 @@ class _SeekProgressBarState extends State { } Future _finish(double fraction, Duration duration) async { - if (widget.locked || !_dragging) return; + final startedAt = _dragStartFraction; + _dragStartFraction = null; + if (widget.locked || !_dragging || startedAt == null) return; setState(() => _dragging = false); _pointerMoved = false; _pointerStart = null; - _dragStartFraction = null; widget.thumbnailService?.cancelPending(); _previewFraction.value = null; widget.onScrubEnd?.call(); diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index c409d8f..e060687 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -6,15 +6,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; -import '../../core/data/services/catalog_controller.dart'; import '../../core/data/services/category_controller.dart'; -import '../../core/data/services/download_controller.dart'; import '../../core/data/services/home_feed_controller.dart'; -import '../../core/data/services/wallet_controller.dart'; -import '../../core/navigation/fade_page_route.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; +import '../player/open_player.dart'; import '../player/player_view.dart'; import 'recommend_types.dart'; import 'widgets/all_dramas_filter_grid.dart'; @@ -187,30 +184,6 @@ class _RecommendPageState extends State { } Future _openDrama(Drama drama, {String? episodeId}) async { - final catalog = context.read(); - final wallet = context.read(); - final downloads = context.read(); - var playable = drama; - try { - playable = await catalog.requireDetail(drama); - } catch (_) { - // 卡片预览仍可直接进入播放器。 - } - if (!mounted) return; - final episodeIndex = episodeId == null - ? 0 - : playable.episodes.indexWhere((episode) => episode.id == episodeId); - await Navigator.of(context).push( - FadePageRoute( - builder: (_) => PlayerPage( - drama: playable, - initialIndex: episodeIndex < 0 ? 0 : episodeIndex, - homeMode: true, - feedDramas: [playable], - wallet: wallet, - downloads: downloads, - ), - ), - ); + await playDramaFromCatalog(context, drama, episodeId: episodeId); } } diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index b0af80e..5bd0f70 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -282,7 +282,7 @@ void main() { await tester.pumpAndSettle(); expect( tester.widget(find.byType(PlayerPage).last).homeMode, - isTrue, + isFalse, ); }); @@ -321,7 +321,7 @@ void main() { episode: const Episode( id: 'feed-episode', number: 2, - title: 'Ignored Episode Title', + title: 'Episode 2', isFree: true, ), controller: null, @@ -353,10 +353,11 @@ void main() { expect( tester.widget(find.byKey(const Key('home-feed-title'))).data, - 'Ignored Episode Title', + 'Feed Drama', + ); + final seriesTitle = tester.widget( + find.byKey(const Key('home-feed-series-title')), ); - expect(find.text('Feed Drama'), findsOneWidget); - final seriesTitle = tester.widget(find.text('Feed Drama')); expect(seriesTitle.style?.fontSize, 19); expect(seriesTitle.style?.fontWeight, FontWeight.w600); expect(seriesTitle.style?.color, AppColors.textPrimary); @@ -370,7 +371,9 @@ void main() { expect(seriesChevron.color, AppColors.textPrimary); expect( tester.getTopLeft(find.byIcon(Icons.chevron_right_rounded)).dx - - tester.getTopRight(find.text('Feed Drama')).dx, + tester + .getTopRight(find.byKey(const Key('home-feed-series-title'))) + .dx, 4, ); expect(find.byKey(const Key('home-feed-info-card')), findsNothing); diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index b96f63b..d56c2a3 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -278,6 +278,30 @@ void main() { expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget); }); + testWidgets('should_open_standalone_player_from_title_and_watch_all', ( + tester, + ) async { + await _pumpRecommend(tester); + + await tester.tap(find.byKey(const Key('home-feed-title-open'))); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(PlayerPage).last).homeMode, + isFalse, + ); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-page')), findsOneWidget); + expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); + + await tester.tap(find.byKey(const Key('home-feed-full-series'))); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(PlayerPage).last).homeMode, + isFalse, + ); + }); + testWidgets('should_open_player_and_search_from_recommend_page', ( tester, ) async { @@ -290,7 +314,7 @@ void main() { ); await tester.pumpAndSettle(); final player = tester.widget(find.byType(PlayerPage).last); - expect(player.homeMode, isTrue); + expect(player.homeMode, isFalse); await tester.binding.handlePopRoute(); await tester.pumpAndSettle();