@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user