mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-10 03:01:45 +00:00
fixes
1. Player.js — Added optional chaining (gamepad?.on/gamepad?.off) to prevent null crash
2. GamepadProvider.tsx — Toast messages now use t('GAMEPAD_CONNECTED') / t('GAMEPAD_DISCONNECTED') via i18next, added keys to en-US.json
3. useVerticalNavGamepadNavigation.tsx — Fixed event.nativeEvent?.spatialNavigationPrevented → event.spatialNavigationPrevented (native DOM events don't have nativeEvent)
4. GamepadProvider.tsx — Changed connectedGamepads from useState to useRef to avoid rAF effect restart cycle, removed it from effect deps, simplified the enabled guard
This commit is contained in:
parent
0c1af71aa9
commit
93833d0cd1
4 changed files with 90 additions and 87 deletions
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
|
|||
quitOnCloseToggle,
|
||||
escExitFullscreenToggle,
|
||||
hideSpoilersToggle,
|
||||
gamepadSupportToggle,
|
||||
} = useInterfaceOptions(profile);
|
||||
|
||||
return (
|
||||
|
|
@ -50,6 +51,12 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
|
|||
{...hideSpoilersToggle}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_GAMEPAD'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...gamepadSupportToggle}
|
||||
/>
|
||||
</Option>
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<number>(0);
|
||||
const connectedGamepads = useRef<number>(0);
|
||||
const lastButtonState = useRef<number[]>([]);
|
||||
const lastButtonPressedTime = useRef<number>(0);
|
||||
const axisTimer = useRef<number>(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 (
|
||||
<GamepadContext.Provider value={{ on, off }}>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ const useVerticalGamepadNavigation = (sectionRef: React.RefObject<HTMLDivElement
|
|||
elements[nextIndex]?.click();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (!event.nativeEvent?.spatialNavigationPrevented) {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event as any).spatialNavigationPrevented) {
|
||||
switch (event.key) {
|
||||
case 'Tab':
|
||||
moveFocus('next');
|
||||
|
|
|
|||
Loading…
Reference in a new issue