Merge branch 'development' of https://github.com/Stremio/stremio-web into refactor/player-volume-playback-speed-shortcuts

This commit is contained in:
Tim 2026-05-08 16:04:32 +02:00
commit 3f0950df08
10 changed files with 37 additions and 127 deletions

View file

@ -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);
};
}, []);

View file

@ -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 FullscreenProvider = ({ children }: Props) => {
const shell = useShell();
const [settings] = useSettings();
@ -57,12 +47,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 onWindowVisibilityChanged = (state: WindowVisibility) => {

View file

@ -16,16 +16,26 @@ const ShortcutsContext = createContext<ShortcutsContext>({} 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<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
const lastRepeatTime = useRef<Map<string, number>>(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]);

View file

@ -13,6 +13,11 @@
"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",

View file

@ -1,6 +1,9 @@
.combos {
position: relative;
display: flex;
flex-wrap: wrap;
row-gap: 1rem;
justify-content: end;
overflow: visible;
.combo {

View file

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

View file

@ -1,93 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
function KeyboardShortcuts() {
let active = false;
const events = new EventEmitter();
function onKeyDown(event) {
if (event.keyboardShortcutPrevented || event.target.tagName === 'INPUT' || event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
return;
}
switch (event.code) {
case 'Digit0': {
event.preventDefault();
window.location = '#/search';
break;
}
case 'Digit1': {
event.preventDefault();
window.location = '#/';
break;
}
case 'Digit2': {
event.preventDefault();
window.location = '#/discover';
break;
}
case 'Digit3': {
event.preventDefault();
window.location = '#/library';
break;
}
case 'Digit4': {
event.preventDefault();
window.location = '#/calendar';
break;
}
case 'Digit5': {
event.preventDefault();
window.location = '#/addons';
break;
}
case 'Digit6': {
event.preventDefault();
window.location = '#/settings';
break;
}
case 'Backspace': {
event.preventDefault();
if (event.ctrlKey) {
window.history.forward();
} else {
window.history.back();
}
break;
}
}
}
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
active: {
configurable: false,
enumerable: true,
get: function() {
return active;
}
}
});
this.start = function() {
if (active) {
return;
}
window.addEventListener('keydown', onKeyDown);
active = true;
onStateChanged();
};
this.stop = function() {
window.removeEventListener('keydown', onKeyDown);
active = false;
onStateChanged();
};
}
module.exports = KeyboardShortcuts;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const KeyboardShortcuts = require('./KeyboardShortcuts');
module.exports = KeyboardShortcuts;

View file

@ -1,5 +1,4 @@
type ServicesContext = {
shell: any,
chromecast: any,
keyboardShortcuts: any,
};

View file

@ -1,14 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Chromecast = require('./Chromecast');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
const { GamepadProvider, useGamepad } = require('./GamepadContext');
const Shell = require('./Shell');
module.exports = {
Chromecast,
KeyboardShortcuts,
ServicesProvider,
useServices,
Shell,