diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 6ee2abb..6b77976 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -23,6 +23,7 @@ import '../../core/widgets/download_action.dart'; import '../../l10n/app_localizations.dart'; import 'player_binding.dart'; import 'player_logic.dart'; +import 'widgets/home_feed_header.dart'; import 'widgets/home_feed_overlay.dart'; import 'widgets/exit_clear_screen_button.dart'; import 'widgets/player_center_toggle.dart'; @@ -187,6 +188,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { Widget _buildPlayer(BuildContext context) { final interactions = context.watch(); + final l10n = AppLocalizations.of(context)!; final indices = logic.visibleIndices; final pages = logic.pages; final content = AnimatedBuilder( @@ -205,21 +207,55 @@ class _PlayerPageState extends State with WidgetsBindingObserver { _buildEpisode(context, indices[position], interactions), ), ); + final body = Stack( + fit: StackFit.expand, + children: [ + content, + if (widget.homeMode && !_boosting && !logic.pip && !logic.pipPreparing) + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + bottom: false, + child: HomeFeedHeader( + l10n: l10n, + onRefresh: () async { + final ok = await widget.onRefreshFeed?.call() ?? true; + if (context.mounted && !ok) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.catalogError))); + } + return ok; + }, + onSearch: widget.onSearch ?? () => logic.showDiscover(context), + onMore: _openCurrentMore, + ), + ), + ), + ], + ); return AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Theme( data: AppTheme.light(), child: widget.embedded - ? content + ? body : Scaffold( key: const Key('player-scaffold'), backgroundColor: Colors.transparent, - body: content, + body: body, ), ), ); } + void _openCurrentMore() { + final entry = logic.queue.entryAt(logic.session.currentIndex); + _openMore(context, entry.episode); + } + Widget _buildEpisode( BuildContext context, int index, @@ -417,26 +453,13 @@ class _PlayerPageState extends State with WidgetsBindingObserver { }, onComments: () => logic.showComments(context, drama), onShare: () => logic.shareDrama(context, drama), - onEpisodes: () => logic.showEpisodes(context), onDetail: () => unawaited(logic.openDetail(context)), onOpenFullPlayer: () async { await widget.onOpenFullPlayer?.call(drama, episode); if (!mounted || !widget.active) return; logic.session.setUserWantsPlay(true); }, - onRefreshFeed: () async { - final ok = await widget.onRefreshFeed?.call() ?? true; - if (context.mounted && !ok) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.catalogError))); - } - return ok; - }, - onDownload: () => requestEpisodeDownload(context, episode.id), - onClear: () => setState(() => _chromeVisible = false), - onSearch: widget.onSearch ?? () => logic.showDiscover(context), - onMore: () => _openMore(context, episode), + onMore: _openCurrentMore, onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null, onSeek: ready ? (position) => logic.seekTo(context, videoPort!, position) diff --git a/lib/pages/player/widgets/home_feed_header.dart b/lib/pages/player/widgets/home_feed_header.dart new file mode 100644 index 0000000..fad82ac --- /dev/null +++ b/lib/pages/player/widgets/home_feed_header.dart @@ -0,0 +1,275 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../../l10n/app_localizations.dart'; + +const double _topIconSize = 36; +const double _topIconGlyph = 22; +const double _tabGap = 22; +const double _tabFontSize = 15; +const double _underlineWidth = 18; +const double _underlineHeight = 2; + +/// 首页 feed 固定顶栏:刷新 / 漫剧·真人剧·推荐 / 搜索。 +class HomeFeedHeader extends StatefulWidget { + const HomeFeedHeader({ + super.key, + required this.l10n, + required this.onRefresh, + required this.onSearch, + required this.onMore, + }); + + final AppLocalizations l10n; + final Future Function() onRefresh; + final VoidCallback onSearch; + final VoidCallback onMore; + + @override + State createState() => _HomeFeedHeaderState(); +} + +class _HomeFeedHeaderState extends State { + _FeedTab _tab = _FeedTab.recommend; + bool _refreshing = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 4), + child: Row( + children: [ + _RefreshButton( + tooltip: widget.l10n.retry, + dimension: _topIconSize, + glyphSize: _topIconGlyph, + refreshing: _refreshing, + onTap: _refresh, + onLongPress: widget.onMore, + ), + Expanded( + child: Center( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var index = 0; index < _FeedTab.values.length; index++) + _tabSlot(_FeedTab.values[index], index), + ], + ), + ), + ), + ), + _TopIconButton( + key: const Key('home-feed-search'), + tooltip: widget.l10n.search, + icon: Icons.search_rounded, + dimension: _topIconSize, + glyphSize: _topIconGlyph, + onPressed: widget.onSearch, + ), + ], + ), + ); + } + + Widget _tabSlot(_FeedTab tab, int index) { + final item = _FeedTabItem( + tab: tab, + label: tab.label(widget.l10n), + selected: _tab == tab, + fontSize: _tabFontSize, + underlineWidth: _underlineWidth, + underlineHeight: _underlineHeight, + onTap: () => _selectTab(tab), + ); + if (index == 0) return item; + return Padding( + padding: const EdgeInsets.only(left: _tabGap), + child: item, + ); + } + + void _selectTab(_FeedTab tab) { + if (tab.comingSoon) { + _showComingSoon(); + return; + } + setState(() => _tab = tab); + } + + Future _refresh() async { + if (_refreshing) return; + setState(() => _refreshing = true); + await widget.onRefresh(); + if (mounted) setState(() => _refreshing = false); + } + + void _showComingSoon() { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(widget.l10n.comingSoon))); + } +} + +/// 顶部频道 tab:漫剧点击提示即将上线。 +enum _FeedTab { + anime, + real, + recommend; + + bool get comingSoon => this == _FeedTab.anime; + + String get keyName => switch (this) { + _FeedTab.anime => 'anime', + _FeedTab.real => 'real', + _FeedTab.recommend => 'recommend', + }; + + String label(AppLocalizations l10n) => switch (this) { + _FeedTab.anime => l10n.feedTabAnime, + _FeedTab.real => l10n.feedTabReal, + _FeedTab.recommend => l10n.feedTabForYou, + }; +} + +class _TopIconButton extends StatelessWidget { + const _TopIconButton({ + super.key, + required this.tooltip, + required this.icon, + required this.dimension, + required this.glyphSize, + required this.onPressed, + }); + + final String tooltip; + final IconData icon; + final double dimension; + final double glyphSize; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: dimension, + child: IconButton( + tooltip: tooltip, + onPressed: onPressed, + padding: EdgeInsets.zero, + icon: Icon( + icon, + size: glyphSize, + color: Colors.white, + shadows: const [Shadow(blurRadius: 6, color: Colors.black)], + ), + ), + ); + } +} + +/// 刷新入口:点击重新加载 feed,长按打开更多(清屏等)。 +class _RefreshButton extends StatelessWidget { + const _RefreshButton({ + required this.tooltip, + required this.dimension, + required this.glyphSize, + required this.refreshing, + required this.onTap, + required this.onLongPress, + }); + + final String tooltip; + final double dimension; + final double glyphSize; + final bool refreshing; + final VoidCallback onTap; + final VoidCallback onLongPress; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: GestureDetector( + key: const Key('home-feed-refresh'), + behavior: HitTestBehavior.opaque, + onTap: onTap, + onLongPress: onLongPress, + child: SizedBox.square( + dimension: dimension, + child: refreshing + ? const Center( + child: SizedBox.square( + dimension: 18, + child: CircularProgressIndicator( + key: Key('home-feed-refreshing'), + strokeWidth: 2, + color: Colors.white, + ), + ), + ) + : Icon( + Icons.refresh_rounded, + size: glyphSize, + color: Colors.white, + shadows: const [Shadow(blurRadius: 6, color: Colors.black)], + ), + ), + ), + ); + } +} + +class _FeedTabItem extends StatelessWidget { + const _FeedTabItem({ + required this.tab, + required this.label, + required this.selected, + required this.fontSize, + required this.underlineWidth, + required this.underlineHeight, + required this.onTap, + }); + + final _FeedTab tab; + final String label; + final bool selected; + final double fontSize; + final double underlineWidth; + final double underlineHeight; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: Key('home-feed-tab-${tab.keyName}'), + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + maxLines: 1, + style: TextStyle( + color: selected ? Colors.white : Colors.white70, + fontSize: fontSize, + fontWeight: selected ? FontWeight.w800 : FontWeight.w600, + shadows: const [Shadow(blurRadius: 6, color: Colors.black)], + ), + ), + const SizedBox(height: 4), + Container( + width: underlineWidth, + height: underlineHeight, + decoration: BoxDecoration( + color: selected ? Colors.white : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 18fd684..35a3374 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -15,12 +15,6 @@ part 'home_feed_overlay_parts.dart'; const Color _overlayPill = Color(0x66000000); const Color _fullBarBackground = Color(0x4D000000); -const double _topIconSize = 36; -const double _topIconGlyph = 22; -const double _tabGap = 22; -const double _tabFontSize = 15; -const double _underlineWidth = 18; -const double _underlineHeight = 2; const double _titleFontSize = 17; const double _metaFontSize = 12; const double _tagFontSize = 11; @@ -33,7 +27,7 @@ const List _boostTextShadows = [ const int _maxTags = 4; final RegExp _tagSplitter = RegExp(r'[,/、·]'); -/// 首页沉浸式 feed 浮层:顶部频道 tab、右侧互动栏、底部信息与整集进度条。 +/// 首页沉浸式 feed 浮层:手势层、右侧互动栏、底部信息与整集进度条。 class HomeFeedOverlay extends StatefulWidget { const HomeFeedOverlay({ super.key, @@ -49,23 +43,18 @@ class HomeFeedOverlay extends StatefulWidget { required this.onSave, required this.onComments, required this.onShare, - required this.onEpisodes, required this.onDetail, required this.onOpenFullPlayer, - required this.onRefreshFeed, - required this.onDownload, - required this.onClear, - required this.onSearch, - required this.onMore, required this.onTogglePlay, required this.onBoostStart, required this.onBoostEnd, - this.onScrubStart, - this.onSeek, + required this.onMore, this.likeCount, this.favoriteCount, this.commentCount, this.authorNote = '', + this.onScrubStart, + this.onSeek, }); final AppLocalizations l10n; @@ -84,28 +73,20 @@ class HomeFeedOverlay extends StatefulWidget { final VoidCallback onSave; final VoidCallback onComments; final VoidCallback onShare; - final VoidCallback onEpisodes; final VoidCallback onDetail; final VoidCallback onOpenFullPlayer; - final Future Function() onRefreshFeed; - final VoidCallback onDownload; - final VoidCallback onClear; - final VoidCallback onSearch; - final VoidCallback onMore; final VoidCallback onTogglePlay; - final VoidCallback? onScrubStart; - final ValueChanged? onSeek; final VoidCallback onBoostStart; final VoidCallback onBoostEnd; + final VoidCallback onMore; + final VoidCallback? onScrubStart; + final ValueChanged? onSeek; @override State createState() => _HomeFeedOverlayState(); } class _HomeFeedOverlayState extends State { - _FeedTab _tab = _FeedTab.recommend; - bool _refreshing = false; - @override Widget build(BuildContext context) { return SafeArea( @@ -134,7 +115,6 @@ class _HomeFeedOverlayState extends State { else Column( children: [ - _header(context), const Spacer(), if (widget.visible) _bottomInfo(), _fullSeriesBar(), @@ -185,77 +165,6 @@ class _HomeFeedOverlayState extends State { ); } - Future _refresh() async { - if (_refreshing) return; - setState(() => _refreshing = true); - await widget.onRefreshFeed(); - if (mounted) setState(() => _refreshing = false); - } - - Widget _header(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 8, 4), - child: Row( - children: [ - _RefreshButton( - tooltip: widget.l10n.retry, - dimension: _topIconSize, - glyphSize: _topIconGlyph, - refreshing: _refreshing, - onTap: _refresh, - onLongPress: widget.onMore, - ), - Expanded( - child: Center( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var index = 0; index < _FeedTab.values.length; index++) - _tabSlot(_FeedTab.values[index], index), - ], - ), - ), - ), - ), - _TopIconButton( - key: const Key('home-feed-search'), - tooltip: widget.l10n.search, - icon: Icons.search_rounded, - dimension: _topIconSize, - glyphSize: _topIconGlyph, - onPressed: widget.onSearch, - ), - ], - ), - ); - } - - Widget _tabSlot(_FeedTab tab, int index) { - final item = _FeedTabItem( - tab: tab, - label: tab.label(widget.l10n), - selected: _tab == tab, - fontSize: _tabFontSize, - underlineWidth: _underlineWidth, - underlineHeight: _underlineHeight, - onTap: () => _selectTab(tab), - ); - if (index == 0) return item; - return Padding( - padding: const EdgeInsets.only(left: _tabGap), - child: item, - ); - } - - void _selectTab(_FeedTab tab) { - if (tab.comingSoon) { - _showComingSoon(); - return; - } - setState(() => _tab = tab); - } - Widget _rail() { return InteractionRail( items: [ @@ -377,11 +286,7 @@ class _HomeFeedOverlayState extends State { ), ), ), - const Icon( - Icons.chevron_right_rounded, - color: Colors.white, - size: 20, - ), + const Icon(Icons.chevron_right_rounded, color: Colors.white, size: 20), ], ), ); @@ -494,10 +399,4 @@ class _HomeFeedOverlayState extends State { ), ); } - - void _showComingSoon() { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(widget.l10n.comingSoon))); - } } diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index 084fd93..c5cc0fe 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -7,167 +7,6 @@ const double _thumbActiveSize = 14; const double _trackActiveHeight = 6; const double _lineCenterFromBottom = 8; -/// 顶部频道 tab:漫剧点击提示即将上线。 -enum _FeedTab { - anime, - real, - recommend; - - bool get comingSoon => this == _FeedTab.anime; - - String get keyName => switch (this) { - _FeedTab.anime => 'anime', - _FeedTab.real => 'real', - _FeedTab.recommend => 'recommend', - }; - - String label(AppLocalizations l10n) => switch (this) { - _FeedTab.anime => l10n.feedTabAnime, - _FeedTab.real => l10n.feedTabReal, - _FeedTab.recommend => l10n.feedTabForYou, - }; -} - -class _TopIconButton extends StatelessWidget { - const _TopIconButton({ - super.key, - required this.tooltip, - required this.icon, - required this.dimension, - required this.glyphSize, - required this.onPressed, - }); - - final String tooltip; - final IconData icon; - final double dimension; - final double glyphSize; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox.square( - dimension: dimension, - child: IconButton( - tooltip: tooltip, - onPressed: onPressed, - padding: EdgeInsets.zero, - icon: Icon( - icon, - size: glyphSize, - color: Colors.white, - shadows: const [Shadow(blurRadius: 6, color: Colors.black)], - ), - ), - ); - } -} - -/// 刷新入口:点击重新加载 feed,长按仍可打开更多(清屏等)。 -class _RefreshButton extends StatelessWidget { - const _RefreshButton({ - required this.tooltip, - required this.dimension, - required this.glyphSize, - required this.refreshing, - required this.onTap, - required this.onLongPress, - }); - - final String tooltip; - final double dimension; - final double glyphSize; - final bool refreshing; - final VoidCallback onTap; - final VoidCallback onLongPress; - - @override - Widget build(BuildContext context) { - return Tooltip( - message: tooltip, - child: GestureDetector( - key: const Key('home-feed-refresh'), - behavior: HitTestBehavior.opaque, - onTap: onTap, - onLongPress: onLongPress, - child: SizedBox.square( - dimension: dimension, - child: refreshing - ? const Center( - child: SizedBox.square( - dimension: 18, - child: CircularProgressIndicator( - key: Key('home-feed-refreshing'), - strokeWidth: 2, - color: Colors.white, - ), - ), - ) - : Icon( - Icons.refresh_rounded, - size: glyphSize, - color: Colors.white, - shadows: const [Shadow(blurRadius: 6, color: Colors.black)], - ), - ), - ), - ); - } -} - -class _FeedTabItem extends StatelessWidget { - const _FeedTabItem({ - required this.tab, - required this.label, - required this.selected, - required this.fontSize, - required this.underlineWidth, - required this.underlineHeight, - required this.onTap, - }); - - final _FeedTab tab; - final String label; - final bool selected; - final double fontSize; - final double underlineWidth; - final double underlineHeight; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - key: Key('home-feed-tab-${tab.keyName}'), - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - maxLines: 1, - style: TextStyle( - color: selected ? Colors.white : Colors.white70, - fontSize: fontSize, - fontWeight: selected ? FontWeight.w800 : FontWeight.w600, - shadows: const [Shadow(blurRadius: 6, color: Colors.black)], - ), - ), - const SizedBox(height: 4), - Container( - width: underlineWidth, - height: underlineHeight, - decoration: BoxDecoration( - color: selected ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(1), - ), - ), - ], - ), - ); - } -} - /// 整集条底部的进度线:20px 手势区内可点按/拖动 seek。 class _FeedProgressBar extends StatefulWidget { const _FeedProgressBar({ diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index b281dfc..17224fc 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -15,6 +15,7 @@ import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; import 'package:video_player/video_player.dart'; @@ -118,13 +119,8 @@ void main() { onSave: () {}, onComments: () {}, onShare: () {}, - onEpisodes: () {}, onDetail: () {}, onOpenFullPlayer: () {}, - onRefreshFeed: () async => true, - onDownload: () {}, - onClear: () {}, - onSearch: () {}, onMore: () {}, ), ), @@ -156,10 +152,7 @@ void main() { ); }); - testWidgets('should_refresh_seek_and_open_player_from_feed_actions', ( - tester, - ) async { - var refreshed = 0; + testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async { var opened = 0; Duration? seeked; final controller = ValueNotifier( @@ -206,16 +199,8 @@ void main() { onSave: () {}, onComments: () {}, onShare: () {}, - onEpisodes: () {}, onDetail: () {}, onOpenFullPlayer: () => opened++, - onRefreshFeed: () async { - refreshed++; - return true; - }, - onDownload: () {}, - onClear: () {}, - onSearch: () {}, onMore: () {}, onScrubStart: () {}, onSeek: (value) => seeked = value, @@ -260,10 +245,6 @@ void main() { ), ); - await tester.tap(find.byKey(const Key('home-feed-refresh'))); - await tester.pump(); - expect(refreshed, 1); - final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); @@ -321,42 +302,12 @@ void main() { localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( - body: HomeFeedOverlay( + body: HomeFeedHeader( l10n: AppLocalizations.of(context)!, - drama: const Drama( - id: 'refresh-drama', - title: 'Refresh Drama', - synopsis: 'synopsis', - posterUrl: '', - totalEpisodes: 8, - ), - episode: const Episode( - id: 'refresh-episode', - number: 1, - title: 'Episode 1', - isFree: true, - ), - controller: null, - liked: false, - saved: false, - visible: true, - boosting: false, - onTogglePlay: () {}, - onBoostStart: () {}, - onBoostEnd: () {}, - onLike: () {}, - onSave: () {}, - onComments: () {}, - onShare: () {}, - onEpisodes: () {}, - onDetail: () {}, - onOpenFullPlayer: () {}, - onRefreshFeed: () { + onRefresh: () { calls++; return completer.future; }, - onDownload: () {}, - onClear: () {}, onSearch: () {}, onMore: () {}, ), @@ -427,13 +378,8 @@ void main() { onSave: () {}, onComments: () {}, onShare: () {}, - onEpisodes: () {}, onDetail: () {}, onOpenFullPlayer: () {}, - onRefreshFeed: () async => true, - onDownload: () {}, - onClear: () {}, - onSearch: () {}, onMore: () {}, onScrubStart: () {}, onSeek: (value) => seeked = value, @@ -444,7 +390,6 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-feed-refresh')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index bff3ca7..e4f86c5 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -170,12 +170,14 @@ void main() { expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-title')), findsNothing); expect(find.byKey(const Key('rail-save')), findsNothing); + expect(find.byKey(const Key('home-feed-search')), findsNothing); await gesture.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-speed-boost-hint')), findsNothing); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); expect(find.byKey(const Key('rail-save')), findsOneWidget); + expect(find.byKey(const Key('home-feed-search')), findsOneWidget); }); testWidgets('should_resume_playback_when_boosting_while_paused', ( @@ -243,6 +245,19 @@ void main() { expect(after, isNot(before)); }); + testWidgets('should_keep_feed_header_fixed_when_swiping', (tester) async { + await _pumpApp(tester); + final before = tester.getRect(find.byKey(const Key('home-feed-search'))); + + await tester.drag(find.byType(PageView).first, const Offset(0, -600)); + await tester.pumpAndSettle(); + + final after = tester.getRect(find.byKey(const Key('home-feed-search'))); + expect(after.top, closeTo(before.top, 0.5)); + expect(find.byKey(const Key('home-feed-tab-recommend')), findsOneWidget); + expect(find.byKey(const Key('home-feed-refresh')), findsOneWidget); + }); + testWidgets('should_resume_feed_after_returning_from_full_player', ( tester, ) async {