diff --git a/package.json b/package.json index 5bed50260..71020bef8 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#c23317eec194b5a3318e98c2ea6acae5cfa32e2a", + "stremio-translations": "github:Stremio/stremio-translations#176c69f3bdede824f37979a046ee27cef355affc", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6aacf8a4..5b9107a42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ importers: specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6 version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6 stremio-translations: - specifier: github:Stremio/stremio-translations#c23317eec194b5a3318e98c2ea6acae5cfa32e2a - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a + specifier: github:Stremio/stremio-translations#176c69f3bdede824f37979a046ee27cef355affc + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/176c69f3bdede824f37979a046ee27cef355affc url: specifier: 0.11.4 version: 0.11.4 @@ -4133,9 +4133,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a} - version: 1.51.0 + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/176c69f3bdede824f37979a046ee27cef355affc: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/176c69f3bdede824f37979a046ee27cef355affc} + version: 1.52.0 string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -9378,7 +9378,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/176c69f3bdede824f37979a046ee27cef355affc: {} string-length@4.0.2: dependencies: diff --git a/src/App/App.js b/src/App/App.js index c27cc2901..375c1f5db 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -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, FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); @@ -12,6 +12,7 @@ const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); const { default: UpdaterBanner } = require('./UpdaterBanner'); const { default: ShortcutsModal } = require('./ShortcutsModal'); +const { default: GamepadModal } = require('./GamepadModal'); const ErrorDialog = require('./ErrorDialog'); const withProtectedRoutes = require('./withProtectedRoutes'); const routerViewsConfig = require('./routerViewsConfig'); @@ -22,6 +23,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; }, []); @@ -40,12 +42,18 @@ const App = () => { }, []); const [initialized, setInitialized] = React.useState(false); const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false); + const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false); const onShortcut = React.useCallback((name) => { - if (name === 'shortcuts') { - toggleShortcutModal(); + switch (name) { + case 'shortcuts': + toggleShortcutModal(); + break; + case 'gamepadGuide': + toggleGamepadModal(); + break; } - }, [toggleShortcutModal]); + }, [toggleShortcutModal, toggleGamepadModal]); React.useEffect(() => { let prevPath = window.location.hash.slice(1); @@ -141,6 +149,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 +166,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,22 +229,27 @@ const App = () => { - - - { - shortcutModalOpen && - } - - - - - - - + + + + { + shortcutModalOpen && + } + { + gamepadModalOpen && + } + + + + + + + + diff --git a/src/App/GamepadModal/GamepadDiagram.tsx b/src/App/GamepadModal/GamepadDiagram.tsx new file mode 100644 index 000000000..4cda3b966 --- /dev/null +++ b/src/App/GamepadModal/GamepadDiagram.tsx @@ -0,0 +1,214 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGamepad } from 'stremio/services'; +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: '→' }; + +const GamepadDiagram = () => { + const { t } = useTranslation(); + const gamepad = useGamepad(); + const [active, setActive] = useState(null); + + useEffect(() => { + let timeout: ReturnType; + const flash = (button: string) => () => { + setActive(button); + clearTimeout(timeout); + 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('analog', 'gamepad-diagram', (dir) => dir && flash('stick-' + dir)()); + gamepad?.on('analogRight', 'gamepad-diagram', (dir) => dir && flash('rstick-' + dir)()); + + return () => { + clearTimeout(timeout); + gamepad?.off('buttonA', 'gamepad-diagram'); + gamepad?.off('buttonB', 'gamepad-diagram'); + gamepad?.off('buttonX', 'gamepad-diagram'); + gamepad?.off('buttonY', 'gamepad-diagram'); + gamepad?.off('buttonLT', 'gamepad-diagram'); + gamepad?.off('buttonRT', 'gamepad-diagram'); + gamepad?.off('analog', 'gamepad-diagram'); + gamepad?.off('analogRight', 'gamepad-diagram'); + }; + }, [gamepad]); + + const glow = (id: string) => active === id ? '#7b5bf5' : undefined; + const glowOp = (id: string) => active === id ? 1 : undefined; + + const SX = 130; + const BX = 120; + const STX = 75; + const BY = 30; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + {BTN.L2} + + {BTN.R2} + + + + + + + + {BTN.L1} + + + + {BTN.R1} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {ARROW.UP} + {ARROW.DOWN} + {ARROW.LEFT} + {ARROW.RIGHT} + + + + + + + + + + + + + + + + + + + + + + + + + {t('GAMEPAD_ACTION_PREV_TAB')} + {t('GAMEPAD_ACTION_NAVIGATE')} + {t('GAMEPAD_ACTION_GUIDE')} + {t('GAMEPAD_LABEL_PLAY_PAUSE_PLAYER')} + {t('GAMEPAD_ACTION_NEXT_TAB')} + {t('GAMEPAD_ACTION_FULLSCREEN')} + {t('GAMEPAD_ACTION_BACK')} + {t('GAMEPAD_ACTION_SELECT')} + {t('GAMEPAD_LABEL_SEEK_VOL')} + {t('GAMEPAD_LABEL_COMPAT')} + + + ); +}; + +export default GamepadDiagram; diff --git a/src/App/GamepadModal/GamepadModal.tsx b/src/App/GamepadModal/GamepadModal.tsx new file mode 100644 index 000000000..dd213eca4 --- /dev/null +++ b/src/App/GamepadModal/GamepadModal.tsx @@ -0,0 +1,139 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import Icon from '@stremio/stremio-icons/react'; +import { Button } from 'stremio/components'; +import { useGamepad } from 'stremio/services'; +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 Props = { + onClose: () => void, +}; + +const GamepadModal = ({ onClose }: Props) => { + const { t } = useTranslation(); + const gamepad = useGamepad(); + + useEffect(() => { + const onKeyDown = ({ key }: KeyboardEvent) => { + key === 'Escape' && onClose(); + }; + + document.addEventListener('keydown', onKeyDown); + gamepad?.on('buttonB', 'gamepad-modal', onClose); + return () => { + document.removeEventListener('keydown', onKeyDown); + gamepad?.off('buttonB', 'gamepad-modal'); + }; + }, [gamepad]); + + return createPortal(( +
+
+ +
+
+
+ {t('GAMEPAD_CONTROLS_TITLE')} +
+ + +
+ +
+ + +
+
+
{t('GAMEPAD_SECTION_NAVIGATION')}
+
+ {L_STICK} + + {t('GAMEPAD_ACTION_NAVIGATE')} +
+
+ {CROSS} + + {t('GAMEPAD_ACTION_SELECT')} +
+
+ {CIRCLE} + + {t('GAMEPAD_ACTION_BACK')} +
+
+ {TRIANGLE} + + {t('GAMEPAD_ACTION_FULLSCREEN')} +
+
+ {SQUARE} + + {t('GAMEPAD_ACTION_GUIDE')} +
+
+ {L1} + + {t('GAMEPAD_ACTION_PREV_TAB')} +
+
+ {R1} + + {t('GAMEPAD_ACTION_NEXT_TAB')} +
+
+ +
+
{t('GAMEPAD_SECTION_PLAYER')}
+
+ {SQUARE} + + {t('GAMEPAD_ACTION_PLAY_PAUSE')} +
+
+ {R_STICK} + {LEFT} + {t('GAMEPAD_ACTION_SEEK_BACK')} +
+
+ {R_STICK} + {RIGHT} + {t('GAMEPAD_ACTION_SEEK_FWD')} +
+
+ {R_STICK} + {UP} + {t('GAMEPAD_ACTION_VOL_UP')} +
+
+ {R_STICK} + {DOWN} + {t('GAMEPAD_ACTION_VOL_DOWN')} +
+
+
+
+
+
+ ), document.body); +}; + +export default GamepadModal; diff --git a/src/App/GamepadModal/index.ts b/src/App/GamepadModal/index.ts new file mode 100644 index 000000000..7ea9c5b43 --- /dev/null +++ b/src/App/GamepadModal/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import GamepadModal from './GamepadModal'; +export default GamepadModal; diff --git a/src/App/GamepadModal/styles.less b/src/App/GamepadModal/styles.less new file mode 100644 index 000000000..e512aa333 --- /dev/null +++ b/src/App/GamepadModal/styles.less @@ -0,0 +1,214 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.gamepad-modal { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + + .backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: @color-background-dark5-40; + cursor: pointer; + } + + .container { + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 90%; + max-width: 72rem; + width: 92%; + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + box-shadow: var(--outer-glow); + overflow-y: auto; + + .header { + flex: none; + display: flex; + justify-content: space-between; + align-items: center; + height: 5rem; + padding-left: 2.5rem; + padding-right: 1rem; + + .title { + position: relative; + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + } + + .close-button { + position: relative; + width: 3rem; + height: 3rem; + padding: 0.5rem; + border-radius: var(--border-radius); + z-index: 2; + + .icon { + display: block; + width: 100%; + height: 100%; + color: var(--primary-foreground-color); + opacity: 0.4; + } + + &:hover, &:focus { + .icon { + opacity: 1; + color: var(--primary-foreground-color); + } + } + + &:focus { + outline-color: var(--primary-foreground-color); + } + } + } + + .content { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 3rem; + padding: 0 3rem; + padding-bottom: 3rem; + overflow-y: auto; + } + } + + @keyframes draw-stroke { + from { stroke-dashoffset: 2000; } + to { stroke-dashoffset: 0; } + } + + @keyframes draw-line { + from { stroke-dashoffset: 800; } + to { stroke-dashoffset: 0; } + } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + + .diagram { + width: 100%; + max-width: 48rem; + height: auto; + + .anim-body { + stroke-dasharray: 2000; + stroke-dashoffset: 0; + animation: draw-stroke 1.4s ease-in-out; + } + + .anim-controls { + animation: fade-in 0.6s ease-out 1s both; + } + + .anim-lines { + line { + stroke-dasharray: 800; + stroke-dashoffset: 0; + animation: draw-line 0.8s ease-out 1.6s both; + } + circle { + animation: fade-in 0.3s ease-out 2s both; + } + } + + .anim-labels { + animation: fade-in 0.5s ease-out 2.2s both; + } + } + + .sections { + display: flex; + flex-direction: row; + gap: 5rem; + width: 100%; + max-width: 56rem; + } + + .section { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.2rem; + } + + .section-title { + font-size: 1rem; + font-weight: 600; + color: var(--primary-foreground-color); + margin-bottom: 0.4rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.7; + } + + .mapping { + display: grid; + grid-template-columns: auto 1.2rem 1fr; + align-items: center; + column-gap: 0.5rem; + } + + .kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2rem; + height: 1.8rem; + padding: 0 0.5rem; + border-radius: 0.35rem; + background-color: rgba(123, 91, 245, 0.15); + border: 1px solid rgba(123, 91, 245, 0.3); + box-shadow: none; + color: #c4b5fd; + font-size: 0.8rem; + font-weight: 600; + font-family: inherit; + } + + .dir { + color: #8b7faa; + font-size: 1rem; + } + + .action { + color: var(--primary-foreground-color); + font-size: 0.9rem; + opacity: 0.8; + } +} + +@media only screen and (max-width: 640px) { + .gamepad-modal { + .container { + max-width: 95%; + } + + .sections { + flex-direction: column; + gap: 2rem; + } + } +} diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx index c9198a857..348471484 100644 --- a/src/common/Shortcuts/Shortcuts.tsx +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -19,10 +19,20 @@ type Props = { onShortcut: (name: ShortcutName) => void, }; +const REPEAT_THROTTLE_MS = 130; + const ShortcutsProvider = ({ children, onShortcut }: Props) => { const listeners = useRef>>(new Map()); + const lastRepeatTime = useRef>(new Map()); + + const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key, repeat }: KeyboardEvent) => { + if (repeat) { + const now = Date.now(); + const last = lastRepeatTime.current.get(code) ?? 0; + if (now - last < REPEAT_THROTTLE_MS) return; + lastRepeatTime.current.set(code, now); + } - const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key }: KeyboardEvent) => { SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => { const modifers = (keys.includes('Ctrl') ? ctrlKey : true) && (keys.includes('Shift') ? shiftKey : true); diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json index 659c3c373..536b9f7f7 100644 --- a/src/common/Shortcuts/shortcuts.json +++ b/src/common/Shortcuts/shortcuts.json @@ -27,6 +27,11 @@ "name": "shortcuts", "label": "SETTINGS_SHORTCUT_SHORTCUTS", "combos": [["Ctrl", "/"]] + }, + { + "name": "gamepadGuide", + "label": "GAMEPAD_ACTION_GUIDE", + "combos": [["Ctrl", "G"]] } ] }, diff --git a/src/components/ActionsGroup/ActionsGroup.less b/src/components/ActionsGroup/ActionsGroup.less index 09e903435..b360ebdad 100644 --- a/src/components/ActionsGroup/ActionsGroup.less +++ b/src/components/ActionsGroup/ActionsGroup.less @@ -27,18 +27,27 @@ width: @width; padding: 0 1rem; cursor: pointer; + border-radius: 2rem; + outline: none; .icon { width: calc(@width / 2); height: calc(@height / 2); color: var(--primary-foreground-color); opacity: 0.7; + } - &:hover { + &:hover, &:focus { + .icon { opacity: 1; } } + &:focus-visible { + outline: 2px solid var(--primary-foreground-color); + outline-offset: -2px; + } + &.disabled { pointer-events: none; } diff --git a/src/components/ActionsGroup/ActionsGroup.tsx b/src/components/ActionsGroup/ActionsGroup.tsx index 052f25016..844efd3d0 100644 --- a/src/components/ActionsGroup/ActionsGroup.tsx +++ b/src/components/ActionsGroup/ActionsGroup.tsx @@ -28,6 +28,7 @@ const ActionsGroup = ({ items, className }: Props) => {
{ diff --git a/src/components/Chips/Chip/Chip.tsx b/src/components/Chips/Chip/Chip.tsx index cbb2487f7..4cde13bb4 100644 --- a/src/components/Chips/Chip/Chip.tsx +++ b/src/components/Chips/Chip/Chip.tsx @@ -33,7 +33,7 @@ const Chip = memo(({ label, value, active, onSelect }: Props) => { ref={ref} key={value} className={classNames(styles['chip'], { [styles['active']]: active })} - tabIndex={-1} + tabIndex={0} data-value={value} onClick={onClick} > diff --git a/src/components/MainNavBars/MainNavBars.tsx b/src/components/MainNavBars/MainNavBars.tsx index 43d08f5c8..03e314b93 100644 --- a/src/components/MainNavBars/MainNavBars.tsx +++ b/src/components/MainNavBars/MainNavBars.tsx @@ -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 ?? 'board'); + useVerticalNavGamepadNavigation(navRef, route ?? 'board'); + return (
{ navMenu={true} /> -
{children}
+
{children}
); }); diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js index 5fa7d8ff0..6fba9c098 100644 --- a/src/components/MetaPreview/MetaPreview.js +++ b/src/components/MetaPreview/MetaPreview.js @@ -159,7 +159,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou title={linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label} href={linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).href} target={'_blank'} - {...(compact ? { tabIndex: -1 } : null)} + tabIndex={0} >
{linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}
@@ -214,7 +214,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou className={styles['action-button']} icon={'trailer'} label={t('TRAILER')} - tabIndex={compact ? -1 : 0} + tabIndex={0} href={trailerHref} tooltip={compact} /> @@ -232,7 +232,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou className={classnames(styles['action-button'], styles['show-button'])} icon={'play'} label={t('SHOW')} - tabIndex={compact ? -1 : 0} + tabIndex={0} href={showHref} /> : @@ -255,7 +255,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou icon={'share'} label={t('CTX_SHARE')} tooltip={true} - tabIndex={compact ? -1 : 0} + tabIndex={0} onClick={openShareModal} /> { diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx index 329ee4945..8ba867800 100644 --- a/src/components/MetaPreview/Ratings/Ratings.tsx +++ b/src/components/MetaPreview/Ratings/Ratings.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import useRating from './useRating'; import { ActionsGroup } from 'stremio/components'; @@ -11,17 +12,20 @@ type Props = { }; const Ratings = ({ ratingInfo, className }: Props) => { + const { t } = useTranslation(); const { onLiked, onLoved, liked, loved } = useRating(ratingInfo); const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]); const items = useMemo(() => [ { icon: liked ? 'thumbs-up' : 'thumbs-up-outline', + label: liked ? t('RATING_UNLIKE') : t('RATING_LIKE'), disabled, onClick: onLiked, }, { icon: loved ? 'heart' : 'heart-outline', + label: loved ? t('RATING_UNLOVE') : t('RATING_LOVE'), disabled, onClick: onLoved, }, diff --git a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js index 4b0655918..b1644c2b3 100644 --- a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -7,6 +7,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image } = require('stremio/components'); const { useFullscreen } = require('stremio/common/Fullscreen'); 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} ), []); + useHorizontalNavGamepadNavigation(route || className, backButton); return (