diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index a03c011..5d2bb9d 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; @@ -32,10 +33,7 @@ class HomePage extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return Theme( data: AppTheme.light(), - child: DecoratedBox( - decoration: const BoxDecoration(gradient: AppTheme.pageGradient), - child: _buildHome(context, catalog, l10n), - ), + child: _buildHome(context, catalog, l10n), ); } @@ -136,7 +134,8 @@ class _HomeTabsState extends State<_HomeTabs> { Widget _scaffold(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return PopScope( + final feed = logic.tab == 0; + final content = PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; @@ -144,11 +143,21 @@ class _HomeTabsState extends State<_HomeTabs> { }, child: Scaffold( key: logic.scaffoldKey, - backgroundColor: logic.tab == 0 ? Colors.black : Colors.transparent, + backgroundColor: Colors.transparent, body: _body(context), bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n), ), ); + return AnnotatedRegion( + value: feed ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, + child: feed + ? content + : DecoratedBox( + key: const Key('home-page-gradient'), + decoration: const BoxDecoration(gradient: AppTheme.pageGradient), + child: content, + ), + ); } Widget _body(BuildContext context) { diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 4d3ff00..5cc01b9 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -75,7 +75,7 @@ class PlayerPage extends StatefulWidget { final List? feedDramas; final ValueChanged? onDramaChanged; final ValueChanged? onImmersiveChanged; - final void Function(Drama drama, Episode episode)? onOpenFullPlayer; + final Future Function(Drama drama, Episode episode)? onOpenFullPlayer; final VoidCallback? onRefreshFeed; final WalletController? wallet; final DownloadController? downloads; @@ -415,8 +415,11 @@ class _PlayerPageState extends State with WidgetsBindingObserver { onShare: () => logic.shareDrama(context, drama), onEpisodes: () => logic.showEpisodes(context), onDetail: () => unawaited(logic.openDetail(context)), - onOpenFullPlayer: () => - widget.onOpenFullPlayer?.call(drama, episode), + onOpenFullPlayer: () async { + await widget.onOpenFullPlayer?.call(drama, episode); + if (!mounted || !widget.active) return; + logic.session.setUserWantsPlay(true); + }, onRefreshFeed: () => widget.onRefreshFeed?.call(), onDownload: () => requestEpisodeDownload(context, episode.id), onClear: () => setState(() => _chromeVisible = false), diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 939109f..7f6d5ad 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -14,7 +14,6 @@ import 'interaction_rail.dart'; part 'home_feed_overlay_parts.dart'; const Color _overlayPill = Color(0x66000000); -const Color _fullBarColor = Color(0xCC000000); const double _topIconSize = 36; const double _topIconGlyph = 22; const double _tabGap = 22; @@ -24,7 +23,7 @@ const double _underlineHeight = 2; const double _titleFontSize = 17; const double _metaFontSize = 12; const double _tagFontSize = 11; -const double _fullBarHeight = 48; +const double _fullBarHeight = 44; const double _progressHeight = 2; const double _progressHitHeight = 20; const int _maxTags = 4; @@ -96,56 +95,25 @@ class _HomeFeedOverlayState extends State { Widget build(BuildContext context) { return SafeArea( bottom: false, - child: Stack( + child: Column( children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: IgnorePointer(child: _gradient(top: true)), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: IgnorePointer(child: _gradient(top: false)), - ), - Column( - children: [ - _header(context), - Expanded( - child: Align( - alignment: const Alignment(1.0, 0.45), - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 10, 16), - child: _rail(), - ), - ), + _header(context), + Expanded( + child: Align( + alignment: const Alignment(1.0, 0.45), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 10, 16), + child: _rail(), ), - _bottomInfo(), - _fullSeriesBar(), - ], + ), ), + _bottomInfo(), + _fullSeriesBar(), ], ), ); } - Widget _gradient({required bool top}) { - return Container( - height: top ? 140 : 360, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: top ? Alignment.topCenter : Alignment.bottomCenter, - end: top ? Alignment.bottomCenter : Alignment.topCenter, - colors: top - ? const [Color(0xB3000000), Color(0x00000000)] - : const [Color(0xE6000000), Color(0x00000000)], - ), - ), - ); - } - Widget _header(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 4), @@ -386,41 +354,42 @@ class _HomeFeedOverlayState extends State { return GestureDetector( key: const Key('home-feed-full-series'), onTap: widget.onOpenFullPlayer, - child: Container( + child: SizedBox( height: _fullBarHeight, - color: _fullBarColor, child: Stack( children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Row( - children: [ - const Icon( - Icons.play_arrow_rounded, - color: Colors.white, - size: 14, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - widget.l10n.watchFullSeries( - '${widget.drama.episodeCount}', - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w600, + Positioned.fill( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + const Icon( + Icons.play_arrow_rounded, + color: Colors.white, + size: 14, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + widget.l10n.watchFullSeries( + '${widget.drama.episodeCount}', + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w600, + ), ), ), - ), - const Icon( - Icons.chevron_right_rounded, - color: Colors.white, - size: 18, - ), - ], + const Icon( + Icons.chevron_right_rounded, + color: Colors.white, + size: 18, + ), + ], + ), ), ), Positioned( diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index 8e2485e..d1214d7 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -2,6 +2,9 @@ part of 'home_feed_overlay.dart'; +const double _thumbSize = 6; +const double _thumbActiveSize = 10; + /// 顶部频道 tab:漫剧点击提示即将上线。 enum _FeedTab { anime, @@ -208,10 +211,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { _update(details.localPosition.dx / width), onHorizontalDragEnd: (_) => _commit(_preview), onHorizontalDragCancel: () => setState(() => _preview = null), - child: SizedBox( - height: widget.hitHeight, - child: Align(alignment: Alignment.bottomCenter, child: track), - ), + child: track, ); }, ); @@ -233,15 +233,43 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { } Widget _track(double fraction, double width) { + final active = _preview != null; + final size = active ? _thumbActiveSize : _thumbSize; + final left = (width * fraction - size / 2) + .clamp(0.0, width - size) + .toDouble(); return SizedBox( - height: widget.height, + height: widget.hitHeight, child: Stack( + clipBehavior: Clip.none, children: [ - Container(width: width, height: widget.height, color: Colors.white24), - Container( - width: width * fraction, - height: widget.height, - color: AppColors.blue, + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container(height: widget.height, color: Colors.white24), + ), + Positioned( + left: 0, + bottom: 0, + child: Container( + width: width * fraction, + height: widget.height, + color: AppColors.blue, + ), + ), + Positioned( + left: left, + bottom: 0, + child: Container( + key: const Key('home-feed-progress-thumb'), + width: size, + height: size, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), ), ], ), diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index cc195f9..4ac0487 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -209,17 +209,38 @@ void main() { ); await tester.pumpAndSettle(); + final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); + expect(bar.height, 44); + expect( + tester.getRect(find.textContaining('Watch full series')).center.dy, + closeTo(bar.center.dy, 2), + ); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), + const Size(6, 6), + ); + await tester.tap(find.byKey(const Key('home-feed-refresh'))); await tester.pump(); expect(refreshed, 1); - await tester.drag( - find.byKey(const Key('home-feed-progress')), - const Offset(120, 0), + final gesture = await tester.startGesture( + tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); + await gesture.moveBy(const Offset(120, 0)); + await tester.pump(); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), + const Size(10, 10), + ); + await gesture.up(); await tester.pumpAndSettle(); expect(seeked, isNotNull); expect(seeked!.inSeconds, greaterThan(0)); + expect( + tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), + const Size(6, 6), + ); 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 6fcf919..fd5433d 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -1,6 +1,7 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; @@ -70,8 +71,10 @@ void main() { expect(find.byKey(const Key('home-feed-fullscreen')), findsNothing); expect( tester.widget(find.byType(Scaffold).first).backgroundColor, - Colors.black, + Colors.transparent, ); + expect(find.byKey(const Key('home-page-gradient')), findsNothing); + expect(_hasLightStatusBar(tester), isTrue); expect(tester.widget(find.byType(PlayerPage)).homeMode, isTrue); }); @@ -93,6 +96,19 @@ void main() { expect(find.text('Coming soon'), findsOneWidget); }); + testWidgets('should_restore_light_theme_after_leaving_feed_tab', ( + tester, + ) async { + await _pumpApp(tester); + expect(_hasLightStatusBar(tester), isTrue); + + await tester.tap(find.byKey(const Key('tab-theater'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('home-page-gradient')), findsOneWidget); + expect(_hasLightStatusBar(tester), isFalse); + }); + testWidgets('should_switch_feed_drama_when_swiped_up', (tester) async { await _pumpApp(tester); final before = tester @@ -108,6 +124,28 @@ void main() { expect(after, isNot(before)); }); + testWidgets('should_resume_feed_after_returning_from_full_player', ( + tester, + ) async { + await _pumpApp(tester); + await tester.tap(find.byKey(const Key('home-feed-video-toggle'))); + await tester.pump(); + expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget); + + await tester.tap(find.byKey(const Key('home-feed-full-series'))); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(PlayerPage).last).homeMode, + isFalse, + ); + + await tester.tap(find.byKey(const Key('player-back'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(find.byKey(const Key('home-feed-center-play')), findsNothing); + }); + testWidgets('should_hide_feed_when_switching_to_theater', (tester) async { await _pumpApp(tester); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); @@ -196,6 +234,12 @@ const _tabKeys = [ Key('tab-mine'), ]; +bool _hasLightStatusBar(WidgetTester tester) => tester + .widgetList>( + find.byType(AnnotatedRegion), + ) + .any((region) => region.value.statusBarIconBrightness == Brightness.light); + Future _pumpApp( WidgetTester tester, { Size size = const Size(390, 844),