diff --git a/package.json b/package.json index 924787d30..c3e3683a7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@babel/runtime": "7.29.2", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.56.4", + "@stremio/stremio-core-web": "0.57.0", "@stremio/stremio-icons": "5.10.0", "@stremio/stremio-video": "0.0.79", "a-color-picker": "1.2.1", @@ -41,7 +41,7 @@ "react-i18next": "^15.7.4", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#d9cd2fb88268b365b14101452665de698f9c15e9", + "stremio-translations": "github:Stremio/stremio-translations#c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0", "url": "0.11.4", "use-long-press": "^3.3.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f391ae50..a28953bdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: 5.2.0 version: 5.2.0 '@stremio/stremio-core-web': - specifier: 0.56.4 - version: 0.56.4 + specifier: 0.57.0 + version: 0.57.0 '@stremio/stremio-icons': specifier: 5.10.0 version: 5.10.0 @@ -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#d9cd2fb88268b365b14101452665de698f9c15e9 - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9 + specifier: github:Stremio/stremio-translations#c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0 + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0 url: specifier: 0.11.4 version: 0.11.4 @@ -123,12 +123,12 @@ importers: '@types/lodash.throttle': specifier: ^4.1.9 version: 4.1.9 - '@types/node': - specifier: ^25.6.0 - version: 25.6.0 '@types/magnet-uri': specifier: ^5.1.5 version: 5.1.5 + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 '@types/react': specifier: ^18.3.28 version: 18.3.28 @@ -1394,8 +1394,8 @@ packages: '@stremio/stremio-colors@5.2.0': resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==} - '@stremio/stremio-core-web@0.56.4': - resolution: {integrity: sha512-tFAMYgKrJ1bkvHRMpxDykM/844sDjgRPFk6FLhjQiwh01OHIyEgDqGo/NgwFM+CuMR4mW676SDvwNHkK0Xqg3w==} + '@stremio/stremio-core-web@0.57.0': + resolution: {integrity: sha512-go8GZwGm6MFfjez6J/T1HrGNY2330EU3VoVinDYR0rE331aay6fenViLyCYLE829FTebW2eglrmc7MdHjmhqSA==} '@stremio/stremio-icons@5.10.0': resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==} @@ -1497,9 +1497,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node-forge@1.3.14': - resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} @@ -4424,8 +4421,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0} version: 1.52.0 string-length@4.0.2: @@ -6447,7 +6444,7 @@ snapshots: '@stremio/stremio-colors@5.2.0': {} - '@stremio/stremio-core-web@0.56.4': + '@stremio/stremio-core-web@0.57.0': dependencies: '@babel/runtime': 7.24.1 @@ -10003,7 +10000,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0: {} string-length@4.0.2: dependencies: diff --git a/src/App/App.js b/src/App/App.js index 0e5bb3fe1..437332f63 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -5,7 +5,7 @@ const React = require('react'); const { useTranslation } = require('react-i18next'); const { useCore } = require('stremio/core'); const { Router } = require('stremio-router'); -const { Shell, Chromecast, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services'); +const { Shell, Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); @@ -33,13 +33,12 @@ const App = () => { return { shell: new Shell(), chromecast: new Chromecast(), - keyboardShortcuts: new KeyboardShortcuts(), }; }, []); const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false); const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false); - const onShortcut = React.useCallback((name) => { + const onShortcut = React.useCallback((name, combo, key) => { switch (name) { case 'shortcuts': toggleShortcutModal(); @@ -47,6 +46,18 @@ const App = () => { case 'gamepadGuide': toggleGamepadModal(); break; + case 'navigateSearch': + window.location = '#/search'; + break; + case 'navigateTabs': { + const routes = ['', 'discover', 'library', 'calendar', 'addons', 'settings']; + const index = key - 1; + if (index in routes) window.location = `#/${routes[index]}`; + break; + } + case 'navigateHistory': + combo === 0 ? window.history.back() : window.history.forward(); + break; } }, [toggleShortcutModal, toggleGamepadModal]); @@ -90,12 +101,10 @@ const App = () => { services.chromecast.on('stateChanged', onChromecastStateChange); services.shell.start(); services.chromecast.start(); - services.keyboardShortcuts.start(); window.services = services; return () => { services.shell.stop(); services.chromecast.stop(); - services.keyboardShortcuts.stop(); services.chromecast.off('stateChanged', onChromecastStateChange); }; }, []); diff --git a/src/common/Fullscreen/FullscreenProvider.tsx b/src/common/Fullscreen/FullscreenProvider.tsx index 9bc4c26b8..5e4aecccb 100644 --- a/src/common/Fullscreen/FullscreenProvider.tsx +++ b/src/common/Fullscreen/FullscreenProvider.tsx @@ -11,16 +11,6 @@ type Props = { children: React.ReactNode, }; -const isTextInputFocused = () => { - const activeElement = document.activeElement; - - return activeElement instanceof HTMLElement && - (activeElement.tagName === 'INPUT' || - activeElement.tagName === 'TEXTAREA' || - activeElement.tagName === 'SELECT' || - activeElement.isContentEditable); -}; - const hasWebkitFullscreen = typeof HTMLVideoElement !== 'undefined' && typeof HTMLVideoElement.prototype.webkitEnterFullscreen === 'function'; @@ -48,7 +38,11 @@ const FullscreenProvider = ({ children }: Props) => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: true }); } else if (document.fullscreenEnabled) { - await document.documentElement.requestFullscreen(); + try { + await document.documentElement.requestFullscreen(); + } catch (err) { + console.error('Error enabling fullscreen', err); + } } else if (videoElementRef.current && hasWebkitFullscreen) { (videoElementRef.current as any).webkitEnterFullscreen(); } @@ -68,12 +62,7 @@ const FullscreenProvider = ({ children }: Props) => { fullscreen ? exitFullscreen() : requestFullscreen(); }, [fullscreen, exitFullscreen, requestFullscreen]); - const toggleFullscreenFromShortcut = useCallback(() => { - if (isTextInputFocused()) return; - toggleFullscreen(); - }, [toggleFullscreen]); - - onShortcut('fullscreen', toggleFullscreenFromShortcut, [toggleFullscreenFromShortcut]); + onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]); useEffect(() => { const videoElement = videoElementRef.current; diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx index fd558f7d6..c27f5b25e 100644 --- a/src/common/Shortcuts/Shortcuts.tsx +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -16,16 +16,26 @@ const ShortcutsContext = createContext({} as ShortcutsContext) type Props = { children: JSX.Element, - onShortcut: (name: ShortcutName) => void, + onShortcut: (name: ShortcutName, combo: number, key: string) => void, }; const REPEAT_THROTTLE_MS = 130; +const isInputFocused = () => { + const inputElements = ['INPUT', 'TEXTAREA', 'SELECT']; + const activeElement = document.activeElement; + + return activeElement instanceof HTMLElement && + (inputElements.includes(activeElement.tagName) || activeElement.isContentEditable); +}; + const ShortcutsProvider = ({ children, onShortcut }: Props) => { const listeners = useRef>>(new Map()); const lastRepeatTime = useRef>(new Map()); const onKeyDown = useCallback(({ ctrlKey, shiftKey, altKey, metaKey, code, key, repeat }: KeyboardEvent) => { + if (isInputFocused()) return; + if (repeat) { const now = Date.now(); const last = lastRepeatTime.current.get(code) ?? 0; @@ -43,7 +53,7 @@ const ShortcutsProvider = ({ children, onShortcut }: Props) => { const combo = combos.indexOf(keys); listeners.current.get(name)?.forEach((listener) => listener(combo)); - onShortcut(name as ShortcutName); + onShortcut(name as ShortcutName, combo, key); } })); }, [onShortcut]); diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json index 4ff42666d..9b4b5b3e5 100644 --- a/src/common/Shortcuts/shortcuts.json +++ b/src/common/Shortcuts/shortcuts.json @@ -13,16 +13,16 @@ "label": "SETTINGS_SHORTCUT_GO_TO_SEARCH", "combos": [["0"]] }, + { + "name": "navigateHistory", + "label": "SETTINGS_SHORTCUT_NAVIGATE_HISTORY", + "combos": [["Backspace"], ["Ctrl", "Backspace"]] + }, { "name": "fullscreen", "label": "SETTINGS_SHORTCUT_FULLSCREEN", "combos": [["F"]] }, - { - "name": "exit", - "label": "SETTINGS_SHORTCUT_EXIT_BACK", - "combos": [["Backspace"]] - }, { "name": "shortcuts", "label": "SETTINGS_SHORTCUT_SHORTCUTS", @@ -55,14 +55,9 @@ "combos": [["ArrowLeft"], ["Shift", "ArrowLeft"]] }, { - "name": "volumeUp", - "label": "SETTINGS_SHORTCUT_VOLUME_UP", - "combos": [["ArrowUp"]] - }, - { - "name": "volumeDown", - "label": "SETTINGS_SHORTCUT_VOLUME_DOWN", - "combos": [["ArrowDown"]] + "name": "volume", + "label": "SETTINGS_SHORTCUT_VOLUME", + "combos": [["ArrowUp"], ["ArrowDown"]] }, { "name": "mute", @@ -80,14 +75,9 @@ "combos": [["G"], ["H"]] }, { - "name": "speedDown", - "label": "SETTINGS_SHORTCUT_DECREASE_PLAYBACK_SPEED", - "combos": [["["]] - }, - { - "name": "speedUp", - "label": "SETTINGS_SHORTCUT_INCREASE_PLAYBACK_SPEED", - "combos": [["]"]] + "name": "speed", + "label": "SETTINGS_SHORTCUT_PLAYBACK_SPEED", + "combos": [["["], ["]"]] }, { "name": "toggleSubtitles", @@ -123,6 +113,11 @@ "name": "playNext", "label": "SETTINGS_SHORTCUT_PLAY_NEXT", "combos": [["Shift", "N"]] + }, + { + "name": "exit", + "label": "SETTINGS_SHORTCUT_EXIT_BACK", + "combos": [["Escape"]] } ] } diff --git a/src/components/ShortcutsGroup/Combos/Combos.less b/src/components/ShortcutsGroup/Combos/Combos.less index a862d54ca..b3b245f7a 100644 --- a/src/components/ShortcutsGroup/Combos/Combos.less +++ b/src/components/ShortcutsGroup/Combos/Combos.less @@ -1,6 +1,9 @@ .combos { position: relative; display: flex; + flex-wrap: wrap; + row-gap: 1rem; + justify-content: end; overflow: visible; .combo { diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.less b/src/components/ShortcutsGroup/ShortcutsGroup.less index cbc08ada0..9ca472d93 100644 --- a/src/components/ShortcutsGroup/ShortcutsGroup.less +++ b/src/components/ShortcutsGroup/ShortcutsGroup.less @@ -1,7 +1,7 @@ .shortcuts-group { flex: 1 1 0; position: relative; - width: 30rem; + width: 35rem; display: flex; flex-direction: column; gap: 2rem; @@ -26,7 +26,7 @@ .shortcut { position: relative; display: flex; - align-items: center; + align-items: baseline; justify-content: space-between; gap: 2rem; overflow: visible; @@ -35,7 +35,6 @@ position: relative; font-size: 1rem; color: var(--primary-foreground-color); - white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index eb2d8d3a5..4f32e4ec6 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -590,15 +590,10 @@ const Player = ({ urlParams, queryParams }) => { video.state.muted === true ? onUnmuteRequested() : onMuteRequested(); }, [video.state.muted], !menusOpen); - onShortcut('volumeUp', () => { + onShortcut('volume', (combo) => { if (video.state.volume !== null) { - onVolumeChangeRequested(Math.min(video.state.volume + 5, 200)); - } - }, [video.state.volume], !menusOpen); - - onShortcut('volumeDown', () => { - if (video.state.volume !== null) { - onVolumeChangeRequested(Math.max(video.state.volume - 5, 0)); + const volume = combo === 0 ? Math.min(video.state.volume + 5, 200) : Math.max(video.state.volume - 5, 0); + onVolumeChangeRequested(volume); } }, [video.state.volume], !menusOpen); @@ -623,15 +618,10 @@ const Player = ({ urlParams, queryParams }) => { } }, [video.state.playbackSpeed, toggleSpeedMenu]); - onShortcut('speedUp', () => { + onShortcut('speed', (combo) => { if (video.state.playbackSpeed !== null) { - onPlaybackSpeedChanged(Math.min(video.state.playbackSpeed + 0.25, 2)); - } - }, [video.state.playbackSpeed, onPlaybackSpeedChanged], !menusOpen); - - onShortcut('speedDown', () => { - if (video.state.playbackSpeed !== null) { - onPlaybackSpeedChanged(Math.max(video.state.playbackSpeed - 0.25, 0.25)); + const speed = combo === 0 ? Math.max(video.state.playbackSpeed - 0.25, 0.25) : Math.min(video.state.playbackSpeed + 0.25, 2); + onPlaybackSpeedChanged(speed); } }, [video.state.playbackSpeed, onPlaybackSpeedChanged], !menusOpen); diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less index 31168db22..602724a80 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.less +++ b/src/routes/Player/SideDrawer/SideDrawer.less @@ -7,6 +7,11 @@ action-buttons-container: action-buttons-container; } +:import('~stremio/components/MultiselectMenu/Dropdown/Dropdown.less') { + dropdown: dropdown; + open: open; +} + @padding: 1rem; .side-drawer { @@ -69,6 +74,7 @@ .info { padding: @padding; + min-height: 0; overflow-y: auto; .side-drawer-meta-preview { @@ -89,8 +95,11 @@ flex: 2; display: flex; flex-direction: column; + min-height: 0; .videos { + flex: 1; + min-height: 0; overflow-y: auto; } } @@ -109,6 +118,14 @@ @media @phone-landscape { .side-drawer { max-width: 50dvw; + + .info { + max-height: 40dvh; + } + + .dropdown.open { + max-height: calc(3rem * 4); + } } } diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index 05ed63677..e2353c221 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react'; +import React, { useMemo, useCallback, useState, useRef, forwardRef, memo } from 'react'; import classNames from 'classnames'; import Icon from '@stremio/stremio-icons/react'; import { useCore } from 'stremio/core'; @@ -22,6 +22,7 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa const core = useCore(); const [season, setSeason] = useState(seriesInfo?.season); const [selectedVideoId, setSelectedVideoId] = useState(null); + const videosRef = useRef(null); const metaItem = useMemo(() => { return seriesInfo ? @@ -47,8 +48,9 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa .sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER)); }, [props.metaItem.videos]); - const seasonOnSelect = useCallback((event: { value: string }) => { - setSeason(parseInt(event.value)); + const seasonOnSelect = useCallback((event: { value: string | number }) => { + setSeason(parseInt(String(event.value), 10)); + videosRef.current?.scrollTo({ top: 0, left: 0 }); }, []); const seasonWatched = React.useMemo(() => { @@ -109,7 +111,7 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa seasons={seasons} onSelect={seasonOnSelect} /> -
+
{videos.map((video, index) => (