fix(player): restore OSD auto-hide

close #1360
This commit is contained in:
edde746
2026-06-18 10:18:57 +02:00
parent 52d70dd97d
commit 075e4aeb32
9 changed files with 82 additions and 18 deletions
+5 -4
View File
@@ -62,14 +62,15 @@ KeyEventResult handleBackKeyAction(KeyEvent event, VoidCallback onBack) {
return KeyEventResult.handled;
}
// AppleTV physical-keyboard back (Siri Remote Menu via engine-synthesized
// escape): run onBack on KeyDown only; consume KeyUp silently. The
// suppressor-based "arm-on-KeyDown, clear-on-KeyUp" pattern leaks here
// AppleTV back (Siri Remote Menu via engine-synthesized escape): run onBack
// on KeyDown only; consume KeyUp silently. Some engine paths report Menu as
// a non-keyboard device, but the same down-only handling is still required.
// The suppressor-based "arm-on-KeyDown, clear-on-KeyUp" pattern leaks here
// because onBack typically calls Navigator.pop, swapping the focus tree
// before the matching KeyUp is dispatched — the orphaned KeyUp then never
// reaches a consumeIfSuppressed call, pinning the suppressor armed and
// silently swallowing the next press's KeyDown.
if (PlatformDetector.isAppleTV() && event.isPhysicalKeyboardEvent) {
if (PlatformDetector.isAppleTV()) {
if (event is KeyDownEvent) {
BackKeyCoordinator.markHandled();
onBack();
@@ -108,7 +108,7 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
if (widget.chromeController.contentStripVisible) {
_desktopControlsKey.currentState?.dismissContentStrip();
widget.chromeController.setContentStripVisible(false);
_restartHideTimerIfPlaying();
_restartHideTimerForCurrentPlaybackState();
return;
}
_hideControls();
@@ -214,7 +214,7 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
if (widget.chromeController.contentStripVisible) {
_desktopControlsKey.currentState?.dismissContentStrip();
widget.chromeController.setContentStripVisible(false);
_restartHideTimerIfPlaying();
_restartHideTimerForCurrentPlaybackState();
return;
}
_hideControls();
@@ -243,7 +243,7 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
// Reset hide timer on any keyboard/controller input when controls are visible.
if (_showControls) {
_restartHideTimerIfPlaying();
_restartHideTimerForCurrentPlaybackState();
}
final key = event.logicalKey;
@@ -11,7 +11,7 @@ extension _PlexVideoControlsNavigationMethods on _PlexVideoControlsState {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerIfPlaying(),
onPointerDown: (_) => _restartHideTimerForCurrentPlaybackState(),
child: DesktopVideoControls(
key: _desktopControlsKey,
player: widget.player,
@@ -33,7 +33,7 @@ extension _PlexVideoControlsNavigationMethods on _PlexVideoControlsState {
onSeekRequested: widget.onSeekRequested,
getReplayIcon: getReplayIcon,
getForwardIcon: getForwardIcon,
onFocusActivity: _restartHideTimerIfPlaying,
onFocusActivity: _restartHideTimerForCurrentPlaybackState,
onHideControls: _hideControlsFromKeyboard,
trackControlsState: trackControlsState,
onBack: widget.onBack,
@@ -371,7 +371,7 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
_edgeAdjustmentStartValue = null;
_lastEdgeAdjustmentWriteAt = null;
_lastEdgeAdjustmentWriteValue = null;
_restartHideTimerIfPlaying();
_restartHideTimerForCurrentPlaybackState();
_edgeAdjustmentIndicatorHideTimer?.cancel();
_edgeAdjustmentIndicatorClearTimer?.cancel();
if (_edgeAdjustmentIndicator.value.side == null) return;
@@ -63,8 +63,8 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
void _startHideTimer() => widget.chromeController.startAutoHide();
/// Restart the hide timer on user interaction (if video is playing)
void _restartHideTimerIfPlaying() => widget.chromeController.restartAutoHideIfPlaying();
/// Restart the hide timer on user interaction for the current playback state.
void _restartHideTimerForCurrentPlaybackState() => widget.chromeController.restartAutoHideForCurrentPlaybackState();
void _handlePointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent && _keyboardService != null) {
@@ -89,7 +89,7 @@ class PlayerChromeController extends ChangeNotifier implements ValueListenable<b
shouldNotify = true;
}
if (shouldNotify) notifyListeners();
if (restartAutoHide) startAutoHide();
if (restartAutoHide) _startAutoHideForCurrentPlaybackState();
}
PlayerChromeFocusTarget? takeFocusTarget() {
@@ -126,7 +126,7 @@ class PlayerChromeController extends ChangeNotifier implements ValueListenable<b
_lastPointerActivityMs = nowMs;
show(restartAutoHide: false);
restartAutoHideIfPlaying();
_startAutoHideForCurrentPlaybackState();
return true;
}
@@ -156,9 +156,7 @@ class PlayerChromeController extends ChangeNotifier implements ValueListenable<b
}
}
void restartAutoHideIfPlaying() {
if (_playing) startAutoHide();
}
void restartAutoHideForCurrentPlaybackState() => _startAutoHideForCurrentPlaybackState();
void hideForPointerExit() {
if (_holds.contains(PlayerChromeHold.pip)) return;
@@ -798,7 +798,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
behavior: HitTestBehavior.translucent,
onPointerDown: (_) {
if (!widget.chromeController.contentStripVisible) {
_restartHideTimerIfPlaying();
_restartHideTimerForCurrentPlaybackState();
}
},
child: Builder(
+31
View File
@@ -41,6 +41,37 @@ void main() {
expect(downResult, KeyEventResult.handled);
expect(upResult, KeyEventResult.handled);
expect(backs, 1);
await tester.pump();
});
testWidgets('tvOS remote back runs on key down for non-keyboard device types', (tester) async {
TvDetectionService.debugSetAppleTVOverride(true);
var backs = 0;
final downResult = handleBackKeyAction(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.escape,
logicalKey: LogicalKeyboardKey.escape,
timeStamp: Duration.zero,
deviceType: ui.KeyEventDeviceType.directionalPad,
),
() => backs++,
);
final upResult = handleBackKeyAction(
const KeyUpEvent(
physicalKey: PhysicalKeyboardKey.escape,
logicalKey: LogicalKeyboardKey.escape,
timeStamp: Duration.zero,
deviceType: ui.KeyEventDeviceType.directionalPad,
),
() => backs++,
);
expect(downResult, KeyEventResult.handled);
expect(upResult, KeyEventResult.handled);
expect(backs, 1);
await tester.pump();
});
group('dpadKeyHandler trapHorizontalEdges', () {
@@ -71,6 +71,40 @@ void main() {
expect(controller.controlsVisible, isFalse);
});
testWidgets('show while paused restarts paused auto-hide timer', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.setPlaying(false);
await tester.pump(const Duration(milliseconds: 50));
controller.show();
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
testWidgets('pointer activity while paused restarts paused auto-hide timer', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.setPlaying(false);
await tester.pump(const Duration(milliseconds: 50));
expect(controller.recordPointerActivity(), isTrue);
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
test('show stores focus target and notifies even when already visible', () {
final controller = PlayerChromeController();
addTearDown(controller.dispose);