mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 18:30:29 +00:00
feat: add shortcuts modal
This commit is contained in:
parent
10faa1c105
commit
9fa0e46423
22 changed files with 580 additions and 124 deletions
|
|
@ -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 = () => {
|
|||
<ToastProvider className={styles['toasts-container']}>
|
||||
<TooltipProvider className={styles['tooltip-container']}>
|
||||
<FileDropProvider className={styles['file-drop-container']}>
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<SearchParamsHandler />
|
||||
<UpdaterBanner className={styles['updater-banner-container']} />
|
||||
<RouterWithProtectedRoutes
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
<ShortcutsProvider onShortcut={onShortcut}>
|
||||
{
|
||||
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
|
||||
}
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<SearchParamsHandler />
|
||||
<UpdaterBanner className={styles['updater-banner-container']} />
|
||||
<RouterWithProtectedRoutes
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
</ShortcutsProvider>
|
||||
</FileDropProvider>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
|
|
|
|||
59
src/App/ShortcutsModal/ShortcutsModal.tsx
Normal file
59
src/App/ShortcutsModal/ShortcutsModal.tsx
Normal file
|
|
@ -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((
|
||||
<div className={styles['shortcuts-modal']}>
|
||||
<div className={styles['backdrop']} onClick={onClose} />
|
||||
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['header']}>
|
||||
<div className={styles['title']}>
|
||||
{t('SETTINGS_NAV_SHORTCUTS')}
|
||||
</div>
|
||||
|
||||
<Button className={styles['close-button']} title={t('BUTTON_CLOSE')} onClick={onClose}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles['content']}>
|
||||
{
|
||||
grouped.map(({ name, label, shortcuts }) => (
|
||||
<ShortcutsGroup
|
||||
key={name}
|
||||
label={label}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
), document.body);
|
||||
};
|
||||
|
||||
export default ShortcutsModal;
|
||||
2
src/App/ShortcutsModal/index.ts
Normal file
2
src/App/ShortcutsModal/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import ShortcutsModal from './ShortcutsModal';
|
||||
export default ShortcutsModal;
|
||||
91
src/App/ShortcutsModal/styles.less
Normal file
91
src/App/ShortcutsModal/styles.less
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/common/Shortcuts/Shortcuts.tsx
Normal file
54
src/common/Shortcuts/Shortcuts.tsx
Normal file
|
|
@ -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<ShortcutsContext>({} 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 (
|
||||
<ShortcutsContext.Provider value={{ grouped: shortcuts }}>
|
||||
{children}
|
||||
</ShortcutsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useShortcuts = () => {
|
||||
return useContext(ShortcutsContext);
|
||||
};
|
||||
|
||||
export {
|
||||
ShortcutsProvider,
|
||||
useShortcuts
|
||||
};
|
||||
5
src/common/Shortcuts/index.ts
Normal file
5
src/common/Shortcuts/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ShortcutsProvider, useShortcuts } from './Shortcuts';
|
||||
export {
|
||||
ShortcutsProvider,
|
||||
useShortcuts,
|
||||
};
|
||||
91
src/common/Shortcuts/shortcuts.ts
Normal file
91
src/common/Shortcuts/shortcuts.ts
Normal file
|
|
@ -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;
|
||||
11
src/common/Shortcuts/types.d.ts
vendored
Normal file
11
src/common/Shortcuts/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
type Shortcut = {
|
||||
name: string,
|
||||
label: string,
|
||||
combos: string[][],
|
||||
};
|
||||
|
||||
type ShortcutGroup = {
|
||||
name: string,
|
||||
label: string,
|
||||
shortcuts: Shortcut[],
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
22
src/components/ShortcutsGroup/Combos/Combos.less
Normal file
22
src/components/ShortcutsGroup/Combos/Combos.less
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/components/ShortcutsGroup/Combos/Combos.tsx
Normal file
33
src/components/ShortcutsGroup/Combos/Combos.tsx
Normal file
|
|
@ -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 (
|
||||
<div className={styles['combos']}>
|
||||
{
|
||||
combos.map((keys, index) => (
|
||||
<div className={styles['combo']} key={index}>
|
||||
<Keys keys={keys} />
|
||||
{
|
||||
index < (combos.length - 1) && (
|
||||
<div className={styles['separator']}>
|
||||
{ t('SETTINGS_SHORTCUT_OR') }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Combos;
|
||||
26
src/components/ShortcutsGroup/Combos/Keys/Keys.less
Normal file
26
src/components/ShortcutsGroup/Combos/Keys/Keys.less
Normal file
|
|
@ -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);
|
||||
}
|
||||
51
src/components/ShortcutsGroup/Combos/Keys/Keys.tsx
Normal file
51
src/components/ShortcutsGroup/Combos/Keys/Keys.tsx
Normal file
|
|
@ -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<string, string> = 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) => (
|
||||
<Fragment key={key}>
|
||||
<kbd>
|
||||
{keyLabelMap[key] ?? key.toUpperCase()}
|
||||
</kbd>
|
||||
{
|
||||
index < (filteredKeys.length - 1) && (
|
||||
<div className={styles['separator']}>
|
||||
{
|
||||
isRange ? t('SETTINGS_SHORTCUT_TO') : '+'
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
export default Keys;
|
||||
2
src/components/ShortcutsGroup/Combos/Keys/index.ts
Normal file
2
src/components/ShortcutsGroup/Combos/Keys/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Keys from './Keys';
|
||||
export default Keys;
|
||||
2
src/components/ShortcutsGroup/Combos/index.ts
Normal file
2
src/components/ShortcutsGroup/Combos/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Combos from './Combos';
|
||||
export default Combos;
|
||||
44
src/components/ShortcutsGroup/ShortcutsGroup.less
Normal file
44
src/components/ShortcutsGroup/ShortcutsGroup.less
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/components/ShortcutsGroup/ShortcutsGroup.tsx
Normal file
38
src/components/ShortcutsGroup/ShortcutsGroup.tsx
Normal file
|
|
@ -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 (
|
||||
<div className={classNames(className, styles['shortcuts-group'])}>
|
||||
<div className={styles['title']}>
|
||||
{t(label)}
|
||||
</div>
|
||||
|
||||
<div className={styles['shortcuts']}>
|
||||
{
|
||||
shortcuts.map(({ name, label, combos }) => (
|
||||
<div className={styles['shortcut']} key={name}>
|
||||
<div className={styles['label']}>
|
||||
{t(label)}
|
||||
</div>
|
||||
<Combos combos={combos} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortcutsGroup;
|
||||
2
src/components/ShortcutsGroup/index.ts
Normal file
2
src/components/ShortcutsGroup/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import ShortcutsGroup from './ShortcutsGroup';
|
||||
export default ShortcutsGroup;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<HTMLDivElement>((_, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { grouped } = useShortcuts();
|
||||
|
||||
return (
|
||||
<Section ref={ref} label={'SETTINGS_NAV_SHORTCUTS'}>
|
||||
<Option label={'SETTINGS_SHORTCUT_PLAY_PAUSE'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>{t('SETTINGS_SHORTCUT_SPACE')}</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SEEK_FORWARD'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>→</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
|
||||
<kbd>⇧ {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>→</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SEEK_BACKWARD'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>←</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
|
||||
<kbd>⇧ {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>←</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_VOLUME_UP'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>↑</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_VOLUME_DOWN'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>↓</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_SUBTITLES'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_AUDIO'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>A</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_INFO'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>I</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>F</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_SIZE'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>-</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
|
||||
<kbd>=</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_DELAY'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>G</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
|
||||
<kbd>H</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_NAVIGATE_MENUS'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>1</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_TO')}</div>
|
||||
<kbd>6</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_GO_TO_SEARCH'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>0</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_EXIT_BACK'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>{t('SETTINGS_SHORTCUT_ESC')}</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
{
|
||||
grouped.map(({ name, label, shortcuts }) => (
|
||||
<ShortcutsGroup
|
||||
key={name}
|
||||
className={styles['shortcuts-group']}
|
||||
label={label}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [ "ES2016", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react",
|
||||
"baseUrl": "./src",
|
||||
"outDir": "./dist",
|
||||
|
|
|
|||
Loading…
Reference in a new issue