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: 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 f66d13abb..a3ac0f8fe 100644 --- a/src/common/Shortcuts/shortcuts.json +++ b/src/common/Shortcuts/shortcuts.json @@ -88,6 +88,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/Toast/ToastItem/styles.less b/src/common/Toast/ToastItem/styles.less index 56f535695..075edeea5 100644 --- a/src/common/Toast/ToastItem/styles.less +++ b/src/common/Toast/ToastItem/styles.less @@ -27,7 +27,7 @@ &.error { .icon-container { .icon { - color: var(--color-trakt); + color: var(--danger-accent-color); } } } 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/components/Checkbox/Checkbox.less b/src/components/Checkbox/Checkbox.less index 9276990b3..a9df8ea9e 100644 --- a/src/components/Checkbox/Checkbox.less +++ b/src/components/Checkbox/Checkbox.less @@ -70,7 +70,7 @@ } &.error { - border-color: var(--color-trakt); + border-color: var(--danger-accent-color); } &.checked { diff --git a/src/components/MetaPreview/Ratings/Ratings.less b/src/components/MetaPreview/Ratings/Ratings.less index afe7b3637..ffba0415b 100644 --- a/src/components/MetaPreview/Ratings/Ratings.less +++ b/src/components/MetaPreview/Ratings/Ratings.less @@ -17,6 +17,7 @@ border-radius: 2rem; height: @height; width: fit-content; + backdrop-filter: blur(5px); .icon-container { display: flex; diff --git a/src/components/RadioButton/RadioButton.less b/src/components/RadioButton/RadioButton.less index 20d90bdef..69d179362 100644 --- a/src/components/RadioButton/RadioButton.less +++ b/src/components/RadioButton/RadioButton.less @@ -52,7 +52,7 @@ } &.error { - border-color: var(--color-trakt); + border-color: var(--danger-accent-color); } &.selected { diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 1b2c50cca..3ae2f066c 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,121 +597,99 @@ 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 'KeyM': { - (typeof video.state.muted === 'boolean' && video.state.muted) ? onUnmuteRequested() : onMuteRequested(); - 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('mute', () => { + video.state.muted === true ? onUnmuteRequested() : onMuteRequested(); + }, [video.state.muted]); + + 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); @@ -729,43 +707,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.muted, - video.state.audioTracks, - video.state.subtitlesTracks, - video.state.extraSubtitlesTracks, - video.state.playbackSpeed, - toggleSubtitlesMenu, - toggleStatisticsMenu, - toggleSideDrawer, - onUnmuteRequested, - onMuteRequested, - overlayHidden, - onDecreaseSubtitlesDelay, - onIncreaseSubtitlesDelay, - onUpdateSubtitlesSize, - ]); + }, [routeFocused, menusOpen, video.state.volume]); React.useEffect(() => { video.events.on('error', onError); diff --git a/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less index e8de1ecf6..1ced5f14d 100644 --- a/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less +++ b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less @@ -69,7 +69,7 @@ .cancel { &:hover { .icon { - color: var(--color-trakt); + color: var(--danger-accent-color); } } } diff --git a/src/routes/Settings/Streaming/URLsManager/Item/Item.less b/src/routes/Settings/Streaming/URLsManager/Item/Item.less index 1205555cc..5afe71d72 100644 --- a/src/routes/Settings/Streaming/URLsManager/Item/Item.less +++ b/src/routes/Settings/Streaming/URLsManager/Item/Item.less @@ -52,7 +52,7 @@ } &.error { - background-color: var(--color-trakt); + background-color: var(--danger-accent-color); } } @@ -92,7 +92,7 @@ background-color: var(--overlay-color); .icon { - color: var(--color-trakt); + color: var(--danger-accent-color); opacity: 1 !important; } }