{remainingTimeMode && duration !== null && !isNaN(duration)
? formatTime(duration - time, '-')
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 3a02f7c3d..0fdee56e1 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -7,7 +7,8 @@ const debounce = require('lodash.debounce');
const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
-const { useServices } = require('stremio/services');
+const { useServices, useGamepad } = require('stremio/services');
+const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
@@ -33,10 +34,13 @@ const { default: useMediaSession } = require('./useMediaSession');
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
const findTrackById = (tracks, id) => tracks.find((track) => track.id === id);
+const GAMEPAD_HANDLER_ID = 'player';
+
const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const services = useServices();
const shell = useShell();
+ const gamepad = useGamepad();
const forceTranscoding = React.useMemo(() => {
return queryParams.has('forceTranscoding');
}, [queryParams]);
@@ -57,6 +61,7 @@ const Player = ({ urlParams, queryParams }) => {
});
const playbackDevices = React.useMemo(() => streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [], [streamingServer]);
+ const playerRef = React.useRef(null);
const bufferingRef = React.useRef();
const errorRef = React.useRef();
@@ -380,6 +385,79 @@ const Player = ({ urlParams, queryParams }) => {
video.addLocalSubtitles(filename, buffer);
});
+ const onPlayPause = React.useCallback(() => {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequested();
+ setSeeking(false);
+ } else {
+ onPauseRequested();
+ }
+ }
+ }, [menusOpen, nextVideoPopupOpen, video.state.paused]);
+
+ const onSeekPrev = React.useCallback((event) => {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event?.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ const seekTime = video.state.time - seekDuration;
+ setSeeking(true);
+ onSeekRequested(Math.max(seekTime, 0));
+ }
+ }, [menusOpen, nextVideoPopupOpen, video.state.time]);
+
+ const onSeekNext = React.useCallback((event) => {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event?.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ setSeeking(true);
+ onSeekRequested(video.state.time + seekDuration);
+ }
+ }, [menusOpen, nextVideoPopupOpen, video.state.time]);
+
+ const onVolumeUp = React.useCallback(() => {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
+ }
+ }, [menusOpen, nextVideoPopupOpen, video.state.volume]);
+
+ const onVolumeDown = React.useCallback(() => {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
+ }
+ }, [menusOpen, nextVideoPopupOpen, video.state.volume]);
+
+ const onGamepadSeekAndVol = React.useCallback((axis) => {
+ switch(axis) {
+ case 'left': {
+ onSeekPrev();
+ break;
+ }
+ case 'right': {
+ onSeekNext();
+ break;
+ }
+ case 'up': {
+ onVolumeUp();
+ break;
+ }
+ case 'down': {
+ onVolumeDown();
+ break;
+ }
+ }
+ }, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]);
+
+ useContentGamepadNavigation(playerRef, GAMEPAD_HANDLER_ID);
+
+ React.useEffect(() => {
+ gamepad?.on('buttonX', GAMEPAD_HANDLER_ID, onPlayPause);
+ gamepad?.on('analogRight', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
+
+ return () => {
+ gamepad?.off('buttonX', GAMEPAD_HANDLER_ID);
+ gamepad?.off('analogRight', GAMEPAD_HANDLER_ID);
+ };
+ }, [onPlayPause, onGamepadSeekAndVol]);
+
React.useEffect(() => {
setError(null);
video.unload();
@@ -895,7 +973,7 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
return (
-
(({ profile }: Props, ref) =>
quitOnCloseToggle,
escExitFullscreenToggle,
hideSpoilersToggle,
+ gamepadSupportToggle,
} = useInterfaceOptions(profile);
return (
@@ -50,6 +51,12 @@ const Interface = forwardRef(({ profile }: Props, ref) =>
{...hideSpoilersToggle}
/>
+
);
});
diff --git a/src/routes/Settings/Interface/useInterfaceOptions.ts b/src/routes/Settings/Interface/useInterfaceOptions.ts
index 67780b7e1..94243fd5e 100644
--- a/src/routes/Settings/Interface/useInterfaceOptions.ts
+++ b/src/routes/Settings/Interface/useInterfaceOptions.ts
@@ -81,11 +81,28 @@ const useInterfaceOptions = (profile: Profile) => {
}
}), [profile.settings]);
+ const gamepadSupportToggle = useMemo(() => ({
+ checked: profile.settings.gamepadSupport,
+ onClick: () => {
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'UpdateSettings',
+ args: {
+ ...profile.settings,
+ gamepadSupport: !profile.settings.gamepadSupport
+ }
+ }
+ });
+ }
+ }), [profile.settings]);
+
return {
interfaceLanguageSelect,
escExitFullscreenToggle,
quitOnCloseToggle,
hideSpoilersToggle,
+ gamepadSupportToggle,
};
};
diff --git a/src/services/GamepadContext/GamepadContext.ts b/src/services/GamepadContext/GamepadContext.ts
new file mode 100644
index 000000000..c156230aa
--- /dev/null
+++ b/src/services/GamepadContext/GamepadContext.ts
@@ -0,0 +1,10 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import { createContext } from 'react';
+
+const GamepadContext = createContext<{
+ on: (event: string, id: string, callback: (data?: string) => void) => void;
+ off: (event: string, id: string) => 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..a957f0cfd
--- /dev/null
+++ b/src/services/GamepadContext/GamepadProvider.tsx
@@ -0,0 +1,226 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import React, { useEffect, useRef, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import useToast from 'stremio/common/Toast/useToast';
+import GamepadContext from './GamepadContext';
+
+type GamepadEventHandlers = Map void>>;
+
+type GamepadProviderProps = {
+ enabled: boolean;
+ onGuide?: () => void;
+ children: React.ReactNode;
+};
+
+const GamepadProvider = ({ enabled, onGuide, children }: GamepadProviderProps) => {
+ const { t } = useTranslation();
+ const toast = useToast();
+ const connectedGamepads = useRef(0);
+ const lastButtonState = useRef([]);
+ const lastButtonPressedTime = useRef(0);
+ const axisTimer = useRef(0);
+ const axisTimerRight = useRef(0);
+ const eventHandlers = useRef(new Map());
+
+ const on = useCallback((event: string, id: string, callback: (data?: string) => void) => {
+ if (!eventHandlers.current.has(event)) {
+ eventHandlers.current.set(event, new Map());
+ }
+
+ const handlers = eventHandlers.current.get(event)!;
+
+ // Ensure only one handler per component
+ handlers.set(id, callback);
+ }, []);
+
+ const off = useCallback((event: string, id: string) => {
+ const handlersMap = eventHandlers.current.get(event);
+ handlersMap?.delete(id);
+ if (handlersMap?.size === 0) {
+ eventHandlers.current.delete(event);
+ }
+ }, []);
+
+ const emit = (event: string, data?: string) => {
+ if (eventHandlers.current.has(event)) {
+ const handlersMap = eventHandlers.current.get(event)!;
+
+ if (!handlersMap || handlersMap.size === 0) return;
+
+ const latestHandler = Array.from(handlersMap.values()).slice(-1)[0];
+ if (latestHandler) {
+ latestHandler(data);
+ }
+ }
+ };
+
+ const onGamepadConnected = useCallback(() => {
+ // @ts-expect-error show() expects no arguments
+ toast.show({
+ type: 'info',
+ title: t('GAMEPAD_CONNECTED'),
+ timeout: 4000,
+ });
+ }, [toast, t]);
+
+ const onGamepadDisconnected = useCallback(() => {
+ // @ts-expect-error show() expects no arguments
+ toast.show({
+ type: 'info',
+ title: t('GAMEPAD_DISCONNECTED'),
+ timeout: 4000,
+ });
+ }, [toast, t]);
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ window.addEventListener('gamepadconnected', onGamepadConnected);
+ window.addEventListener('gamepaddisconnected', onGamepadDisconnected);
+
+ return () => {
+ window.removeEventListener('gamepadconnected', onGamepadConnected);
+ window.removeEventListener('gamepaddisconnected', onGamepadDisconnected);
+ };
+ }, [enabled, onGamepadConnected, onGamepadDisconnected]);
+
+ useEffect(() => {
+ if (onGuide) {
+ on('buttonX', 'gamepad-guide', onGuide);
+ }
+ return () => {
+ off('buttonX', 'gamepad-guide');
+ };
+ }, [onGuide]);
+
+ useEffect(() => {
+ if (!enabled || typeof navigator.getGamepads !== 'function') return;
+
+ let animationFrameId: number;
+
+ const updateStatus = () => {
+ if (document.hasFocus()) {
+ const currentTime = Date.now();
+ const controllers = Array.from(navigator.getGamepads()).filter(
+ (gp) => gp !== null
+ ) as Gamepad[];
+
+ 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');
+ }
+
+ 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;
+
+ let rightAxisHandled = false;
+
+ if (controller.axes.length > 2) {
+ if (controller.axes[2] < -deadZone) {
+ if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[2]) * 2000)) {
+ emit('analogRight', 'left');
+ rightAxisHandled = true;
+ }
+ }
+ if (controller.axes[2] > deadZone) {
+ if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[2]) * 2000)) {
+ emit('analogRight', 'right');
+ rightAxisHandled = true;
+ }
+ }
+ if (controller.axes[3] < -deadZone) {
+ if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[3]) * 2000)) {
+ emit('analogRight', 'up');
+ rightAxisHandled = true;
+ }
+ }
+ if (controller.axes[3] > deadZone) {
+ if (currentTime - axisTimerRight.current > maxSpeed + (2000 - Math.abs(controller.axes[3]) * 2000)) {
+ emit('analogRight', 'down');
+ rightAxisHandled = true;
+ }
+ }
+ }
+
+ if (rightAxisHandled) axisTimerRight.current = currentTime;
+ });
+ }
+ animationFrameId = requestAnimationFrame(updateStatus);
+ };
+
+ animationFrameId = requestAnimationFrame(updateStatus);
+
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ };
+ }, [enabled]);
+
+ 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..3c6cca72f
--- /dev/null
+++ b/src/services/GamepadContext/index.tsx
@@ -0,0 +1,9 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+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..d3cf91b8d
--- /dev/null
+++ b/src/services/GamepadContext/useGamepad.tsx
@@ -0,0 +1,10 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import { useContext } from 'react';
+import GamepadContext from './GamepadContext';
+
+const useGamepad = () => {
+ return useContext(GamepadContext);
+};
+
+export default useGamepad;
diff --git a/src/services/GamepadNavigation/index.tsx b/src/services/GamepadNavigation/index.tsx
new file mode 100644
index 000000000..2c3199b0d
--- /dev/null
+++ b/src/services/GamepadNavigation/index.tsx
@@ -0,0 +1,11 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import useContentGamepadNavigation from './useContentGamepadNavigation';
+import useVerticalNavGamepadNavigation from './useVerticalNavGamepadNavigation';
+import useHorizontalNavGamepadNavigation from './useHorizontalNavGamepadNavigation';
+
+export {
+ useContentGamepadNavigation,
+ useVerticalNavGamepadNavigation,
+ useHorizontalNavGamepadNavigation,
+};
diff --git a/src/services/GamepadNavigation/useContentGamepadNavigation.tsx b/src/services/GamepadNavigation/useContentGamepadNavigation.tsx
new file mode 100644
index 000000000..0e1486fa6
--- /dev/null
+++ b/src/services/GamepadNavigation/useContentGamepadNavigation.tsx
@@ -0,0 +1,140 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import { useEffect, useRef } from 'react';
+import { useGamepad } from '../GamepadContext';
+
+const FOCUSABLE = '[tabindex="0"]';
+
+const getActiveScope = (fallback: HTMLDivElement | null): HTMLElement | null => {
+ const modal = document.querySelector('.modals-container');
+ if (modal && modal.children.length > 0) return modal;
+
+ const dropdown = fallback?.querySelector('[class*="dropdown"][class*="open"]');
+ if (dropdown) return dropdown;
+
+ return fallback;
+};
+
+const useContentGamepadNavigation = (
+ sectionRef: React.RefObject,
+ gamepadHandlerId: string
+) => {
+ const gamepad = useGamepad();
+ const lastFocused = useRef(null);
+ const wasInOverlay = useRef(false);
+
+ useEffect(() => {
+ const handleGamepadNavigation = (
+ direction: 'left' | 'right' | 'up' | 'down'
+ ) => {
+ const scope = getActiveScope(sectionRef.current);
+ const inOverlay = scope !== sectionRef.current;
+
+ if (inOverlay && !wasInOverlay.current) {
+ const focused = sectionRef.current?.querySelector(':focus');
+ if (focused) lastFocused.current = focused;
+ }
+ wasInOverlay.current = inOverlay;
+
+ const elements = Array.from(
+ scope?.querySelectorAll(FOCUSABLE) || []
+ );
+ if (elements.length === 0) return;
+
+ const activeElement = (scope ?? document)?.querySelector(':focus');
+
+ if (!activeElement) {
+ elements[0].focus();
+ return;
+ }
+
+ let closestElement: HTMLDivElement | null = null;
+ const cur = activeElement.getBoundingClientRect();
+ const cx = cur.left + cur.width / 2;
+ const cy = cur.top + cur.height / 2;
+ let closestDistance = Infinity;
+
+ elements.forEach((el) => {
+ if (el === activeElement) return;
+ const r = el.getBoundingClientRect();
+ const ex = r.left + r.width / 2;
+ const ey = r.top + r.height / 2;
+
+ const isCorrectDirection =
+ (direction === 'left' && ex < cx) ||
+ (direction === 'right' && ex > cx) ||
+ (direction === 'up' && ey < cy) ||
+ (direction === 'down' && ey > cy);
+
+ if (!isCorrectDirection) return;
+
+ const dx = ex - cx;
+ const dy = ey - cy;
+ const isHorizontal = direction === 'left' || direction === 'right';
+ const primary = isHorizontal ? Math.abs(dx) : Math.abs(dy);
+ const secondary = isHorizontal ? Math.abs(dy) : Math.abs(dx);
+ const distance = primary + secondary * 3;
+
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closestElement = el;
+ }
+ });
+
+ if (closestElement) {
+ closestElement.focus();
+ }
+ };
+
+ const onSelect = () => {
+ const scope = getActiveScope(sectionRef.current);
+ const inOverlay = scope !== sectionRef.current;
+
+ if (inOverlay && !wasInOverlay.current) {
+ const focused = sectionRef.current?.querySelector(':focus');
+ if (focused) lastFocused.current = focused;
+ }
+ wasInOverlay.current = inOverlay;
+
+ const elements = Array.from(
+ scope?.querySelectorAll(FOCUSABLE) || []
+ );
+ if (elements.length === 0) {
+ if (lastFocused.current) {
+ lastFocused.current.focus();
+ wasInOverlay.current = false;
+ }
+ return;
+ }
+
+ const activeElement = (scope ?? document)?.querySelector(':focus');
+
+ if (!activeElement) {
+ elements[0].focus();
+ return;
+ }
+ const isSelect = Array.from(activeElement.classList).some((cls) => cls.startsWith('select-input'));
+ if (!isSelect) {
+ activeElement?.click();
+
+ requestAnimationFrame(() => {
+ const stillInOverlay = getActiveScope(sectionRef.current) !== sectionRef.current;
+ if (!stillInOverlay && wasInOverlay.current && lastFocused.current) {
+ lastFocused.current.focus();
+ wasInOverlay.current = false;
+ }
+ });
+ }
+ };
+
+ gamepad?.on('analog', gamepadHandlerId, handleGamepadNavigation);
+ gamepad?.on('buttonA', gamepadHandlerId, onSelect);
+
+ return () => {
+ gamepad?.off('analog', gamepadHandlerId);
+ gamepad?.off('buttonA', gamepadHandlerId);
+ };
+ }, [gamepad, gamepadHandlerId, sectionRef]);
+};
+
+export default useContentGamepadNavigation;
diff --git a/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx b/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx
new file mode 100644
index 000000000..422489e3e
--- /dev/null
+++ b/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx
@@ -0,0 +1,24 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import { useEffect } from 'react';
+import { useGamepad } from '../GamepadContext';
+import useFullscreen from 'stremio/common/useFullscreen';
+
+const useHorizontalNavGamepadNavigation = (gamepadHandlerId: string, enableGoBack: boolean) => {
+ const gamepad = useGamepad();
+ const [fullscreen,,,toggleFullscreen] = useFullscreen();
+
+ useEffect(() => {
+ const goBack = () => enableGoBack && window.history.back();
+
+ gamepad?.on('buttonY', gamepadHandlerId, toggleFullscreen as () => void);
+ gamepad?.on('buttonB', gamepadHandlerId, goBack);
+
+ return () => {
+ gamepad?.off('buttonY', gamepadHandlerId);
+ gamepad?.off('buttonB', gamepadHandlerId);
+ };
+ }, [gamepad, gamepadHandlerId, enableGoBack, fullscreen]);
+};
+
+export default useHorizontalNavGamepadNavigation;
diff --git a/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx b/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx
new file mode 100644
index 000000000..041b4e66c
--- /dev/null
+++ b/src/services/GamepadNavigation/useVerticalNavGamepadNavigation.tsx
@@ -0,0 +1,35 @@
+// Copyright (C) 2017-2026 Smart code 203358507
+
+import { useEffect } from 'react';
+import { useGamepad } from '../GamepadContext';
+
+const ROUTES = ['search', 'board', 'discover', 'library', 'calendar', 'addons', 'settings'];
+
+const useVerticalGamepadNavigation = (_sectionRef: React.RefObject, currentRoute: string) => {
+ const gamepad = useGamepad();
+
+ useEffect(() => {
+ const navigate = (direction: 'prev' | 'next') => {
+ const currentIndex = ROUTES.indexOf(currentRoute);
+ if (currentIndex === -1) return;
+
+ let nextIndex = currentIndex;
+ if (direction === 'next') nextIndex = Math.min(currentIndex + 1, ROUTES.length - 1);
+ if (direction === 'prev') nextIndex = Math.max(currentIndex - 1, 0);
+
+ if (nextIndex !== currentIndex) {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: String(nextIndex), code: `Digit${nextIndex}`, bubbles: true }));
+ }
+ };
+
+ gamepad?.on('buttonLT', currentRoute, () => navigate('prev'));
+ gamepad?.on('buttonRT', currentRoute, () => navigate('next'));
+
+ return () => {
+ gamepad?.off('buttonLT', currentRoute);
+ gamepad?.off('buttonRT', currentRoute);
+ };
+ }, [gamepad, currentRoute]);
+};
+
+export default useVerticalGamepadNavigation;
diff --git a/src/services/index.js b/src/services/index.js
index 84cfcc8b8..3a3b0128a 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,
};
diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts
index fad9c2f4b..4bdb22ec5 100644
--- a/src/types/models/Ctx.d.ts
+++ b/src/types/models/Ctx.d.ts
@@ -24,6 +24,7 @@ type Settings = {
interfaceLanguage: string,
quitOnClose: boolean,
hideSpoilers: boolean,
+ gamepadSupport: boolean,
nextVideoNotificationDuration: number,
playInBackground: boolean,
playerType: string | null,