Merge pull request #1316 from Stremio/fix-virtual-keyboard-shortcuts

Fix virtual keyboard shortcuts
This commit is contained in:
Timothy Z. 2026-07-17 14:48:23 +03:00 committed by GitHub
commit accc308aa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 98 additions and 12 deletions

View file

@ -2,6 +2,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { withCoreSuspender } from '../CoreSuspender';
import { getKeyboardShortcutKey } from '../Shortcuts';
import onShortcut from '../Shortcuts/onShortcut';
import useSettings from '../useSettings';
import FullscreenContext, { type FullscreenContextValue } from './FullscreenContext';
@ -80,11 +81,13 @@ const FullscreenProvider = ({ children }: Props) => {
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.code === 'Escape' && escExitFullscreen) {
const keyboardKey = getKeyboardShortcutKey(event);
if (keyboardKey === 'Escape' && escExitFullscreen) {
exitFullscreen();
}
if (event.code === 'F11' && shell.active) {
if (keyboardKey === 'F11' && shell.active) {
toggleFullscreen();
}
};

View file

@ -1,4 +1,5 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import { getKeyboardShortcutKey, getKeyboardShortcutKeys } from './keyboard';
import shortcuts from './shortcuts.json';
const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
@ -33,14 +34,17 @@ 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) => {
const onKeyDown = useCallback((event: KeyboardEvent) => {
const { ctrlKey, shiftKey, altKey, metaKey, key, repeat } = event;
if (isInputFocused()) return;
const shortcutKeys = getKeyboardShortcutKeys(event);
const repeatKey = getKeyboardShortcutKey(event);
if (repeat) {
const now = Date.now();
const last = lastRepeatTime.current.get(code) ?? 0;
const last = lastRepeatTime.current.get(repeatKey) ?? 0;
if (now - last < REPEAT_THROTTLE_MS) return;
lastRepeatTime.current.set(code, now);
lastRepeatTime.current.set(repeatKey, now);
}
SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => {
@ -48,8 +52,13 @@ const ShortcutsProvider = ({ children, onShortcut }: Props) => {
&& (keys.includes('Shift') === shiftKey)
&& !altKey
&& !metaKey;
const keyMatched = keys.some((shortcutKey) => (
shortcutKey !== 'Ctrl'
&& shortcutKey !== 'Shift'
&& shortcutKeys.includes(shortcutKey)
));
if (modifers && (keys.includes(code) || keys.includes(key.toUpperCase()))) {
if (modifers && keyMatched) {
const combo = combos.indexOf(keys);
listeners.current.get(name)?.forEach((listener) => listener(combo, key));

View file

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

View file

@ -0,0 +1,69 @@
const KEY_CODE_MAP: Record<number, string> = {
8: 'Backspace',
27: 'Escape',
32: 'Space',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
122: 'F11',
187: '=',
189: '-',
191: '/',
219: '[',
221: ']',
};
const KEY_MAP: Record<string, string> = {
' ': 'Space',
Spacebar: 'Space',
Esc: 'Escape',
Left: 'ArrowLeft',
Up: 'ArrowUp',
Right: 'ArrowRight',
Down: 'ArrowDown',
};
const keyFromKeyCode = (keyCode: number) => {
if (KEY_CODE_MAP[keyCode]) {
return KEY_CODE_MAP[keyCode];
}
if (keyCode >= 48 && keyCode <= 57) {
return String.fromCharCode(keyCode);
}
if (keyCode >= 65 && keyCode <= 90) {
return String.fromCharCode(keyCode);
}
if (keyCode >= 96 && keyCode <= 105) {
return String(keyCode - 96);
}
return null;
};
const normalizeKeyboardKey = ({ key, keyCode, which }: KeyboardEvent) => {
const mappedKey = KEY_MAP[key] ?? key;
if (mappedKey && mappedKey !== 'Unidentified') {
return mappedKey.length === 1 ? mappedKey.toUpperCase() : mappedKey;
}
return keyFromKeyCode(keyCode || which) ?? mappedKey;
};
const getKeyboardShortcutKey = (event: KeyboardEvent) => {
return event.code && event.code !== 'Unidentified' ? event.code : normalizeKeyboardKey(event);
};
const getKeyboardShortcutKeys = (event: KeyboardEvent) => {
const normalizedKey = normalizeKeyboardKey(event);
const code = event.code !== 'Unidentified' ? event.code : '';
return code && code !== normalizedKey ? [code, normalizedKey] : [normalizedKey];
};
export {
getKeyboardShortcutKey,
getKeyboardShortcutKeys,
};

View file

@ -5,7 +5,7 @@ const { FullscreenProvider, useFullscreen } = require('./Fullscreen');
const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
const { ShortcutsProvider, useShortcuts, onShortcut, getKeyboardShortcutKey } = require('./Shortcuts');
const { DiscordProvider, useDiscord, EMPTY_DISCORD_TIMESTAMPS, getPlaybackDiscordActivity } = require('./Discord');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
@ -42,6 +42,7 @@ module.exports = {
ShortcutsProvider,
useShortcuts,
onShortcut,
getKeyboardShortcutKey,
ToastProvider,
useToast,
TooltipProvider,

View file

@ -11,7 +11,7 @@ const { default: useRouteFocused } = require('stremio/common/useRouteFocused');
const { useCore } = require('stremio/core');
const { useServices, useGamepad } = require('stremio/services');
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, usePlatform, onShortcut, useDiscord, EMPTY_DISCORD_TIMESTAMPS, getPlaybackDiscordActivity } = require('stremio/common');
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, usePlatform, onShortcut, getKeyboardShortcutKey, useDiscord, EMPTY_DISCORD_TIMESTAMPS, getPlaybackDiscordActivity } = require('stremio/common');
const { default: toPath } = require('stremio-router/toPath');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const { default: Buffering } = require('./Buffering');
@ -673,7 +673,8 @@ const Player = () => {
}
const onKeyDown = (e) => {
if (e.code !== 'Space' || e.repeat) return;
const keyboardKey = getKeyboardShortcutKey(e);
if (keyboardKey !== 'Space' || e.repeat) return;
if (menusOpen || e.ctrlKey || e.metaKey || e.altKey) return;
longPress.current = false;
@ -685,14 +686,15 @@ const Player = () => {
};
const onKeyUp = (e) => {
if (e.code !== 'Space' && e.code !== 'ArrowRight' && e.code !== 'ArrowLeft') return;
const keyboardKey = getKeyboardShortcutKey(e);
if (keyboardKey !== 'Space' && keyboardKey !== 'ArrowRight' && keyboardKey !== 'ArrowLeft') return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.code === 'ArrowRight' || e.code === 'ArrowLeft') {
if (keyboardKey === 'ArrowRight' || keyboardKey === 'ArrowLeft') {
setSeeking(false);
return;
}
if (e.code === 'Space') {
if (keyboardKey === 'Space') {
clearTimeout(pressTimer.current);
pressTimer.current = null;
if (longPress.current) {