fix(player): 独立全屏路由复用播放会话
This commit is contained in:
1 parent
23e80dd637
commit
9d2d3b97a1
4 files changed
+219
-103
No files matched your search
@@ -55,6 +55,20 @@ const double _playerLoadingSize = 24;
|
|||||||
const double _playerLoadingStrokeWidth = 2;
|
const double _playerLoadingStrokeWidth = 2;
|
||||||
const double _feedRefreshTopGap = 56;
|
const double _feedRefreshTopGap = 56;
|
||||||
|
|
||||||
|
class PlayerPageHandoff {
|
||||||
|
PlayerPageHandoff({
|
||||||
|
required this.logic,
|
||||||
|
required this.initialIndex,
|
||||||
|
required this.prepareSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PlayerLogic logic;
|
||||||
|
final int initialIndex;
|
||||||
|
final Future<void> Function() prepareSource;
|
||||||
|
PageController? destinationController;
|
||||||
|
VoidCallback? activateDestination;
|
||||||
|
}
|
||||||
|
|
||||||
class PlayerPage extends StatefulWidget {
|
class PlayerPage extends StatefulWidget {
|
||||||
const PlayerPage({
|
const PlayerPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -82,6 +96,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
this.onChannelSelected,
|
this.onChannelSelected,
|
||||||
this.wallet,
|
this.wallet,
|
||||||
this.downloads,
|
this.downloads,
|
||||||
|
this.handoff,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Drama drama;
|
final Drama drama;
|
||||||
@@ -108,6 +123,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
final ValueChanged<HomeFeedChannel>? onChannelSelected;
|
final ValueChanged<HomeFeedChannel>? onChannelSelected;
|
||||||
final WalletController? wallet;
|
final WalletController? wallet;
|
||||||
final DownloadController? downloads;
|
final DownloadController? downloads;
|
||||||
|
final PlayerPageHandoff? handoff;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
@@ -116,6 +132,8 @@ class PlayerPage extends StatefulWidget {
|
|||||||
class _PlayerPageState extends State<PlayerPage>
|
class _PlayerPageState extends State<PlayerPage>
|
||||||
with WidgetsBindingObserver, RouteAware {
|
with WidgetsBindingObserver, RouteAware {
|
||||||
late final PlayerLogic logic;
|
late final PlayerLogic logic;
|
||||||
|
late final bool _ownsLogic;
|
||||||
|
late PageController _pageController;
|
||||||
late final PlaybackThumbnailService _thumbnailService;
|
late final PlaybackThumbnailService _thumbnailService;
|
||||||
late final SpriteThumbnailAxisService _spriteAxisService;
|
late final SpriteThumbnailAxisService _spriteAxisService;
|
||||||
PageRoute<dynamic>? _subscribedRoute;
|
PageRoute<dynamic>? _subscribedRoute;
|
||||||
@@ -128,7 +146,11 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
DateTime? _lastDoubleTapAt;
|
DateTime? _lastDoubleTapAt;
|
||||||
DateTime? _swipeStartedAt;
|
DateTime? _swipeStartedAt;
|
||||||
bool _swipePrewarmCaptured = false;
|
bool _swipePrewarmCaptured = false;
|
||||||
bool _fullPlayerPresented = false;
|
bool _presentationActive = false;
|
||||||
|
bool _presentationSuspended = false;
|
||||||
|
bool _presentationReturning = false;
|
||||||
|
bool _leavingHandoff = false;
|
||||||
|
PageController? _returnPageController;
|
||||||
final Set<String> _precachedPosterUrls = {};
|
final Set<String> _precachedPosterUrls = {};
|
||||||
|
|
||||||
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
||||||
@@ -138,13 +160,17 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
logic = PlayerBinding().create(
|
final handoff = widget.handoff;
|
||||||
history: context.read<WatchHistoryRepository>(),
|
_ownsLogic = handoff == null;
|
||||||
playback: context.read<PlaybackRepository>(),
|
logic =
|
||||||
positionCache: context.read<PlaybackPositionCache>(),
|
handoff?.logic ??
|
||||||
wallet: widget.wallet,
|
PlayerBinding().create(
|
||||||
downloads: widget.downloads,
|
history: context.read<WatchHistoryRepository>(),
|
||||||
);
|
playback: context.read<PlaybackRepository>(),
|
||||||
|
positionCache: context.read<PlaybackPositionCache>(),
|
||||||
|
wallet: widget.wallet,
|
||||||
|
downloads: widget.downloads,
|
||||||
|
);
|
||||||
logic.attach(widget, isMounted: () => mounted);
|
logic.attach(widget, isMounted: () => mounted);
|
||||||
_thumbnailService = PlaybackThumbnailService(
|
_thumbnailService = PlaybackThumbnailService(
|
||||||
sourceResolver: _resolveThumbnailSource,
|
sourceResolver: _resolveThumbnailSource,
|
||||||
@@ -152,10 +178,20 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
_spriteAxisService = SpriteThumbnailAxisService(spriteCacheCapacity: 8)
|
_spriteAxisService = SpriteThumbnailAxisService(spriteCacheCapacity: 8)
|
||||||
..addListener(_handleSpriteFrameChanged);
|
..addListener(_handleSpriteFrameChanged);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
logic.start(
|
if (handoff == null) {
|
||||||
factory: context.read<PlayerFactory>(),
|
logic.start(
|
||||||
coordinator: context.read<PlaybackCoordinator>(),
|
factory: context.read<PlayerFactory>(),
|
||||||
);
|
coordinator: context.read<PlaybackCoordinator>(),
|
||||||
|
);
|
||||||
|
_pageController = logic.pages;
|
||||||
|
} else {
|
||||||
|
final position = logic.visibleIndices.indexOf(handoff.initialIndex);
|
||||||
|
_pageController = PageController(
|
||||||
|
initialPage: position < 0 ? 0 : position,
|
||||||
|
);
|
||||||
|
handoff.destinationController = _pageController;
|
||||||
|
logic.session.setRouteVisible(widget.active);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -258,13 +294,13 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didPushNext() {
|
void didPushNext() {
|
||||||
if (_fullPlayerPresented) return;
|
if (_presentationActive) return;
|
||||||
logic.onRouteCovered();
|
logic.onRouteCovered();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didPopNext() {
|
void didPopNext() {
|
||||||
if (_fullPlayerPresented) return;
|
if (_presentationActive) return;
|
||||||
logic.onRouteRevealed();
|
logic.onRouteRevealed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +312,11 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
..removeListener(_handleSpriteFrameChanged)
|
..removeListener(_handleSpriteFrameChanged)
|
||||||
..dispose();
|
..dispose();
|
||||||
_thumbnailService.dispose();
|
_thumbnailService.dispose();
|
||||||
logic.dispose();
|
if (_ownsLogic) {
|
||||||
|
logic.dispose();
|
||||||
|
} else {
|
||||||
|
_pageController.dispose();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +553,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlayer(BuildContext context) {
|
Widget _buildPlayer(BuildContext context) {
|
||||||
|
if (_presentationSuspended) return const SizedBox.expand();
|
||||||
|
if (_presentationReturning) return _buildPresentationReturnLayer();
|
||||||
final interactions = context.watch<InteractionController>();
|
final interactions = context.watch<InteractionController>();
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final indices = logic.visibleIndices;
|
final indices = logic.visibleIndices;
|
||||||
@@ -523,7 +565,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
final pages = NotificationListener<ScrollNotification>(
|
final pages = NotificationListener<ScrollNotification>(
|
||||||
onNotification: _handlePageScroll,
|
onNotification: _handlePageScroll,
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
controller: logic.pages,
|
controller: _pageController,
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
allowImplicitScrolling: true,
|
allowImplicitScrolling: true,
|
||||||
physics: widget.onRefreshFeed == null
|
physics: widget.onRefreshFeed == null
|
||||||
@@ -563,7 +605,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
final pageLayer = widget.onRefreshFeed == null
|
final pageLayer = widget.onRefreshFeed == null
|
||||||
? pages
|
? pages
|
||||||
: FeedPullRefresh(
|
: FeedPullRefresh(
|
||||||
pageController: logic.pages,
|
pageController: _pageController,
|
||||||
topInset:
|
topInset:
|
||||||
MediaQuery.paddingOf(context).top + _feedRefreshTopGap,
|
MediaQuery.paddingOf(context).top + _feedRefreshTopGap,
|
||||||
onRefresh: _refreshHomeFeed,
|
onRefresh: _refreshHomeFeed,
|
||||||
@@ -573,7 +615,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
PlayerRenderStack(
|
PlayerRenderStack(
|
||||||
pageController: logic.pages,
|
pageController: _pageController,
|
||||||
slots: _renderSlots(indices),
|
slots: _renderSlots(indices),
|
||||||
),
|
),
|
||||||
pageLayer,
|
pageLayer,
|
||||||
@@ -620,7 +662,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
final player = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
key: const Key('player-system-overlay'),
|
key: const Key('player-system-overlay'),
|
||||||
value: SystemUiOverlayStyle.light.copyWith(
|
value: SystemUiOverlayStyle.light.copyWith(
|
||||||
statusBarColor: AppColors.transparent,
|
statusBarColor: AppColors.transparent,
|
||||||
@@ -641,6 +683,14 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (widget.handoff == null) return player;
|
||||||
|
return PopScope<void>(
|
||||||
|
canPop: _leavingHandoff,
|
||||||
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
if (!didPop) unawaited(_leaveHandoff(context));
|
||||||
|
},
|
||||||
|
child: player,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openCurrentMore() {
|
void _openCurrentMore() {
|
||||||
@@ -701,6 +751,18 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPresentationReturnLayer() {
|
||||||
|
final indices = logic.visibleIndices;
|
||||||
|
final controller = _returnPageController ?? _pageController;
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: logic.session,
|
||||||
|
builder: (context, _) => PlayerRenderStack(
|
||||||
|
pageController: controller,
|
||||||
|
slots: _renderSlots(indices),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
PlayerRenderSlotData _renderSlot(
|
PlayerRenderSlotData _renderSlot(
|
||||||
PlayerRenderSlotRole role,
|
PlayerRenderSlotRole role,
|
||||||
int? index,
|
int? index,
|
||||||
@@ -933,7 +995,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
if (logic.pip || logic.pipPreparing)
|
if (logic.pip || logic.pipPreparing)
|
||||||
const SizedBox.expand()
|
const SizedBox.expand()
|
||||||
else if (widget.homeMode && !_fullPlayerPresented)
|
else if (widget.homeMode && !_presentationActive)
|
||||||
HomeFeedOverlay(
|
HomeFeedOverlay(
|
||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
drama: drama,
|
drama: drama,
|
||||||
@@ -1024,9 +1086,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
Widget _standardPlaybackOverlay(
|
Widget _standardPlaybackOverlay(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int index,
|
int index,
|
||||||
InteractionController interactions, {
|
InteractionController interactions,
|
||||||
bool presentation = false,
|
) {
|
||||||
}) {
|
|
||||||
if (logic.pip || logic.pipPreparing) return const SizedBox.expand();
|
if (logic.pip || logic.pipPreparing) return const SizedBox.expand();
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final entry = logic.queue.entryAt(index);
|
final entry = logic.queue.entryAt(index);
|
||||||
@@ -1067,8 +1128,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
scrubbing: _scrubbing,
|
scrubbing: _scrubbing,
|
||||||
actions: PlayerOverlayActions(
|
actions: PlayerOverlayActions(
|
||||||
onBack: presentation
|
onBack: widget.handoff != null
|
||||||
? () => Navigator.of(context).pop()
|
? () => unawaited(_leaveHandoff(context))
|
||||||
: widget.embedded
|
: widget.embedded
|
||||||
? null
|
? null
|
||||||
: () => unawaited(logic.leave(context)),
|
: () => unawaited(logic.leave(context)),
|
||||||
@@ -1098,56 +1159,82 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
onBoostEnd: _endBoost,
|
onBoostEnd: _endBoost,
|
||||||
),
|
),
|
||||||
controlsVisible: _chromeVisible,
|
controlsVisible: _chromeVisible,
|
||||||
onVerticalDragUpdate: presentation
|
|
||||||
? _handleFullPlayerDragUpdate
|
|
||||||
: null,
|
|
||||||
onVerticalDragEnd: presentation ? _handleFullPlayerDragEnd : null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _presentFullPlayer(BuildContext context, int index) async {
|
Future<void> _presentFullPlayer(BuildContext context, int index) async {
|
||||||
if (_fullPlayerPresented || !logic.pages.hasClients) return;
|
if (_presentationActive || !_pageController.hasClients) return;
|
||||||
// The route owns presentation only; PlaybackSession keeps the controller.
|
final sourcePageController = _pageController;
|
||||||
final interactions = context.read<InteractionController>();
|
final handoff = PlayerPageHandoff(
|
||||||
|
logic: logic,
|
||||||
|
initialIndex: index,
|
||||||
|
prepareSource: _prepareSourceAfterFullPlayer,
|
||||||
|
);
|
||||||
|
handoff.activateDestination = () {
|
||||||
|
final destination = handoff.destinationController;
|
||||||
|
if (destination != null) logic.pages = destination;
|
||||||
|
};
|
||||||
_logFullPlayerRoute('present');
|
_logFullPlayerRoute('present');
|
||||||
setState(() => _fullPlayerPresented = true);
|
setState(() => _presentationActive = true);
|
||||||
await Navigator.of(context).push<void>(
|
final route = Navigator.of(context).push<void>(
|
||||||
PageRouteBuilder<void>(
|
MaterialPageRoute<void>(
|
||||||
opaque: false,
|
builder: (_) => PlayerPage(
|
||||||
barrierColor: null,
|
drama: logic.queue.entryAt(index).drama,
|
||||||
barrierDismissible: false,
|
initialIndex: index,
|
||||||
transitionDuration: const Duration(milliseconds: 180),
|
handoff: handoff,
|
||||||
reverseTransitionDuration: const Duration(milliseconds: 160),
|
wallet: logic.wallet,
|
||||||
pageBuilder: (routeContext, _, _) => Material(
|
downloads: logic.downloads,
|
||||||
color: AppColors.transparent,
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: AppColors.transparent,
|
|
||||||
body: ListenableBuilder(
|
|
||||||
listenable: logic,
|
|
||||||
builder: (context, _) => AnimatedBuilder(
|
|
||||||
animation: logic.session,
|
|
||||||
builder: (context, _) {
|
|
||||||
final current = logic.session.currentIndex;
|
|
||||||
return _standardPlaybackOverlay(
|
|
||||||
routeContext,
|
|
||||||
current,
|
|
||||||
interactions,
|
|
||||||
presentation: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
transitionsBuilder: (context, animation, _, child) =>
|
|
||||||
FadeTransition(opacity: animation, child: child),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _presentationSuspended = true);
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
}
|
||||||
|
sourcePageController.dispose();
|
||||||
|
handoff.activateDestination?.call();
|
||||||
|
await route;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _fullPlayerPresented = false);
|
final currentPage = logic.visibleIndices.indexOf(
|
||||||
|
logic.session.currentIndex,
|
||||||
|
);
|
||||||
|
final restoredPage =
|
||||||
|
_returnPageController ??
|
||||||
|
PageController(initialPage: currentPage < 0 ? 0 : currentPage);
|
||||||
|
logic.pages = restoredPage;
|
||||||
|
_pageController = restoredPage;
|
||||||
|
logic.attach(widget, isMounted: () => mounted);
|
||||||
|
setState(() {
|
||||||
|
_presentationActive = false;
|
||||||
|
_presentationSuspended = false;
|
||||||
|
_presentationReturning = false;
|
||||||
|
});
|
||||||
|
logic.session.setRouteVisible(widget.active);
|
||||||
_logFullPlayerRoute('restored');
|
_logFullPlayerRoute('restored');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _prepareSourceAfterFullPlayer() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
final page = logic.visibleIndices.indexOf(logic.session.currentIndex);
|
||||||
|
_returnPageController = PageController(
|
||||||
|
initialPage: page < 0 ? 0 : page,
|
||||||
|
);
|
||||||
|
logic.attach(widget, isMounted: () => mounted);
|
||||||
|
setState(() {
|
||||||
|
_presentationSuspended = false;
|
||||||
|
_presentationReturning = true;
|
||||||
|
});
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _leaveHandoff(BuildContext context) async {
|
||||||
|
if (_leavingHandoff) return;
|
||||||
|
setState(() => _leavingHandoff = true);
|
||||||
|
await widget.handoff?.prepareSource();
|
||||||
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
void _logFullPlayerRoute(String phase) {
|
void _logFullPlayerRoute(String phase) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
final active = logic.session.active;
|
final active = logic.session.active;
|
||||||
@@ -1161,32 +1248,4 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleFullPlayerDragUpdate(DragUpdateDetails details) {
|
|
||||||
if (!logic.pages.hasClients) return;
|
|
||||||
final position = logic.pages.position;
|
|
||||||
final next = (position.pixels - details.delta.dy).clamp(
|
|
||||||
position.minScrollExtent,
|
|
||||||
position.maxScrollExtent,
|
|
||||||
);
|
|
||||||
if ((next - position.pixels).abs() < 0.5) return;
|
|
||||||
position.jumpTo(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleFullPlayerDragEnd(DragEndDetails details) {
|
|
||||||
if (!logic.pages.hasClients || logic.visibleIndices.isEmpty) return;
|
|
||||||
final position = logic.pages.position;
|
|
||||||
final page = position.pixels / position.viewportDimension;
|
|
||||||
final velocity = details.primaryVelocity ?? 0;
|
|
||||||
var target = page.round();
|
|
||||||
if (velocity < -260) target = page.floor() + 1;
|
|
||||||
if (velocity > 260) target = page.ceil() - 1;
|
|
||||||
target = target.clamp(0, logic.visibleIndices.length - 1);
|
|
||||||
unawaited(
|
|
||||||
logic.pages.animateToPage(
|
|
||||||
target,
|
|
||||||
duration: const Duration(milliseconds: 220),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,6 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
required this.actions,
|
required this.actions,
|
||||||
required this.controlsVisible,
|
required this.controlsVisible,
|
||||||
required this.scrubbing,
|
required this.scrubbing,
|
||||||
this.onVerticalDragUpdate,
|
|
||||||
this.onVerticalDragEnd,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 展示数据。
|
/// 展示数据。
|
||||||
@@ -51,8 +49,6 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
/// 正在拖动进度条。拖动期间清屏,只留进度条。
|
/// 正在拖动进度条。拖动期间清屏,只留进度条。
|
||||||
final bool scrubbing;
|
final bool scrubbing;
|
||||||
final GestureDragUpdateCallback? onVerticalDragUpdate;
|
|
||||||
final GestureDragEndCallback? onVerticalDragEnd;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -69,8 +65,6 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
_handleLongPressStart(details, constraints.maxWidth),
|
_handleLongPressStart(details, constraints.maxWidth),
|
||||||
onLongPressEnd: (_) => actions.onBoostEnd(),
|
onLongPressEnd: (_) => actions.onBoostEnd(),
|
||||||
onLongPressCancel: actions.onBoostEnd,
|
onLongPressCancel: actions.onBoostEnd,
|
||||||
onVerticalDragUpdate: onVerticalDragUpdate,
|
|
||||||
onVerticalDragEnd: onVerticalDragEnd,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:fruitshorts/pages/auth/auth_view.dart';
|
|||||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
|
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/player_render_stack.dart';
|
import 'package:fruitshorts/pages/player/widgets/player_render_stack.dart';
|
||||||
|
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
|
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
@@ -104,13 +105,15 @@ void main() {
|
|||||||
expect(recommendPlayer, findsOneWidget);
|
expect(recommendPlayer, findsOneWidget);
|
||||||
expect(renderStack, findsOneWidget);
|
expect(renderStack, findsOneWidget);
|
||||||
final playerState = tester.state(recommendPlayer);
|
final playerState = tester.state(recommendPlayer);
|
||||||
final renderState = tester.state(renderStack);
|
|
||||||
final currentSurface = find.descendant(
|
final currentSurface = find.descendant(
|
||||||
of: recommendPlayer,
|
of: recommendPlayer,
|
||||||
matching: find.byKey(
|
matching: find.byKey(
|
||||||
const ValueKey('player-render-slot-current'),
|
const ValueKey('player-render-slot-current'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final sourceRender = tester.widget<PlayerRenderStack>(renderStack);
|
||||||
|
final sourceIndex =
|
||||||
|
sourceRender.slots[PlayerRenderSlotRole.current]?.index;
|
||||||
final controller = factory.created.first.controller;
|
final controller = factory.created.first.controller;
|
||||||
final createdBefore = factory.created.length;
|
final createdBefore = factory.created.length;
|
||||||
final requestsBefore = playback.requests.length;
|
final requestsBefore = playback.requests.length;
|
||||||
@@ -126,19 +129,37 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
|
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
|
||||||
|
expect(fullPlayer.homeMode, isFalse);
|
||||||
|
expect(fullPlayer.handoff, isNotNull);
|
||||||
|
expect(fullPlayer.handoff!.initialIndex, sourceIndex);
|
||||||
|
expect(fullPlayer.handoff!.logic.session.currentIndex, sourceIndex);
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('home-feed-gesture')), findsNothing);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
expect(factory.created.length, createdBefore);
|
expect(factory.created.length, createdBefore);
|
||||||
expect(playback.requests.length, requestsBefore);
|
expect(playback.requests.length, requestsBefore);
|
||||||
expect(controller.value.position, const Duration(seconds: 5));
|
expect(controller.value.position, const Duration(seconds: 5));
|
||||||
|
final fullCurrentSurface = find.descendant(
|
||||||
|
of: find.byType(PlayerPage),
|
||||||
|
matching: find.byKey(
|
||||||
|
const ValueKey('player-render-slot-current'),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: currentSurface,
|
of: fullCurrentSurface,
|
||||||
matching: find.byKey(const Key('player-single-placeholder')),
|
matching: find.byKey(const Key('player-single-placeholder')),
|
||||||
),
|
),
|
||||||
findsNothing,
|
findsNothing,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerVideoSurface>(fullCurrentSurface).controller,
|
||||||
|
same(controller),
|
||||||
|
);
|
||||||
|
|
||||||
tester.state<NavigatorState>(find.byType(Navigator).first).pop();
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
@@ -147,8 +168,9 @@ void main() {
|
|||||||
expect(factory.created.length, createdBefore);
|
expect(factory.created.length, createdBefore);
|
||||||
expect(playback.requests.length, requestsBefore);
|
expect(playback.requests.length, requestsBefore);
|
||||||
expect(controller.value.position, const Duration(seconds: 5));
|
expect(controller.value.position, const Duration(seconds: 5));
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget);
|
||||||
expect(tester.state(recommendPlayer), same(playerState));
|
expect(tester.state(recommendPlayer), same(playerState));
|
||||||
expect(tester.state(renderStack), same(renderState));
|
|
||||||
expect(find.byKey(const Key('recommend-feed-skeleton')), findsNothing);
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsNothing);
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
@@ -157,6 +179,10 @@ void main() {
|
|||||||
),
|
),
|
||||||
findsNothing,
|
findsNothing,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerVideoSurface>(currentSurface).controller,
|
||||||
|
same(controller),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_swipe_full_player_without_creating_another_session', (
|
testWidgets('should_swipe_full_player_without_creating_another_session', (
|
||||||
@@ -179,6 +205,35 @@ void main() {
|
|||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
expect(factory.created.length, lessThanOrEqualTo(createdBefore + 1));
|
expect(factory.created.length, lessThanOrEqualTo(createdBefore + 1));
|
||||||
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
|
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
|
||||||
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_show_one_paused_overlay_when_entering_full_player', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final factory = _RouteVideoFactory();
|
||||||
|
await _pumpApp(tester, playerFactory: factory.create);
|
||||||
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home-feed-gesture')));
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
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(find.byType(PlayerPage), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('home-feed-center-play')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_render_player_loading_indicators_at_compact_size', (
|
testWidgets('should_render_player_loading_indicators_at_compact_size', (
|
||||||
|
|||||||
@@ -474,10 +474,11 @@ void main() {
|
|||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
||||||
isTrue,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
tester.state<NavigatorState>(find.byType(Navigator).first).pop();
|
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
||||||
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
|
||||||
@@ -485,7 +486,14 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
||||||
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_pull_refresh_recommend_feed_from_the_first_video', (
|
testWidgets('should_pull_refresh_recommend_feed_from_the_first_video', (
|
||||||
|
|||||||
Reference in new issue
Block a user