From 82fe237c2478657f589000f24ddc499f5a133261 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 14:11:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=E9=A6=96=E9=A1=B5=20feed=20?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E5=88=B7=E6=96=B0=E4=B8=8E=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E6=9D=A1=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/services/catalog_controller.dart | 13 ++++ lib/pages/home/home_view.dart | 4 +- lib/pages/player/player_view.dart | 10 ++- .../player/widgets/home_feed_overlay.dart | 11 ++- .../widgets/home_feed_overlay_parts.dart | 27 ++++--- .../home/catalog_controller_refresh_test.dart | 72 +++++++++++++++++++ test/home/home_preview_layout_test.dart | 44 ++++++++++++ test/home/k28_a_navigation_test.dart | 26 +++++++ 8 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 test/home/catalog_controller_refresh_test.dart diff --git a/lib/core/data/services/catalog_controller.dart b/lib/core/data/services/catalog_controller.dart index 34cccdb..dfab65e 100644 --- a/lib/core/data/services/catalog_controller.dart +++ b/lib/core/data/services/catalog_controller.dart @@ -44,6 +44,19 @@ class CatalogController extends ChangeNotifier { } } + /// 静默刷新首页聚合:成功替换数据,失败返回 false。 + /// + /// 不触碰 [loading]/[error],因此页面不会切到全屏 loading 或清空旧数据。 + Future refresh() async { + try { + feed = await repository.fetchHomeFeed(); + notifyListeners(); + return true; + } catch (_) { + return false; + } + } + /// 需要完整剧集时补一次详情;已加载过则直接返回,避免重复请求。 Future requireDetail(Drama drama) async { if (drama.detailLoaded) return drama; diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 5d2bb9d..c174448 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -71,7 +71,7 @@ class HomePage extends StatelessWidget { return _HomeTabs( key: ValueKey(catalog.dramas.first.id), dramas: catalog.dramas, - onRefreshFeed: catalog.load, + onRefreshFeed: catalog.refresh, ); } } @@ -103,7 +103,7 @@ class _HomeTabs extends StatefulWidget { }); final List dramas; - final VoidCallback onRefreshFeed; + final Future Function() onRefreshFeed; @override State<_HomeTabs> createState() => _HomeTabsState(); diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 5cc01b9..953e7cc 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -76,7 +76,7 @@ class PlayerPage extends StatefulWidget { final ValueChanged? onDramaChanged; final ValueChanged? onImmersiveChanged; final Future Function(Drama drama, Episode episode)? onOpenFullPlayer; - final VoidCallback? onRefreshFeed; + final Future Function()? onRefreshFeed; final WalletController? wallet; final DownloadController? downloads; @@ -420,7 +420,13 @@ class _PlayerPageState extends State with WidgetsBindingObserver { if (!mounted || !widget.active) return; logic.session.setUserWantsPlay(true); }, - onRefreshFeed: () => widget.onRefreshFeed?.call(), + onRefreshFeed: () async { + final ok = await widget.onRefreshFeed?.call() ?? true; + if (!context.mounted || ok) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.catalogError))); + }, onDownload: () => requestEpisodeDownload(context, episode.id), onClear: () => setState(() => _chromeVisible = false), onSearch: diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 7f6d5ad..d92e950 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -14,6 +14,7 @@ import 'interaction_rail.dart'; 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; @@ -25,7 +26,7 @@ const double _metaFontSize = 12; const double _tagFontSize = 11; const double _fullBarHeight = 44; const double _progressHeight = 2; -const double _progressHitHeight = 20; +const double _progressHitHeight = 24; const int _maxTags = 4; final RegExp _tagSplitter = RegExp(r'[,/、·]'); @@ -100,7 +101,7 @@ class _HomeFeedOverlayState extends State { _header(context), Expanded( child: Align( - alignment: const Alignment(1.0, 0.45), + alignment: const Alignment(1.0, 0.68), child: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 10, 16), child: _rail(), @@ -358,6 +359,12 @@ class _HomeFeedOverlayState extends State { height: _fullBarHeight, child: Stack( children: [ + Positioned.fill( + child: Container( + key: const Key('home-feed-full-series-bg'), + color: _fullBarBackground, + ), + ), Positioned.fill( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14), diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index d1214d7..5e4898a 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -4,6 +4,8 @@ part of 'home_feed_overlay.dart'; const double _thumbSize = 6; const double _thumbActiveSize = 10; +const double _trackActiveHeight = 4; +const double _lineCenterFromBottom = 6; /// 顶部频道 tab:漫剧点击提示即将上线。 enum _FeedTab { @@ -199,10 +201,6 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { return GestureDetector( key: const Key('home-feed-progress'), behavior: HitTestBehavior.opaque, - onTapDown: (details) { - widget.onScrubStart?.call(); - _commit(details.localPosition.dx / width); - }, onHorizontalDragStart: (details) { widget.onScrubStart?.call(); _update(details.localPosition.dx / width); @@ -233,11 +231,14 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { } Widget _track(double fraction, double width) { - final active = _preview != null; - final size = active ? _thumbActiveSize : _thumbSize; + 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) .toDouble(); + final lineBottom = _lineCenterFromBottom - trackHeight / 2; + final thumbBottom = _lineCenterFromBottom - size / 2; return SizedBox( height: widget.hitHeight, child: Stack( @@ -246,21 +247,25 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { Positioned( left: 0, right: 0, - bottom: 0, - child: Container(height: widget.height, color: Colors.white24), + bottom: lineBottom, + child: Container( + key: const Key('home-feed-progress-track'), + height: trackHeight, + color: Colors.white24, + ), ), Positioned( left: 0, - bottom: 0, + bottom: lineBottom, child: Container( width: width * fraction, - height: widget.height, + height: trackHeight, color: AppColors.blue, ), ), Positioned( left: left, - bottom: 0, + bottom: thumbBottom, child: Container( key: const Key('home-feed-progress-thumb'), width: size, diff --git a/test/home/catalog_controller_refresh_test.dart b/test/home/catalog_controller_refresh_test.dart new file mode 100644 index 0000000..87a36f1 --- /dev/null +++ b/test/home/catalog_controller_refresh_test.dart @@ -0,0 +1,72 @@ +// Author: fruit.com.cn + +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/data/models/drama.dart'; +import 'package:fruitshorts/core/data/models/home_feed.dart'; +import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; +import 'package:fruitshorts/core/data/services/catalog_controller.dart'; + +void main() { + test('should_keep_state_when_silent_refresh_fails', () async { + final repository = _FakeCatalog(fail: true); + final controller = CatalogController(repository); + const oldFeed = HomeFeed(); + controller.feed = oldFeed; + + final ok = await controller.refresh(); + + expect(ok, isFalse); + expect(controller.feed, same(oldFeed)); + expect(controller.loading, isFalse); + expect(controller.error, isNull); + }); + + test( + 'should_replace_feed_without_touching_loading_when_refresh_succeeds', + () async { + final repository = _FakeCatalog(); + final controller = CatalogController(repository); + controller.feed = const HomeFeed(); + final next = HomeFeed( + sections: [ + FeedSection(code: 'new', title: 'New', dramas: const [_drama]), + ], + ); + repository.next = next; + + final ok = await controller.refresh(); + + expect(ok, isTrue); + expect(controller.feed, same(next)); + expect(controller.loading, isFalse); + expect(controller.error, isNull); + }, + ); +} + +class _FakeCatalog implements CatalogRepository { + _FakeCatalog({this.fail = false}); + + bool fail; + HomeFeed next = const HomeFeed(); + + @override + Future fetchHomeFeed() async { + if (fail) throw StateError('offline'); + return next; + } + + @override + Future fetchDiscoverFeed() async => const HomeFeed(); + + @override + Future fetchDrama(String dramaId) async => _drama; +} + +const _drama = Drama( + id: 'refresh-drama', + title: 'Refresh Drama', + synopsis: 'synopsis', + posterUrl: '', + detailLoaded: true, +); diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 4ac0487..62f3fd4 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -211,6 +211,12 @@ void main() { final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); expect(bar.height, 44); + expect( + tester + .widget(find.byKey(const Key('home-feed-full-series-bg'))) + .color, + const Color(0x4D000000), + ); expect( tester.getRect(find.textContaining('Watch full series')).center.dy, closeTo(bar.center.dy, 2), @@ -219,6 +225,23 @@ void main() { tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(6, 6), ); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress'))).height, + greaterThanOrEqualTo(24), + ); + expect( + tester + .getRect(find.byKey(const Key('home-feed-progress-thumb'))) + .center + .dy, + closeTo( + tester + .getRect(find.byKey(const Key('home-feed-progress-track'))) + .center + .dy, + 0.5, + ), + ); await tester.tap(find.byKey(const Key('home-feed-refresh'))); await tester.pump(); @@ -233,6 +256,23 @@ void main() { tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(10, 10), ); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, + 4, + ); + expect( + tester + .getRect(find.byKey(const Key('home-feed-progress-thumb'))) + .center + .dy, + closeTo( + tester + .getRect(find.byKey(const Key('home-feed-progress-track'))) + .center + .dy, + 0.5, + ), + ); await gesture.up(); await tester.pumpAndSettle(); expect(seeked, isNotNull); @@ -241,6 +281,10 @@ void main() { tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(6, 6), ); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, + 2, + ); await tester.tap(find.byKey(const Key('home-feed-full-series'))); await tester.pump(); diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index fd5433d..549714d 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -96,6 +96,32 @@ void main() { expect(find.text('Coming soon'), findsOneWidget); }); + testWidgets('should_refresh_feed_without_blank_loading', (tester) async { + await _pumpApp(tester); + + await tester.tap(find.byKey(const Key('home-feed-refresh'))); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + }); + + testWidgets('should_keep_feed_rail_above_bottom_panels', (tester) async { + await _pumpApp(tester); + final railTop = tester.getCenter(find.byKey(const Key('rail-save'))).dy; + final railBottom = tester + .getRect(find.byKey(const Key('rail-share'))) + .bottom; + final infoTop = tester + .getRect(find.byKey(const Key('home-feed-series-meta'))) + .top; + + expect(railTop, greaterThan(844 * 0.35)); + expect(railBottom, lessThanOrEqualTo(infoTop)); + }); + testWidgets('should_restore_light_theme_after_leaving_feed_tab', ( tester, ) async {