diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 146e1f51..5fe69e32 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -171,6 +171,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin // key events never escape the video player route. late final FocusNode _screenFocusNode; + // Cached setting: when false on Windows/Linux, ESC should not exit the player + bool _videoPlayerNavigationEnabled = false; + // App lifecycle state tracking bool _wasPlayingBeforeInactive = false; @@ -393,6 +396,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { // Load buffer size from settings final settingsService = await SettingsService.getInstance(); + _videoPlayerNavigationEnabled = settingsService.getVideoPlayerNavigationEnabled(); final bufferSizeMB = settingsService.getBufferSize(); final enableHardwareDecoding = settingsService.getEnableHardwareDecoding(); final debugLoggingEnabled = settingsService.getEnableDebugLogging(); @@ -2327,7 +2331,13 @@ class VideoPlayerScreenState extends State with WidgetsBindin canRequestFocus: isCurrentRoute, onKeyEvent: (node, event) { if (!isCurrentRoute) return KeyEventResult.ignored; - // Back keys always pass through — handled by PopScope (system back + // On Windows/Linux with navigation off, consume ESC so Flutter's + // DismissAction doesn't trigger a route pop. The video controls' + // global key handler manages fullscreen/controls toggle instead. + if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) { + return KeyEventResult.handled; + } + // Back keys pass through — handled by PopScope (system back // gesture) or overlay sheet's onKeyEvent. if (event.logicalKey.isBackKey) return KeyEventResult.ignored; // Self-heal: if this node itself has primary focus (no descendant diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index d9017c55..0864f22c 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -14,6 +14,7 @@ import 'package:flutter/services.dart' PhysicalKeyboardKey, KeyEvent, KeyDownEvent, + KeyUpEvent, HardwareKeyboard; import '../../services/fullscreen_state_manager.dart'; import '../../services/macos_window_service.dart'; @@ -1288,6 +1289,14 @@ class _PlexVideoControlsState extends State with WindowListen } } + /// Exit fullscreen if the window is actually fullscreen (async check). + /// Used by ESC handler on Windows/Linux to avoid relying on _isFullscreen flag. + Future _exitFullscreenIfNeeded() async { + if (await windowManager.isFullScreen()) { + await FullscreenStateManager().exitFullscreen(); + } + } + /// Initialize always-on-top state from window manager (desktop only) Future _initAlwaysOnTopState() async { final isOnTop = await windowManager.isAlwaysOnTop(); @@ -1370,8 +1379,9 @@ class _PlexVideoControlsState extends State with WindowListen bool _handleGlobalKeyEvent(KeyEvent event) { if (!mounted) return false; - // TV back key fallback — Focus.onKeyEvent won't fire if _focusNode lost focus - if (PlatformDetector.isTV() && event.logicalKey.isBackKey) { + // Back key fallback when _focusNode lost focus (TV, or desktop with nav on). + // Focus.onKeyEvent won't fire if _focusNode lost focus, so handle ESC here. + if ((_videoPlayerNavigationEnabled || PlatformDetector.isTV()) && event.logicalKey.isBackKey) { if (!_focusNode.hasFocus) { // Skip if an overlay sheet is open — the sheet's FocusScope handles // back keys via its own onKeyEvent. Without this check, this global @@ -1407,6 +1417,20 @@ class _PlexVideoControlsState extends State with WindowListen // (e.g. after controls auto-hide). The !hasFocus guard prevents // double-handling when the Focus onKeyEvent already processes the event. if (!_focusNode.hasFocus && _keyboardService != null) { + // On Windows/Linux with navigation off, ESC only exits fullscreen — + // never exits the player. Intercept before the keyboard shortcuts + // service which would call onBack and pop the route. + // Skip if an overlay sheet is open — let the sheet handle ESC. + if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) { + final sheetOpen = OverlaySheetController.maybeOf(context)?.isOpen ?? false; + if (!sheetOpen) { + if (event is KeyUpEvent) { + _exitFullscreenIfNeeded(); + } + _focusNode.requestFocus(); + return true; + } + } final result = _keyboardService!.handleVideoPlayerKeyEvent( event, widget.player, @@ -1521,12 +1545,16 @@ class _PlexVideoControlsState extends State with WindowListen focusNode: _focusNode, autofocus: true, onKeyEvent: (node, event) { - final backResult = handleBackKeyAction(event, () { - // On Windows/Linux with navigation off, ESC first exits fullscreen - if (!_videoPlayerNavigationEnabled && _isFullscreen && (Platform.isWindows || Platform.isLinux)) { - _toggleFullscreen(); - return; + // On Windows/Linux with navigation off, ESC only exits fullscreen — + // never exits the player. Consume all back key events and check + // actual window state asynchronously. + if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) { + if (event is KeyUpEvent) { + _exitFullscreenIfNeeded(); } + return KeyEventResult.handled; + } + final backResult = handleBackKeyAction(event, () { if (!_showControls) { _showControlsWithFocus(); return;