From 30b55afdd4fda7267ba0e58f72b27e45592c8d5d Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 20:01:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(recommend):=20=E7=A8=B3=E5=AE=9A=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=8B=96=E5=8A=A8=E4=B8=8E=E6=B8=85=E5=B1=8F=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 3 +- lib/pages/home/home_logic.dart | 7 + lib/pages/home/home_view.dart | 12 +- lib/pages/player/player_view.dart | 64 ++++++- .../player/widgets/home_feed_overlay.dart | 48 +++-- .../widgets/home_feed_overlay_parts.dart | 168 ++++++++++++------ lib/pages/recommend/recommend_page.dart | 66 +++++-- test/home/home_preview_layout_test.dart | 51 +++++- test/recommend/recommend_sections_test.dart | 16 ++ 9 files changed, 339 insertions(+), 96 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 8a02993..f5d2cc1 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1183,4 +1183,5 @@ $ flutter test - 按最新反馈移除整行榜单与热度胶囊,信息顺序收敛为标题、剧名、标签、简介、观看全集。 - 剧名 19/w600,箭头 20 白色并紧邻剧名;标签行改为 13/w600 亮灰内联布局。 - 简介「展开/收起」沿用现有三语文案;进度条热区恢复 24 并接入拖动自动清屏与预览卡片。 -- 拖动期间隐藏顶部导航、操作栏、文字及根底栏,松手恢复;按当前流程未执行 analyze/test。 +- 拖动期间隐藏操作栏、文字及根底栏,顶部导航常驻;松手后持续清屏直到点击画面。 +- 进度条固定中心并动画到 6 高,时间气泡限制在左右 12;按当前流程未执行 analyze/test。 diff --git a/lib/pages/home/home_logic.dart b/lib/pages/home/home_logic.dart index dfd1f00..dab2d46 100644 --- a/lib/pages/home/home_logic.dart +++ b/lib/pages/home/home_logic.dart @@ -23,6 +23,7 @@ class HomeLogic extends ChangeNotifier { bool drawerOpen = false; bool detailOpen = false; bool videoImmersive = false; + bool scrubHidden = false; bool _dramaNotifyScheduled = false; bool _disposed = false; @@ -144,6 +145,12 @@ class HomeLogic extends ChangeNotifier { notify(); } + void onScrubVisibilityChanged(bool value) { + if (scrubHidden == value) return; + scrubHidden = value; + notify(); + } + @override void dispose() { _disposed = true; diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 4586dbb..615bf61 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -239,7 +239,15 @@ class _HomeTabsState extends State<_HomeTabs> { backgroundColor: AppColors.transparent, resizeToAvoidBottomInset: false, body: _body(context), - bottomNavigationBar: logic.videoImmersive ? null : _nav(context), + bottomNavigationBar: logic.videoImmersive + ? null + : Visibility( + visible: !logic.scrubHidden, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: _nav(context), + ), ), ); return AnnotatedRegion( @@ -288,7 +296,7 @@ class _HomeTabsState extends State<_HomeTabs> { active: logic.tab == 1, onSearch: () => _openSearch(context), onOpenFilter: () => _pushPage(context, const FilterPage()), - onScrubVisibilityChanged: logic.onImmersiveChanged, + onScrubVisibilityChanged: logic.onScrubVisibilityChanged, ); } diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index ea3026a..062c93f 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -59,6 +59,7 @@ class PlayerPage extends StatefulWidget { this.active = true, this.resumeRequest = 0, this.backRequest = 0, + this.scrubCancelRequest = 0, this.onDiscover, this.onMy, this.onSearch, @@ -85,6 +86,7 @@ class PlayerPage extends StatefulWidget { final bool active; final int resumeRequest; final int backRequest; + final int scrubCancelRequest; final VoidCallback? onDiscover; final VoidCallback? onMy; final VoidCallback? onSearch; @@ -117,6 +119,7 @@ class _PlayerPageState extends State double _playbackSpeed = 1.0; bool _boosting = false; bool _scrubbing = false; + bool _clearScreenFromScrub = false; double? _speedBeforeBoost; bool? _chromeBeforeBoost; @@ -135,6 +138,9 @@ class _PlayerPageState extends State downloads: widget.downloads, ); logic.attach(widget, isMounted: () => mounted); + if (oldWidget.scrubCancelRequest != widget.scrubCancelRequest) { + _cancelFeedScrub(); + } _thumbnailService = PlaybackThumbnailService( sourceResolver: _resolveThumbnailSource, ); @@ -266,7 +272,9 @@ class _PlayerPageState extends State } void _hideChrome() { + if (!_chromeVisible) return; setState(() => _chromeVisible = false); + widget.onScrubVisibilityChanged?.call(true); } /// 长按两侧:切 2 倍速并清屏,记下进入前的状态用于松手恢复。 @@ -281,6 +289,7 @@ class _PlayerPageState extends State _playbackSpeed = _boostPlaybackSpeed; _chromeVisible = false; }); + widget.onScrubVisibilityChanged?.call(true); unawaited(logic.setPlaybackSpeed(_boostPlaybackSpeed)); } @@ -296,6 +305,7 @@ class _PlayerPageState extends State _playbackSpeed = speed; _chromeVisible = chrome; }); + widget.onScrubVisibilityChanged?.call(!chrome); unawaited(logic.setPlaybackSpeed(speed)); } @@ -305,6 +315,39 @@ class _PlayerPageState extends State widget.onScrubVisibilityChanged?.call(value); } + void _finishFeedScrub() { + setState(() { + _scrubbing = false; + _chromeVisible = false; + _clearScreenFromScrub = true; + }); + } + + void _cancelFeedScrub() { + logic.cancelScrub(); + _scrubbing = false; + _chromeVisible = true; + _clearScreenFromScrub = false; + } + + void _restoreFeedChrome() { + setState(() { + _scrubbing = false; + _chromeVisible = true; + _clearScreenFromScrub = false; + }); + widget.onScrubVisibilityChanged?.call(false); + } + + void _handleFeedSurfaceTap() { + if (_scrubbing) return; + if (!_chromeVisible) { + _restoreFeedChrome(); + return; + } + logic.session.setUserWantsPlay(!logic.session.userWantsPlay); + } + /// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。 void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) { setState(() { @@ -374,6 +417,7 @@ class _PlayerPageState extends State if (widget.homeMode && widget.showHomeFeedHeader && !_scrubbing && + _chromeVisible && !_boosting && !logic.pip && !logic.pipPreparing) @@ -479,16 +523,14 @@ class _PlayerPageState extends State controller: videoPort?.controller, videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl, theater: theater, - onTap: () => - logic.session.setUserWantsPlay(!logic.session.userWantsPlay), + onTap: () => _handleFeedSurfaceTap(), ), if (widget.homeMode && _chromeVisible && !logic.chromeHidden) Positioned.fill( child: GestureDetector( key: const Key('home-feed-video-toggle'), behavior: HitTestBehavior.opaque, - onTap: () => - logic.session.setUserWantsPlay(!logic.session.userWantsPlay), + onTap: _handleFeedSurfaceTap, ), ), // 票据三态之一:起播前正在向后端要签名地址 @@ -595,8 +637,7 @@ class _PlayerPageState extends State boosting: _boosting, onBoostStart: _startBoost, onBoostEnd: _endBoost, - onTogglePlay: () => - logic.session.setUserWantsPlay(!logic.session.userWantsPlay), + onTogglePlay: _handleFeedSurfaceTap, liked: interactions.isEpisodeLiked(episode.id), likeCount: interactions.episodeLikeCount(episode.id) ?? episode.likeCount, @@ -634,7 +675,9 @@ class _PlayerPageState extends State onEpisodes: () => logic.showEpisodes(context), onMore: _openCurrentMore, onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null, + onScrubEnd: _finishFeedScrub, onScrubVisibilityChanged: _setScrubbing, + cancelScrubRequest: widget.scrubCancelRequest, onSeek: ready ? (position) => logic.seekTo(context, videoPort!, position) : null, @@ -706,18 +749,23 @@ class _PlayerPageState extends State ? null : () => _openUnlockSheet(context, index, episode), ), - if (widget.homeMode && !_chromeVisible && !_boosting && !_scrubbing) + if (widget.homeMode && + !_chromeVisible && + !_boosting && + !_scrubbing && + !_clearScreenFromScrub) Positioned( right: 12, bottom: 56, child: ExitClearScreenButton( key: const Key('home-feed-exit-clear-screen'), tooltip: l10n.exitClearScreen, - onPressed: () => setState(() => _chromeVisible = true), + onPressed: _restoreFeedChrome, ), ), if (widget.homeMode && !_scrubbing && + _chromeVisible && !logic.chromeHidden && selected && !logic.session.userWantsPlay) diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index e2906b7..13891d0 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -13,13 +13,12 @@ import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import 'home_feed_recommend_panel.dart'; import 'loading_progress_track.dart'; -import 'scrub_preview_card.dart'; import 'seek_progress_bar.dart'; part 'home_feed_overlay_parts.dart'; const double _progressHeight = 2; -const double _progressHitHeight = 24; +const double _progressHitHeight = 28; const double _pageHorizontal = 14; const double _railRight = 8; const double _railGap = 12; @@ -60,6 +59,7 @@ class HomeFeedOverlay extends StatefulWidget { this.onScrubEnd, this.onScrubVisibilityChanged, this.onSeek, + this.cancelScrubRequest = 0, this.thumbnailService, this.spriteAxisService, }); @@ -92,6 +92,7 @@ class HomeFeedOverlay extends StatefulWidget { final VoidCallback? onScrubEnd; final ValueChanged? onScrubVisibilityChanged; final ValueChanged? onSeek; + final int cancelScrubRequest; final PlaybackThumbnailService? thumbnailService; final SpriteThumbnailAxisService? spriteAxisService; @@ -101,6 +102,16 @@ class HomeFeedOverlay extends StatefulWidget { class _HomeFeedOverlayState extends State { bool _scrubbing = false; + bool _clearAfterScrub = false; + + @override + void didUpdateWidget(covariant HomeFeedOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.cancelScrubRequest != widget.cancelScrubRequest) { + _scrubbing = false; + _clearAfterScrub = false; + } + } @override Widget build(BuildContext context) { @@ -116,7 +127,7 @@ class _HomeFeedOverlayState extends State { child: GestureDetector( key: const Key('home-feed-gesture'), behavior: HitTestBehavior.opaque, - onTap: widget.onTogglePlay, + onTap: _handleSurfaceTap, onLongPressStart: (details) => _handleLongPressStart(details, constraints.maxWidth), onLongPressEnd: (_) => widget.onBoostEnd(), @@ -127,7 +138,7 @@ class _HomeFeedOverlayState extends State { ), if (widget.boosting) Positioned(left: 0, right: 0, bottom: 20, child: _boostHint()) - else if (widget.visible && !_scrubbing) + else if (widget.visible && !_scrubbing && !_clearAfterScrub) Positioned( left: _pageHorizontal, right: _railRight, @@ -153,14 +164,10 @@ class _HomeFeedOverlayState extends State { locked: widget.locked, height: _progressHeight, hitHeight: _progressHitHeight, + cancelScrubRequest: widget.cancelScrubRequest, onScrubStart: _startScrub, onScrubEnd: _endScrub, onSeek: widget.onSeek, - thumbnailService: widget.thumbnailService, - spriteAxisService: widget.spriteAxisService, - thumbnailsUrl: widget.episode.thumbnailsUrl, - episodeId: widget.episode.id, - posterUrl: widget.drama.posterUrl, ), ), ], @@ -169,17 +176,34 @@ class _HomeFeedOverlayState extends State { } void _startScrub() { - if (!_scrubbing) setState(() => _scrubbing = true); + if (!_scrubbing || _clearAfterScrub) { + setState(() { + _scrubbing = true; + _clearAfterScrub = false; + }); + } widget.onScrubVisibilityChanged?.call(true); widget.onScrubStart?.call(); } void _endScrub() { - if (_scrubbing) setState(() => _scrubbing = false); - widget.onScrubVisibilityChanged?.call(false); + if (_scrubbing || !_clearAfterScrub) { + setState(() { + _scrubbing = false; + _clearAfterScrub = true; + }); + } widget.onScrubEnd?.call(); } + void _handleSurfaceTap() { + if (_clearAfterScrub) { + setState(() => _clearAfterScrub = false); + widget.onScrubVisibilityChanged?.call(false); + } + widget.onTogglePlay(); + } + void _handleLongPressStart(LongPressStartDetails details, double width) { final third = width / 3; final dx = details.localPosition.dx; diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index 4a7c6dc..2e054ac 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -6,6 +6,9 @@ const double _thumbSize = 6; const double _thumbActiveSize = 14; const double _trackActiveHeight = 6; const double _lineCenterFromBottom = 8; +const Duration _trackResizeDuration = Duration(milliseconds: 120); +const double _timeBubbleWidth = 96; +const double _timeBubbleMargin = 12; const Key _inactiveTrackKey = Key('home-feed-progress-inactive'); /// 整集条底部的进度线:20px 手势区内可点按/拖动 seek。 @@ -15,28 +18,20 @@ class _FeedProgressBar extends StatefulWidget { required this.locked, required this.height, required this.hitHeight, + this.cancelScrubRequest = 0, this.onScrubStart, this.onScrubEnd, this.onSeek, - this.thumbnailService, - this.spriteAxisService, - this.thumbnailsUrl, - required this.episodeId, - required this.posterUrl, }); final ValueListenable? controller; final bool locked; final double height; final double hitHeight; + final int cancelScrubRequest; final VoidCallback? onScrubStart; final VoidCallback? onScrubEnd; final ValueChanged? onSeek; - final PlaybackThumbnailService? thumbnailService; - final SpriteThumbnailAxisService? spriteAxisService; - final String? thumbnailsUrl; - final String episodeId; - final String posterUrl; @override State<_FeedProgressBar> createState() => _FeedProgressBarState(); @@ -63,15 +58,16 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { _bufferTimer?.cancel(); _bufferTimer = null; _buffering = false; - widget.thumbnailService?.cancelPending(); _attachSource(); } + if (oldWidget.cancelScrubRequest != widget.cancelScrubRequest) { + _preview = null; + } if (oldWidget.locked != widget.locked) { _bufferTimer?.cancel(); _bufferTimer = null; _buffering = false; if (widget.locked) { - widget.thumbnailService?.cancelPending(); _preview = null; } else { _syncBufferState(notify: false); @@ -83,7 +79,6 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { void dispose() { _detachSource(widget.controller); _bufferTimer?.cancel(); - if (_preview != null) widget.thumbnailService?.cancelPending(); super.dispose(); } @@ -123,10 +118,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { final previewPosition = _preview == null || video == null ? null : seekPositionForFraction(_preview!, video.duration); - final showPreview = - previewPosition != null && - widget.thumbnailService != null && - widget.episodeId.isNotEmpty; + final showPreview = previewPosition != null; return GestureDetector( key: const Key('home-feed-progress'), behavior: HitTestBehavior.opaque, @@ -157,17 +149,10 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { track, if (showPreview) Positioned( - left: scrubPreviewLeft( - trackWidth: width, - fraction: _preview!, - ), + left: _timeBubbleLeft(width, _preview!), bottom: widget.hitHeight + 8, - child: ScrubPreviewCard( - service: widget.thumbnailService!, - spriteAxisService: widget.spriteAxisService, - thumbnailsUrl: widget.thumbnailsUrl, - episodeId: widget.episodeId, - posterUrl: widget.posterUrl, + child: _ScrubTimeBubble( + key: const Key('scrub-preview-time-bubble'), position: previewPosition, duration: video!.duration, ), @@ -286,12 +271,10 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { Duration(milliseconds: (total * fraction.clamp(0.0, 1.0)).round()), ); } - widget.thumbnailService?.cancelPending(); setState(() => _preview = null); } void _cancelPreview() { - widget.thumbnailService?.cancelPending(); setState(() => _preview = null); } @@ -299,11 +282,11 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { final dragging = _preview != null; final trackHeight = dragging ? _trackActiveHeight : widget.height; final size = dragging ? _thumbActiveSize : _thumbSize; - final left = (width * fraction - size / 2) - .clamp(0.0, width - size) + final thumbSlotLeft = (width * fraction - _thumbActiveSize / 2) + .clamp(0.0, width - _thumbActiveSize) .toDouble(); - final lineBottom = _lineCenterFromBottom - trackHeight / 2; - final thumbBottom = _lineCenterFromBottom - size / 2; + final trackSlotBottom = _lineCenterFromBottom - _trackActiveHeight / 2; + final thumbSlotBottom = _lineCenterFromBottom - _thumbActiveSize / 2; return SizedBox( height: widget.hitHeight, child: Stack( @@ -312,32 +295,53 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { Positioned( left: 0, right: 0, - bottom: lineBottom, - child: Container( - key: const Key('home-feed-progress-track'), - height: trackHeight, - color: AppColors.border, + bottom: trackSlotBottom, + child: SizedBox( + height: _trackActiveHeight, + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: AnimatedContainer( + key: const Key('home-feed-progress-track'), + duration: _trackResizeDuration, + curve: Curves.easeOut, + width: width, + height: trackHeight, + color: AppColors.border, + ), + ), + Align( + alignment: Alignment.centerLeft, + child: AnimatedContainer( + duration: _trackResizeDuration, + curve: Curves.easeOut, + width: width * fraction, + height: trackHeight, + color: AppColors.blue, + ), + ), + ], + ), ), ), Positioned( - left: 0, - bottom: lineBottom, - child: Container( - width: width * fraction, - height: trackHeight, - color: AppColors.blue, - ), - ), - Positioned( - left: left, - bottom: thumbBottom, - child: Container( - key: const Key('home-feed-progress-thumb'), - width: size, - height: size, - decoration: const BoxDecoration( - color: AppColors.onPrimary, - shape: BoxShape.circle, + left: thumbSlotLeft, + bottom: thumbSlotBottom, + child: SizedBox.square( + dimension: _thumbActiveSize, + child: Center( + child: AnimatedContainer( + key: const Key('home-feed-progress-thumb'), + duration: _trackResizeDuration, + curve: Curves.easeOut, + width: size, + height: size, + decoration: const BoxDecoration( + color: AppColors.onPrimary, + shape: BoxShape.circle, + ), + ), ), ), ), @@ -345,4 +349,56 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { ), ); } + + double _timeBubbleLeft(double width, double fraction) { + final maxLeft = width - _timeBubbleWidth - _timeBubbleMargin; + if (maxLeft < _timeBubbleMargin) { + return ((width - _timeBubbleWidth) / 2) + .clamp(0.0, width - _timeBubbleWidth) + .toDouble(); + } + return (width * fraction - _timeBubbleWidth / 2) + .clamp(_timeBubbleMargin, maxLeft) + .toDouble(); + } +} + +class _ScrubTimeBubble extends StatelessWidget { + const _ScrubTimeBubble({ + super.key, + required this.position, + required this.duration, + }); + + final Duration position; + final Duration duration; + + @override + Widget build(BuildContext context) { + return Container( + width: _timeBubbleWidth, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: AppColors.overlayMedium, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + '${_formatTime(position)} / ${_formatTime(duration)}', + key: const Key('scrub-preview-time'), + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + String _formatTime(Duration value) { + final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } } diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index 098bfa6..3b524f9 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -44,6 +44,7 @@ class RecommendPage extends StatefulWidget { class _RecommendPageState extends State { RecommendInnerTab _tab = RecommendInnerTab.feed; bool _scrubbing = false; + int _scrubCancelRequest = 0; @override Widget build(BuildContext context) { @@ -80,21 +81,40 @@ class _RecommendPageState extends State { ), ), ), - if (!_scrubbing) - Positioned( - top: 0, - left: 0, - right: 0, - child: SafeArea( - bottom: false, - child: RecommendTopNav( - selected: _tab, - onVideo: _tab == RecommendInnerTab.feed, - onSelected: (tab) => setState(() => _tab = tab), - onSearch: widget.onSearch, + Positioned( + top: 0, + left: 0, + right: 0, + child: IgnorePointer( + child: SizedBox( + height: 72, + child: DecoratedBox( + key: const Key('recommend-top-gradient'), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.mediaBlack, AppColors.transparent], + ), + ), ), ), ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + bottom: false, + child: RecommendTopNav( + selected: _tab, + onVideo: _tab == RecommendInnerTab.feed, + onSelected: _selectTab, + onSearch: _handleSearch, + ), + ), + ), ], ), ); @@ -141,9 +161,31 @@ class _RecommendPageState extends State { onRefreshFeed: widget.feedController.refreshCurrent, onLoadMore: widget.feedController.loadMore, onScrubVisibilityChanged: _setScrubbing, + scrubCancelRequest: _scrubCancelRequest, ); } + void _selectTab(RecommendInnerTab tab) { + final wasScrubbing = _scrubbing; + setState(() { + _tab = tab; + if (wasScrubbing) _scrubCancelRequest++; + _scrubbing = false; + }); + if (wasScrubbing) widget.onScrubVisibilityChanged(false); + } + + void _handleSearch() { + if (_scrubbing) { + setState(() { + _scrubbing = false; + _scrubCancelRequest++; + }); + widget.onScrubVisibilityChanged(false); + } + widget.onSearch(); + } + void _setScrubbing(bool value) { if (_scrubbing == value) return; setState(() => _scrubbing = value); diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index f6a9c88..cfd815d 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -23,7 +23,6 @@ import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/home/home_feed_channel.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; -import 'package:fruitshorts/pages/player/widgets/scrub_preview_card.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:video_player/video_player.dart'; @@ -681,8 +680,12 @@ void main() { ); expect( tester.getSize(find.byKey(const Key('home-feed-progress'))).height, - greaterThanOrEqualTo(24), + greaterThanOrEqualTo(28), ); + final idleTrackCenter = tester + .getRect(find.byKey(const Key('home-feed-progress-track'))) + .center + .dy; expect( tester .getRect(find.byKey(const Key('home-feed-progress-thumb'))) @@ -705,7 +708,33 @@ void main() { expect(scrubVisibility.last, isTrue); expect(find.byKey(const Key('home-feed-title')), findsNothing); expect(find.byKey(const Key('rail-save')), findsNothing); - expect(find.byType(ScrubPreviewCard), findsOneWidget); + expect(find.byKey(const Key('scrub-preview-time-bubble')), findsOneWidget); + final bubbleRect = tester.getRect( + find.byKey(const Key('scrub-preview-time-bubble')), + ); + expect(bubbleRect.left, greaterThanOrEqualTo(12)); + expect( + bubbleRect.right, + lessThanOrEqualTo( + tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 12, + ), + ); + await gesture.moveBy(const Offset(-600, 0)); + await tester.pump(); + expect( + tester.getRect(find.byKey(const Key('scrub-preview-time-bubble'))).left, + closeTo(12, 0.1), + ); + await gesture.moveBy(const Offset(1200, 0)); + await tester.pump(); + expect( + tester.getRect(find.byKey(const Key('scrub-preview-time-bubble'))).right, + closeTo( + tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 12, + 0.1, + ), + ); + await tester.pump(const Duration(milliseconds: 130)); expect( tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(14, 14), @@ -714,6 +743,13 @@ void main() { tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 6, ); + expect( + tester + .getRect(find.byKey(const Key('home-feed-progress-track'))) + .center + .dy, + closeTo(idleTrackCenter, 0.1), + ); expect( tester .getRect(find.byKey(const Key('home-feed-progress-thumb'))) @@ -729,8 +765,8 @@ void main() { ); await gesture.up(); await tester.pumpAndSettle(); - expect(scrubVisibility.last, isFalse); - expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(scrubVisibility.last, isTrue); + expect(find.byKey(const Key('home-feed-title')), findsNothing); expect(seeked, isNotNull); expect(seeked!.inSeconds, greaterThan(0)); expect( @@ -742,6 +778,11 @@ void main() { 2, ); + await tester.tap(find.byKey(const Key('home-feed-gesture'))); + await tester.pump(); + expect(scrubVisibility.last, isFalse); + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + await tester.tap(find.byKey(const Key('home-feed-full-series'))); await tester.pump(); expect(opened, 1); diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index d7aff3d..1cbfb35 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -35,6 +35,7 @@ void main() { expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget); expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + expect(find.byKey(const Key('recommend-top-gradient')), findsOneWidget); final recommendText = tester.widget( find.descendant( of: find.byKey(const Key('recommend-tab-recommend')), @@ -93,6 +94,21 @@ void main() { expect(find.byType(PageView), findsOneWidget); expect(find.byKey(const Key('recommend-section-hits')), findsNothing); expect(find.byType(AppBottomNav), findsOneWidget); + + final progress = find.byKey(const Key('home-feed-progress')); + final progressCenterY = tester.getCenter(progress).dy; + final scrub = await tester.startGesture(tester.getCenter(progress)); + await scrub.moveBy(const Offset(40, 0)); + await tester.pump(const Duration(milliseconds: 130)); + expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); + expect(tester.getCenter(progress).dy, closeTo(progressCenterY, 0.1)); + await scrub.up(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); + + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); }); testWidgets('should_render_playlist_sections_and_horizontal_cards', (