fix: prevent key event leaks from video player
This commit is contained in:
@@ -109,6 +109,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
late final FocusNode _playNextCancelFocusNode;
|
||||
late final FocusNode _playNextConfirmFocusNode;
|
||||
|
||||
// Screen-level focus node: persists across loading/initialized phases so
|
||||
// key events never escape the video player route.
|
||||
late final FocusNode _screenFocusNode;
|
||||
|
||||
// App lifecycle state tracking
|
||||
bool _wasPlayingBeforeInactive = false;
|
||||
|
||||
@@ -146,6 +150,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_playNextCancelFocusNode = FocusNode(debugLabel: 'PlayNextCancel');
|
||||
_playNextConfirmFocusNode = FocusNode(debugLabel: 'PlayNextConfirm');
|
||||
|
||||
// Screen-level focus node that wraps the entire build output.
|
||||
// Ensures a single stable focus target across loading → initialized phases.
|
||||
_screenFocusNode = FocusNode(debugLabel: 'VideoPlayerScreen');
|
||||
_screenFocusNode.addListener(_onScreenFocusChanged);
|
||||
|
||||
appLogger.d('VideoPlayerScreen initialized for: ${widget.metadata.title}');
|
||||
if (widget.preferredAudioTrack != null) {
|
||||
appLogger.d(
|
||||
@@ -1138,6 +1147,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_playNextCancelFocusNode.dispose();
|
||||
_playNextConfirmFocusNode.dispose();
|
||||
|
||||
// Dispose screen-level focus node
|
||||
_screenFocusNode.removeListener(_onScreenFocusChanged);
|
||||
_screenFocusNode.dispose();
|
||||
|
||||
// Clear media controls and dispose manager
|
||||
_mediaControlsManager?.clear();
|
||||
_mediaControlsManager?.dispose();
|
||||
@@ -1186,6 +1199,20 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// When focus leaves the entire video player subtree, reclaim it.
|
||||
/// `_screenFocusNode.hasFocus` is true when the node itself OR any
|
||||
/// descendant has focus, so internal movement between child controls
|
||||
/// does NOT trigger this.
|
||||
void _onScreenFocusChanged() {
|
||||
if (!_screenFocusNode.hasFocus && mounted && !_isExiting.value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && !_isExiting.value && !_screenFocusNode.hasFocus) {
|
||||
_screenFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlayingStateChanged(bool isPlaying) {
|
||||
// Toggle wakelock based on playback state
|
||||
if (isPlaying) {
|
||||
@@ -1606,20 +1633,30 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoadingSpinner() {
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(child: CircularProgressIndicator(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Show loading indicator while player initializes
|
||||
if (!_isPlayerInitialized || player == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) => KeyEventResult.handled,
|
||||
child: const Center(child: CircularProgressIndicator(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Screen-level Focus wraps ALL phases (loading + initialized).
|
||||
// - autofocus: grabs focus when no deeper child claims it.
|
||||
// - onKeyEvent: catch-all that consumes any event children didn't handle,
|
||||
// preventing leaks to previous routes.
|
||||
return Focus(
|
||||
focusNode: _screenFocusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) => KeyEventResult.handled,
|
||||
child: _isPlayerInitialized && player != null
|
||||
? _buildVideoPlayer(context)
|
||||
: _buildLoadingSpinner(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoPlayer(BuildContext context) {
|
||||
// Cache platform detection to avoid multiple calls
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
|
||||
|
||||
@@ -1295,7 +1295,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Let event continue to other handlers
|
||||
return true; // Consume all events while video player is active
|
||||
}
|
||||
|
||||
/// Show controls and optionally focus play/pause on keyboard input (desktop only)
|
||||
|
||||
Reference in New Issue
Block a user