diff --git a/src/App/App.js b/src/App/App.js
index 803515b09..32930fffa 100644
--- a/src/App/App.js
+++ b/src/App/App.js
@@ -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 = () => {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/services/GamepadContext/GamepadContext.ts b/src/services/GamepadContext/GamepadContext.ts
new file mode 100644
index 000000000..d422b7153
--- /dev/null
+++ b/src/services/GamepadContext/GamepadContext.ts
@@ -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;
diff --git a/src/services/GamepadContext/GamepadProvider.tsx b/src/services/GamepadContext/GamepadProvider.tsx
new file mode 100644
index 000000000..224894917
--- /dev/null
+++ b/src/services/GamepadContext/GamepadProvider.tsx
@@ -0,0 +1,128 @@
+import React, {
+ useEffect,
+ useRef,
+ useState,
+ useCallback,
+} from 'react';
+import GamepadContext from './GamepadContext';
+
+type GamepadEventHandlers = Record void)[]>;
+
+const GamepadProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [connectedGamepads, setConnectedGamepads] = useState(0);
+ const lastButtonState = useRef([]);
+ const lastButtonPressedTime = useRef(0);
+ const axisTimer = useRef(0);
+ const eventHandlers = useRef({});
+
+ 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 (
+
+ {children}
+
+ );
+};
+
+export default GamepadProvider;
diff --git a/src/services/GamepadContext/index.tsx b/src/services/GamepadContext/index.tsx
new file mode 100644
index 000000000..aac999a2f
--- /dev/null
+++ b/src/services/GamepadContext/index.tsx
@@ -0,0 +1,7 @@
+import GamepadProvider from './GamepadProvider';
+import useGamepad from './useGamepad';
+
+export {
+ GamepadProvider,
+ useGamepad
+};
diff --git a/src/services/GamepadContext/useGamepad.tsx b/src/services/GamepadContext/useGamepad.tsx
new file mode 100644
index 000000000..14970bbff
--- /dev/null
+++ b/src/services/GamepadContext/useGamepad.tsx
@@ -0,0 +1,8 @@
+import { useContext } from 'react';
+import GamepadContext from './GamepadContext';
+
+const useGamepad = () => {
+ return useContext(GamepadContext);
+};
+
+export default useGamepad;
diff --git a/src/services/index.js b/src/services/index.js
index 84cfcc8b8..f591c6e75 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -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
};