diff --git a/CHANGELOG.md b/CHANGELOG.md index 92223dc30..8284cfff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- Video: switching to PiP when going home with gesture navigation - Viewer: multi-page context update when removing burst entries - prevent editing item when Exif editing changes mime type - parsing videos with skippable boxes in `meta` box diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index ad6a1836d..a9fe93396 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -61,6 +61,11 @@ class AvesApp extends StatefulWidget { static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); + // children widgets registering as `WidgetsBinding` observers and implementing `didChangeAppLifecycleState` + // do not receive events fast enough for time sensitive actions (like PiP when leaving by gesture to home) + // so we use this notifier to propagate events as soon as received by the top widget `AvesApp` + static final ValueNotifier lifecycleStateNotifier = ValueNotifier(AppLifecycleState.detached); + // do not monitor all `ModalRoute`s, which would include popup menus, // so that we can react to fullscreen `PageRoute`s only static final RouteObserver pageRouteObserver = RouteObserver(); @@ -364,6 +369,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { reportService.log('Lifecycle ${state.name}'); + AvesApp.lifecycleStateNotifier.value = state; switch (state) { case AppLifecycleState.inactive: switch (_appModeNotifier.value) { diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index fa6f3d39f..16b93149f 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -62,7 +62,7 @@ class EntryViewerStack extends StatefulWidget { State createState() => _EntryViewerStackState(); } -class _EntryViewerStackState extends State with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver { +class _EntryViewerStackState extends State with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin { final Floating _floating = Floating(); late int _currentEntryIndex; late ValueNotifier _currentVerticalPage; @@ -155,7 +155,7 @@ class _EntryViewerStackState extends State with EntryViewContr ); initEntryControllers(entry); _registerWidget(widget); - WidgetsBinding.instance.addObserver(this); + AvesApp.lifecycleStateNotifier.addListener(_onAppLifecycleStateChanged); WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); } @@ -178,7 +178,7 @@ class _EntryViewerStackState extends State with EntryViewContr _verticalPager.dispose(); _heroInfoNotifier.dispose(); _stopOverlayHidingTimer(); - WidgetsBinding.instance.removeObserver(this); + AvesApp.lifecycleStateNotifier.removeListener(_onAppLifecycleStateChanged); _unregisterWidget(widget); super.dispose(); } @@ -191,33 +191,6 @@ class _EntryViewerStackState extends State with EntryViewContr widget.collection?.removeListener(_onCollectionChanged); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.inactive: - _onAppInactive(); - break; - case AppLifecycleState.paused: - case AppLifecycleState.detached: - pauseVideoControllers(); - break; - case AppLifecycleState.resumed: - availability.onResume(); - break; - } - } - - Future _onAppInactive() async { - viewerController.autopilot = false; - bool enabledPip = false; - if (settings.videoBackgroundMode == VideoBackgroundMode.pip) { - enabledPip |= await _enablePictureInPicture(); - } - if (!enabledPip) { - await pauseVideoControllers(); - } - } - @override Widget build(BuildContext context) { final viewStateConductor = context.read(); @@ -292,6 +265,36 @@ class _EntryViewerStackState extends State with EntryViewContr ); } + void _onAppLifecycleStateChanged() { + switch (AvesApp.lifecycleStateNotifier.value) { + case AppLifecycleState.inactive: + _onAppInactive(); + break; + case AppLifecycleState.paused: + case AppLifecycleState.detached: + pauseVideoControllers(); + break; + case AppLifecycleState.resumed: + availability.onResume(); + break; + } + } + + Future _onAppInactive() async { + final playingController = context.read().getPlayingController(); + viewerController.autopilot = false; + bool enabledPip = false; + if (settings.videoBackgroundMode == VideoBackgroundMode.pip) { + enabledPip |= await _enablePictureInPicture(); + } + if (enabledPip) { + // ensure playback, in case lifecycle paused/resumed events happened when switching to PiP + await playingController?.play(); + } else { + await pauseVideoControllers(); + } + } + Widget _decorateOverlay(Widget overlay) { return ValueListenableBuilder( valueListenable: _overlayAnimationController,