From 70e14c4871b00bd3784ceeff9e9bb7e689e3d3c8 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 28 Apr 2026 23:49:46 +0300 Subject: [PATCH] refactor: improve navigation --- src/App/GamepadModal/GamepadDiagram.tsx | 19 +++++++++-- src/App/GamepadModal/GamepadModal.tsx | 9 +++--- src/routes/Player/Player.js | 10 ++++-- .../GamepadContext/GamepadProvider.tsx | 32 +++++++++++++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/App/GamepadModal/GamepadDiagram.tsx b/src/App/GamepadModal/GamepadDiagram.tsx index 3747e68ed..fa89154fe 100644 --- a/src/App/GamepadModal/GamepadDiagram.tsx +++ b/src/App/GamepadModal/GamepadDiagram.tsx @@ -9,6 +9,7 @@ type ActiveButton = string | null; const CX = 400; const BTN = { L1: 'L1', L2: 'L2', R1: 'R1', R2: 'R2' }; +const ARROW = { UP: '↑', DOWN: '↓', LEFT: '←', RIGHT: '→' }; const GamepadDiagram = () => { const { t } = useTranslation(); @@ -30,6 +31,7 @@ const GamepadDiagram = () => { gamepad?.on('buttonLT', 'gamepad-diagram', flash('l1')); gamepad?.on('buttonRT', 'gamepad-diagram', flash('r1')); gamepad?.on('analog', 'gamepad-diagram', (dir: string) => flash('stick-' + dir)()); + gamepad?.on('analogRight', 'gamepad-diagram', (dir: string) => flash('rstick-' + dir)()); return () => { clearTimeout(timeout); @@ -40,6 +42,7 @@ const GamepadDiagram = () => { gamepad?.off('buttonLT', 'gamepad-diagram'); gamepad?.off('buttonRT', 'gamepad-diagram'); gamepad?.off('analog', 'gamepad-diagram'); + gamepad?.off('analogRight', 'gamepad-diagram'); }; }, [gamepad]); @@ -168,8 +171,14 @@ const GamepadDiagram = () => { {/* ===== RIGHT STICK (CX+STX) ===== */} - - + + + + {ARROW.UP} + {ARROW.DOWN} + {ARROW.LEFT} + {ARROW.RIGHT} + {/* ============================= */} {/* ===== LABELS — LEFT ===== */} @@ -184,7 +193,6 @@ const GamepadDiagram = () => { {t('GAMEPAD_ACTION_NAVIGATE')} - {t('GAMEPAD_LABEL_STICK_PLAYER')} {/* □ Square */} @@ -216,6 +224,11 @@ const GamepadDiagram = () => { {t('GAMEPAD_ACTION_SELECT')} {t('GAMEPAD_LABEL_PLAY_PAUSE_PLAYER')} + {/* Right stick */} + + + {t('GAMEPAD_LABEL_SEEK_VOL')} + {/* Compat note */} {t('GAMEPAD_LABEL_COMPAT')} diff --git a/src/App/GamepadModal/GamepadModal.tsx b/src/App/GamepadModal/GamepadModal.tsx index c3f68940c..4a6cd0c0a 100644 --- a/src/App/GamepadModal/GamepadModal.tsx +++ b/src/App/GamepadModal/GamepadModal.tsx @@ -14,6 +14,7 @@ const CIRCLE = '○'; const TRIANGLE = '△'; const SQUARE = '□'; const L_STICK = 'L stick'; +const R_STICK = 'R stick'; const L1 = 'L1'; const R1 = 'R1'; const LEFT = '←'; @@ -108,22 +109,22 @@ const GamepadModal = ({ onClose }: Props) => { {t('GAMEPAD_ACTION_PLAY_PAUSE')}
- {L_STICK} + {R_STICK} {LEFT} {t('GAMEPAD_ACTION_SEEK_BACK')}
- {L_STICK} + {R_STICK} {RIGHT} {t('GAMEPAD_ACTION_SEEK_FWD')}
- {L_STICK} + {R_STICK} {UP} {t('GAMEPAD_ACTION_VOL_UP')}
- {L_STICK} + {R_STICK} {DOWN} {t('GAMEPAD_ACTION_VOL_DOWN')}
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 382447f0f..dee1c1983 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,6 +8,7 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices, useGamepad } = require('stremio/services'); +const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation'); const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common'); const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); @@ -60,6 +61,7 @@ const Player = ({ urlParams, queryParams }) => { }); const playbackDevices = React.useMemo(() => streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [], [streamingServer]); + const playerRef = React.useRef(null); const bufferingRef = React.useRef(); const errorRef = React.useRef(); @@ -444,13 +446,15 @@ const Player = ({ urlParams, queryParams }) => { } }, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]); + useContentGamepadNavigation(playerRef, GAMEPAD_HANDLER_ID); + React.useEffect(() => { gamepad?.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause); - gamepad?.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol); + gamepad?.on('analogRight', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol); return () => { gamepad?.off('buttonA', GAMEPAD_HANDLER_ID); - gamepad?.off('analog', GAMEPAD_HANDLER_ID); + gamepad?.off('analogRight', GAMEPAD_HANDLER_ID); }; }, [onPlayPause, onGamepadSeekAndVol]); @@ -969,7 +973,7 @@ const Player = ({ urlParams, queryParams }) => { }, []); return ( -
([]); const lastButtonPressedTime = useRef(0); const axisTimer = useRef(0); + const axisTimerRight = useRef(0); const eventHandlers = useRef(new Map()); const on = useCallback((event: string, id: string, callback: (data?: any) => void) => { @@ -170,6 +171,37 @@ const GamepadProvider: React.FC<{ } if (axisHandled) axisTimer.current = currentTime; + + let rightAxisHandled = false; + + if (controller.axes.length > 2) { + if (controller.axes[2] < -deadZone) { + if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[2]) * 2000)) { + emit('analogRight', 'left'); + rightAxisHandled = true; + } + } + if (controller.axes[2] > deadZone) { + if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[2]) * 2000)) { + emit('analogRight', 'right'); + rightAxisHandled = true; + } + } + if (controller.axes[3] < -deadZone) { + if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[3]) * 2000)) { + emit('analogRight', 'up'); + rightAxisHandled = true; + } + } + if (controller.axes[3] > deadZone) { + if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[3]) * 2000)) { + emit('analogRight', 'down'); + rightAxisHandled = true; + } + } + } + + if (rightAxisHandled) axisTimerRight.current = currentTime; }); } animationFrameId = requestAnimationFrame(updateStatus);