diff --git a/src/App/App.js b/src/App/App.js
index 3e816be9f..09aca6c1c 100644
--- a/src/App/App.js
+++ b/src/App/App.js
@@ -6,11 +6,12 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
-const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common');
+const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
const { default: UpdaterBanner } = require('./UpdaterBanner');
+const { default: ShortcutsModal } = require('./ShortcutsModal');
const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
@@ -38,6 +39,14 @@ const App = () => {
};
}, []);
const [initialized, setInitialized] = React.useState(false);
+ const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
+
+ const onShortcut = React.useCallback((name) => {
+ if (name === 'shortcuts') {
+ toggleShortcutModal();
+ }
+ }, [toggleShortcutModal]);
+
React.useEffect(() => {
let prevPath = window.location.hash.slice(1);
const onLocationHashChange = () => {
@@ -203,15 +212,20 @@ const App = () => {
-
-
-
-
-
+
+ {
+ shortcutModalOpen &&
+ }
+
+
+
+
+
+
diff --git a/src/App/ShortcutsModal/ShortcutsModal.tsx b/src/App/ShortcutsModal/ShortcutsModal.tsx
new file mode 100644
index 000000000..5fec24837
--- /dev/null
+++ b/src/App/ShortcutsModal/ShortcutsModal.tsx
@@ -0,0 +1,59 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import React, { useEffect } from 'react';
+import { createPortal } from 'react-dom';
+import { useTranslation } from 'react-i18next';
+import Icon from '@stremio/stremio-icons/react';
+import { useShortcuts } from 'stremio/common';
+import { Button, ShortcutsGroup } from 'stremio/components';
+import styles from './styles.less';
+
+type Props = {
+ onClose: () => void,
+};
+
+const ShortcutsModal = ({ onClose }: Props) => {
+ const { t } = useTranslation();
+ const { grouped } = useShortcuts();
+
+ useEffect(() => {
+ const onKeyDown = ({ key }: KeyboardEvent) => {
+ key === 'Escape' && onClose();
+ };
+
+ document.addEventListener('keydown', onKeyDown);
+ return () => document.removeEventListener('keydown', onKeyDown);
+ }, []);
+
+ return createPortal((
+
+
+
+
+
+
+ {t('SETTINGS_NAV_SHORTCUTS')}
+
+
+
+
+
+
+ {
+ grouped.map(({ name, label, shortcuts }) => (
+
+ ))
+ }
+
+
+
+ ), document.body);
+};
+
+export default ShortcutsModal;
diff --git a/src/App/ShortcutsModal/index.ts b/src/App/ShortcutsModal/index.ts
new file mode 100644
index 000000000..5a7549fac
--- /dev/null
+++ b/src/App/ShortcutsModal/index.ts
@@ -0,0 +1,2 @@
+import ShortcutsModal from './ShortcutsModal';
+export default ShortcutsModal;
diff --git a/src/App/ShortcutsModal/styles.less b/src/App/ShortcutsModal/styles.less
new file mode 100644
index 000000000..ebbc19c62
--- /dev/null
+++ b/src/App/ShortcutsModal/styles.less
@@ -0,0 +1,91 @@
+@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
+
+.shortcuts-modal {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .backdrop {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: @color-background-dark5-40;
+ cursor: pointer;
+ }
+
+ .container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-height: 80%;
+ max-width: 80%;
+ border-radius: var(--border-radius);
+ background-color: var(--modal-background-color);
+ box-shadow: var(--outer-glow);
+ overflow-y: auto;
+
+ .header {
+ flex: none;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 5rem;
+ padding-left: 2.5rem;
+ padding-right: 1rem;
+
+ .title {
+ position: relative;
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: var(--primary-foreground-color);
+ }
+
+ .close-button {
+ position: relative;
+ width: 3rem;
+ height: 3rem;
+ padding: 0.5rem;
+ border-radius: var(--border-radius);
+ z-index: 2;
+
+ .icon {
+ display: block;
+ width: 100%;
+ height: 100%;
+ color: var(--primary-foreground-color);
+ opacity: 0.4;
+ }
+
+ &:hover, &:focus {
+ .icon {
+ opacity: 1;
+ color: var(--primary-foreground-color);
+ }
+ }
+
+ &:focus {
+ outline-color: var(--primary-foreground-color);
+ }
+ }
+ }
+
+ .content {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 3rem;
+ padding: 0 2.5rem;
+ padding-bottom: 2rem;
+ overflow-y: auto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx
new file mode 100644
index 000000000..f41e08271
--- /dev/null
+++ b/src/common/Shortcuts/Shortcuts.tsx
@@ -0,0 +1,54 @@
+import React, { createContext, useCallback, useContext, useEffect } from 'react';
+import shortcuts from './shortcuts';
+
+const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
+
+export type ShortcutName = string;
+export type ShortcutListener = () => void;
+
+interface ShortcutsContext {
+ grouped: ShortcutGroup[],
+}
+
+const ShortcutsContext = createContext({} as ShortcutsContext);
+
+type Props = {
+ children: JSX.Element,
+ onShortcut: (name: ShortcutName) => void,
+};
+
+const ShortcutsProvider = ({ children, onShortcut }: Props) => {
+ const onKeyDown = useCallback(({ ctrlKey, shiftKey, 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())) {
+ onShortcut(name as ShortcutName);
+ }
+ }));
+ }, [onShortcut]);
+
+ useEffect(() => {
+ document.addEventListener('keydown', onKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', onKeyDown);
+ };
+ }, [onKeyDown]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useShortcuts = () => {
+ return useContext(ShortcutsContext);
+};
+
+export {
+ ShortcutsProvider,
+ useShortcuts
+};
diff --git a/src/common/Shortcuts/index.ts b/src/common/Shortcuts/index.ts
new file mode 100644
index 000000000..f7fa38a18
--- /dev/null
+++ b/src/common/Shortcuts/index.ts
@@ -0,0 +1,5 @@
+import { ShortcutsProvider, useShortcuts } from './Shortcuts';
+export {
+ ShortcutsProvider,
+ useShortcuts,
+};
diff --git a/src/common/Shortcuts/shortcuts.ts b/src/common/Shortcuts/shortcuts.ts
new file mode 100644
index 000000000..d918733af
--- /dev/null
+++ b/src/common/Shortcuts/shortcuts.ts
@@ -0,0 +1,91 @@
+const shortcuts: ShortcutGroup[] = [
+ {
+ name: 'general',
+ label: 'SETTINGS_NAV_GENERAL',
+ shortcuts: [
+ {
+ name: 'navigateTabs',
+ label: 'SETTINGS_SHORTCUT_NAVIGATE_MENUS',
+ combos: [['1', '2', '3', '4', '5', '6']],
+ },
+ {
+ name: 'navigateSearch',
+ label: 'SETTINGS_SHORTCUT_GO_TO_SEARCH',
+ combos: [['0']],
+ },
+ {
+ name: 'fullscreen',
+ label: 'SETTINGS_SHORTCUT_FULLSCREEN',
+ combos: [['F']],
+ },
+ {
+ name: 'exit',
+ label: 'SETTINGS_SHORTCUT_EXIT_BACK',
+ combos: [['Escape']],
+ },
+ {
+ name: 'shortcuts',
+ label: 'SETTINGS_SHORTCUT_SHORTCUTS',
+ combos: [['Ctrl', '?']],
+ },
+ ]
+ },
+ {
+ name: 'player',
+ label: 'SETTINGS_NAV_PLAYER',
+ shortcuts: [
+ {
+ name: 'playPause',
+ label: 'SETTINGS_SHORTCUT_PLAY_PAUSE',
+ combos: [['Space']],
+ },
+ {
+ name: 'seekForward',
+ label: 'SETTINGS_SHORTCUT_SEEK_FORWARD',
+ combos: [['ArrowRight'], ['Shift', 'ArrowRight']],
+ },
+ {
+ name: 'seekBackward',
+ label: 'SETTINGS_SHORTCUT_SEEK_BACKWARD',
+ combos: [['ArrowLeft'], ['Shift', 'ArrowLeft']],
+ },
+ {
+ name: 'volumeUp',
+ label: 'SETTINGS_SHORTCUT_VOLUME_UP',
+ combos: [['ArrowUp']],
+ },
+ {
+ name: 'volumeDown',
+ label: 'SETTINGS_SHORTCUT_VOLUME_DOWN',
+ combos: [['ArrowDown']],
+ },
+ {
+ name: 'subtitlesSize',
+ label: 'SETTINGS_SHORTCUT_SUBTITLES_SIZE',
+ combos: [['-'], ['=']],
+ },
+ {
+ name: 'subtitlesDelay',
+ label: 'SETTINGS_SHORTCUT_SUBTITLES_DELAY',
+ combos: [['G'], ['H']],
+ },
+ {
+ name: 'subtitlesMenu',
+ label: 'SETTINGS_SHORTCUT_MENU_SUBTITLES',
+ combos: [['S']],
+ },
+ {
+ name: 'audioMenu',
+ label: 'SETTINGS_SHORTCUT_MENU_AUDIO',
+ combos: [['A']],
+ },
+ {
+ name: 'infoMenu',
+ label: 'SETTINGS_SHORTCUT_MENU_INFO',
+ combos: [['I']],
+ },
+ ]
+ },
+];
+
+export default shortcuts;
diff --git a/src/common/Shortcuts/types.d.ts b/src/common/Shortcuts/types.d.ts
new file mode 100644
index 000000000..e4180616d
--- /dev/null
+++ b/src/common/Shortcuts/types.d.ts
@@ -0,0 +1,11 @@
+type Shortcut = {
+ name: string,
+ label: string,
+ combos: string[][],
+};
+
+type ShortcutGroup = {
+ name: string,
+ label: string,
+ shortcuts: Shortcut[],
+};
diff --git a/src/common/index.js b/src/common/index.js
index 25df5c158..0b9cb252f 100644
--- a/src/common/index.js
+++ b/src/common/index.js
@@ -4,6 +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 comparatorWithPriorities = require('./comparatorWithPriorities');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
@@ -35,6 +36,8 @@ module.exports = {
onFileDrop,
PlatformProvider,
usePlatform,
+ ShortcutsProvider,
+ useShortcuts,
ToastProvider,
useToast,
TooltipProvider,
diff --git a/src/components/ShortcutsGroup/Combos/Combos.less b/src/components/ShortcutsGroup/Combos/Combos.less
new file mode 100644
index 000000000..a862d54ca
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/Combos.less
@@ -0,0 +1,22 @@
+.combos {
+ position: relative;
+ display: flex;
+ overflow: visible;
+
+ .combo {
+ position: relative;
+ display: flex;
+ overflow: visible;
+
+ .separator {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 3.5rem;
+ font-size: 1rem;
+ color: var(--primary-foreground-color);
+ opacity: 0.6;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/ShortcutsGroup/Combos/Combos.tsx b/src/components/ShortcutsGroup/Combos/Combos.tsx
new file mode 100644
index 000000000..0168441bc
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/Combos.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Keys from './Keys';
+import styles from './Combos.less';
+
+type Props = {
+ combos: string[][],
+};
+
+const Combos = ({ combos }: Props) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {
+ combos.map((keys, index) => (
+
+
+ {
+ index < (combos.length - 1) && (
+
+ { t('SETTINGS_SHORTCUT_OR') }
+
+ )
+ }
+
+ ))
+ }
+
+ );
+};
+
+export default Combos;
diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.less b/src/components/ShortcutsGroup/Combos/Keys/Keys.less
new file mode 100644
index 000000000..7bb8c76e7
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.less
@@ -0,0 +1,26 @@
+kbd {
+ flex: none;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 2.5rem;
+ min-width: 2.5rem;
+ padding: 0 1rem;
+ font-size: 1rem;
+ font-weight: 500;
+ color: var(--primary-foreground-color);
+ border-radius: 0.25em;
+ box-shadow: 0 4px 0 1px rgba(255, 255, 255, 0.1);
+ background-color: var(--overlay-color);
+}
+
+.separator {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ font-size: 1rem;
+ color: var(--primary-foreground-color);
+}
\ No newline at end of file
diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx
new file mode 100644
index 000000000..71ec610da
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx
@@ -0,0 +1,51 @@
+import React, { Fragment, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import styles from './Keys.less';
+
+type Props = {
+ keys: string[],
+};
+
+const Keys = ({ keys }: Props) => {
+ const { t } = useTranslation();
+
+ const keyLabelMap: Record = useMemo(() => ({
+ 'Shift': `⇧ ${t('SETTINGS_SHORTCUT_SHIFT')}`,
+ 'Space': t('SETTINGS_SHORTCUT_SPACE'),
+ 'Ctrl': t('SETTINGS_SHORTCUT_CTRL'),
+ 'Escape': t('SETTINGS_SHORTCUT_ESC'),
+ 'ArrowUp': '↑',
+ 'ArrowDown': '↓',
+ 'ArrowLeft': '←',
+ 'ArrowRight': '→',
+ }), [t]);
+
+ const isRange = useMemo(() => {
+ return keys.length > 1 && keys.every((key) => !Number.isNaN(parseInt(key)));
+ }, [keys]);
+
+ const filteredKeys = useMemo(() => {
+ return isRange ? [keys[0], keys[keys.length - 1]] : keys;
+ }, [keys, isRange]);
+
+ return (
+ filteredKeys.map((key, index) => (
+
+
+ {keyLabelMap[key] ?? key.toUpperCase()}
+
+ {
+ index < (filteredKeys.length - 1) && (
+
+ {
+ isRange ? t('SETTINGS_SHORTCUT_TO') : '+'
+ }
+
+ )
+ }
+
+ ))
+ );
+};
+
+export default Keys;
diff --git a/src/components/ShortcutsGroup/Combos/Keys/index.ts b/src/components/ShortcutsGroup/Combos/Keys/index.ts
new file mode 100644
index 000000000..ba8d58731
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/Keys/index.ts
@@ -0,0 +1,2 @@
+import Keys from './Keys';
+export default Keys;
diff --git a/src/components/ShortcutsGroup/Combos/index.ts b/src/components/ShortcutsGroup/Combos/index.ts
new file mode 100644
index 000000000..c66667f91
--- /dev/null
+++ b/src/components/ShortcutsGroup/Combos/index.ts
@@ -0,0 +1,2 @@
+import Combos from './Combos';
+export default Combos;
diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.less b/src/components/ShortcutsGroup/ShortcutsGroup.less
new file mode 100644
index 000000000..f0fdd975c
--- /dev/null
+++ b/src/components/ShortcutsGroup/ShortcutsGroup.less
@@ -0,0 +1,44 @@
+.shortcuts-group {
+ flex: 1 1 0;
+ position: relative;
+ min-width: 30rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ overflow: visible;
+
+ .title {
+ flex: none;
+ display: flex;
+ font-size: 1rem;
+ font-weight: 400;
+ color: var(--primary-foreground-color);
+ opacity: 0.6;
+ }
+
+ .shortcuts {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ overflow: visible;
+
+ .shortcut {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+ overflow: visible;
+
+ .label {
+ position: relative;
+ font-size: 1rem;
+ color: var(--primary-foreground-color);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ }
+}
diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.tsx b/src/components/ShortcutsGroup/ShortcutsGroup.tsx
new file mode 100644
index 000000000..069d5d1e8
--- /dev/null
+++ b/src/components/ShortcutsGroup/ShortcutsGroup.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import Combos from './Combos';
+import styles from './ShortcutsGroup.less';
+
+type Props = {
+ className?: string,
+ label: string,
+ shortcuts: Shortcut[],
+};
+
+const ShortcutsGroup = ({ className, label, shortcuts }: Props) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t(label)}
+
+
+
+ {
+ shortcuts.map(({ name, label, combos }) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default ShortcutsGroup;
diff --git a/src/components/ShortcutsGroup/index.ts b/src/components/ShortcutsGroup/index.ts
new file mode 100644
index 000000000..11f8d0678
--- /dev/null
+++ b/src/components/ShortcutsGroup/index.ts
@@ -0,0 +1,2 @@
+import ShortcutsGroup from './ShortcutsGroup';
+export default ShortcutsGroup;
diff --git a/src/components/index.ts b/src/components/index.ts
index a5638007e..a47c2c709 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -25,6 +25,7 @@ import RadioButton from './RadioButton';
import SearchBar from './SearchBar';
import SharePrompt from './SharePrompt';
import Slider from './Slider';
+import ShortcutsGroup from './ShortcutsGroup';
import TextInput from './TextInput';
import Toggle from './Toggle';
import Transition from './Transition';
@@ -59,6 +60,7 @@ export {
SearchBar,
SharePrompt,
Slider,
+ ShortcutsGroup,
TextInput,
Toggle,
Transition,
diff --git a/src/routes/Settings/Shortcuts/Shortcuts.less b/src/routes/Settings/Shortcuts/Shortcuts.less
index 40d97987d..186cfa837 100644
--- a/src/routes/Settings/Shortcuts/Shortcuts.less
+++ b/src/routes/Settings/Shortcuts/Shortcuts.less
@@ -1,27 +1,4 @@
-.shortcut-container {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- overflow: visible;
-
- kbd {
- flex: 0 1 auto;
- height: 2.5rem;
- min-width: 2.5rem;
- line-height: 2.5rem;
- padding: 0 1rem;
- font-weight: 500;
- color: var(--primary-foreground-color);
- border-radius: 0.25em;
- box-shadow: 0 4px 0 1px var(--modal-background-color);
- background-color: var(--overlay-color);
- }
-
- .label {
- flex: none;
- margin: 0 1rem;
- white-space: nowrap;
- color: var(--primary-foreground-color);
- }
+.shortcuts-group {
+ width: 100%;
+ margin-bottom: 3rem;
}
\ No newline at end of file
diff --git a/src/routes/Settings/Shortcuts/Shortcuts.tsx b/src/routes/Settings/Shortcuts/Shortcuts.tsx
index d852280a6..a0599a503 100644
--- a/src/routes/Settings/Shortcuts/Shortcuts.tsx
+++ b/src/routes/Settings/Shortcuts/Shortcuts.tsx
@@ -1,97 +1,24 @@
import React, { forwardRef } from 'react';
-import { Section, Option } from '../components';
+import { Section } from '../components';
+import { ShortcutsGroup } from 'stremio/components';
+import { useShortcuts } from 'stremio/common';
import styles from './Shortcuts.less';
-import { useTranslation } from 'react-i18next';
const Shortcuts = forwardRef((_, ref) => {
- const { t } = useTranslation();
+ const { grouped } = useShortcuts();
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {
+ grouped.map(({ name, label, shortcuts }) => (
+
+ ))
+ }
);
});
diff --git a/tsconfig.json b/tsconfig.json
index d55ac9356..c4b0a8626 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "lib": [ "ES2016", "DOM", "DOM.Iterable"],
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "react",
"baseUrl": "./src",
"outDir": "./dist",