mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-10 19:40:41 +00:00
Merge pull request #1264 from Stremio/refactor/shortcuts-logic
Dev: Move shortcuts logic to provider
This commit is contained in:
commit
5727fc0322
12 changed files with 43 additions and 133 deletions
|
|
@ -41,7 +41,7 @@
|
||||||
"react-i18next": "^15.7.4",
|
"react-i18next": "^15.7.4",
|
||||||
"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#d9cd2fb88268b365b14101452665de698f9c15e9",
|
"stremio-translations": "github:Stremio/stremio-translations#5b00ce88c124eedfc34cba7f363dedeab38071d9",
|
||||||
"url": "0.11.4",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.3.0"
|
"use-long-press": "^3.3.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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#d9cd2fb88268b365b14101452665de698f9c15e9
|
specifier: github:Stremio/stremio-translations#5b00ce88c124eedfc34cba7f363dedeab38071d9
|
||||||
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9
|
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/5b00ce88c124eedfc34cba7f363dedeab38071d9
|
||||||
url:
|
url:
|
||||||
specifier: 0.11.4
|
specifier: 0.11.4
|
||||||
version: 0.11.4
|
version: 0.11.4
|
||||||
|
|
@ -4421,8 +4421,8 @@ 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/d9cd2fb88268b365b14101452665de698f9c15e9:
|
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/5b00ce88c124eedfc34cba7f363dedeab38071d9:
|
||||||
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9}
|
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/5b00ce88c124eedfc34cba7f363dedeab38071d9}
|
||||||
version: 1.52.0
|
version: 1.52.0
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
|
|
@ -10000,7 +10000,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/d9cd2fb88268b365b14101452665de698f9c15e9: {}
|
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/5b00ce88c124eedfc34cba7f363dedeab38071d9: {}
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const React = require('react');
|
||||||
const { useTranslation } = require('react-i18next');
|
const { useTranslation } = require('react-i18next');
|
||||||
const { useCore } = require('stremio/core');
|
const { useCore } = require('stremio/core');
|
||||||
const { Router } = require('stremio-router');
|
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 { NotFound } = require('stremio/routes');
|
||||||
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
|
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
|
||||||
const ServicesToaster = require('./ServicesToaster');
|
const ServicesToaster = require('./ServicesToaster');
|
||||||
|
|
@ -33,13 +33,12 @@ const App = () => {
|
||||||
return {
|
return {
|
||||||
shell: new Shell(),
|
shell: new Shell(),
|
||||||
chromecast: new Chromecast(),
|
chromecast: new Chromecast(),
|
||||||
keyboardShortcuts: new KeyboardShortcuts(),
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
|
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
|
||||||
const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false);
|
const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false);
|
||||||
|
|
||||||
const onShortcut = React.useCallback((name) => {
|
const onShortcut = React.useCallback((name, combo, key) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'shortcuts':
|
case 'shortcuts':
|
||||||
toggleShortcutModal();
|
toggleShortcutModal();
|
||||||
|
|
@ -47,6 +46,18 @@ const App = () => {
|
||||||
case 'gamepadGuide':
|
case 'gamepadGuide':
|
||||||
toggleGamepadModal();
|
toggleGamepadModal();
|
||||||
break;
|
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]);
|
}, [toggleShortcutModal, toggleGamepadModal]);
|
||||||
|
|
||||||
|
|
@ -90,12 +101,10 @@ const App = () => {
|
||||||
services.chromecast.on('stateChanged', onChromecastStateChange);
|
services.chromecast.on('stateChanged', onChromecastStateChange);
|
||||||
services.shell.start();
|
services.shell.start();
|
||||||
services.chromecast.start();
|
services.chromecast.start();
|
||||||
services.keyboardShortcuts.start();
|
|
||||||
window.services = services;
|
window.services = services;
|
||||||
return () => {
|
return () => {
|
||||||
services.shell.stop();
|
services.shell.stop();
|
||||||
services.chromecast.stop();
|
services.chromecast.stop();
|
||||||
services.keyboardShortcuts.stop();
|
|
||||||
services.chromecast.off('stateChanged', onChromecastStateChange);
|
services.chromecast.off('stateChanged', onChromecastStateChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,6 @@ type Props = {
|
||||||
children: React.ReactNode,
|
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 FullscreenProvider = ({ children }: Props) => {
|
||||||
const shell = useShell();
|
const shell = useShell();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
@ -57,12 +47,7 @@ const FullscreenProvider = ({ children }: Props) => {
|
||||||
fullscreen ? exitFullscreen() : requestFullscreen();
|
fullscreen ? exitFullscreen() : requestFullscreen();
|
||||||
}, [fullscreen, exitFullscreen, requestFullscreen]);
|
}, [fullscreen, exitFullscreen, requestFullscreen]);
|
||||||
|
|
||||||
const toggleFullscreenFromShortcut = useCallback(() => {
|
onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]);
|
||||||
if (isTextInputFocused()) return;
|
|
||||||
toggleFullscreen();
|
|
||||||
}, [toggleFullscreen]);
|
|
||||||
|
|
||||||
onShortcut('fullscreen', toggleFullscreenFromShortcut, [toggleFullscreenFromShortcut]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onWindowVisibilityChanged = (state: WindowVisibility) => {
|
const onWindowVisibilityChanged = (state: WindowVisibility) => {
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,26 @@ const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext)
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: JSX.Element,
|
children: JSX.Element,
|
||||||
onShortcut: (name: ShortcutName) => void,
|
onShortcut: (name: ShortcutName, combo: number, key: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const REPEAT_THROTTLE_MS = 130;
|
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 ShortcutsProvider = ({ children, onShortcut }: Props) => {
|
||||||
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
|
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
|
||||||
const lastRepeatTime = useRef<Map<string, number>>(new Map());
|
const lastRepeatTime = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
const onKeyDown = useCallback(({ ctrlKey, shiftKey, altKey, metaKey, code, key, repeat }: KeyboardEvent) => {
|
const onKeyDown = useCallback(({ ctrlKey, shiftKey, altKey, metaKey, code, key, repeat }: KeyboardEvent) => {
|
||||||
|
if (isInputFocused()) return;
|
||||||
|
|
||||||
if (repeat) {
|
if (repeat) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const last = lastRepeatTime.current.get(code) ?? 0;
|
const last = lastRepeatTime.current.get(code) ?? 0;
|
||||||
|
|
@ -43,7 +53,7 @@ const ShortcutsProvider = ({ children, onShortcut }: Props) => {
|
||||||
const combo = combos.indexOf(keys);
|
const combo = combos.indexOf(keys);
|
||||||
listeners.current.get(name)?.forEach((listener) => listener(combo));
|
listeners.current.get(name)?.forEach((listener) => listener(combo));
|
||||||
|
|
||||||
onShortcut(name as ShortcutName);
|
onShortcut(name as ShortcutName, combo, key);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}, [onShortcut]);
|
}, [onShortcut]);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@
|
||||||
"label": "SETTINGS_SHORTCUT_GO_TO_SEARCH",
|
"label": "SETTINGS_SHORTCUT_GO_TO_SEARCH",
|
||||||
"combos": [["0"]]
|
"combos": [["0"]]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "navigateHistory",
|
||||||
|
"label": "SETTINGS_SHORTCUT_NAVIGATE_HISTORY",
|
||||||
|
"combos": [["Backspace"], ["Ctrl", "Backspace"]]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "fullscreen",
|
"name": "fullscreen",
|
||||||
"label": "SETTINGS_SHORTCUT_FULLSCREEN",
|
"label": "SETTINGS_SHORTCUT_FULLSCREEN",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
.combos {
|
.combos {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 1rem;
|
||||||
|
justify-content: end;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
.combo {
|
.combo {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
.shortcuts-group {
|
.shortcuts-group {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 30rem;
|
width: 35rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
.shortcut {
|
.shortcut {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
@ -35,7 +35,6 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
|
||||||
|
|
||||||
const KeyboardShortcuts = require('./KeyboardShortcuts');
|
|
||||||
|
|
||||||
module.exports = KeyboardShortcuts;
|
|
||||||
1
src/services/ServicesContext/types.d.ts
vendored
1
src/services/ServicesContext/types.d.ts
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
type ServicesContext = {
|
type ServicesContext = {
|
||||||
shell: any,
|
shell: any,
|
||||||
chromecast: any,
|
chromecast: any,
|
||||||
keyboardShortcuts: any,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
// Copyright (C) 2017-2023 Smart code 203358507
|
||||||
|
|
||||||
const Chromecast = require('./Chromecast');
|
const Chromecast = require('./Chromecast');
|
||||||
const KeyboardShortcuts = require('./KeyboardShortcuts');
|
|
||||||
const { ServicesProvider, useServices } = require('./ServicesContext');
|
const { ServicesProvider, useServices } = require('./ServicesContext');
|
||||||
const { GamepadProvider, useGamepad } = require('./GamepadContext');
|
const { GamepadProvider, useGamepad } = require('./GamepadContext');
|
||||||
const Shell = require('./Shell');
|
const Shell = require('./Shell');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Chromecast,
|
Chromecast,
|
||||||
KeyboardShortcuts,
|
|
||||||
ServicesProvider,
|
ServicesProvider,
|
||||||
useServices,
|
useServices,
|
||||||
Shell,
|
Shell,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue