mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add list playback and navigation hotkeys (#1469)
This commit is contained in:
@@ -802,6 +802,11 @@
|
|||||||
"hotkey_favoritePreviousSong": "favorite $t(common.previousSong)",
|
"hotkey_favoritePreviousSong": "favorite $t(common.previousSong)",
|
||||||
"hotkey_globalSearch": "global search",
|
"hotkey_globalSearch": "global search",
|
||||||
"hotkey_localSearch": "in-page search",
|
"hotkey_localSearch": "in-page search",
|
||||||
|
"hotkey_listNavigateToPage": "list navigate to item page",
|
||||||
|
"hotkey_listPlayDefault": "list play",
|
||||||
|
"hotkey_listPlayLast": "list play last",
|
||||||
|
"hotkey_listPlayNext": "list play next",
|
||||||
|
"hotkey_listPlayNow": "list play now",
|
||||||
"hotkey_navigateHome": "navigate to home",
|
"hotkey_navigateHome": "navigate to home",
|
||||||
"hotkey_playbackNext": "next track",
|
"hotkey_playbackNext": "next track",
|
||||||
"hotkey_playbackPause": "pause",
|
"hotkey_playbackPause": "pause",
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
||||||
|
import {
|
||||||
|
ItemListStateActions,
|
||||||
|
ItemListStateItemWithRequiredProperties,
|
||||||
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
|
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const useListHotkeys = ({
|
||||||
|
controls,
|
||||||
|
focused,
|
||||||
|
internalState,
|
||||||
|
itemType,
|
||||||
|
}: {
|
||||||
|
controls: ItemControls;
|
||||||
|
focused: boolean;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => {
|
||||||
|
const { bindings } = useHotkeySettings();
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Helper to check if item has required properties
|
||||||
|
const hasRequiredStateItemProperties = (
|
||||||
|
item: unknown,
|
||||||
|
): item is ItemListStateItemWithRequiredProperties => {
|
||||||
|
return (
|
||||||
|
typeof item === 'object' &&
|
||||||
|
item !== null &&
|
||||||
|
'id' in item &&
|
||||||
|
typeof (item as any).id === 'string' &&
|
||||||
|
'_serverId' in item &&
|
||||||
|
typeof (item as any)._serverId === 'string' &&
|
||||||
|
'_itemType' in item &&
|
||||||
|
typeof (item as any)._itemType === 'string'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys([
|
||||||
|
[
|
||||||
|
'mod+a',
|
||||||
|
() => {
|
||||||
|
if (focused) {
|
||||||
|
if (internalState.isAllSelected()) {
|
||||||
|
internalState.deselectAll();
|
||||||
|
} else {
|
||||||
|
internalState.selectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bindings.listPlayDefault.hotkey,
|
||||||
|
() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const selected = internalState.getSelected();
|
||||||
|
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||||
|
if (validSelected.length === 0) return;
|
||||||
|
|
||||||
|
const item = validSelected[0];
|
||||||
|
const playType = playButtonBehavior;
|
||||||
|
controls.onPlay?.({ item, itemType, playType } as any);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bindings.listPlayNow.hotkey,
|
||||||
|
() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const selected = internalState.getSelected();
|
||||||
|
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||||
|
if (validSelected.length === 0) return;
|
||||||
|
|
||||||
|
const item = validSelected[0];
|
||||||
|
controls.onPlay?.({ item, itemType, playType: Play.NOW } as any);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bindings.listPlayNext.hotkey,
|
||||||
|
() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const selected = internalState.getSelected();
|
||||||
|
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||||
|
if (validSelected.length === 0) return;
|
||||||
|
|
||||||
|
const item = validSelected[0];
|
||||||
|
controls.onPlay?.({ item, itemType, playType: Play.NEXT } as any);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bindings.listPlayLast.hotkey,
|
||||||
|
() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const selected = internalState.getSelected();
|
||||||
|
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||||
|
if (validSelected.length === 0) return;
|
||||||
|
|
||||||
|
const item = validSelected[0];
|
||||||
|
controls.onPlay?.({ item, itemType, playType: Play.LAST } as any);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
bindings.listNavigateToPage.hotkey,
|
||||||
|
() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const selected = internalState.getSelected();
|
||||||
|
const validSelected = selected.filter(hasRequiredStateItemProperties);
|
||||||
|
if (validSelected.length === 0) return;
|
||||||
|
|
||||||
|
const item = validSelected[0];
|
||||||
|
const path = getTitlePath(itemType, item.id);
|
||||||
|
if (path) {
|
||||||
|
navigate(path, { state: { item } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -41,11 +41,11 @@ import {
|
|||||||
useItemListState,
|
useItemListState,
|
||||||
useItemListStateSubscription,
|
useItemListStateSubscription,
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||||
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { useElementSize } from '/@/shared/hooks/use-element-size';
|
import { useElementSize } from '/@/shared/hooks/use-element-size';
|
||||||
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -714,20 +714,12 @@ const BaseItemGridList = ({
|
|||||||
|
|
||||||
useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
|
useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
|
||||||
|
|
||||||
useHotkeys([
|
useListHotkeys({
|
||||||
[
|
controls,
|
||||||
'mod+a',
|
focused,
|
||||||
() => {
|
internalState,
|
||||||
if (focused) {
|
itemType,
|
||||||
if (internalState.isAllSelected()) {
|
});
|
||||||
internalState.deselectAll();
|
|
||||||
} else {
|
|
||||||
internalState.selectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
useItemListStateSubscription,
|
useItemListStateSubscription,
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
||||||
|
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||||
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
||||||
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +43,6 @@ import {
|
|||||||
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { TableColumn } from '/@/shared/types/types';
|
import { TableColumn } from '/@/shared/types/types';
|
||||||
@@ -2262,20 +2262,12 @@ const BaseItemTableList = ({
|
|||||||
stickyGroupTop,
|
stickyGroupTop,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useHotkeys([
|
useListHotkeys({
|
||||||
[
|
controls,
|
||||||
'mod+a',
|
focused,
|
||||||
() => {
|
internalState,
|
||||||
if (focused) {
|
itemType,
|
||||||
if (internalState.isAllSelected()) {
|
});
|
||||||
internalState.deselectAll();
|
|
||||||
} else {
|
|
||||||
internalState.selectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import { useSettingSearchContext } from '/@/renderer/features/settings/context/s
|
|||||||
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Table } from '/@/shared/components/table/table';
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
@@ -55,6 +55,23 @@ const BINDINGS_MAP: Record<BindingActions, string> = {
|
|||||||
context: 'globalSearch',
|
context: 'globalSearch',
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
|
listNavigateToPage: i18n.t('setting.hotkey', {
|
||||||
|
context: 'listNavigateToPage',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
listPlayDefault: i18n.t('setting.hotkey', {
|
||||||
|
context: 'listPlayDefault',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
listPlayLast: i18n.t('setting.hotkey', {
|
||||||
|
context: 'listPlayLast',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
listPlayNext: i18n.t('setting.hotkey', {
|
||||||
|
context: 'listPlayNext',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
listPlayNow: i18n.t('setting.hotkey', { context: 'listPlayNow', postProcess: 'sentenceCase' }),
|
||||||
localSearch: i18n.t('setting.hotkey', { context: 'localSearch', postProcess: 'sentenceCase' }),
|
localSearch: i18n.t('setting.hotkey', { context: 'localSearch', postProcess: 'sentenceCase' }),
|
||||||
navigateHome: i18n.t('setting.hotkey', {
|
navigateHome: i18n.t('setting.hotkey', {
|
||||||
context: 'navigateHome',
|
context: 'navigateHome',
|
||||||
@@ -251,20 +268,21 @@ export const HotkeyManagerSettings = () => {
|
|||||||
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
|
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
|
||||||
/>
|
/>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
<Table withColumnBorders withRowBorders>
|
||||||
{filteredBindings.map((binding) => (
|
{filteredBindings.map((binding) => (
|
||||||
<Group key={`hotkey-${binding}`} wrap="nowrap">
|
<Table.Tr key={`hotkey-${binding}`}>
|
||||||
<TextInput
|
<Table.Td style={{ userSelect: 'none' }}>
|
||||||
readOnly
|
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||||
style={{ userSelect: 'none' }}
|
</Table.Td>
|
||||||
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
<Table.Td>
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
id={`hotkey-${binding}`}
|
id={`hotkey-${binding}`}
|
||||||
leftSection={<Icon icon="keyboard" />}
|
leftSection={<Icon icon="keyboard" />}
|
||||||
onBlur={() => setSelected(null)}
|
onBlur={() => setSelected(null)}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
onKeyDownCapture={(e) => {
|
onKeyDownCapture={(e) => {
|
||||||
if (selected !== (binding as BindingActions)) return;
|
if (selected !== (binding as BindingActions))
|
||||||
|
return;
|
||||||
handleSetHotkey(binding as BindingActions, e);
|
handleSetHotkey(binding as BindingActions, e);
|
||||||
}}
|
}}
|
||||||
readOnly
|
readOnly
|
||||||
@@ -281,48 +299,68 @@ export const HotkeyManagerSettings = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
|
opacity:
|
||||||
|
selected === (binding as BindingActions)
|
||||||
|
? 0.8
|
||||||
|
: 1,
|
||||||
outline: duplicateHotkeyMap.includes(
|
outline: duplicateHotkeyMap.includes(
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
|
bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
|
.hotkey!,
|
||||||
)
|
)
|
||||||
? '1px dashed red'
|
? '1px dashed red'
|
||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
|
value={
|
||||||
|
bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
|
.hotkey
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</Table.Td>
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
|
<Table.Td>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
|
bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
|
.isGlobal
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
bindings[binding as keyof typeof BINDINGS_MAP]
|
bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
.hotkey === ''
|
.hotkey === ''
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleSetGlobalHotkey(binding as BindingActions, e)
|
handleSetGlobalHotkey(
|
||||||
|
binding as BindingActions,
|
||||||
|
e,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
size="md"
|
size="md"
|
||||||
style={{
|
style={{
|
||||||
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
|
opacity: bindings[
|
||||||
.allowGlobal
|
binding as keyof typeof BINDINGS_MAP
|
||||||
|
].allowGlobal
|
||||||
? 1
|
? 1
|
||||||
: 0,
|
: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Table.Td>
|
||||||
)}
|
)}
|
||||||
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
|
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
|
||||||
|
<Table.Td>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="x"
|
icon="x"
|
||||||
iconProps={{
|
iconProps={{
|
||||||
color: 'error',
|
color: 'error',
|
||||||
}}
|
}}
|
||||||
onClick={() => handleClearHotkey(binding as BindingActions)}
|
onClick={() =>
|
||||||
|
handleClearHotkey(binding as BindingActions)
|
||||||
|
}
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
|
</Table.Td>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ const BindingActionsSchema = z.enum([
|
|||||||
'volumeUp',
|
'volumeUp',
|
||||||
'zoomIn',
|
'zoomIn',
|
||||||
'zoomOut',
|
'zoomOut',
|
||||||
|
'listPlayDefault',
|
||||||
|
'listPlayNow',
|
||||||
|
'listPlayNext',
|
||||||
|
'listPlayLast',
|
||||||
|
'listNavigateToPage',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const DiscordDisplayTypeSchema = z.enum(['artist', 'feishin', 'song']);
|
const DiscordDisplayTypeSchema = z.enum(['artist', 'feishin', 'song']);
|
||||||
@@ -647,6 +652,11 @@ export enum BindingActions {
|
|||||||
FAVORITE_PREVIOUS_REMOVE = 'favoritePreviousRemove',
|
FAVORITE_PREVIOUS_REMOVE = 'favoritePreviousRemove',
|
||||||
FAVORITE_PREVIOUS_TOGGLE = 'favoritePreviousToggle',
|
FAVORITE_PREVIOUS_TOGGLE = 'favoritePreviousToggle',
|
||||||
GLOBAL_SEARCH = 'globalSearch',
|
GLOBAL_SEARCH = 'globalSearch',
|
||||||
|
LIST_NAVIGATE_TO_PAGE = 'listNavigateToPage',
|
||||||
|
LIST_PLAY_DEFAULT = 'listPlayDefault',
|
||||||
|
LIST_PLAY_LAST = 'listPlayLast',
|
||||||
|
LIST_PLAY_NEXT = 'listPlayNext',
|
||||||
|
LIST_PLAY_NOW = 'listPlayNow',
|
||||||
LOCAL_SEARCH = 'localSearch',
|
LOCAL_SEARCH = 'localSearch',
|
||||||
MUTE = 'volumeMute',
|
MUTE = 'volumeMute',
|
||||||
NAVIGATE_HOME = 'navigateHome',
|
NAVIGATE_HOME = 'navigateHome',
|
||||||
@@ -986,6 +996,11 @@ const initialState: SettingsState = {
|
|||||||
favoritePreviousRemove: { allowGlobal: true, hotkey: '', isGlobal: false },
|
favoritePreviousRemove: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
favoritePreviousToggle: { allowGlobal: true, hotkey: '', isGlobal: false },
|
favoritePreviousToggle: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
globalSearch: { allowGlobal: false, hotkey: 'mod+k', isGlobal: false },
|
globalSearch: { allowGlobal: false, hotkey: 'mod+k', isGlobal: false },
|
||||||
|
listNavigateToPage: { allowGlobal: false, hotkey: 'mod+g', isGlobal: false },
|
||||||
|
listPlayDefault: { allowGlobal: false, hotkey: 'enter', isGlobal: false },
|
||||||
|
listPlayLast: { allowGlobal: false, hotkey: '', isGlobal: false },
|
||||||
|
listPlayNext: { allowGlobal: false, hotkey: '', isGlobal: false },
|
||||||
|
listPlayNow: { allowGlobal: false, hotkey: '', isGlobal: false },
|
||||||
localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false },
|
localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false },
|
||||||
navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false },
|
navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false },
|
||||||
next: { allowGlobal: true, hotkey: '', isGlobal: false },
|
next: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user