diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index a8cef065..81ff4678 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -339,13 +339,13 @@ class _PlexVideoControlsState extends State with WindowListen }); } - /// Listen to playback state changes to manage auto-hide timer on iOS/mobile + /// Listen to playback state changes to manage auto-hide timer void _listenToPlayingState() { _playingSubscription = widget.player.streams.playing.listen((isPlaying) { if (isPlaying && _showControls) { _startHideTimer(); - } else if (!isPlaying) { - _hideTimer?.cancel(); + } else if (!isPlaying && _showControls) { + _startPausedHideTimer(); } }); } @@ -606,6 +606,32 @@ class _PlexVideoControlsState extends State with WindowListen // Lag during resize is now handled in native code (glViewport + resize signal handler) } + /// Controls hide delay: 5s on mobile/TV/keyboard-nav, 3s on desktop with mouse. + Duration get _hideDelay { + final isMobile = PlatformDetector.isMobile(context) && !PlatformDetector.isTV(); + if (isMobile || PlatformDetector.isTV() || _videoPlayerNavigationEnabled) { + return const Duration(seconds: 5); + } + return const Duration(seconds: 3); + } + + /// Shared hide logic: hides controls, notifies parent, updates traffic lights, restores focus. + void _hideControls() { + if (!mounted || !_showControls) return; + setState(() { + _showControls = false; + }); + widget.controlsVisible?.value = false; + if (Platform.isMacOS) { + _updateTrafficLightVisibility(); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + }); + } + void _startHideTimer() { _hideTimer?.cancel(); @@ -615,31 +641,24 @@ class _PlexVideoControlsState extends State with WindowListen // Only auto-hide if playing if (widget.player.state.playing) { - _hideTimer = Timer(const Duration(seconds: 3), () { + _hideTimer = Timer(_hideDelay, () { // Also check hasFirstFrame in callback (in case it changed) final stillLoading = !(widget.hasFirstFrame?.value ?? true); if (mounted && widget.player.state.playing && !stillLoading) { - setState(() { - _showControls = false; - }); - // Notify parent of visibility change (for popup positioning) - widget.controlsVisible?.value = false; - // Hide traffic lights on macOS when controls auto-hide - if (Platform.isMacOS) { - _updateTrafficLightVisibility(); - } - // Restore focus to the main node so keyboard shortcuts keep working - // after FocusScope(canRequestFocus: false) ejects child focus. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && !_focusNode.hasFocus) { - _focusNode.requestFocus(); - } - }); + _hideControls(); } }); } } + /// Auto-hide controls after pause (does not check playing state in callback). + void _startPausedHideTimer() { + _hideTimer?.cancel(); + _hideTimer = Timer(_hideDelay, () { + _hideControls(); + }); + } + /// Restart the hide timer on user interaction (if video is playing) void _restartHideTimerIfPlaying() { if (widget.player.state.playing) { @@ -647,6 +666,15 @@ class _PlexVideoControlsState extends State with WindowListen } } + /// Hide controls immediately when the mouse leaves the player area (desktop only). + void _hideControlsFromPointerExit() { + final isMobile = PlatformDetector.isMobile(context) && !PlatformDetector.isTV(); + if (isMobile) return; + + _hideTimer?.cancel(); + _hideControls(); + } + /// Show controls in response to pointer activity (mouse/trackpad movement). void _showControlsFromPointerActivity() { if (!_showControls) { @@ -1533,6 +1561,7 @@ class _PlexVideoControlsState extends State with WindowListen child: MouseRegion( cursor: _showControls ? SystemMouseCursors.basic : SystemMouseCursors.none, onHover: (_) => _showControlsFromPointerActivity(), + onExit: (_) => _hideControlsFromPointerExit(), child: Stack( children: [ // Keep-alive: 1px widget that continuously repaints to prevent