feat(GamepadProvider): added Gamepad context and provider

This commit is contained in:
Botzy 2025-03-24 13:37:39 +02:00
parent acb441bbcf
commit 4617d030aa
6 changed files with 167 additions and 11 deletions

View file

@ -4,7 +4,7 @@ require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
@ -204,15 +204,17 @@ const App = () => {
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
<GamepadProvider>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</GamepadProvider>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>

View file

@ -0,0 +1,8 @@
import { createContext } from 'react';
const GamepadContext = createContext<{
on: (event: string, callback: (data?: any) => void) => void;
off: (event: string, callback: (data?: any) => void) => void;
} | null>(null);
export default GamepadContext;

View file

@ -0,0 +1,128 @@
import React, {
useEffect,
useRef,
useState,
useCallback,
} from 'react';
import GamepadContext from './GamepadContext';
type GamepadEventHandlers = Record<string, ((data?: any) => void)[]>;
const GamepadProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [connectedGamepads, setConnectedGamepads] = useState<number>(0);
const lastButtonState = useRef<number[]>([]);
const lastButtonPressedTime = useRef<number>(0);
const axisTimer = useRef<number>(0);
const eventHandlers = useRef<GamepadEventHandlers>({});
const on = useCallback((event: string, callback: (data?: any) => void) => {
if (!eventHandlers.current[event]) {
eventHandlers.current[event] = [];
}
eventHandlers.current[event].push(callback);
}, []);
const off = useCallback((event: string, callback: (data?: any) => void) => {
if (eventHandlers.current[event]) {
eventHandlers.current[event] = eventHandlers.current[event].filter(
(cb) => cb !== callback
);
}
}, []);
const emit = (event: string, data?: any) => {
if (eventHandlers.current[event]) {
eventHandlers.current[event].forEach((callback) => callback(data));
}
};
useEffect(() => {
if (typeof navigator.getGamepads !== 'function') return;
let animationFrameId: number;
const updateStatus = () => {
// TODO: add check if profile settings allows this feature
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);
}
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');
}
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[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 (axisHandled) axisTimer.current = currentTime;
});
}
animationFrameId = requestAnimationFrame(updateStatus);
};
animationFrameId = requestAnimationFrame(updateStatus);
return () => cancelAnimationFrame(animationFrameId);
}, [connectedGamepads]);
return (
<GamepadContext.Provider value={{ on, off }}>
{children}
</GamepadContext.Provider>
);
};
export default GamepadProvider;

View file

@ -0,0 +1,7 @@
import GamepadProvider from './GamepadProvider';
import useGamepad from './useGamepad';
export {
GamepadProvider,
useGamepad
};

View file

@ -0,0 +1,8 @@
import { useContext } from 'react';
import GamepadContext from './GamepadContext';
const useGamepad = () => {
return useContext(GamepadContext);
};
export default useGamepad;

View file

@ -5,6 +5,7 @@ const Core = require('./Core');
const DragAndDrop = require('./DragAndDrop');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
const { GamepadProvider, useGamepad } = require('./GamepadContext');
const Shell = require('./Shell');
module.exports = {
@ -14,5 +15,7 @@ module.exports = {
KeyboardShortcuts,
ServicesProvider,
useServices,
Shell
Shell,
GamepadProvider,
useGamepad
};