diff --git a/src/App/GamepadModal/GamepadDiagram.tsx b/src/App/GamepadModal/GamepadDiagram.tsx index 4cda3b966..20df63418 100644 --- a/src/App/GamepadModal/GamepadDiagram.tsx +++ b/src/App/GamepadModal/GamepadDiagram.tsx @@ -3,19 +3,56 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useGamepad } from 'stremio/services'; +import type { ControllerType } from 'stremio/services/GamepadContext'; import styles from './styles.less'; type ActiveButton = string | null; const CX = 400; -const BTN = { L1: 'L1', L2: 'L2', R1: 'R1', R2: 'R2' }; const ARROW = { UP: '↑', DOWN: '↓', LEFT: '←', RIGHT: '→' }; +type FaceLayout = { + top: { glyph: string; fontSize: number; weight: number }; + right: { glyph: string; fontSize: number; weight: number }; + bottom: { glyph: string; fontSize: number; weight: number }; + left: { glyph: string; fontSize: number; weight: number }; + lb: string; + rb: string; + lt: string; + rt: string; +}; + +const LAYOUTS: Record = { + playstation: { + top: { glyph: '△', fontSize: 12, weight: 400 }, + right: { glyph: '○', fontSize: 12, weight: 400 }, + bottom: { glyph: '✕', fontSize: 12, weight: 400 }, + left: { glyph: '□', fontSize: 12, weight: 400 }, + lb: 'L1', rb: 'R1', lt: 'L2', rt: 'R2', + }, + xbox: { + top: { glyph: 'Y', fontSize: 11, weight: 700 }, + right: { glyph: 'B', fontSize: 11, weight: 700 }, + bottom: { glyph: 'A', fontSize: 11, weight: 700 }, + left: { glyph: 'X', fontSize: 11, weight: 700 }, + lb: 'LB', rb: 'RB', lt: 'LT', rt: 'RT', + }, + generic: { + top: { glyph: '△', fontSize: 12, weight: 400 }, + right: { glyph: '○', fontSize: 12, weight: 400 }, + bottom: { glyph: '✕', fontSize: 12, weight: 400 }, + left: { glyph: '□', fontSize: 12, weight: 400 }, + lb: 'L1', rb: 'R1', lt: 'L2', rt: 'R2', + }, +}; + const GamepadDiagram = () => { const { t } = useTranslation(); const gamepad = useGamepad(); const [active, setActive] = useState(null); + const layout = LAYOUTS[gamepad?.controllerType ?? 'generic']; + useEffect(() => { let timeout: ReturnType; const flash = (button: string) => () => { @@ -24,12 +61,12 @@ const GamepadDiagram = () => { timeout = setTimeout(() => setActive(null), 400); }; - gamepad?.on('buttonA', 'gamepad-diagram', flash('cross')); - gamepad?.on('buttonB', 'gamepad-diagram', flash('circle')); - gamepad?.on('buttonX', 'gamepad-diagram', flash('square')); - gamepad?.on('buttonY', 'gamepad-diagram', flash('triangle')); - gamepad?.on('buttonLT', 'gamepad-diagram', flash('l1')); - gamepad?.on('buttonRT', 'gamepad-diagram', flash('r1')); + gamepad?.on('buttonA', 'gamepad-diagram', flash('bottom')); + gamepad?.on('buttonB', 'gamepad-diagram', flash('right')); + gamepad?.on('buttonX', 'gamepad-diagram', flash('left')); + gamepad?.on('buttonY', 'gamepad-diagram', flash('top')); + gamepad?.on('buttonLT', 'gamepad-diagram', flash('lb')); + gamepad?.on('buttonRT', 'gamepad-diagram', flash('rb')); gamepad?.on('analog', 'gamepad-diagram', (dir) => dir && flash('stick-' + dir)()); gamepad?.on('analogRight', 'gamepad-diagram', (dir) => dir && flash('rstick-' + dir)()); @@ -54,6 +91,19 @@ const GamepadDiagram = () => { const STX = 75; const BY = 30; + // Xbox controllers are asymmetric — left stick sits upper-left (where the + // d-pad is on PlayStation) and the d-pad drops to the lower-left. + const isXbox = (gamepad?.controllerType ?? 'generic') === 'xbox'; + const lstickPos = isXbox + ? { cx: CX - BX, cy: 148 + BY } + : { cx: CX - STX, cy: 240 + BY }; + const dpadPos = isXbox + ? { cx: CX - STX, cy: 240 + BY } + : { cx: CX - BX, cy: 149 + BY }; + const navLine = isXbox + ? { x1: CX - BX - 24, y1: 148 + BY } + : { x1: CX - STX - 24, y1: 232 + BY }; + return ( @@ -83,12 +133,12 @@ const GamepadDiagram = () => { d={`M${CX - SX - 38},68 Q${CX - SX - 40},48 ${CX - SX - 28},42 L${CX - SX + 28},42 Q${CX - SX + 40},48 ${CX - SX + 38},68 Z`} fill={'url(#triggerGrad)'} stroke={'#3d3660'} strokeWidth={'1'} opacity={'0.7'} /> - {BTN.L2} + {layout.lt} - {BTN.R2} + {layout.rt} { - + - {BTN.L1} + {layout.lb} - + - {BTN.R1} + {layout.rb} - - - + + + {layout.top.glyph} - - - + + + {layout.right.glyph} - - - + + + {layout.bottom.glyph} - - - + + + {layout.left.glyph} - - + + - - - - - - + + + + + + @@ -179,7 +229,7 @@ const GamepadDiagram = () => { - + diff --git a/src/App/GamepadModal/GamepadModal.tsx b/src/App/GamepadModal/GamepadModal.tsx index dd213eca4..1ef0f2290 100644 --- a/src/App/GamepadModal/GamepadModal.tsx +++ b/src/App/GamepadModal/GamepadModal.tsx @@ -6,22 +6,44 @@ import { useTranslation } from 'react-i18next'; import Icon from '@stremio/stremio-icons/react'; import { Button } from 'stremio/components'; import { useGamepad } from 'stremio/services'; +import type { ControllerType } from 'stremio/services/GamepadContext'; import GamepadDiagram from './GamepadDiagram'; import styles from './styles.less'; -const CROSS = '✕'; -const CIRCLE = '○'; -const TRIANGLE = '△'; -const SQUARE = '□'; -const L_STICK = 'L stick'; -const R_STICK = 'R stick'; -const L1 = 'L1'; -const R1 = 'R1'; const LEFT = '←'; const RIGHT = '→'; const UP = '↑'; const DOWN = '↓'; +type FaceLabels = { + bottom: string; + right: string; + left: string; + top: string; + lb: string; + rb: string; + lStick: string; + rStick: string; +}; + +const LABELS: Record = { + playstation: { + bottom: '✕', right: '○', left: '□', top: '△', + lb: 'L1', rb: 'R1', + lStick: 'L stick', rStick: 'R stick', + }, + xbox: { + bottom: 'A', right: 'B', left: 'X', top: 'Y', + lb: 'LB', rb: 'RB', + lStick: 'L stick', rStick: 'R stick', + }, + generic: { + bottom: '✕', right: '○', left: '□', top: '△', + lb: 'L1', rb: 'R1', + lStick: 'L stick', rStick: 'R stick', + }, +}; + type Props = { onClose: () => void, }; @@ -30,6 +52,8 @@ const GamepadModal = ({ onClose }: Props) => { const { t } = useTranslation(); const gamepad = useGamepad(); + const labels = LABELS[gamepad?.controllerType ?? 'generic']; + useEffect(() => { const onKeyDown = ({ key }: KeyboardEvent) => { key === 'Escape' && onClose(); @@ -44,7 +68,7 @@ const GamepadModal = ({ onClose }: Props) => { }, [gamepad]); return createPortal(( -
+
@@ -65,37 +89,37 @@ const GamepadModal = ({ onClose }: Props) => {
{t('GAMEPAD_SECTION_NAVIGATION')}
- {L_STICK} + {labels.lStick} {t('GAMEPAD_ACTION_NAVIGATE')}
- {CROSS} + {labels.bottom} {t('GAMEPAD_ACTION_SELECT')}
- {CIRCLE} + {labels.right} {t('GAMEPAD_ACTION_BACK')}
- {TRIANGLE} + {labels.top} {t('GAMEPAD_ACTION_FULLSCREEN')}
- {SQUARE} + {labels.left} {t('GAMEPAD_ACTION_GUIDE')}
- {L1} + {labels.lb} {t('GAMEPAD_ACTION_PREV_TAB')}
- {R1} + {labels.rb} {t('GAMEPAD_ACTION_NEXT_TAB')}
@@ -104,27 +128,27 @@ const GamepadModal = ({ onClose }: Props) => {
{t('GAMEPAD_SECTION_PLAYER')}
- {SQUARE} + {labels.left} {t('GAMEPAD_ACTION_PLAY_PAUSE')}
- {R_STICK} + {labels.rStick} {LEFT} {t('GAMEPAD_ACTION_SEEK_BACK')}
- {R_STICK} + {labels.rStick} {RIGHT} {t('GAMEPAD_ACTION_SEEK_FWD')}
- {R_STICK} + {labels.rStick} {UP} {t('GAMEPAD_ACTION_VOL_UP')}
- {R_STICK} + {labels.rStick} {DOWN} {t('GAMEPAD_ACTION_VOL_DOWN')}
diff --git a/src/components/MainNavBars/MainNavBars.tsx b/src/components/MainNavBars/MainNavBars.tsx index 03e314b93..1dae27a16 100644 --- a/src/components/MainNavBars/MainNavBars.tsx +++ b/src/components/MainNavBars/MainNavBars.tsx @@ -26,8 +26,9 @@ const MainNavBars = memo(({ className, route, query, children }: Props) => { const navRef = React.useRef(null); const contentRef = React.useRef(null); - useContentGamepadNavigation(contentRef, route ?? 'board'); - useVerticalNavGamepadNavigation(navRef, route ?? 'board'); + const navRoute = route === 'continue_watching' ? 'library' : (route ?? ''); + useContentGamepadNavigation(contentRef, navRoute); + useVerticalNavGamepadNavigation(navRef, navRoute); return (
diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index a58acc14f..c9ba5cd7e 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -117,18 +117,18 @@ const ControlBar = React.forwardRef(({ onSeekRequested={onSeekRequested} />
- { nextVideo !== null ? - : null } -
- - - - - { metaItem?.content?.videos?.length > 0 ? - : null } - -
diff --git a/src/routes/Player/ControlBar/SeekBar/SeekBar.js b/src/routes/Player/ControlBar/SeekBar/SeekBar.js index f60902a28..3140bf5d1 100644 --- a/src/routes/Player/ControlBar/SeekBar/SeekBar.js +++ b/src/routes/Player/ControlBar/SeekBar/SeekBar.js @@ -59,7 +59,7 @@ const SeekBar = ({ className, time, duration, buffered, onSeekRequested }) => { onSlide={onSlide} onComplete={onComplete} /> -