diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 787f8ecf1..382447f0f 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -445,12 +445,12 @@ const Player = ({ urlParams, queryParams }) => { }, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]); React.useEffect(() => { - gamepad.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause); - gamepad.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol); + gamepad?.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause); + gamepad?.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol); return () => { - gamepad.off('buttonA', GAMEPAD_HANDLER_ID); - gamepad.off('analog', GAMEPAD_HANDLER_ID); + gamepad?.off('buttonA', GAMEPAD_HANDLER_ID); + gamepad?.off('analog', GAMEPAD_HANDLER_ID); }; }, [onPlayPause, onGamepadSeekAndVol]); diff --git a/src/routes/Settings/Interface/Interface.tsx b/src/routes/Settings/Interface/Interface.tsx index a4b429a56..330d41931 100644 --- a/src/routes/Settings/Interface/Interface.tsx +++ b/src/routes/Settings/Interface/Interface.tsx @@ -16,6 +16,7 @@ const Interface = forwardRef(({ profile }: Props, ref) => quitOnCloseToggle, escExitFullscreenToggle, hideSpoilersToggle, + gamepadSupportToggle, } = useInterfaceOptions(profile); return ( @@ -50,6 +51,12 @@ const Interface = forwardRef(({ profile }: Props, ref) => {...hideSpoilersToggle} /> + ); }); diff --git a/src/services/GamepadContext/GamepadProvider.tsx b/src/services/GamepadContext/GamepadProvider.tsx index a62e3871f..3fc315a08 100644 --- a/src/services/GamepadContext/GamepadProvider.tsx +++ b/src/services/GamepadContext/GamepadProvider.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React, { useEffect, useRef, useState, useCallback } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import useToast from 'stremio/common/Toast/useToast'; import GamepadContext from './GamepadContext'; @@ -10,8 +11,9 @@ const GamepadProvider: React.FC<{ enabled: boolean; children: React.ReactNode; }> = ({ enabled, children }) => { + const { t } = useTranslation(); const toast = useToast(); - const [connectedGamepads, setConnectedGamepads] = useState(0); + const connectedGamepads = useRef(0); const lastButtonState = useRef([]); const lastButtonPressedTime = useRef(0); const axisTimer = useRef(0); @@ -53,7 +55,7 @@ const GamepadProvider: React.FC<{ // @ts-expect-error show() expects no arguments toast.show({ type: 'info', - title: 'Gamepad detected', + title: t('GAMEPAD_CONNECTED'), timeout: 4000, }); }; @@ -62,7 +64,7 @@ const GamepadProvider: React.FC<{ // @ts-expect-error show() expects no arguments toast.show({ type: 'info', - title: 'Gamepad disconnected', + title: t('GAMEPAD_DISCONNECTED'), timeout: 4000, }); }; @@ -82,101 +84,95 @@ const GamepadProvider: React.FC<{ }, [enabled]); useEffect(() => { - if (typeof navigator.getGamepads !== 'function') return; + if (!enabled || typeof navigator.getGamepads !== 'function') return; let animationFrameId: number; - if (enabled) { - const updateStatus = () => { - if (document.hasFocus()) { - const currentTime = Date.now(); - const controllers = Array.from(navigator.getGamepads()).filter( - (gp) => gp !== null - ) as Gamepad[]; + const updateStatus = () => { + if (document.hasFocus()) { + const currentTime = Date.now(); + const controllers = Array.from(navigator.getGamepads()).filter( + (gp) => gp !== null + ) as Gamepad[]; - if (controllers.length !== connectedGamepads) { - setConnectedGamepads(controllers.length); + connectedGamepads.current = controllers.length; + + controllers.forEach((controller, index) => { + const buttonsState = controller.buttons.reduce( + (buttons, button, i) => buttons | (button.pressed ? 1 << i : 0), + 0 + ); + + const processButton = + currentTime - lastButtonPressedTime.current > 250; + if ( + lastButtonState.current[index] !== buttonsState || + processButton + ) { + lastButtonPressedTime.current = currentTime; + lastButtonState.current[index] = buttonsState; + + if (buttonsState & (1 << 0)) emit('buttonA'); + if (buttonsState & (1 << 1)) emit('buttonB'); + if (buttonsState & (1 << 2)) emit('buttonX'); + if (buttonsState & (1 << 3)) emit('buttonY'); + if (buttonsState & (1 << 4)) emit('buttonLT'); + if (buttonsState & (1 << 5)) emit('buttonRT'); } - controllers.forEach((controller, index) => { - const buttonsState = controller.buttons.reduce( - (buttons, button, i) => buttons | (button.pressed ? 1 << i : 0), - 0 - ); + const deadZone = 0.05; + const maxSpeed = 100; + let axisHandled = false; - const processButton = - currentTime - lastButtonPressedTime.current > 250; + if (controller.axes[0] < -deadZone) { if ( - lastButtonState.current[index] !== buttonsState || - processButton + currentTime - axisTimer.current > + maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000) ) { - lastButtonPressedTime.current = currentTime; - lastButtonState.current[index] = buttonsState; - - if (buttonsState & (1 << 0)) emit('buttonA'); - if (buttonsState & (1 << 1)) emit('buttonB'); - if (buttonsState & (1 << 2)) emit('buttonX'); - if (buttonsState & (1 << 3)) emit('buttonY'); - if (buttonsState & (1 << 4)) emit('buttonLT'); - if (buttonsState & (1 << 5)) emit('buttonRT'); + emit('analog', 'left'); + axisHandled = true; } - - const deadZone = 0.05; - const maxSpeed = 100; - let axisHandled = false; - - if (controller.axes[0] < -deadZone) { - if ( - currentTime - axisTimer.current > - maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000) - ) { - emit('analog', 'left'); - axisHandled = true; - } + } + if (controller.axes[0] > deadZone) { + if ( + currentTime - axisTimer.current > + maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000) + ) { + emit('analog', 'right'); + axisHandled = true; } - if (controller.axes[0] > deadZone) { - if ( - currentTime - axisTimer.current > - maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000) - ) { - emit('analog', 'right'); - axisHandled = true; - } + } + if (controller.axes[1] < -deadZone) { + if ( + currentTime - axisTimer.current > + maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000) + ) { + emit('analog', 'up'); + axisHandled = true; } - if (controller.axes[1] < -deadZone) { - if ( - currentTime - axisTimer.current > - maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000) - ) { - emit('analog', 'up'); - axisHandled = true; - } - } - if (controller.axes[1] > deadZone) { - if ( - currentTime - axisTimer.current > - maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000) - ) { - emit('analog', 'down'); - axisHandled = true; - } + } + if (controller.axes[1] > deadZone) { + if ( + currentTime - axisTimer.current > + maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000) + ) { + emit('analog', 'down'); + axisHandled = true; } + } - if (axisHandled) axisTimer.current = currentTime; - }); - } - animationFrameId = requestAnimationFrame(updateStatus); - }; - + if (axisHandled) axisTimer.current = currentTime; + }); + } animationFrameId = requestAnimationFrame(updateStatus); - } + }; + + animationFrameId = requestAnimationFrame(updateStatus); return () => { - if (enabled) { - cancelAnimationFrame(animationFrameId); - } + cancelAnimationFrame(animationFrameId); }; - }, [connectedGamepads, enabled]); + }, [enabled]); return ( diff --git a/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx b/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx index 4372b7e5c..a076a6d53 100644 --- a/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx +++ b/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx @@ -28,8 +28,8 @@ const useVerticalGamepadNavigation = (sectionRef: React.RefObject { - if (!event.nativeEvent?.spatialNavigationPrevented) { + const handleKeyDown = (event: KeyboardEvent) => { + if (!(event as any).spatialNavigationPrevented) { switch (event.key) { case 'Tab': moveFocus('next');