mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-08-09 08:47:17 +00:00
feat(GamepadProvider): added Gamepad context and provider
This commit is contained in:
parent
acb441bbcf
commit
4617d030aa
6 changed files with 167 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
|
|||
8
src/services/GamepadContext/GamepadContext.ts
Normal file
8
src/services/GamepadContext/GamepadContext.ts
Normal 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;
|
||||
128
src/services/GamepadContext/GamepadProvider.tsx
Normal file
128
src/services/GamepadContext/GamepadProvider.tsx
Normal 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;
|
||||
7
src/services/GamepadContext/index.tsx
Normal file
7
src/services/GamepadContext/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import GamepadProvider from './GamepadProvider';
|
||||
import useGamepad from './useGamepad';
|
||||
|
||||
export {
|
||||
GamepadProvider,
|
||||
useGamepad
|
||||
};
|
||||
8
src/services/GamepadContext/useGamepad.tsx
Normal file
8
src/services/GamepadContext/useGamepad.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { useContext } from 'react';
|
||||
import GamepadContext from './GamepadContext';
|
||||
|
||||
const useGamepad = () => {
|
||||
return useContext(GamepadContext);
|
||||
};
|
||||
|
||||
export default useGamepad;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue