From 41865276d52224bfdedbcf480d50ab9a0f59fa20 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 25 Mar 2025 19:07:01 +0200 Subject: [PATCH] fix(GamepadSupport): fix issue with multiple handler events being fired --- src/routes/Player/Player.js | 10 +-- src/services/GamepadContext/GamepadContext.ts | 4 +- .../GamepadContext/GamepadProvider.tsx | 62 ++++++++++++++----- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index c45ed8fc7..d44c24a25 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -29,6 +29,8 @@ const useVideo = require('./useVideo'); const styles = require('./styles'); const Video = require('./Video'); +const GAMEPAD_HANDLER_ID = 'player'; + const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); const { chromecast, shell, core } = useServices(); @@ -344,12 +346,12 @@ const Player = ({ urlParams, queryParams }) => { }, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]); React.useEffect(() => { - gamepad.on('buttonA', onPlayPause); - gamepad.on('analog', onGamepadSeekAndVol); + gamepad.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause); + gamepad.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol); return () => { - gamepad.off('buttonA', onPlayPause); - gamepad.off('analog', onGamepadSeekAndVol); + gamepad.off('buttonA', GAMEPAD_HANDLER_ID); + gamepad.off('analog', GAMEPAD_HANDLER_ID); }; }, [onPlayPause, onGamepadSeekAndVol]); diff --git a/src/services/GamepadContext/GamepadContext.ts b/src/services/GamepadContext/GamepadContext.ts index d422b7153..c18a65dd6 100644 --- a/src/services/GamepadContext/GamepadContext.ts +++ b/src/services/GamepadContext/GamepadContext.ts @@ -1,8 +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; + on: (event: string, id: string, callback: (data?: any) => 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 index 263b2b965..e60894c36 100644 --- a/src/services/GamepadContext/GamepadProvider.tsx +++ b/src/services/GamepadContext/GamepadProvider.tsx @@ -1,39 +1,73 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; +import useToast from 'stremio/common/Toast/useToast'; import GamepadContext from './GamepadContext'; -type GamepadEventHandlers = Record void)[]>; +type GamepadEventHandlers = Map void>>; const GamepadProvider: React.FC<{ enabled: boolean; children: React.ReactNode; }> = ({ enabled, children }) => { + const toast = useToast(); const [connectedGamepads, setConnectedGamepads] = useState(0); const lastButtonState = useRef([]); const lastButtonPressedTime = useRef(0); const axisTimer = useRef(0); - const eventHandlers = useRef({}); + const eventHandlers = useRef(new Map()); - const on = useCallback((event: string, callback: (data?: any) => void) => { - if (!eventHandlers.current[event]) { - eventHandlers.current[event] = []; + const on = useCallback((event: string, id: string, callback: (data?: any) => void) => { + if (!eventHandlers.current.has(event)) { + eventHandlers.current.set(event, new Map()); } - eventHandlers.current[event].push(callback); + + const handlers = eventHandlers.current.get(event)!; + + // Ensure only one handler per component + handlers.set(id, 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 off = useCallback((event: string, id: string) => { + eventHandlers.current.get(event)?.delete(id); }, []); const emit = (event: string, data?: any) => { - if (eventHandlers.current[event]) { - eventHandlers.current[event].forEach((callback) => callback(data)); + if (eventHandlers.current.has(event)) { + eventHandlers.current.get(event)!.forEach((callback) => callback(data)); } }; + const onGamepadConnected = () => { + // @ts-ignore + toast.show({ + type: 'info', + title: 'Gamepad detected', + timeout: 4000, + }); + }; + + const onGamepadDisconnected = () => { + // @ts-ignore + toast.show({ + type: 'info', + title: '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 (typeof navigator.getGamepads !== 'function') return;