[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:
ameer2468
2023-11-06 13:37:24 +03:00
committed by GitHub
parent a3f2ca10b6
commit f4e89da4d7
17 changed files with 449 additions and 222 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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;
}}
/>

View File

@@ -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);

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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]);
};

View File

@@ -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;
});

View File

@@ -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);

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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);
});

View File

@@ -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: {

View File

@@ -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';

View File

@@ -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 (

View 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);
};