This commit is contained in:
botsy 2026-04-28 14:14:18 +00:00 committed by GitHub
commit b3b0cecc70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 570 additions and 22 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, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
@ -22,6 +22,7 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
const App = () => {
const { i18n } = useTranslation();
const shell = useShell();
const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false);
const onPathNotMatch = React.useCallback(() => {
return NotFound;
}, []);
@ -141,6 +142,10 @@ const App = () => {
i18n.changeLanguage(args.settings.interfaceLanguage);
}
if (args?.settings?.gamepadSupport !== undefined) {
setGamepadSupportEnabled(args.settings.gamepadSupport);
}
if (args?.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
@ -154,6 +159,10 @@ const App = () => {
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
}
if (typeof state.profile.settings.gamepadSupport === 'boolean') {
setGamepadSupportEnabled(state.profile.settings.gamepadSupport);
}
if (state?.profile?.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
@ -213,20 +222,22 @@ const App = () => {
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-container']}>
<ShortcutsProvider onShortcut={onShortcut}>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ShortcutsProvider>
<GamepadProvider enabled={gamepadSupportEnabled}>
<ShortcutsProvider onShortcut={onShortcut}>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ShortcutsProvider>
</GamepadProvider>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>

View file

@ -3,6 +3,7 @@
import React, { memo } from 'react';
import classnames from 'classnames';
import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar';
import { useContentGamepadNavigation, useVerticalNavGamepadNavigation } from 'stremio/services/GamepadNavigation';
import styles from './MainNavBars.less';
const TABS = [
@ -22,6 +23,12 @@ type Props = {
};
const MainNavBars = memo(({ className, route, query, children }: Props) => {
const navRef = React.useRef(null);
const contentRef = React.useRef(null);
useContentGamepadNavigation(contentRef, route || '');
useVerticalNavGamepadNavigation(navRef, route || '');
return (
<div className={classnames(className, styles['main-nav-bars-container'])}>
<HorizontalNavBar
@ -34,11 +41,12 @@ const MainNavBars = memo(({ className, route, query, children }: Props) => {
navMenu={true}
/>
<VerticalNavBar
ref={navRef}
className={styles['vertical-nav-bar']}
selected={route}
tabs={TABS}
/>
<div className={styles['nav-content-container']}>{children}</div>
<div ref={contentRef} className={styles['nav-content-container']}>{children}</div>
</div>
);
});

View file

@ -7,6 +7,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button, Image } = require('stremio/components');
const { default: useFullscreen } = require('stremio/common/useFullscreen');
const usePWA = require('stremio/common/usePWA');
const { useHorizontalNavGamepadNavigation } = require('stremio/services/GamepadNavigation');
const SearchBar = require('./SearchBar');
const NavMenu = require('./NavMenu');
const styles = require('./styles');
@ -24,6 +25,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
{children}
</Button>
), []);
useHorizontalNavGamepadNavigation(route || className, backButton);
return (
<nav {...props} className={classnames(className, styles['horizontal-nav-bar-container'])}>
{

View file

@ -7,10 +7,10 @@ const { useTranslation } = require('react-i18next');
const NavTabButton = require('./NavTabButton');
const styles = require('./styles');
const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
const VerticalNavBar = React.memo(React.forwardRef(({ className, selected, tabs }, ref) => {
const { t } = useTranslation();
return (
<nav className={classnames(className, styles['vertical-nav-bar-container'])}>
<nav ref={ref} className={classnames(className, styles['vertical-nav-bar-container'])}>
{
Array.isArray(tabs) ?
tabs.map((tab, index) => (
@ -30,7 +30,7 @@ const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
}
</nav>
);
});
}));
VerticalNavBar.displayName = 'VerticalNavBar';

View file

@ -5,6 +5,7 @@ const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useServices } = require('stremio/services');
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { withCoreSuspender } = require('stremio/common');
const { VerticalNavBar, HorizontalNavBar, DelayedRenderer, Image, MetaPreview, ModalDialog } = require('stremio/components');
const StreamsList = require('./StreamsList');
@ -15,6 +16,7 @@ const useMetaExtensionTabs = require('./useMetaExtensionTabs');
const styles = require('./styles');
const MetaDetails = ({ urlParams, queryParams }) => {
const contentRef = React.useRef(null);
const { t } = useTranslation();
const { core } = useServices();
const metaDetails = useMetaDetails(urlParams);
@ -111,6 +113,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
metaDetails.metaItem.content.content.background.length > 0
), [metaPath, metaDetails]);
useContentGamepadNavigation(contentRef, urlParams.path);
return (
<div className={styles['metadetails-container']}>
{
@ -132,7 +135,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
fullscreenButton={true}
navMenu={true}
/>
<div className={styles['metadetails-content']}>
<div ref={contentRef} className={styles['metadetails-content']}>
{
tabs.length > 0 ?
<VerticalNavBar
@ -238,6 +241,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
MetaDetails.propTypes = {
urlParams: PropTypes.shape({
path: PropTypes.string,
type: PropTypes.string,
id: PropTypes.string,
videoId: PropTypes.string

View file

@ -7,7 +7,7 @@ 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 { 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 +33,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]);
@ -380,6 +383,77 @@ 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]);
React.useEffect(() => {
gamepad?.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause);
gamepad?.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
return () => {
gamepad?.off('buttonA', GAMEPAD_HANDLER_ID);
gamepad?.off('analog', GAMEPAD_HANDLER_ID);
};
}, [onPlayPause, onGamepadSeekAndVol]);
React.useEffect(() => {
setError(null);
video.unload();

View file

@ -16,6 +16,7 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
quitOnCloseToggle,
escExitFullscreenToggle,
hideSpoilersToggle,
gamepadSupportToggle,
} = useInterfaceOptions(profile);
return (
@ -50,6 +51,12 @@ const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) =>
{...hideSpoilersToggle}
/>
</Option>
<Option label={'SETTINGS_GAMEPAD'}>
<Toggle
tabIndex={-1}
{...gamepadSupportToggle}
/>
</Option>
</Section>
);
});

View file

@ -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,
};
};

View file

@ -0,0 +1,10 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { createContext } from 'react';
const GamepadContext = createContext<{
on: (event: string, id: string, callback: (data?: any) => void) => void;
off: (event: string, id: string) => void;
} | null>(null);
export default GamepadContext;

View file

@ -0,0 +1,184 @@
// Copyright (C) 2017-2025 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<string, Map<string, (data?: any) => void>>;
const GamepadProvider: React.FC<{
enabled: boolean;
children: React.ReactNode;
}> = ({ enabled, children }) => {
const { t } = useTranslation();
const toast = useToast();
const connectedGamepads = useRef<number>(0);
const lastButtonState = useRef<number[]>([]);
const lastButtonPressedTime = useRef<number>(0);
const axisTimer = useRef<number>(0);
const eventHandlers = useRef<GamepadEventHandlers>(new Map());
const on = useCallback((event: string, id: string, callback: (data?: any) => 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?: any) => {
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 = () => {
// @ts-expect-error show() expects no arguments
toast.show({
type: 'info',
title: t('GAMEPAD_CONNECTED'),
timeout: 4000,
});
};
const onGamepadDisconnected = () => {
// @ts-expect-error show() expects no arguments
toast.show({
type: 'info',
title: t('GAMEPAD_DISCONNECTED'),
timeout: 4000,
});
};
useEffect(() => {
if (enabled) {
window.addEventListener('gamepadconnected', onGamepadConnected);
window.addEventListener('gamepaddisconnected', onGamepadDisconnected);
}
return () => {
if (enabled) {
window.removeEventListener('gamepadconnected', onGamepadConnected);
window.removeEventListener('gamepaddisconnected', onGamepadDisconnected);
}
};
}, [enabled]);
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;
});
}
animationFrameId = requestAnimationFrame(updateStatus);
};
animationFrameId = requestAnimationFrame(updateStatus);
return () => {
cancelAnimationFrame(animationFrameId);
};
}, [enabled]);
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,10 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { useContext } from 'react';
import GamepadContext from './GamepadContext';
const useGamepad = () => {
return useContext(GamepadContext);
};
export default useGamepad;

View file

@ -0,0 +1,9 @@
import useContentGamepadNavigation from './useContentGamepadNavigation';
import useVerticalNavGamepadNavigation from './useVerticalNavGamepadNavigation';
import useHorizontalNavGamepadNavigation from './useHorizontalNavGamepadNavigation';
export {
useContentGamepadNavigation,
useVerticalNavGamepadNavigation,
useHorizontalNavGamepadNavigation,
};

View file

@ -0,0 +1,122 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { useEffect } from 'react';
import { useGamepad } from '../GamepadContext';
const useContentGamepadNavigation = (
sectionRef: React.RefObject<HTMLDivElement>,
gamepadHandlerId: string
) => {
const gamepad = useGamepad();
useEffect(() => {
const handleGamepadNavigation = (
direction: 'left' | 'right' | 'up' | 'down'
) => {
const elements = Array.from(
sectionRef.current?.querySelectorAll<HTMLDivElement>('[tabindex="0"]') || []
);
if (elements.length === 0) return;
const activeElement = sectionRef.current?.querySelector<HTMLDivElement>(':focus');
if (!activeElement) {
elements[0].focus();
return;
}
let closestElement: HTMLDivElement | null = null;
const currentRect = activeElement.getBoundingClientRect();
let closestDistance = Infinity;
elements.forEach((el) => {
if (el === activeElement) return;
const rect = el.getBoundingClientRect();
let distance = Infinity;
switch (direction) {
case 'left':
if (
rect.right <= currentRect.left &&
(rect.top === currentRect.top ||
(rect.top < currentRect.top && rect.bottom > currentRect.top)
)
) {
distance = currentRect.left - rect.right;
}
break;
case 'right':
if (
currentRect.right <= rect.left &&
(rect.top === currentRect.top ||
(rect.top < currentRect.top && rect.bottom > currentRect.top)
)
) {
distance = rect.left - currentRect.right;
}
break;
case 'up':
if (
rect.bottom <= currentRect.top &&
(rect.left === currentRect.left ||
(rect.left < currentRect.left && rect.right > currentRect.left)
)
) {
distance = currentRect.top - rect.bottom;
}
break;
case 'down':
if (
rect.top >= currentRect.bottom &&
(rect.left === currentRect.left ||
(rect.left < currentRect.left && rect.right > currentRect.left)
)
) {
distance = rect.top - currentRect.bottom;
}
break;
}
if (distance < closestDistance) {
closestDistance = distance;
closestElement = el;
}
});
if (closestElement) {
closestElement.focus();
}
};
const onSelect = () => {
const elements = Array.from(
sectionRef.current?.querySelectorAll<HTMLDivElement>('[tabindex="0"]') || []
);
if (elements.length === 0) return;
const activeElement = sectionRef.current?.querySelector<HTMLDivElement>(':focus');
if (!activeElement) {
elements[0].focus();
return;
}
const isActiveSelectElement = [activeElement.classList].some((className) => /^select-input/.test(className.toString()));
if (!isActiveSelectElement) {
activeElement?.click();
}
};
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;

View file

@ -0,0 +1,24 @@
// Copyright (C) 2017-2025 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;

View file

@ -0,0 +1,55 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { useEffect } from 'react';
import { useGamepad } from '../GamepadContext';
const useVerticalGamepadNavigation = (sectionRef: React.RefObject<HTMLDivElement>, gamepadHandlerId: string) => {
const gamepad = useGamepad();
useEffect(() => {
const focusableSelector = 'a';
const focusableElements = () =>
Array.from(sectionRef.current?.querySelectorAll(focusableSelector) || []);
const moveFocus = (direction: 'prev' | 'next') => {
const route = window.location.hash.replace('#/', '') || 'board';
const elements = focusableElements();
if (!elements.length || route !== gamepadHandlerId) return;
const currentIndex = elements.findIndex((item) => item.classList.contains('selected'));
let nextIndex = currentIndex;
if (direction === 'next')
nextIndex = (elements.length + currentIndex + 1) % elements.length;
if (direction === 'prev')
nextIndex = (elements.length + currentIndex - 1) % elements.length;
elements[nextIndex]?.click();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (!(event as any).spatialNavigationPrevented) {
switch (event.key) {
case 'Tab':
moveFocus('next');
break;
default:
break;
}
}
};
document.addEventListener('keydown', handleKeyDown);
gamepad?.on('buttonLT', gamepadHandlerId, () => moveFocus('prev'));
gamepad?.on('buttonRT', gamepadHandlerId, () => moveFocus('next'));
return () => {
document.removeEventListener('keydown', handleKeyDown);
gamepad?.off('buttonLT', gamepadHandlerId);
gamepad?.off('buttonRT', gamepadHandlerId);
};
}, [gamepad, sectionRef]);
};
export default useVerticalGamepadNavigation;

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,
};

View file

@ -24,6 +24,7 @@ type Settings = {
interfaceLanguage: string,
quitOnClose: boolean,
hideSpoilers: boolean,
gamepadSupport: boolean,
nextVideoNotificationDuration: number,
playInBackground: boolean,
playerType: string | null,