mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-13 18:30:46 +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]);
|
}, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
gamepad.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause);
|
gamepad?.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause);
|
||||||
gamepad.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
|
gamepad?.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
gamepad.off('buttonA', GAMEPAD_HANDLER_ID);
|
gamepad?.off('buttonA', GAMEPAD_HANDLER_ID);
|
||||||
gamepad.off('analog', GAMEPAD_HANDLER_ID);
|
gamepad?.off('analog', GAMEPAD_HANDLER_ID);
|
||||||
};
|
};
|
||||||
}, [onPlayPause, onGamepadSeekAndVol]);
|
}, [onPlayPause, onGamepadSeekAndVol]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
|
||||||
quitOnCloseToggle,
|
quitOnCloseToggle,
|
||||||
escExitFullscreenToggle,
|
escExitFullscreenToggle,
|
||||||
hideSpoilersToggle,
|
hideSpoilersToggle,
|
||||||
|
gamepadSupportToggle,
|
||||||
} = useInterfaceOptions(profile);
|
} = useInterfaceOptions(profile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -50,6 +51,12 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
|
||||||
{...hideSpoilersToggle}
|
{...hideSpoilersToggle}
|
||||||
/>
|
/>
|
||||||
</Option>
|
</Option>
|
||||||
|
<Option label={'SETTINGS_GAMEPAD'}>
|
||||||
|
<Toggle
|
||||||
|
tabIndex={-1}
|
||||||
|
{...gamepadSupportToggle}
|
||||||
|
/>
|
||||||
|
</Option>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (C) 2017-2025 Smart code 203358507
|
// 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 useToast from 'stremio/common/Toast/useToast';
|
||||||
import GamepadContext from './GamepadContext';
|
import GamepadContext from './GamepadContext';
|
||||||
|
|
||||||
|
|
@ -10,8 +11,9 @@ const GamepadProvider: React.FC<{
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ enabled, children }) => {
|
}> = ({ enabled, children }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [connectedGamepads, setConnectedGamepads] = useState<number>(0);
|
const connectedGamepads = useRef<number>(0);
|
||||||
const lastButtonState = useRef<number[]>([]);
|
const lastButtonState = useRef<number[]>([]);
|
||||||
const lastButtonPressedTime = useRef<number>(0);
|
const lastButtonPressedTime = useRef<number>(0);
|
||||||
const axisTimer = useRef<number>(0);
|
const axisTimer = useRef<number>(0);
|
||||||
|
|
@ -53,7 +55,7 @@ const GamepadProvider: React.FC<{
|
||||||
// @ts-expect-error show() expects no arguments
|
// @ts-expect-error show() expects no arguments
|
||||||
toast.show({
|
toast.show({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Gamepad detected',
|
title: t('GAMEPAD_CONNECTED'),
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -62,7 +64,7 @@ const GamepadProvider: React.FC<{
|
||||||
// @ts-expect-error show() expects no arguments
|
// @ts-expect-error show() expects no arguments
|
||||||
toast.show({
|
toast.show({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Gamepad disconnected',
|
title: t('GAMEPAD_DISCONNECTED'),
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -82,101 +84,95 @@ const GamepadProvider: React.FC<{
|
||||||
}, [enabled]);
|
}, [enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof navigator.getGamepads !== 'function') return;
|
if (!enabled || typeof navigator.getGamepads !== 'function') return;
|
||||||
|
|
||||||
let animationFrameId: number;
|
let animationFrameId: number;
|
||||||
if (enabled) {
|
|
||||||
|
|
||||||
const updateStatus = () => {
|
const updateStatus = () => {
|
||||||
if (document.hasFocus()) {
|
if (document.hasFocus()) {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
const controllers = Array.from(navigator.getGamepads()).filter(
|
const controllers = Array.from(navigator.getGamepads()).filter(
|
||||||
(gp) => gp !== null
|
(gp) => gp !== null
|
||||||
) as Gamepad[];
|
) as Gamepad[];
|
||||||
|
|
||||||
if (controllers.length !== connectedGamepads) {
|
connectedGamepads.current = controllers.length;
|
||||||
setConnectedGamepads(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 deadZone = 0.05;
|
||||||
const buttonsState = controller.buttons.reduce(
|
const maxSpeed = 100;
|
||||||
(buttons, button, i) => buttons | (button.pressed ? 1 << i : 0),
|
let axisHandled = false;
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const processButton =
|
if (controller.axes[0] < -deadZone) {
|
||||||
currentTime - lastButtonPressedTime.current > 250;
|
|
||||||
if (
|
if (
|
||||||
lastButtonState.current[index] !== buttonsState ||
|
currentTime - axisTimer.current >
|
||||||
processButton
|
maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000)
|
||||||
) {
|
) {
|
||||||
lastButtonPressedTime.current = currentTime;
|
emit('analog', 'left');
|
||||||
lastButtonState.current[index] = buttonsState;
|
axisHandled = true;
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const deadZone = 0.05;
|
if (controller.axes[0] > deadZone) {
|
||||||
const maxSpeed = 100;
|
if (
|
||||||
let axisHandled = false;
|
currentTime - axisTimer.current >
|
||||||
|
maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000)
|
||||||
if (controller.axes[0] < -deadZone) {
|
) {
|
||||||
if (
|
emit('analog', 'right');
|
||||||
currentTime - axisTimer.current >
|
axisHandled = true;
|
||||||
maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000)
|
|
||||||
) {
|
|
||||||
emit('analog', 'left');
|
|
||||||
axisHandled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (controller.axes[0] > deadZone) {
|
}
|
||||||
if (
|
if (controller.axes[1] < -deadZone) {
|
||||||
currentTime - axisTimer.current >
|
if (
|
||||||
maxSpeed + (2000 - Math.abs(controller.axes[0]) * 2000)
|
currentTime - axisTimer.current >
|
||||||
) {
|
maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000)
|
||||||
emit('analog', 'right');
|
) {
|
||||||
axisHandled = true;
|
emit('analog', 'up');
|
||||||
}
|
axisHandled = true;
|
||||||
}
|
}
|
||||||
if (controller.axes[1] < -deadZone) {
|
}
|
||||||
if (
|
if (controller.axes[1] > deadZone) {
|
||||||
currentTime - axisTimer.current >
|
if (
|
||||||
maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000)
|
currentTime - axisTimer.current >
|
||||||
) {
|
maxSpeed + (2000 - Math.abs(controller.axes[1]) * 2000)
|
||||||
emit('analog', 'up');
|
) {
|
||||||
axisHandled = true;
|
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;
|
if (axisHandled) axisTimer.current = currentTime;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
animationFrameId = requestAnimationFrame(updateStatus);
|
|
||||||
};
|
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(updateStatus);
|
animationFrameId = requestAnimationFrame(updateStatus);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(updateStatus);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (enabled) {
|
cancelAnimationFrame(animationFrameId);
|
||||||
cancelAnimationFrame(animationFrameId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [connectedGamepads, enabled]);
|
}, [enabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GamepadContext.Provider value={{ on, off }}>
|
<GamepadContext.Provider value={{ on, off }}>
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ const useVerticalGamepadNavigation = (sectionRef: React.RefObject<HTMLDivElement
|
||||||
elements[nextIndex]?.click();
|
elements[nextIndex]?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!event.nativeEvent?.spatialNavigationPrevented) {
|
if (!(event as any).spatialNavigationPrevented) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
moveFocus('next');
|
moveFocus('next');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue