mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-26 01:19:10 -04:00
[ENG-1190] New shortcuts system (#1718)
* wip new shortcuts system * Shortcuts system and cleanup add duplicate short, fix delete object windows context menu symbol, shortcuts system Add cut to Edit menu if quickPreview is open do not open the folder/doubleClick() Update index.tsx Update RenameTextBox.tsx * add listObjectsNav for list view * refactored * Update useShortcut.ts * Update useShortcut.ts * remove imports * fix copy pasting and conflicts
This commit is contained in:
@@ -74,6 +74,7 @@ fn custom_menu_bar() -> Menu {
|
||||
let edit_menu = Menu::new()
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Undo)
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Image, Package, Trash, TrashSimple } from '@phosphor-icons/react';
|
||||
import { libraryClient, useLibraryMutation } from '@sd/client';
|
||||
import { ContextMenu, dialogManager, ModifierKeys, toast } from '@sd/ui';
|
||||
import {
|
||||
ContextMenu,
|
||||
dialogManager,
|
||||
keySymbols,
|
||||
ModifierKeys,
|
||||
modifierSymbols,
|
||||
toast
|
||||
} from '@sd/ui';
|
||||
import { Menu } from '~/components/Menu';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
|
||||
import { useQuickRescan } from '~/hooks/useQuickRescan';
|
||||
import { isNonEmpty } from '~/util';
|
||||
@@ -24,8 +32,6 @@ export const Delete = new ConditionalItem({
|
||||
return { selectedFilePaths, selectedEphemeralPaths };
|
||||
},
|
||||
Component: ({ selectedFilePaths, selectedEphemeralPaths }) => {
|
||||
const keybind = useKeybindFactory();
|
||||
|
||||
const rescan = useQuickRescan();
|
||||
|
||||
const dirCount =
|
||||
@@ -55,7 +61,6 @@ export const Delete = new ConditionalItem({
|
||||
icon={Trash}
|
||||
label="Delete"
|
||||
variant="danger"
|
||||
keybind={keybind([ModifierKeys.Control], ['Delete'])}
|
||||
onClick={() =>
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteDialog
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import TruncateMarkup from 'react-truncate-markup';
|
||||
import { useKey } from 'rooks';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { useOperatingSystem, useShortcut } from '~/hooks';
|
||||
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
|
||||
@@ -76,14 +75,12 @@ export const RenameTextBox = forwardRef<HTMLDivElement, Props>(
|
||||
blur();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Escape': {
|
||||
e.stopPropagation();
|
||||
reset();
|
||||
blur();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'z': {
|
||||
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
|
||||
reset();
|
||||
@@ -108,9 +105,8 @@ export const RenameTextBox = forwardRef<HTMLDivElement, Props>(
|
||||
return `...${name.slice(-8)}`;
|
||||
}, [name]);
|
||||
|
||||
useKey(['F2', 'Enter'], (e) => {
|
||||
useShortcut('renameObject', (e) => {
|
||||
e.preventDefault();
|
||||
if (os === 'windows' && e.key === 'Enter') return;
|
||||
if (allowRename) blur();
|
||||
else if (!disabled) setAllowRename(true);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import { createOrdering, getOrderingDirection, orderingKey, useExplorerStore } f
|
||||
const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`;
|
||||
|
||||
export default () => {
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorer = useExplorerContext();
|
||||
const layoutStore = useExplorerLayoutStore();
|
||||
|
||||
@@ -145,7 +144,6 @@ export default () => {
|
||||
name="showHiddenFiles"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
|
||||
explorer.settingsStore.showHiddenFiles = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -22,17 +22,8 @@ import {
|
||||
useRspcLibraryContext,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import {
|
||||
dialogManager,
|
||||
DropdownMenu,
|
||||
Form,
|
||||
ModifierKeys,
|
||||
toast,
|
||||
ToastMessage,
|
||||
Tooltip,
|
||||
z
|
||||
} from '@sd/ui';
|
||||
import { useIsDark, useKeybind, useOperatingSystem } from '~/hooks';
|
||||
import { dialogManager, DropdownMenu, Form, toast, ToastMessage, Tooltip, z } from '@sd/ui';
|
||||
import { useIsDark, useKeybind, useOperatingSystem, useShortcut } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { useExplorerContext } from '../Context';
|
||||
@@ -43,7 +34,6 @@ import ExplorerContextMenu, {
|
||||
SharedItems
|
||||
} from '../ContextMenu';
|
||||
import { Conditional } from '../ContextMenu/ConditionalItem';
|
||||
import DeleteDialog from '../FilePath/DeleteDialog';
|
||||
import { FileThumb } from '../FilePath/Thumb';
|
||||
import { SingleItemMetadata } from '../Inspector';
|
||||
import { getQuickPreviewStore, useQuickPreviewStore } from './store';
|
||||
@@ -63,11 +53,10 @@ const useQuickPreviewContext = () => {
|
||||
};
|
||||
|
||||
export const QuickPreview = () => {
|
||||
const os = useOperatingSystem();
|
||||
const rspc = useRspcLibraryContext();
|
||||
const isDark = useIsDark();
|
||||
const { library } = useLibraryContext();
|
||||
const { openFilePaths, revealItems, openEphemeralFiles } = usePlatform();
|
||||
const { openFilePaths, openEphemeralFiles } = usePlatform();
|
||||
|
||||
const explorer = useExplorerContext();
|
||||
const { open, itemIndex } = useQuickPreviewStore();
|
||||
@@ -78,6 +67,7 @@ export const QuickPreview = () => {
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
|
||||
const [isRenaming, setIsRenaming] = useState<boolean>(false);
|
||||
const [newName, setNewName] = useState<string | null>(null);
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const items = useMemo(
|
||||
() => (open ? [...explorer.selectedItems] : []),
|
||||
@@ -136,7 +126,8 @@ export const QuickPreview = () => {
|
||||
}, [item, open]);
|
||||
|
||||
// Toggle quick preview
|
||||
useKeybind(['space'], (e) => {
|
||||
useShortcut('toggleQuickPreview', (e) => {
|
||||
console.log(e.key);
|
||||
if (isRenaming) return;
|
||||
|
||||
e.preventDefault();
|
||||
@@ -144,21 +135,17 @@ export const QuickPreview = () => {
|
||||
getQuickPreviewStore().open = !open;
|
||||
});
|
||||
|
||||
useKeybind('Escape', (e) => open && e.stopPropagation());
|
||||
|
||||
// Move between items
|
||||
useKeybind([['left'], ['right']], (e) => {
|
||||
useShortcut('quickPreviewMoveBetweenItems', (e) => {
|
||||
if (isContextMenuOpen || isRenaming) return;
|
||||
changeCurrentItem(e.key === 'ArrowLeft' ? itemIndex - 1 : itemIndex + 1);
|
||||
});
|
||||
|
||||
// Toggle metadata
|
||||
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'i'], () =>
|
||||
setShowMetadata(!showMetadata)
|
||||
);
|
||||
useShortcut('toggleMetaData', () => setShowMetadata(!showMetadata));
|
||||
|
||||
// Open file
|
||||
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'o'], () => {
|
||||
useShortcut('quickPreviewOpenNative', () => {
|
||||
if (!item || !openFilePaths || !openEphemeralFiles) return;
|
||||
|
||||
try {
|
||||
@@ -179,66 +166,6 @@ export const QuickPreview = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Reveal in native explorer
|
||||
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'y'], () => {
|
||||
if (!item || !revealItems) return;
|
||||
|
||||
try {
|
||||
const toReveal = [];
|
||||
if (item.type === 'Location') {
|
||||
toReveal.push({ Location: { id: item.item.id } });
|
||||
} else if (item.type === 'NonIndexedPath') {
|
||||
toReveal.push({ Ephemeral: { path: item.item.path } });
|
||||
} else {
|
||||
const filePath = getIndexedItemFilePath(item);
|
||||
if (!filePath) throw 'No file path found';
|
||||
toReveal.push({ FilePath: { id: filePath.id } });
|
||||
}
|
||||
|
||||
revealItems(library.uuid, toReveal);
|
||||
} catch (error) {
|
||||
toast.error({
|
||||
title: 'Failed to reveal',
|
||||
body: `Couldn't reveal file, due to an error: ${error}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Open delete dialog
|
||||
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'backspace'], () => {
|
||||
if (!item) return;
|
||||
|
||||
const path = getIndexedItemFilePath(item);
|
||||
|
||||
if (path != null && path.location_id !== null) {
|
||||
return dialogManager.create((dp) => (
|
||||
<DeleteDialog
|
||||
{...dp}
|
||||
indexedArgs={{
|
||||
locationId: path.location_id!,
|
||||
pathIds: [path.id]
|
||||
}}
|
||||
dirCount={path.is_dir ? 1 : 0}
|
||||
fileCount={path.is_dir ? 0 : 1}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
const ephemeralFile = getEphemeralPath(item);
|
||||
if (ephemeralFile != null) {
|
||||
return dialogManager.create((dp) => (
|
||||
<DeleteDialog
|
||||
{...dp}
|
||||
ephemeralArgs={{
|
||||
paths: [ephemeralFile.path]
|
||||
}}
|
||||
dirCount={ephemeralFile.is_dir ? 1 : 0}
|
||||
fileCount={ephemeralFile.is_dir ? 0 : 1}
|
||||
/>
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
const { kind, ...itemData } = getExplorerItemData(item);
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
type ReactNode
|
||||
} from 'react';
|
||||
import Selecto from 'react-selecto';
|
||||
import { useKey } from 'rooks';
|
||||
import { type ExplorerItem } from '@sd/client';
|
||||
import { useMouseNavigate, useOperatingSystem } from '~/hooks';
|
||||
import { useMouseNavigate, useOperatingSystem, useShortcut } from '~/hooks';
|
||||
|
||||
import { useExplorerContext } from '../Context';
|
||||
import { getQuickPreviewStore } from '../QuickPreview/store';
|
||||
@@ -84,7 +83,7 @@ const GridListItem = (props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full"
|
||||
className="w-full h-full"
|
||||
data-selectable=""
|
||||
data-selectable-index={props.index}
|
||||
data-selectable-id={itemId}
|
||||
@@ -244,32 +243,14 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
activeItem.current = null;
|
||||
}, [explorer.selectedItems]);
|
||||
|
||||
useKey(['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft', 'Escape'], (e) => {
|
||||
useShortcut('explorerEscape', () => {
|
||||
if (!explorerView.selectable) return;
|
||||
explorer.resetSelectedItems([]);
|
||||
selecto.current?.setSelectedTargets([]);
|
||||
});
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
explorer.resetSelectedItems([]);
|
||||
selecto.current?.setSelectedTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown' && explorer.selectedItems.size === 0) {
|
||||
const item = grid.getItem(0);
|
||||
if (!item?.data) return;
|
||||
|
||||
const id = uniqueId(item.data);
|
||||
|
||||
const selectedItemDom = document.querySelector(
|
||||
`[data-selectable-id="${realOS === 'windows' ? id.replaceAll('\\', '\\\\') : id}"]`
|
||||
);
|
||||
|
||||
if (selectedItemDom) {
|
||||
explorer.resetSelectedItems([item.data]);
|
||||
selecto.current?.setSelectedTargets([selectedItemDom as HTMLElement]);
|
||||
activeItem.current = item.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const keyboardHandler = (e: KeyboardEvent, newIndex: number) => {
|
||||
if (!explorerView.selectable) return;
|
||||
|
||||
if (explorer.selectedItems.size > 0) e.preventDefault();
|
||||
|
||||
@@ -283,24 +264,9 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
if (!gridItem) return;
|
||||
|
||||
const currentIndex = gridItem.index;
|
||||
let newIndex = currentIndex;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
newIndex -= grid.columnCount;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
newIndex += grid.columnCount;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
newIndex += 1;
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
newIndex -= 1;
|
||||
break;
|
||||
}
|
||||
|
||||
const newSelectedItem = grid.getItem(newIndex);
|
||||
let updatedIndex = currentIndex;
|
||||
updatedIndex = newIndex;
|
||||
const newSelectedItem = grid.getItem(updatedIndex);
|
||||
if (!newSelectedItem?.data) return;
|
||||
if (!explorer.allowMultiSelect) explorer.resetSelectedItems([newSelectedItem.data]);
|
||||
else {
|
||||
@@ -366,6 +332,76 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const getGridItemHandler = (key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight') => {
|
||||
const lastItem = activeItem.current;
|
||||
if (!lastItem) return;
|
||||
|
||||
const lastItemIndex = explorer.items?.findIndex((item) => item === lastItem);
|
||||
if (lastItemIndex === undefined || lastItemIndex === -1) return;
|
||||
|
||||
const gridItem = grid.getItem(lastItemIndex);
|
||||
if (!gridItem) return;
|
||||
|
||||
let newIndex = gridItem.index;
|
||||
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
newIndex -= grid.columnCount;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
newIndex += grid.columnCount;
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
newIndex -= 1;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
newIndex += 1;
|
||||
break;
|
||||
}
|
||||
return newIndex;
|
||||
};
|
||||
|
||||
useShortcut('explorerDown', (e) => {
|
||||
if (!explorerView.selectable) return;
|
||||
if (explorer.selectedItems.size === 0) {
|
||||
const item = grid.getItem(0);
|
||||
if (!item?.data) return;
|
||||
|
||||
const id = uniqueId(item.data);
|
||||
|
||||
const selectedItemDom = document.querySelector(
|
||||
`[data-selectable-id="${realOS === 'windows' ? id.replaceAll('\\', '\\\\') : id}"]`
|
||||
);
|
||||
|
||||
if (selectedItemDom) {
|
||||
explorer.resetSelectedItems([item.data]);
|
||||
selecto.current?.setSelectedTargets([selectedItemDom as HTMLElement]);
|
||||
activeItem.current = item.data;
|
||||
}
|
||||
} else {
|
||||
const newIndex = getGridItemHandler('ArrowDown');
|
||||
if (newIndex === undefined) return;
|
||||
keyboardHandler(e, newIndex);
|
||||
}
|
||||
});
|
||||
|
||||
useShortcut('explorerUp', (e) => {
|
||||
const newIndex = getGridItemHandler('ArrowUp');
|
||||
if (newIndex === undefined) return;
|
||||
keyboardHandler(e, newIndex);
|
||||
});
|
||||
|
||||
useShortcut('explorerLeft', (e) => {
|
||||
const newIndex = getGridItemHandler('ArrowLeft');
|
||||
if (newIndex === undefined) return;
|
||||
keyboardHandler(e, newIndex);
|
||||
});
|
||||
|
||||
useShortcut('explorerRight', (e) => {
|
||||
const newIndex = getGridItemHandler('ArrowRight');
|
||||
if (newIndex === undefined) return;
|
||||
keyboardHandler(e, newIndex);
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import BasicSticky from 'react-sticky-el';
|
||||
import { useKey, useMutationObserver, useWindowEventListener } from 'rooks';
|
||||
import { useMutationObserver, useWindowEventListener } from 'rooks';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { getItemFilePath, type ExplorerItem } from '@sd/client';
|
||||
import { ContextMenu, Tooltip } from '@sd/ui';
|
||||
import { useIsTextTruncated, useMouseNavigate } from '~/hooks';
|
||||
import { useIsTextTruncated, useMouseNavigate, useShortcut } from '~/hooks';
|
||||
import { isNonEmptyObject } from '~/util';
|
||||
|
||||
import { useLayoutContext } from '../../../Layout/Context';
|
||||
@@ -51,7 +51,7 @@ const ListViewItem = memo((props: ListViewItemProps) => {
|
||||
return (
|
||||
<ViewItem
|
||||
data={props.row.original}
|
||||
className="relative flex h-full items-center"
|
||||
className="relative flex items-center h-full"
|
||||
style={{ paddingLeft: props.paddingLeft, paddingRight: props.paddingRight }}
|
||||
>
|
||||
{props.row.getVisibleCells().map((cell) => (
|
||||
@@ -607,15 +607,14 @@ export default () => {
|
||||
};
|
||||
}, [sized, isLeftMouseDown]);
|
||||
|
||||
// Handle key selection
|
||||
useKey(['ArrowUp', 'ArrowDown', 'Escape'], (e) => {
|
||||
const keyboardHandler = (e: KeyboardEvent, direction: 'ArrowDown' | 'ArrowUp') => {
|
||||
if (!explorerView.selectable) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const range = getRangeByIndex(ranges.length - 1);
|
||||
|
||||
if (e.key === 'ArrowDown' && explorer.selectedItems.size === 0) {
|
||||
if (explorer.selectedItems.size === 0) {
|
||||
const item = rows[0]?.original;
|
||||
if (item) {
|
||||
explorer.addSelectedItem(item);
|
||||
@@ -626,13 +625,7 @@ export default () => {
|
||||
|
||||
if (!range) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
explorer.resetSelectedItems([]);
|
||||
setRanges([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyDirection = e.key === 'ArrowDown' ? 'down' : 'up';
|
||||
const keyDirection = direction === 'ArrowDown' ? 'down' : 'up';
|
||||
|
||||
const nextRow = rows[range.end.index + (keyDirection === 'up' ? -1 : 1)];
|
||||
|
||||
@@ -766,6 +759,20 @@ export default () => {
|
||||
} else explorer.resetSelectedItems([item]);
|
||||
|
||||
scrollToRow(nextRow);
|
||||
};
|
||||
|
||||
useShortcut('explorerEscape', () => {
|
||||
explorer.resetSelectedItems([]);
|
||||
setRanges([]);
|
||||
return;
|
||||
});
|
||||
|
||||
useShortcut('explorerUp', (e) => {
|
||||
keyboardHandler(e, 'ArrowUp');
|
||||
});
|
||||
|
||||
useShortcut('explorerDown', (e) => {
|
||||
keyboardHandler(e, 'ArrowDown');
|
||||
});
|
||||
|
||||
// Reset resizing cursor
|
||||
@@ -1002,7 +1009,7 @@ export default () => {
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className="absolute left-0 top-0 min-w-full"
|
||||
className="absolute top-0 left-0 min-w-full"
|
||||
style={{
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${
|
||||
@@ -1033,7 +1040,7 @@ export default () => {
|
||||
}}
|
||||
>
|
||||
{selectedPrior && (
|
||||
<div className="absolute inset-x-3 top-0 h-px bg-accent/10" />
|
||||
<div className="absolute top-0 h-px inset-x-3 bg-accent/10" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
type ReactNode
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useKey, useKeys } from 'rooks';
|
||||
import { useKeys } from 'rooks';
|
||||
import { ExplorerLayout, getItemObject, type Object } from '@sd/client';
|
||||
import { dialogManager, ModifierKeys } from '@sd/ui';
|
||||
import { Loader } from '~/components';
|
||||
import { useKeyCopyCutPaste, useKeyMatcher, useOperatingSystem } from '~/hooks';
|
||||
import { useKeyCopyCutPaste, useOperatingSystem, useShortcut } from '~/hooks';
|
||||
import { isNonEmpty } from '~/util';
|
||||
|
||||
import CreateDialog from '../../settings/library/tags/CreateDialog';
|
||||
@@ -61,13 +61,10 @@ export default memo(
|
||||
const explorer = useExplorerContext();
|
||||
const quickPreview = useQuickPreviewContext();
|
||||
const quickPreviewStore = useQuickPreviewStore();
|
||||
const os = useOperatingSystem();
|
||||
const { doubleClick } = useViewItemDoubleClick();
|
||||
|
||||
const { layoutMode } = explorer.useSettingsSnapshot();
|
||||
|
||||
const metaCtrlKey = useKeyMatcher('Meta').key;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
@@ -103,16 +100,10 @@ export default memo(
|
||||
explorer.settingsStore.layoutMode = layout ?? 'grid';
|
||||
}, [layoutMode, explorer.layouts, explorer.settingsStore]);
|
||||
|
||||
useKey(['Enter'], (e) => {
|
||||
useShortcut('openObject', (e) => {
|
||||
e.stopPropagation();
|
||||
if (os === 'windows' && !isRenaming) {
|
||||
doubleClick();
|
||||
}
|
||||
});
|
||||
|
||||
useKeys([metaCtrlKey, 'KeyO'], (e) => {
|
||||
e.stopPropagation();
|
||||
if (os === 'windows') return;
|
||||
e.preventDefault();
|
||||
if (quickPreviewStore.open || isRenaming) return;
|
||||
doubleClick();
|
||||
});
|
||||
|
||||
@@ -233,26 +224,13 @@ const useKeyDownHandlers = ({ disabled }: { disabled: boolean }) => {
|
||||
[os, explorer.selectedItems]
|
||||
);
|
||||
|
||||
const handleExplorerShortcut = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key.toUpperCase() !== 'I' ||
|
||||
!event.getModifierState(os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control)
|
||||
)
|
||||
return;
|
||||
|
||||
getExplorerStore().showInspector = !getExplorerStore().showInspector;
|
||||
},
|
||||
[os]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handlers = [handleNewTag, handleExplorerShortcut];
|
||||
const handlers = [handleNewTag];
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.repeat || disabled) return;
|
||||
for (const handler of handlers) handler(event);
|
||||
};
|
||||
document.body.addEventListener('keydown', handler);
|
||||
return () => document.body.removeEventListener('keydown', handler);
|
||||
}, [disabled, handleNewTag, handleExplorerShortcut]);
|
||||
}, [disabled, handleNewTag]);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import { useKeys } from 'rooks';
|
||||
import { getExplorerLayoutStore, useExplorerLayoutStore, useLibrarySubscription } from '@sd/client';
|
||||
import { useKeysMatcher, useOperatingSystem } from '~/hooks';
|
||||
import { useShortcut } from '~/hooks';
|
||||
|
||||
import { TOP_BAR_HEIGHT } from '../TopBar';
|
||||
import { useExplorerContext } from './Context';
|
||||
@@ -10,7 +9,7 @@ import ContextMenu from './ContextMenu';
|
||||
import DismissibleNotice from './DismissibleNotice';
|
||||
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
||||
import ExplorerContextMenu from './ParentContextMenu';
|
||||
import { useExplorerStore } from './store';
|
||||
import { getExplorerStore, useExplorerStore } from './store';
|
||||
import { useKeyRevealFinder } from './useKeyRevealFinder';
|
||||
import View, { EmptyNotice, ExplorerViewProps } from './View';
|
||||
import { ExplorerPath, PATH_BAR_HEIGHT } from './View/ExplorerPath';
|
||||
@@ -28,10 +27,6 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorer = useExplorerContext();
|
||||
const layoutStore = useExplorerLayoutStore();
|
||||
const shortcuts = useKeysMatcher(['Meta', 'Shift', 'Alt']);
|
||||
const os = useOperatingSystem();
|
||||
const hiddenFilesShortcut =
|
||||
os === 'macOS' ? [shortcuts.Meta.key, 'Shift', '.'] : [shortcuts.Meta.key, 'KeyH'];
|
||||
|
||||
const showPathBar = explorer.showPathBar && layoutStore.showPathBar;
|
||||
|
||||
@@ -48,12 +43,17 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
}
|
||||
});
|
||||
|
||||
useKeys([shortcuts.Alt.key, shortcuts.Meta.key, 'KeyP'], (e) => {
|
||||
useShortcut('showPathBar', (e) => {
|
||||
e.stopPropagation();
|
||||
getExplorerLayoutStore().showPathBar = !layoutStore.showPathBar;
|
||||
});
|
||||
|
||||
useKeys(hiddenFilesShortcut, (e) => {
|
||||
useShortcut('showInspector', (e) => {
|
||||
e.stopPropagation();
|
||||
getExplorerStore().showInspector = !explorerStore.showInspector;
|
||||
});
|
||||
|
||||
useShortcut('showHiddenFiles', (e) => {
|
||||
e.stopPropagation();
|
||||
explorer.settingsStore.showHiddenFiles = !explorer.settingsStore.showHiddenFiles;
|
||||
});
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useKeys } from 'rooks';
|
||||
import { useLibraryContext } from '@sd/client';
|
||||
import { useExplorerContext } from '~/app/$libraryId/Explorer/Context';
|
||||
import { useKeysMatcher } from '~/hooks';
|
||||
import { useShortcut } from '~/hooks';
|
||||
import { usePlatform, type Platform } from '~/util/Platform';
|
||||
|
||||
export const useKeyRevealFinder = () => {
|
||||
const explorer = useExplorerContext();
|
||||
const { revealItems } = usePlatform();
|
||||
const shortcuts = useKeysMatcher(['Meta']);
|
||||
const { library } = useLibraryContext();
|
||||
|
||||
const items = useMemo(() => {
|
||||
@@ -55,7 +53,7 @@ export const useKeyRevealFinder = () => {
|
||||
return array;
|
||||
}, [explorer.selectedItems]);
|
||||
|
||||
useKeys([shortcuts.Meta.key, 'KeyY'], (e) => {
|
||||
useShortcut('revealNative', (e) => {
|
||||
e.stopPropagation();
|
||||
if (!revealItems) return;
|
||||
revealItems(library.uuid, items);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ArrowsClockwise, Planet } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useKeys } from 'rooks';
|
||||
import { LibraryContextProvider, useClientContext, useFeatureFlag } from '@sd/client';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useKeysMatcher } from '~/hooks';
|
||||
import { useKeysMatcher, useShortcut } from '~/hooks';
|
||||
|
||||
import { EphemeralSection } from './EphemeralSection';
|
||||
import Icon from './Icon';
|
||||
@@ -13,9 +12,9 @@ import SidebarLink from './Link';
|
||||
export default () => {
|
||||
const { library } = useClientContext();
|
||||
const navigate = useNavigate();
|
||||
const shortcuts = useKeysMatcher(['Meta', 'Shift']);
|
||||
const symbols = useKeysMatcher(['Meta', 'Shift']);
|
||||
|
||||
useKeys([shortcuts.Meta.key, 'Shift', 'KeyO'], (e) => {
|
||||
useShortcut('navToOverview', (e) => {
|
||||
e.stopPropagation();
|
||||
navigate('overview');
|
||||
});
|
||||
@@ -26,7 +25,7 @@ export default () => {
|
||||
<Tooltip
|
||||
position="right"
|
||||
label="Overview"
|
||||
keybinds={[shortcuts.Shift.icon, shortcuts.Meta.icon, 'O']}
|
||||
keybinds={[symbols.Shift.icon, symbols.Meta.icon, 'O']}
|
||||
>
|
||||
<SidebarLink to="overview">
|
||||
<Icon component={Planet} />
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Gear } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useKeys } from 'rooks';
|
||||
import { JobManagerContextProvider, useClientContext, useDebugState } from '@sd/client';
|
||||
import { Button, ButtonLink, Popover, Tooltip, usePopover } from '@sd/ui';
|
||||
import { useKeysMatcher } from '~/hooks';
|
||||
import { useKeysMatcher, useShortcut } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import DebugPopover from './DebugPopover';
|
||||
@@ -14,9 +13,9 @@ export default () => {
|
||||
const { library } = useClientContext();
|
||||
const debugState = useDebugState();
|
||||
const navigate = useNavigate();
|
||||
const shortcuts = useKeysMatcher(['Meta', 'Shift']);
|
||||
const symbols = useKeysMatcher(['Meta', 'Shift']);
|
||||
|
||||
useKeys([shortcuts.Meta.key, 'Shift', 'KeyT'], (e) => {
|
||||
useShortcut('navToSettings', (e) => {
|
||||
e.stopPropagation();
|
||||
navigate('settings/client/general');
|
||||
});
|
||||
@@ -50,7 +49,7 @@ export default () => {
|
||||
<Tooltip
|
||||
position="top"
|
||||
label="Settings"
|
||||
keybinds={[shortcuts.Shift.icon, shortcuts.Meta.icon, 'T']}
|
||||
keybinds={[symbols.Shift.icon, symbols.Meta.icon, 'T']}
|
||||
>
|
||||
<Gear className="h-5 w-5" />
|
||||
</Tooltip>
|
||||
@@ -58,7 +57,7 @@ export default () => {
|
||||
<JobManagerContextProvider>
|
||||
<Popover
|
||||
popover={usePopover()}
|
||||
keybind={[shortcuts.Meta.key, 'j']}
|
||||
keybind={[symbols.Meta.key, 'j']}
|
||||
trigger={
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -70,7 +69,7 @@ export default () => {
|
||||
<Tooltip
|
||||
label="Recent Jobs"
|
||||
position="top"
|
||||
keybinds={[shortcuts.Meta.icon, 'J']}
|
||||
keybinds={[symbols.Meta.icon, 'J']}
|
||||
>
|
||||
<IsRunningJob />
|
||||
</Tooltip>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ArrowLeft, ArrowRight } from '@phosphor-icons/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useKeybind, useKeyMatcher, useOperatingSystem, useSearchStore } from '~/hooks';
|
||||
import { useKeyMatcher, useOperatingSystem, useSearchStore, useShortcut } from '~/hooks';
|
||||
|
||||
import TopBarButton from './TopBarButton';
|
||||
|
||||
@@ -11,13 +11,13 @@ export const NavigationButtons = () => {
|
||||
const { isFocused } = useSearchStore();
|
||||
const idx = history.state.idx as number;
|
||||
const os = useOperatingSystem();
|
||||
const { icon, key } = useKeyMatcher('Meta');
|
||||
const { icon } = useKeyMatcher('Meta');
|
||||
|
||||
useKeybind([key, '['], () => {
|
||||
useShortcut('navBackwardHistory', () => {
|
||||
if (idx === 0 || isFocused) return;
|
||||
navigate(-1);
|
||||
});
|
||||
useKeybind([key, ']'], () => {
|
||||
useShortcut('navForwardHistory', () => {
|
||||
if (idx === history.length - 1 || isFocused) return;
|
||||
navigate(1);
|
||||
});
|
||||
|
||||
@@ -186,6 +186,50 @@ const shortcutCategories: Record<string, Shortcut[]> = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Copy selected item(s)',
|
||||
keys: {
|
||||
macOS: {
|
||||
value: [modifierSymbols.Meta.macOS, 'C']
|
||||
},
|
||||
all: {
|
||||
value: [modifierSymbols.Control.Other, 'C']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Cut selected item(s)',
|
||||
keys: {
|
||||
macOS: {
|
||||
value: [modifierSymbols.Meta.macOS, 'X']
|
||||
},
|
||||
all: {
|
||||
value: [modifierSymbols.Control.Other, 'X']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Paste selected item(s)',
|
||||
keys: {
|
||||
macOS: {
|
||||
value: [modifierSymbols.Meta.macOS, 'V']
|
||||
},
|
||||
all: {
|
||||
value: [modifierSymbols.Control.Other, 'V']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Duplicate selected item(s)',
|
||||
keys: {
|
||||
macOS: {
|
||||
value: [modifierSymbols.Meta.macOS, 'D']
|
||||
},
|
||||
all: {
|
||||
value: [modifierSymbols.Control.Other, 'D']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Reveal in Explorer/Finder',
|
||||
keys: {
|
||||
@@ -197,6 +241,17 @@ const shortcutCategories: Record<string, Shortcut[]> = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Rescan',
|
||||
keys: {
|
||||
macOS: {
|
||||
value: [modifierSymbols.Meta.macOS, 'R']
|
||||
},
|
||||
all: {
|
||||
value: [modifierSymbols.Control.Other, 'R']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'Rename file or folder',
|
||||
keys: {
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from './useKeybind';
|
||||
export * from './useOperatingSystem';
|
||||
export * from './useScrolled';
|
||||
export * from './useSearchStore';
|
||||
export * from './useShortcut';
|
||||
export * from './useShowControls';
|
||||
export * from './useSpacedropState';
|
||||
export * from './useTheme';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useKeys } from 'rooks';
|
||||
import { useItemsAsEphemeralPaths, useItemsAsFilePaths, useLibraryMutation } from '@sd/client';
|
||||
import { toast } from '@sd/ui';
|
||||
import { useExplorerContext } from '~/app/$libraryId/Explorer/Context';
|
||||
@@ -6,13 +5,12 @@ import { getExplorerStore, useExplorerStore } from '~/app/$libraryId/Explorer/st
|
||||
import { useExplorerSearchParams } from '~/app/$libraryId/Explorer/util';
|
||||
import { isNonEmpty } from '~/util';
|
||||
|
||||
import { useKeyMatcher } from './useKeyMatcher';
|
||||
import { useShortcut } from './useShortcut';
|
||||
|
||||
export const useKeyCopyCutPaste = () => {
|
||||
const { cutCopyState } = useExplorerStore();
|
||||
const [{ path }] = useExplorerSearchParams();
|
||||
|
||||
const metaCtrlKey = useKeyMatcher('Meta').key;
|
||||
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||
const copyEphemeralFiles = useLibraryMutation('ephemeralFiles.copyFiles');
|
||||
const cutFiles = useLibraryMutation('files.cutFiles');
|
||||
@@ -25,7 +23,7 @@ export const useKeyCopyCutPaste = () => {
|
||||
const selectedEphemeralPaths = useItemsAsEphemeralPaths(Array.from(explorer.selectedItems));
|
||||
|
||||
const indexedArgs =
|
||||
parent?.type === 'Location' && !isNonEmpty(selectedFilePaths)
|
||||
parent?.type === 'Location'
|
||||
? {
|
||||
sourceLocationId: parent.location.id,
|
||||
sourcePathIds: selectedFilePaths.map((p) => p.id)
|
||||
@@ -33,35 +31,54 @@ export const useKeyCopyCutPaste = () => {
|
||||
: undefined;
|
||||
|
||||
const ephemeralArgs =
|
||||
parent?.type === 'Ephemeral' && !isNonEmpty(selectedEphemeralPaths)
|
||||
parent?.type === 'Ephemeral'
|
||||
? { sourcePaths: selectedEphemeralPaths.map((p) => p.path) }
|
||||
: undefined;
|
||||
|
||||
useKeys([metaCtrlKey, 'KeyC'], (e) => {
|
||||
useShortcut('copyObject', (e) => {
|
||||
e.stopPropagation();
|
||||
if (explorer.parent?.type === 'Location') {
|
||||
getExplorerStore().cutCopyState = {
|
||||
sourceParentPath: path ?? '/',
|
||||
type: 'Copy',
|
||||
indexedArgs,
|
||||
ephemeralArgs,
|
||||
type: 'Copy'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useKeys([metaCtrlKey, 'KeyX'], (e) => {
|
||||
useShortcut('cutObject', (e) => {
|
||||
e.stopPropagation();
|
||||
if (explorer.parent?.type === 'Location') {
|
||||
getExplorerStore().cutCopyState = {
|
||||
sourceParentPath: path ?? '/',
|
||||
type: 'Cut',
|
||||
indexedArgs,
|
||||
ephemeralArgs,
|
||||
type: 'Cut'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useKeys([metaCtrlKey, 'KeyV'], async (e) => {
|
||||
useShortcut('duplicateObject', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (parent?.type === 'Location') {
|
||||
try {
|
||||
await copyFiles.mutateAsync({
|
||||
source_location_id: parent.location.id,
|
||||
sources_file_path_ids: selectedFilePaths.map((p) => p.id),
|
||||
target_location_id: parent.location.id,
|
||||
target_location_relative_directory_path: path ?? '/'
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error({
|
||||
title: 'Failed to duplicate file',
|
||||
body: `Error: ${error}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useShortcut('pasteObject', async (e) => {
|
||||
e.stopPropagation();
|
||||
const parent = explorer.parent;
|
||||
if (
|
||||
|
||||
210
interface/hooks/useShortcut.ts
Normal file
210
interface/hooks/useShortcut.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useKeys } from 'rooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { valtioPersist } from '@sd/client';
|
||||
|
||||
import { OperatingSystem } from '../util/Platform';
|
||||
import { useOperatingSystem } from './useOperatingSystem';
|
||||
|
||||
const state = {
|
||||
gridView: {
|
||||
keys: {
|
||||
macOS: ['Meta', '1'],
|
||||
all: ['Control', '1']
|
||||
}
|
||||
},
|
||||
listView: {
|
||||
keys: {
|
||||
macOS: ['Meta', '2'],
|
||||
all: ['Control', '2']
|
||||
}
|
||||
},
|
||||
mediaView: {
|
||||
keys: {
|
||||
macOS: ['Meta', '3'],
|
||||
all: ['Control', '3']
|
||||
}
|
||||
},
|
||||
showHiddenFiles: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'Shift', '.'],
|
||||
all: ['Control', 'Shift', '.']
|
||||
}
|
||||
},
|
||||
showPathBar: {
|
||||
keys: {
|
||||
macOS: ['Alt', 'Meta', 'KeyP'],
|
||||
all: ['Alt', 'Control', 'KeyP']
|
||||
}
|
||||
},
|
||||
showInspector: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyI'],
|
||||
all: ['Control', 'KeyI']
|
||||
}
|
||||
},
|
||||
toggleJobManager: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyJ'],
|
||||
all: ['Control', 'KeyJ']
|
||||
}
|
||||
},
|
||||
toggleQuickPreview: {
|
||||
keys: {
|
||||
all: ['space']
|
||||
}
|
||||
},
|
||||
toggleMetaData: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyI'],
|
||||
all: ['Control', 'KeyI']
|
||||
}
|
||||
},
|
||||
quickPreviewMoveBetweenItems: {
|
||||
keys: {
|
||||
all: ['ArrowLeft', 'ArrowRight']
|
||||
}
|
||||
},
|
||||
revealNative: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyY'],
|
||||
all: ['Control', 'KeyY']
|
||||
}
|
||||
},
|
||||
renameObject: {
|
||||
keys: {
|
||||
macOS: ['Enter'],
|
||||
all: ['F2']
|
||||
}
|
||||
},
|
||||
rescan: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyR'],
|
||||
all: ['Control', 'KeyR']
|
||||
}
|
||||
},
|
||||
cutObject: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyX'],
|
||||
all: ['Control', 'KeyX']
|
||||
}
|
||||
},
|
||||
copyObject: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyC'],
|
||||
all: ['Control', 'KeyC']
|
||||
}
|
||||
},
|
||||
pasteObject: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyV'],
|
||||
all: ['Control', 'KeyV']
|
||||
}
|
||||
},
|
||||
duplicateObject: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyD'],
|
||||
all: ['Control', 'KeyD']
|
||||
}
|
||||
},
|
||||
openObject: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyO'],
|
||||
all: ['Enter']
|
||||
}
|
||||
},
|
||||
quickPreviewOpenNative: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'KeyO'],
|
||||
all: ['Enter']
|
||||
}
|
||||
},
|
||||
delItem: {
|
||||
keys: {
|
||||
macOS: ['Meta', 'Backspace'],
|
||||
all: ['Delete']
|
||||
}
|
||||
},
|
||||
explorerEscape: {
|
||||
keys: {
|
||||
all: ['Escape']
|
||||
}
|
||||
},
|
||||
explorerDown: {
|
||||
keys: {
|
||||
all: ['ArrowDown']
|
||||
}
|
||||
},
|
||||
explorerUp: {
|
||||
keys: {
|
||||
all: ['ArrowUp']
|
||||
}
|
||||
},
|
||||
explorerLeft: {
|
||||
keys: {
|
||||
all: ['ArrowLeft']
|
||||
}
|
||||
},
|
||||
explorerRight: {
|
||||
keys: {
|
||||
all: ['ArrowRight']
|
||||
}
|
||||
},
|
||||
navBackwardHistory: {
|
||||
keys: {
|
||||
macOS: ['Meta', '['],
|
||||
all: ['Control', '[']
|
||||
}
|
||||
},
|
||||
navForwardHistory: {
|
||||
keys: {
|
||||
macOS: ['Meta', ']'],
|
||||
all: ['Control', ']']
|
||||
}
|
||||
},
|
||||
navToSettings: {
|
||||
keys: {
|
||||
macOS: ['Shift', 'Meta', 'KeyT'],
|
||||
all: ['Shift', 'Control', 'KeyT']
|
||||
}
|
||||
},
|
||||
navToOverview: {
|
||||
keys: {
|
||||
macOS: ['Shift', 'Meta', 'KeyO'],
|
||||
all: ['Shift', 'Control', 'KeyO']
|
||||
}
|
||||
},
|
||||
navExpObjects: {
|
||||
keys: {
|
||||
all: ['Control', 'ArrowRight']
|
||||
}
|
||||
}
|
||||
} satisfies Record<
|
||||
string,
|
||||
{
|
||||
keys: {
|
||||
[os in OperatingSystem | 'all']?: string[];
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const shortcutsStore = valtioPersist('sd-shortcuts', state);
|
||||
|
||||
export function useShortcutsStore() {
|
||||
return useSnapshot(shortcutsStore);
|
||||
}
|
||||
|
||||
export function getShortcutsStore() {
|
||||
return shortcutsStore;
|
||||
}
|
||||
|
||||
type shortcutKeys = keyof typeof state;
|
||||
type osKeys = keyof (typeof state)[shortcutKeys]['keys'];
|
||||
|
||||
export const useShortcut = (shortcut: shortcutKeys, func: (e: KeyboardEvent) => void) => {
|
||||
const os = useOperatingSystem();
|
||||
const shortcutsStore = getShortcutsStore();
|
||||
const shortcutKeys =
|
||||
shortcutsStore[shortcut].keys[os as osKeys] || shortcutsStore[shortcut].keys.all;
|
||||
|
||||
useKeys(shortcutKeys, func);
|
||||
};
|
||||
Reference in New Issue
Block a user