diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 8d11fa75..af1e2c68 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -46,6 +46,7 @@ import '../utils/video_player_navigation.dart'; import '../widgets/video_controls/video_controls.dart'; import '../focus/focusable_wrapper.dart'; import '../focus/input_mode_tracker.dart'; +import '../focus/dpad_navigator.dart'; import '../focus/key_event_utils.dart'; import '../i18n/strings.g.dart'; import '../watch_together/providers/watch_together_provider.dart'; @@ -1726,7 +1727,26 @@ class VideoPlayerScreenState extends State with WidgetsBindin focusNode: _screenFocusNode, autofocus: isCurrentRoute, canRequestFocus: isCurrentRoute, - onKeyEvent: (node, event) => isCurrentRoute ? KeyEventResult.handled : KeyEventResult.ignored, + onKeyEvent: (node, event) { + if (!isCurrentRoute) return KeyEventResult.ignored; + // Safety net: if this screen-level node itself has primary focus + // (no descendant focused, e.g. after controls auto-hide), self-heal. + if (node.hasPrimaryFocus) { + // Handle BACK immediately so the user is never stuck. + final backResult = handleBackKeyAction(event, _handleBackButton); + if (backResult != KeyEventResult.ignored) return backResult; + // Redirect focus to the first traversable descendant (video controls) + // and show controls immediately so the first key press isn't swallowed. + if (event.isActionable) { + _controlsVisible.value = true; + final descendants = node.traversalDescendants; + if (descendants.isNotEmpty) { + descendants.first.requestFocus(); + } + } + } + return KeyEventResult.handled; + }, child: _isPlayerInitialized && player != null ? _buildVideoPlayer(context) : _buildLoadingSpinner(), ); } diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 8ee3f85e..bc8b7774 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -260,6 +260,8 @@ class _PlexVideoControlsState extends State with WindowListen HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); // Listen for first frame to start auto-hide timer widget.hasFirstFrame?.addListener(_onFirstFrameReady); + // Listen for external requests to show controls (e.g. screen-level focus recovery) + widget.controlsVisible?.addListener(_onControlsVisibleExternal); } /// Called when hasFirstFrame changes - start auto-hide timer when first frame is ready @@ -269,6 +271,14 @@ class _PlexVideoControlsState extends State with WindowListen } } + /// Called when controlsVisible is set externally (e.g. screen-level focus recovery + /// after controls auto-hide ejects focus on Android TV). + void _onControlsVisibleExternal() { + if (widget.controlsVisible?.value == true && !_showControls && mounted) { + _showControlsWithFocus(); + } + } + /// Focus play/pause button if we're in keyboard navigation mode (desktop/TV only) void _focusPlayPauseIfKeyboardMode() { if (!mounted) return; @@ -515,6 +525,7 @@ class _PlexVideoControlsState extends State with WindowListen @override void dispose() { HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); + widget.controlsVisible?.removeListener(_onControlsVisibleExternal); widget.hasFirstFrame?.removeListener(_onFirstFrameReady); _hideTimer?.cancel(); _feedbackTimer?.cancel();