Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-mute-shortcut-2

This commit is contained in:
Tim 2026-01-10 00:56:47 +01:00
commit fc2d906a42
14 changed files with 161 additions and 169 deletions

View file

@ -41,7 +41,7 @@
"react-i18next": "^15.1.3", "react-i18next": "^15.1.3",
"react-is": "18.3.1", "react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", "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", "url": "0.11.4",
"use-long-press": "^3.2.0" "use-long-press": "^3.2.0"
}, },

View file

@ -90,8 +90,8 @@ importers:
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6 specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6 version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
stremio-translations: stremio-translations:
specifier: github:Stremio/stremio-translations#01aaa201e419782b26b9f2cbe4430795021426e5 specifier: github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5 version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac
url: url:
specifier: 0.11.4 specifier: 0.11.4
version: 0.11.4 version: 0.11.4
@ -4527,9 +4527,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5: stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac:
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5} resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac}
version: 1.44.13 version: 1.44.14
string-length@4.0.2: string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
@ -10283,7 +10283,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
internal-slot: 1.1.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: string-length@4.0.2:
dependencies: dependencies:

View file

@ -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'; import shortcuts from './shortcuts.json';
const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat(); const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
export type ShortcutName = string; export type ShortcutName = string;
export type ShortcutListener = () => void; export type ShortcutListener = (combo: number) => void;
interface ShortcutsContext { interface ShortcutsContext {
grouped: ShortcutGroup[], grouped: ShortcutGroup[],
on: (name: ShortcutName, listener: ShortcutListener) => void,
off: (name: ShortcutName, listener: ShortcutListener) => void,
} }
const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext); const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext);
@ -18,27 +20,38 @@ type Props = {
}; };
const ShortcutsProvider = ({ children, onShortcut }: Props) => { const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const onKeyDown = useCallback(({ ctrlKey, shiftKey, key }: KeyboardEvent) => { const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key }: KeyboardEvent) => {
SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => { SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => {
const modifers = (keys.includes('Ctrl') ? ctrlKey : true) const modifers = (keys.includes('Ctrl') ? ctrlKey : true)
&& (keys.includes('Shift') ? shiftKey : 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(name as ShortcutName);
} }
})); }));
}, [onShortcut]); }, [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(() => { useEffect(() => {
document.addEventListener('keydown', onKeyDown); document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [onKeyDown]); }, [onKeyDown]);
return ( return (
<ShortcutsContext.Provider value={{ grouped: shortcuts }}> <ShortcutsContext.Provider value={{ grouped: shortcuts, on, off }}>
{children} {children}
</ShortcutsContext.Provider> </ShortcutsContext.Provider>
); );
@ -50,5 +63,5 @@ const useShortcuts = () => {
export { export {
ShortcutsProvider, ShortcutsProvider,
useShortcuts useShortcuts,
}; };

View file

@ -1,5 +1,8 @@
import { ShortcutsProvider, useShortcuts } from './Shortcuts'; import { ShortcutsProvider, useShortcuts } from './Shortcuts';
import onShortcut from './onShortcut';
export { export {
ShortcutsProvider, ShortcutsProvider,
useShortcuts, useShortcuts,
onShortcut,
}; };

View file

@ -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;

View file

@ -88,6 +88,16 @@
"name": "infoMenu", "name": "infoMenu",
"label": "SETTINGS_SHORTCUT_MENU_INFO", "label": "SETTINGS_SHORTCUT_MENU_INFO",
"combos": [["I"]] "combos": [["I"]]
},
{
"name": "speedMenu",
"label": "SETTINGS_SHORTCUT_MENU_PLAYBACK_SPEED",
"combos": [["R"]]
},
{
"name": "statisticsMenu",
"label": "SETTINGS_SHORTCUT_MENU_STATISTICS",
"combos": [["D"]]
} }
] ]
} }

View file

@ -27,7 +27,7 @@
&.error { &.error {
.icon-container { .icon-container {
.icon { .icon {
color: var(--color-trakt); color: var(--danger-accent-color);
} }
} }
} }

View file

@ -4,7 +4,7 @@ const { FileDropProvider, onFileDrop } = require('./FileDrop');
const { PlatformProvider, usePlatform } = require('./Platform'); const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast'); const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips'); const { TooltipProvider, Tooltip } = require('./Tooltips');
const { ShortcutsProvider, useShortcuts } = require('./Shortcuts'); const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
const comparatorWithPriorities = require('./comparatorWithPriorities'); const comparatorWithPriorities = require('./comparatorWithPriorities');
const CONSTANTS = require('./CONSTANTS'); const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
@ -38,6 +38,7 @@ module.exports = {
usePlatform, usePlatform,
ShortcutsProvider, ShortcutsProvider,
useShortcuts, useShortcuts,
onShortcut,
ToastProvider, ToastProvider,
useToast, useToast,
TooltipProvider, TooltipProvider,

View file

@ -70,7 +70,7 @@
} }
&.error { &.error {
border-color: var(--color-trakt); border-color: var(--danger-accent-color);
} }
&.checked { &.checked {

View file

@ -17,6 +17,7 @@
border-radius: 2rem; border-radius: 2rem;
height: @height; height: @height;
width: fit-content; width: fit-content;
backdrop-filter: blur(5px);
.icon-container { .icon-container {
display: flex; display: flex;

View file

@ -52,7 +52,7 @@
} }
&.error { &.error {
border-color: var(--color-trakt); border-color: var(--danger-accent-color);
} }
&.selected { &.selected {

View file

@ -8,7 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services'); 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 { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader'); const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator'); const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -597,121 +597,99 @@ const Player = ({ urlParams, queryParams }) => {
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback); navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]); }, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
React.useLayoutEffect(() => { onShortcut('playPause', () => {
const onKeyDown = (event) => { if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
switch (event.code) { if (video.state.paused) {
case 'Space': { onPlayRequested();
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) { setSeeking(false);
if (video.state.paused) { } else {
onPlayRequested(); onPauseRequested();
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;
}
} }
}; }
}, [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) => { const onKeyUp = (event) => {
if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') { if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
setSeeking(false); setSeeking(false);
@ -729,43 +707,14 @@ const Player = ({ urlParams, queryParams }) => {
} }
}; };
if (routeFocused) { if (routeFocused) {
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', onKeyUp);
window.addEventListener('wheel', onWheel); window.addEventListener('wheel', onWheel);
} }
return () => { return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp); window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('wheel', onWheel); window.removeEventListener('wheel', onWheel);
}; };
}, [ }, [routeFocused, menusOpen, video.state.volume]);
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,
]);
React.useEffect(() => { React.useEffect(() => {
video.events.on('error', onError); video.events.on('error', onError);

View file

@ -69,7 +69,7 @@
.cancel { .cancel {
&:hover { &:hover {
.icon { .icon {
color: var(--color-trakt); color: var(--danger-accent-color);
} }
} }
} }

View file

@ -52,7 +52,7 @@
} }
&.error { &.error {
background-color: var(--color-trakt); background-color: var(--danger-accent-color);
} }
} }
@ -92,7 +92,7 @@
background-color: var(--overlay-color); background-color: var(--overlay-color);
.icon { .icon {
color: var(--color-trakt); color: var(--danger-accent-color);
opacity: 1 !important; opacity: 1 !important;
} }
} }