From c9a40aabd7db97fb71b99f5fb9887f413c0901fc Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 15 Dec 2025 17:43:40 +0100 Subject: [PATCH 1/2] refactor: use shortcuts provider on player --- src/common/Shortcuts/Shortcuts.tsx | 33 ++-- src/common/Shortcuts/index.ts | 3 + src/common/Shortcuts/onShortcut.ts | 15 ++ src/common/Shortcuts/shortcuts.json | 10 ++ src/common/index.js | 3 +- src/routes/Player/Player.js | 227 +++++++++++----------------- 6 files changed, 143 insertions(+), 148 deletions(-) create mode 100644 src/common/Shortcuts/onShortcut.ts diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx index 532e9a409..c9198a857 100644 --- a/src/common/Shortcuts/Shortcuts.tsx +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -1,13 +1,15 @@ -import React, { createContext, useCallback, useContext, useEffect } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'; import shortcuts from './shortcuts.json'; const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat(); export type ShortcutName = string; -export type ShortcutListener = () => void; +export type ShortcutListener = (combo: number) => void; interface ShortcutsContext { grouped: ShortcutGroup[], + on: (name: ShortcutName, listener: ShortcutListener) => void, + off: (name: ShortcutName, listener: ShortcutListener) => void, } const ShortcutsContext = createContext({} as ShortcutsContext); @@ -18,27 +20,38 @@ type Props = { }; const ShortcutsProvider = ({ children, onShortcut }: Props) => { - const onKeyDown = useCallback(({ ctrlKey, shiftKey, key }: KeyboardEvent) => { + const listeners = useRef>>(new Map()); + + 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); - if (modifers && keys.includes(key.toUpperCase())) { + if (modifers && (keys.includes(code) || keys.includes(key.toUpperCase()))) { + const combo = combos.indexOf(keys); + listeners.current.get(name)?.forEach((listener) => listener(combo)); + onShortcut(name as ShortcutName); } })); }, [onShortcut]); + const on = (name: ShortcutName, listener: ShortcutListener) => { + !listeners.current.has(name) && listeners.current.set(name, new Set()); + listeners.current.get(name)!.add(listener); + }; + + const off = (name: ShortcutName, listener: ShortcutListener) => { + listeners.current.get(name)?.delete(listener); + }; + useEffect(() => { document.addEventListener('keydown', onKeyDown); - - return () => { - document.removeEventListener('keydown', onKeyDown); - }; + return () => document.removeEventListener('keydown', onKeyDown); }, [onKeyDown]); return ( - + {children} ); @@ -50,5 +63,5 @@ const useShortcuts = () => { export { ShortcutsProvider, - useShortcuts + useShortcuts, }; diff --git a/src/common/Shortcuts/index.ts b/src/common/Shortcuts/index.ts index f7fa38a18..971003a38 100644 --- a/src/common/Shortcuts/index.ts +++ b/src/common/Shortcuts/index.ts @@ -1,5 +1,8 @@ import { ShortcutsProvider, useShortcuts } from './Shortcuts'; +import onShortcut from './onShortcut'; + export { ShortcutsProvider, useShortcuts, + onShortcut, }; diff --git a/src/common/Shortcuts/onShortcut.ts b/src/common/Shortcuts/onShortcut.ts new file mode 100644 index 000000000..f13c970cc --- /dev/null +++ b/src/common/Shortcuts/onShortcut.ts @@ -0,0 +1,15 @@ +import { DependencyList, useCallback, useEffect } from 'react'; +import { ShortcutListener, ShortcutName, useShortcuts } from './Shortcuts'; + +const onShortcut = (name: ShortcutName, listener: ShortcutListener, deps: DependencyList) => { + const shortcuts = useShortcuts(); + + const listenerCallback = useCallback(listener, deps); + + useEffect(() => { + shortcuts.on(name, listenerCallback); + return () => shortcuts.off(name, listenerCallback); + }, [listenerCallback]); +}; + +export default onShortcut; diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json index 766288fb0..86abe5d73 100644 --- a/src/common/Shortcuts/shortcuts.json +++ b/src/common/Shortcuts/shortcuts.json @@ -83,6 +83,16 @@ "name": "infoMenu", "label": "SETTINGS_SHORTCUT_MENU_INFO", "combos": [["I"]] + }, + { + "name": "speedMenu", + "label": "SETTINGS_SHORTCUT_MENU_PLAYBACK_SPEED", + "combos": [["R"]] + }, + { + "name": "statisticsMenu", + "label": "SETTINGS_SHORTCUT_MENU_STATISTICS", + "combos": [["D"]] } ] } diff --git a/src/common/index.js b/src/common/index.js index 0b9cb252f..1b248c1ff 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -4,7 +4,7 @@ const { FileDropProvider, onFileDrop } = require('./FileDrop'); const { PlatformProvider, usePlatform } = require('./Platform'); const { ToastProvider, useToast } = require('./Toast'); const { TooltipProvider, Tooltip } = require('./Tooltips'); -const { ShortcutsProvider, useShortcuts } = require('./Shortcuts'); +const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); @@ -38,6 +38,7 @@ module.exports = { usePlatform, ShortcutsProvider, useShortcuts, + onShortcut, ToastProvider, useToast, TooltipProvider, diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 27f705be8..f2697db58 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,7 +8,7 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform } = require('stremio/common'); +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'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); @@ -597,117 +597,95 @@ const Player = ({ urlParams, queryParams }) => { navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback); }, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]); - React.useLayoutEffect(() => { - const onKeyDown = (event) => { - switch (event.code) { - case 'Space': { - if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) { - if (video.state.paused) { - onPlayRequested(); - setSeeking(false); - } else { - onPauseRequested(); - } - } - - break; - } - case 'ArrowRight': { - if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { - const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; - setSeeking(true); - onSeekRequested(video.state.time + seekDuration); - } - - break; - } - case 'ArrowLeft': { - if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { - const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; - setSeeking(true); - onSeekRequested(video.state.time - seekDuration); - } - - break; - } - case 'ArrowUp': { - if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { - onVolumeChangeRequested(Math.min(video.state.volume + 5, 200)); - } - - break; - } - case 'ArrowDown': { - if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { - onVolumeChangeRequested(Math.max(video.state.volume - 5, 0)); - } - - break; - } - case 'KeyS': { - closeMenus(); - if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) || - (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0)) { - toggleSubtitlesMenu(); - } - - break; - } - case 'KeyA': { - closeMenus(); - if (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0) { - toggleAudioMenu(); - } - - break; - } - case 'KeyI': { - closeMenus(); - if (player.metaItem !== null && player.metaItem.type === 'Ready') { - toggleSideDrawer(); - } - - break; - } - case 'KeyR': { - closeMenus(); - if (video.state.playbackSpeed !== null) { - toggleSpeedMenu(); - } - - break; - } - case 'KeyD': { - closeMenus(); - if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') { - toggleStatisticsMenu(); - } - - break; - } - case 'KeyG': { - onDecreaseSubtitlesDelay(); - break; - } - case 'KeyH': { - onIncreaseSubtitlesDelay(); - break; - } - case 'Minus': { - onUpdateSubtitlesSize(-1); - break; - } - case 'Equal': { - onUpdateSubtitlesSize(1); - break; - } - case 'Escape': { - closeMenus(); - !settings.escExitFullscreen && window.history.back(); - break; - } + onShortcut('playPause', () => { + if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) { + if (video.state.paused) { + onPlayRequested(); + setSeeking(false); + } else { + onPauseRequested(); } - }; + } + }, [menusOpen, nextVideoPopupOpen, video.state.paused, onPlayRequested, onPauseRequested]); + + onShortcut('seekForward', (combo) => { + if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { + const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration; + setSeeking(true); + onSeekRequested(video.state.time + seekDuration); + } + }, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]); + + onShortcut('seekBackward', (combo) => { + if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { + const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration; + setSeeking(true); + onSeekRequested(video.state.time - seekDuration); + } + }, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]); + + onShortcut('volumeUp', () => { + if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { + onVolumeChangeRequested(Math.min(video.state.volume + 5, 200)); + } + }, [menusOpen, nextVideoPopupOpen, video.state.volume]); + + onShortcut('volumeDown', () => { + if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { + onVolumeChangeRequested(Math.min(video.state.volume - 5, 200)); + } + }, [menusOpen, nextVideoPopupOpen, video.state.volume]); + + onShortcut('subtitlesDelay', (combo) => { + combo === 1 ? onIncreaseSubtitlesDelay() : onDecreaseSubtitlesDelay(); + }, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay]); + + onShortcut('subtitlesSize', (combo) => { + combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1); + }, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]); + + onShortcut('subtitlesMenu', () => { + closeMenus(); + if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) { + toggleSubtitlesMenu(); + } + }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, toggleSubtitlesMenu]); + + onShortcut('audioMenu', () => { + closeMenus(); + if (video.state?.audioTracks?.length > 0) { + toggleAudioMenu(); + } + }, [video.state.audioTracks, toggleAudioMenu]); + + onShortcut('infoMenu', () => { + closeMenus(); + if (player.metaItem?.type === 'Ready') { + toggleSideDrawer(); + } + }, [player.metaItem, toggleSideDrawer]); + + onShortcut('speedMenu', () => { + closeMenus(); + if (video.state.playbackSpeed !== null) { + toggleSpeedMenu(); + } + }, [video.state.playbackSpeed, toggleSpeedMenu]); + + onShortcut('statisticsMenu', () => { + closeMenus(); + const stream = player.selected?.stream; + if (streamingServer?.statistics?.type !== 'Err' && typeof stream === 'string' && typeof stream === 'number') { + toggleStatisticsMenu(); + } + }, [player.selected, streamingServer.statistics, toggleStatisticsMenu]); + + onShortcut('exit', () => { + closeMenus(); + !settings.escExitFullscreen && window.history.back(); + }, [settings.escExitFullscreen]); + + React.useLayoutEffect(() => { const onKeyUp = (event) => { if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') { setSeeking(false); @@ -725,39 +703,14 @@ const Player = ({ urlParams, queryParams }) => { } }; if (routeFocused) { - window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); window.addEventListener('wheel', onWheel); } return () => { - window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); window.removeEventListener('wheel', onWheel); }; - }, [ - player.metaItem, - player.selected, - streamingServer.statistics, - settings.seekTimeDuration, - settings.seekShortTimeDuration, - settings.escExitFullscreen, - routeFocused, - menusOpen, - nextVideoPopupOpen, - video.state.paused, - video.state.time, - video.state.volume, - video.state.audioTracks, - video.state.subtitlesTracks, - video.state.extraSubtitlesTracks, - video.state.playbackSpeed, - toggleSubtitlesMenu, - toggleStatisticsMenu, - toggleSideDrawer, - onDecreaseSubtitlesDelay, - onIncreaseSubtitlesDelay, - onUpdateSubtitlesSize, - ]); + }, [routeFocused, menusOpen, video.state.volume]); React.useEffect(() => { video.events.on('error', onError); From 2bc0f3468cdb304a7ccfc81bb3a9e952e04368bb Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 18 Dec 2025 16:11:53 +0100 Subject: [PATCH 2/2] chore: update translations --- package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e8655f8d4..bf2b7a498 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#01aaa201e419782b26b9f2cbe4430795021426e5", + "stremio-translations": "github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2a4883e5..ca4485af9 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#01aaa201e419782b26b9f2cbe4430795021426e5 - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5 + specifier: github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac url: specifier: 0.11.4 version: 0.11.4 @@ -4527,9 +4527,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5} - version: 1.44.13 + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac} + version: 1.44.14 string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -10283,7 +10283,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac: {} string-length@4.0.2: dependencies: