refactor: improve navigation

This commit is contained in:
Timothy Z. 2026-04-28 23:49:46 +03:00
parent 972bd23991
commit 70e14c4871
4 changed files with 60 additions and 10 deletions

View file

@ -9,6 +9,7 @@ type ActiveButton = string | null;
const CX = 400;
const BTN = { L1: 'L1', L2: 'L2', R1: 'R1', R2: 'R2' };
const ARROW = { UP: '↑', DOWN: '↓', LEFT: '←', RIGHT: '→' };
const GamepadDiagram = () => {
const { t } = useTranslation();
@ -30,6 +31,7 @@ const GamepadDiagram = () => {
gamepad?.on('buttonLT', 'gamepad-diagram', flash('l1'));
gamepad?.on('buttonRT', 'gamepad-diagram', flash('r1'));
gamepad?.on('analog', 'gamepad-diagram', (dir: string) => flash('stick-' + dir)());
gamepad?.on('analogRight', 'gamepad-diagram', (dir: string) => flash('rstick-' + dir)());
return () => {
clearTimeout(timeout);
@ -40,6 +42,7 @@ const GamepadDiagram = () => {
gamepad?.off('buttonLT', 'gamepad-diagram');
gamepad?.off('buttonRT', 'gamepad-diagram');
gamepad?.off('analog', 'gamepad-diagram');
gamepad?.off('analogRight', 'gamepad-diagram');
};
}, [gamepad]);
@ -168,8 +171,14 @@ const GamepadDiagram = () => {
</g>
{/* ===== RIGHT STICK (CX+STX) ===== */}
<circle cx={CX + STX} cy={240 + BY} r={'26'} fill={'#1a1530'} stroke={'#3d3660'} strokeWidth={'1.5'} />
<circle cx={CX + STX} cy={240 + BY} r={'17'} fill={'#1e1a35'} stroke={'#3d3660'} strokeWidth={'1'} opacity={'0.35'} />
<g filter={active?.startsWith('rstick-') ? 'url(#glow)' : undefined}>
<circle cx={CX + STX} cy={240 + BY} r={'26'} fill={'#1a1530'} stroke={active?.startsWith('rstick-') ? '#7b5bf5' : '#3d3660'} strokeWidth={'2'} />
<circle cx={CX + STX} cy={240 + BY} r={'17'} fill={'#252040'} stroke={'#4a4075'} strokeWidth={'1.5'} />
<text x={CX + STX} y={232 + BY} textAnchor={'middle'} fill={active === 'rstick-up' ? '#fff' : '#5848a0'} fontSize={'9'} fontWeight={active === 'rstick-up' ? '700' : '400'}>{ARROW.UP}</text>
<text x={CX + STX} y={253 + BY} textAnchor={'middle'} fill={active === 'rstick-down' ? '#fff' : '#5848a0'} fontSize={'9'} fontWeight={active === 'rstick-down' ? '700' : '400'}>{ARROW.DOWN}</text>
<text x={CX + STX - 11} y={244 + BY} textAnchor={'middle'} fill={active === 'rstick-left' ? '#fff' : '#5848a0'} fontSize={'9'} fontWeight={active === 'rstick-left' ? '700' : '400'}>{ARROW.LEFT}</text>
<text x={CX + STX + 11} y={244 + BY} textAnchor={'middle'} fill={active === 'rstick-right' ? '#fff' : '#5848a0'} fontSize={'9'} fontWeight={active === 'rstick-right' ? '700' : '400'}>{ARROW.RIGHT}</text>
</g>
{/* ============================= */}
{/* ===== LABELS — LEFT ===== */}
@ -184,7 +193,6 @@ const GamepadDiagram = () => {
<line x1={CX - STX - 24} y1={232 + BY} x2={'85'} y2={168} stroke={'#7b5bf5'} strokeWidth={'1'} opacity={'0.4'} />
<circle cx={'85'} cy={168} r={'2'} fill={'#7b5bf5'} />
<text x={'80'} y={164} textAnchor={'end'} fill={'#c4b5fd'} fontSize={'12'} fontWeight={'500'}>{t('GAMEPAD_ACTION_NAVIGATE')}</text>
<text x={'80'} y={179} textAnchor={'end'} fill={'#8b7faa'} fontSize={'10'}>{t('GAMEPAD_LABEL_STICK_PLAYER')}</text>
{/* □ Square */}
<line x1={CX + BX - 44} y1={148 + BY} x2={'85'} y2={248} stroke={'#5848a0'} strokeWidth={'1'} opacity={'0.35'} />
@ -216,6 +224,11 @@ const GamepadDiagram = () => {
<text x={'720'} y={204} textAnchor={'start'} fill={'#c4b5fd'} fontSize={'12'} fontWeight={'500'}>{t('GAMEPAD_ACTION_SELECT')}</text>
<text x={'720'} y={219} textAnchor={'start'} fill={'#8b7faa'} fontSize={'10'}>{t('GAMEPAD_LABEL_PLAY_PAUSE_PLAYER')}</text>
{/* Right stick */}
<line x1={CX + STX + 24} y1={234 + BY} x2={'715'} y2={268} stroke={'#5848a0'} strokeWidth={'1'} opacity={'0.4'} />
<circle cx={'715'} cy={268} r={'2'} fill={'#5848a0'} />
<text x={'720'} y={264} textAnchor={'start'} fill={'#c4b5fd'} fontSize={'12'} fontWeight={'500'}>{t('GAMEPAD_LABEL_SEEK_VOL')}</text>
{/* Compat note */}
<text x={CX} y={'475'} textAnchor={'middle'} fill={'#5848a0'} fontSize={'11'}>{t('GAMEPAD_LABEL_COMPAT')}</text>
</svg>

View file

@ -14,6 +14,7 @@ const CIRCLE = '○';
const TRIANGLE = '△';
const SQUARE = '□';
const L_STICK = 'L stick';
const R_STICK = 'R stick';
const L1 = 'L1';
const R1 = 'R1';
const LEFT = '←';
@ -108,22 +109,22 @@ const GamepadModal = ({ onClose }: Props) => {
<span className={styles['action']}>{t('GAMEPAD_ACTION_PLAY_PAUSE')}</span>
</div>
<div className={styles['mapping']}>
<kbd className={styles['kbd']}>{L_STICK}</kbd>
<kbd className={styles['kbd']}>{R_STICK}</kbd>
<span className={styles['dir']}>{LEFT}</span>
<span className={styles['action']}>{t('GAMEPAD_ACTION_SEEK_BACK')}</span>
</div>
<div className={styles['mapping']}>
<kbd className={styles['kbd']}>{L_STICK}</kbd>
<kbd className={styles['kbd']}>{R_STICK}</kbd>
<span className={styles['dir']}>{RIGHT}</span>
<span className={styles['action']}>{t('GAMEPAD_ACTION_SEEK_FWD')}</span>
</div>
<div className={styles['mapping']}>
<kbd className={styles['kbd']}>{L_STICK}</kbd>
<kbd className={styles['kbd']}>{R_STICK}</kbd>
<span className={styles['dir']}>{UP}</span>
<span className={styles['action']}>{t('GAMEPAD_ACTION_VOL_UP')}</span>
</div>
<div className={styles['mapping']}>
<kbd className={styles['kbd']}>{L_STICK}</kbd>
<kbd className={styles['kbd']}>{R_STICK}</kbd>
<span className={styles['dir']}>{DOWN}</span>
<span className={styles['action']}>{t('GAMEPAD_ACTION_VOL_DOWN')}</span>
</div>

View file

@ -8,6 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
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');
@ -60,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();
@ -444,13 +446,15 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [onSeekPrev, onSeekNext, onVolumeUp, onVolumeDown]);
useContentGamepadNavigation(playerRef, GAMEPAD_HANDLER_ID);
React.useEffect(() => {
gamepad?.on('buttonA', GAMEPAD_HANDLER_ID, onPlayPause);
gamepad?.on('analog', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
gamepad?.on('analogRight', GAMEPAD_HANDLER_ID, onGamepadSeekAndVol);
return () => {
gamepad?.off('buttonA', GAMEPAD_HANDLER_ID);
gamepad?.off('analog', GAMEPAD_HANDLER_ID);
gamepad?.off('analogRight', GAMEPAD_HANDLER_ID);
};
}, [onPlayPause, onGamepadSeekAndVol]);
@ -969,7 +973,7 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
return (
<div className={classnames(styles['player-container'], { [styles['overlayHidden']]: overlayHidden })}
<div ref={playerRef} className={classnames(styles['player-container'], { [styles['overlayHidden']]: overlayHidden })}
onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove}

View file

@ -18,6 +18,7 @@ const GamepadProvider: React.FC<{
const lastButtonState = useRef<number[]>([]);
const lastButtonPressedTime = useRef<number>(0);
const axisTimer = useRef<number>(0);
const axisTimerRight = useRef<number>(0);
const eventHandlers = useRef<GamepadEventHandlers>(new Map());
const on = useCallback((event: string, id: string, callback: (data?: any) => void) => {
@ -170,6 +171,37 @@ const GamepadProvider: React.FC<{
}
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);