feat(ui): misc perf/rerender improvements

More efficient selectors, memoized/stable references to objects, lazy popover/menu rendering.
This commit is contained in:
psychedelicious
2023-12-30 23:55:39 +11:00
committed by Kent Keirsey
parent 2ba505cce9
commit 539887b215
8 changed files with 345 additions and 332 deletions

View File

@ -6,9 +6,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion'; import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { KeyboardEvent, ReactNode } from 'react'; import type { KeyboardEvent, PropsWithChildren } from 'react';
import { memo, useCallback, useEffect, useState } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import type { FileRejection } from 'react-dropzone'; import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUploadImageMutation } from 'services/api/endpoints/images'; import { useUploadImageMutation } from 'services/api/endpoints/images';
@ -16,6 +16,13 @@ import type { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay'; import ImageUploadOverlay from './ImageUploadOverlay';
const accept: Accept = {
'image/png': ['.png'],
'image/jpeg': ['.jpg', '.jpeg', '.png'],
};
const dropzoneRootProps = { style: {} };
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector], [stateSelector, activeTabNameSelector],
({ gallery }, activeTabName) => { ({ gallery }, activeTabName) => {
@ -38,12 +45,7 @@ const selector = createMemoizedSelector(
} }
); );
type ImageUploaderProps = { const ImageUploader = (props: PropsWithChildren) => {
children: ReactNode;
};
const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
const { autoAddBoardId, postUploadAction } = useAppSelector(selector); const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
@ -111,7 +113,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragActive, isDragActive,
inputRef, inputRef,
} = useDropzone({ } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, accept,
noClick: true, noClick: true,
onDrop, onDrop,
onDragOver, onDragOver,
@ -149,9 +151,9 @@ const ImageUploader = (props: ImageUploaderProps) => {
}, []); }, []);
return ( return (
<Box {...getRootProps({ style: {} })} onKeyDown={handleKeyDown}> <Box {...getRootProps(dropzoneRootProps)} onKeyDown={handleKeyDown}>
<input {...getInputProps()} /> <input {...getInputProps()} />
{children} {props.children}
<AnimatePresence> <AnimatePresence>
{isDragActive && isHandlingUpload && ( {isDragActive && isHandlingUpload && (
<motion.div <motion.div

View File

@ -1,6 +1,4 @@
import { Flex, FormLabel, forwardRef } from '@chakra-ui/react'; import { Flex, FormLabel, forwardRef } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup'; import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup';
@ -8,18 +6,16 @@ import { memo, useContext } from 'react';
import type { InvLabelProps } from './types'; import type { InvLabelProps } from './types';
const selector = createSelector(
stateSelector,
({ system }) => system.shouldEnableInformationalPopovers
);
export const InvLabel = memo( export const InvLabel = memo(
forwardRef<InvLabelProps, typeof FormLabel>( forwardRef<InvLabelProps, typeof FormLabel>(
( (
{ feature, renderInfoPopoverInPortal, children, ...rest }: InvLabelProps, { feature, renderInfoPopoverInPortal, children, ...rest }: InvLabelProps,
ref ref
) => { ) => {
const shouldEnableInformationalPopovers = useAppSelector(selector); const shouldEnableInformationalPopovers = useAppSelector(
(state) => state.system.shouldEnableInformationalPopovers
);
const ctx = useContext(InvControlGroupContext); const ctx = useContext(InvControlGroupContext);
if (feature && shouldEnableInformationalPopovers) { if (feature && shouldEnableInformationalPopovers) {
return ( return (

View File

@ -1,30 +1,23 @@
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import roundToHundreth from 'features/canvas/util/roundToHundreth'; import roundToHundreth from 'features/canvas/util/roundToHundreth';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const cursorPositionSelector = createMemoizedSelector( const cursorPositionSelector = createSelector([stateSelector], ({ canvas }) => {
[stateSelector], const { cursorPosition } = canvas;
({ canvas }) => {
const { cursorPosition } = canvas;
const { cursorX, cursorY } = cursorPosition const { cursorX, cursorY } = cursorPosition
? { cursorX: cursorPosition.x, cursorY: cursorPosition.y } ? { cursorX: cursorPosition.x, cursorY: cursorPosition.y }
: { cursorX: -1, cursorY: -1 }; : { cursorX: -1, cursorY: -1 };
return { return `(${roundToHundreth(cursorX)}, ${roundToHundreth(cursorY)})`;
cursorCoordinatesString: `(${roundToHundreth(cursorX)}, ${roundToHundreth( });
cursorY
)})`,
};
}
);
const IAICanvasStatusTextCursorPos = () => { const IAICanvasStatusTextCursorPos = () => {
const { cursorCoordinatesString } = useAppSelector(cursorPositionSelector); const cursorCoordinatesString = useAppSelector(cursorPositionSelector);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

View File

@ -68,7 +68,7 @@ const GallerySettingsPopover = () => {
); );
return ( return (
<InvPopover> <InvPopover isLazy>
<InvPopoverTrigger> <InvPopoverTrigger>
<InvIconButton <InvIconButton
tooltip={t('gallery.gallerySettings')} tooltip={t('gallery.gallerySettings')}

View File

@ -7,13 +7,9 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
import type { import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
InvocationNode,
InvocationTemplate,
} from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation';
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { memo } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import EditableNodeTitle from './details/EditableNodeTitle'; import EditableNodeTitle from './details/EditableNodeTitle';
@ -30,38 +26,47 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
? nodes.nodeTemplates[lastSelectedNode.data.type] ? nodes.nodeTemplates[lastSelectedNode.data.type]
: undefined; : undefined;
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
return;
}
return { return {
node: lastSelectedNode, nodeId: lastSelectedNode.data.id,
template: lastSelectedNodeTemplate, nodeVersion: lastSelectedNode.data.version,
templateTitle: lastSelectedNodeTemplate.title,
}; };
}); });
const InspectorDetailsTab = () => { const InspectorDetailsTab = () => {
const { node, template } = useAppSelector(selector); const data = useAppSelector(selector);
const { t } = useTranslation(); const { t } = useTranslation();
if (!template || !isInvocationNode(node)) { if (!data) {
return ( return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} /> <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
); );
} }
return <Content node={node} template={template} />; return (
<Content
nodeId={data.nodeId}
nodeVersion={data.nodeVersion}
templateTitle={data.templateTitle}
/>
);
}; };
export default memo(InspectorDetailsTab); export default memo(InspectorDetailsTab);
type ContentProps = { type ContentProps = {
node: InvocationNode; nodeId: string;
template: InvocationTemplate; nodeVersion: string;
templateTitle: string;
}; };
const Content = memo(({ node, template }: ContentProps) => { const Content = memo((props: ContentProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const needsUpdate = useMemo( const needsUpdate = useNodeNeedsUpdate(props.nodeId);
() => getNeedsUpdate(node, template),
[node, template]
);
return ( return (
<Box position="relative" w="full" h="full"> <Box position="relative" w="full" h="full">
<ScrollableContent> <ScrollableContent>
@ -73,20 +78,20 @@ const Content = memo(({ node, template }: ContentProps) => {
p={1} p={1}
gap={2} gap={2}
> >
<EditableNodeTitle nodeId={node.data.id} /> <EditableNodeTitle nodeId={props.nodeId} />
<HStack> <HStack>
<InvControl label={t('nodes.nodeType')}> <InvControl label={t('nodes.nodeType')}>
<InvText fontSize="sm" fontWeight="semibold"> <InvText fontSize="sm" fontWeight="semibold">
{template.title} {props.templateTitle}
</InvText> </InvText>
</InvControl> </InvControl>
<InvControl label={t('nodes.nodeVersion')} isInvalid={needsUpdate}> <InvControl label={t('nodes.nodeVersion')} isInvalid={needsUpdate}>
<InvText fontSize="sm" fontWeight="semibold"> <InvText fontSize="sm" fontWeight="semibold">
{node.data.version} {props.nodeVersion}
</InvText> </InvText>
</InvControl> </InvControl>
</HStack> </HStack>
<NotesTextarea nodeId={node.data.id} /> <NotesTextarea nodeId={props.nodeId} />
</Flex> </Flex>
</ScrollableContent> </ScrollableContent>
</Box> </Box>

View File

@ -28,24 +28,31 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
const nes = const nes =
nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__'];
if (
!isInvocationNode(lastSelectedNode) ||
!nes ||
!lastSelectedNodeTemplate
) {
return;
}
return { return {
node: lastSelectedNode, outputs: nes.outputs,
template: lastSelectedNodeTemplate, outputType: lastSelectedNodeTemplate.outputType,
nes,
}; };
}); });
const InspectorOutputsTab = () => { const InspectorOutputsTab = () => {
const { node, template, nes } = useAppSelector(selector); const data = useAppSelector(selector);
const { t } = useTranslation(); const { t } = useTranslation();
if (!node || !nes || !isInvocationNode(node)) { if (!data) {
return ( return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} /> <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
); );
} }
if (nes.outputs.length === 0) { if (data.outputs.length === 0) {
return ( return (
<IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} /> <IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} />
); );
@ -63,15 +70,15 @@ const InspectorOutputsTab = () => {
h="full" h="full"
w="full" w="full"
> >
{template?.outputType === 'image_output' ? ( {data.outputType === 'image_output' ? (
nes.outputs.map((result, i) => ( data.outputs.map((result, i) => (
<ImageOutputPreview <ImageOutputPreview
key={getKey(result, i)} key={getKey(result, i)}
output={result as ImageOutput} output={result as ImageOutput}
/> />
)) ))
) : ( ) : (
<DataViewer data={nes.outputs} label={t('nodes.nodeOutputs')} /> <DataViewer data={data.outputs} label={t('nodes.nodeOutputs')} />
)} )}
</Flex> </Flex>
</ScrollableContent> </ScrollableContent>

View File

@ -11,20 +11,15 @@ export const useNodeNeedsUpdate = (nodeId: string) => {
createMemoizedSelector(stateSelector, ({ nodes }) => { createMemoizedSelector(stateSelector, ({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId); const node = nodes.nodes.find((node) => node.id === nodeId);
const template = nodes.nodeTemplates[node?.data.type ?? '']; const template = nodes.nodeTemplates[node?.data.type ?? ''];
return { node, template }; if (isInvocationNode(node) && template) {
return getNeedsUpdate(node, template);
}
return false;
}), }),
[nodeId] [nodeId]
); );
const { node, template } = useAppSelector(selector); const needsUpdate = useAppSelector(selector);
const needsUpdate = useMemo(
() =>
isInvocationNode(node) && template
? getNeedsUpdate(node, template)
: false,
[node, template]
);
return needsUpdate; return needsUpdate;
}; };

View File

@ -16,7 +16,7 @@ import {
InvModalOverlay, InvModalOverlay,
} from 'common/components/InvModal/wrapper'; } from 'common/components/InvModal/wrapper';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { cloneElement, memo } from 'react'; import { cloneElement, memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import HotkeysModalItem from './HotkeysModalItem'; import HotkeysModalItem from './HotkeysModalItem';
@ -32,6 +32,21 @@ type HotkeyList = {
hotkey: string; hotkey: string;
}; };
const renderHotkeyModalItems = (hotkeys: HotkeyList[]) => (
<Flex flexDir="column" gap={4}>
{hotkeys.map((hotkey, i) => (
<Flex flexDir="column" px={2} gap={4} key={i}>
<HotkeysModalItem
title={hotkey.title}
description={hotkey.desc}
hotkey={hotkey.hotkey}
/>
{i < hotkeys.length - 1 && <Divider />}
</Flex>
))}
</Flex>
);
const HotkeysModal = ({ children }: HotkeysModalProps) => { const HotkeysModal = ({ children }: HotkeysModalProps) => {
const { const {
isOpen: isHotkeyModalOpen, isOpen: isHotkeyModalOpen,
@ -41,264 +56,264 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const appHotkeys = [ const appHotkeys = useMemo(
{ () => [
title: t('hotkeys.invoke.title'), {
desc: t('hotkeys.invoke.desc'), title: t('hotkeys.invoke.title'),
hotkey: 'Ctrl+Enter', desc: t('hotkeys.invoke.desc'),
}, hotkey: 'Ctrl+Enter',
{ },
title: t('hotkeys.cancel.title'), {
desc: t('hotkeys.cancel.desc'), title: t('hotkeys.cancel.title'),
hotkey: 'Shift+X', desc: t('hotkeys.cancel.desc'),
}, hotkey: 'Shift+X',
{ },
title: t('hotkeys.focusPrompt.title'), {
desc: t('hotkeys.focusPrompt.desc'), title: t('hotkeys.focusPrompt.title'),
hotkey: 'Alt+A', desc: t('hotkeys.focusPrompt.desc'),
}, hotkey: 'Alt+A',
{ },
title: t('hotkeys.toggleOptions.title'), {
desc: t('hotkeys.toggleOptions.desc'), title: t('hotkeys.toggleOptions.title'),
hotkey: 'O', desc: t('hotkeys.toggleOptions.desc'),
}, hotkey: 'O',
{ },
title: t('hotkeys.toggleGallery.title'), {
desc: t('hotkeys.toggleGallery.desc'), title: t('hotkeys.toggleGallery.title'),
hotkey: 'G', desc: t('hotkeys.toggleGallery.desc'),
}, hotkey: 'G',
{ },
title: t('hotkeys.maximizeWorkSpace.title'), {
desc: t('hotkeys.maximizeWorkSpace.desc'), title: t('hotkeys.maximizeWorkSpace.title'),
hotkey: 'F', desc: t('hotkeys.maximizeWorkSpace.desc'),
}, hotkey: 'F',
{ },
title: t('hotkeys.changeTabs.title'), {
desc: t('hotkeys.changeTabs.desc'), title: t('hotkeys.changeTabs.title'),
hotkey: '1-5', desc: t('hotkeys.changeTabs.desc'),
}, hotkey: '1-5',
]; },
],
[t]
);
const generalHotkeys = [ const generalHotkeys = useMemo(
{ () => [
title: t('hotkeys.setPrompt.title'), {
desc: t('hotkeys.setPrompt.desc'), title: t('hotkeys.setPrompt.title'),
hotkey: 'P', desc: t('hotkeys.setPrompt.desc'),
}, hotkey: 'P',
{ },
title: t('hotkeys.setSeed.title'), {
desc: t('hotkeys.setSeed.desc'), title: t('hotkeys.setSeed.title'),
hotkey: 'S', desc: t('hotkeys.setSeed.desc'),
}, hotkey: 'S',
{ },
title: t('hotkeys.setParameters.title'), {
desc: t('hotkeys.setParameters.desc'), title: t('hotkeys.setParameters.title'),
hotkey: 'A', desc: t('hotkeys.setParameters.desc'),
}, hotkey: 'A',
{ },
title: t('hotkeys.upscale.title'), {
desc: t('hotkeys.upscale.desc'), title: t('hotkeys.upscale.title'),
hotkey: 'Shift+U', desc: t('hotkeys.upscale.desc'),
}, hotkey: 'Shift+U',
{ },
title: t('hotkeys.showInfo.title'), {
desc: t('hotkeys.showInfo.desc'), title: t('hotkeys.showInfo.title'),
hotkey: 'I', desc: t('hotkeys.showInfo.desc'),
}, hotkey: 'I',
{ },
title: t('hotkeys.sendToImageToImage.title'), {
desc: t('hotkeys.sendToImageToImage.desc'), title: t('hotkeys.sendToImageToImage.title'),
hotkey: 'Shift+I', desc: t('hotkeys.sendToImageToImage.desc'),
}, hotkey: 'Shift+I',
{ },
title: t('hotkeys.deleteImage.title'), {
desc: t('hotkeys.deleteImage.desc'), title: t('hotkeys.deleteImage.title'),
hotkey: 'Del', desc: t('hotkeys.deleteImage.desc'),
}, hotkey: 'Del',
{ },
title: t('hotkeys.closePanels.title'), {
desc: t('hotkeys.closePanels.desc'), title: t('hotkeys.closePanels.title'),
hotkey: 'Esc', desc: t('hotkeys.closePanels.desc'),
}, hotkey: 'Esc',
]; },
],
[t]
);
const galleryHotkeys = [ const galleryHotkeys = useMemo(
{ () => [
title: t('hotkeys.previousImage.title'), {
desc: t('hotkeys.previousImage.desc'), title: t('hotkeys.previousImage.title'),
hotkey: 'Arrow Left', desc: t('hotkeys.previousImage.desc'),
}, hotkey: 'Arrow Left',
{ },
title: t('hotkeys.nextImage.title'), {
desc: t('hotkeys.nextImage.desc'), title: t('hotkeys.nextImage.title'),
hotkey: 'Arrow Right', desc: t('hotkeys.nextImage.desc'),
}, hotkey: 'Arrow Right',
{ },
title: t('hotkeys.increaseGalleryThumbSize.title'), {
desc: t('hotkeys.increaseGalleryThumbSize.desc'), title: t('hotkeys.increaseGalleryThumbSize.title'),
hotkey: 'Shift+Up', desc: t('hotkeys.increaseGalleryThumbSize.desc'),
}, hotkey: 'Shift+Up',
{ },
title: t('hotkeys.decreaseGalleryThumbSize.title'), {
desc: t('hotkeys.decreaseGalleryThumbSize.desc'), title: t('hotkeys.decreaseGalleryThumbSize.title'),
hotkey: 'Shift+Down', desc: t('hotkeys.decreaseGalleryThumbSize.desc'),
}, hotkey: 'Shift+Down',
]; },
],
[t]
);
const unifiedCanvasHotkeys = [ const unifiedCanvasHotkeys = useMemo(
{ () => [
title: t('hotkeys.selectBrush.title'), {
desc: t('hotkeys.selectBrush.desc'), title: t('hotkeys.selectBrush.title'),
hotkey: 'B', desc: t('hotkeys.selectBrush.desc'),
}, hotkey: 'B',
{ },
title: t('hotkeys.selectEraser.title'), {
desc: t('hotkeys.selectEraser.desc'), title: t('hotkeys.selectEraser.title'),
hotkey: 'E', desc: t('hotkeys.selectEraser.desc'),
}, hotkey: 'E',
{ },
title: t('hotkeys.decreaseBrushSize.title'), {
desc: t('hotkeys.decreaseBrushSize.desc'), title: t('hotkeys.decreaseBrushSize.title'),
hotkey: '[', desc: t('hotkeys.decreaseBrushSize.desc'),
}, hotkey: '[',
{ },
title: t('hotkeys.increaseBrushSize.title'), {
desc: t('hotkeys.increaseBrushSize.desc'), title: t('hotkeys.increaseBrushSize.title'),
hotkey: ']', desc: t('hotkeys.increaseBrushSize.desc'),
}, hotkey: ']',
{ },
title: t('hotkeys.decreaseBrushOpacity.title'), {
desc: t('hotkeys.decreaseBrushOpacity.desc'), title: t('hotkeys.decreaseBrushOpacity.title'),
hotkey: 'Shift + [', desc: t('hotkeys.decreaseBrushOpacity.desc'),
}, hotkey: 'Shift + [',
{ },
title: t('hotkeys.increaseBrushOpacity.title'), {
desc: t('hotkeys.increaseBrushOpacity.desc'), title: t('hotkeys.increaseBrushOpacity.title'),
hotkey: 'Shift + ]', desc: t('hotkeys.increaseBrushOpacity.desc'),
}, hotkey: 'Shift + ]',
{ },
title: t('hotkeys.moveTool.title'), {
desc: t('hotkeys.moveTool.desc'), title: t('hotkeys.moveTool.title'),
hotkey: 'V', desc: t('hotkeys.moveTool.desc'),
}, hotkey: 'V',
{ },
title: t('hotkeys.fillBoundingBox.title'), {
desc: t('hotkeys.fillBoundingBox.desc'), title: t('hotkeys.fillBoundingBox.title'),
hotkey: 'Shift + F', desc: t('hotkeys.fillBoundingBox.desc'),
}, hotkey: 'Shift + F',
{ },
title: t('hotkeys.eraseBoundingBox.title'), {
desc: t('hotkeys.eraseBoundingBox.desc'), title: t('hotkeys.eraseBoundingBox.title'),
hotkey: 'Delete / Backspace', desc: t('hotkeys.eraseBoundingBox.desc'),
}, hotkey: 'Delete / Backspace',
{ },
title: t('hotkeys.colorPicker.title'), {
desc: t('hotkeys.colorPicker.desc'), title: t('hotkeys.colorPicker.title'),
hotkey: 'C', desc: t('hotkeys.colorPicker.desc'),
}, hotkey: 'C',
{ },
title: t('hotkeys.toggleSnap.title'), {
desc: t('hotkeys.toggleSnap.desc'), title: t('hotkeys.toggleSnap.title'),
hotkey: 'N', desc: t('hotkeys.toggleSnap.desc'),
}, hotkey: 'N',
{ },
title: t('hotkeys.quickToggleMove.title'), {
desc: t('hotkeys.quickToggleMove.desc'), title: t('hotkeys.quickToggleMove.title'),
hotkey: 'Hold Space', desc: t('hotkeys.quickToggleMove.desc'),
}, hotkey: 'Hold Space',
{ },
title: t('hotkeys.toggleLayer.title'), {
desc: t('hotkeys.toggleLayer.desc'), title: t('hotkeys.toggleLayer.title'),
hotkey: 'Q', desc: t('hotkeys.toggleLayer.desc'),
}, hotkey: 'Q',
{ },
title: t('hotkeys.clearMask.title'), {
desc: t('hotkeys.clearMask.desc'), title: t('hotkeys.clearMask.title'),
hotkey: 'Shift+C', desc: t('hotkeys.clearMask.desc'),
}, hotkey: 'Shift+C',
{ },
title: t('hotkeys.hideMask.title'), {
desc: t('hotkeys.hideMask.desc'), title: t('hotkeys.hideMask.title'),
hotkey: 'H', desc: t('hotkeys.hideMask.desc'),
}, hotkey: 'H',
{ },
title: t('hotkeys.showHideBoundingBox.title'), {
desc: t('hotkeys.showHideBoundingBox.desc'), title: t('hotkeys.showHideBoundingBox.title'),
hotkey: 'Shift+H', desc: t('hotkeys.showHideBoundingBox.desc'),
}, hotkey: 'Shift+H',
{ },
title: t('hotkeys.mergeVisible.title'), {
desc: t('hotkeys.mergeVisible.desc'), title: t('hotkeys.mergeVisible.title'),
hotkey: 'Shift+M', desc: t('hotkeys.mergeVisible.desc'),
}, hotkey: 'Shift+M',
{ },
title: t('hotkeys.saveToGallery.title'), {
desc: t('hotkeys.saveToGallery.desc'), title: t('hotkeys.saveToGallery.title'),
hotkey: 'Shift+S', desc: t('hotkeys.saveToGallery.desc'),
}, hotkey: 'Shift+S',
{ },
title: t('hotkeys.copyToClipboard.title'), {
desc: t('hotkeys.copyToClipboard.desc'), title: t('hotkeys.copyToClipboard.title'),
hotkey: 'Ctrl+C', desc: t('hotkeys.copyToClipboard.desc'),
}, hotkey: 'Ctrl+C',
{ },
title: t('hotkeys.downloadImage.title'), {
desc: t('hotkeys.downloadImage.desc'), title: t('hotkeys.downloadImage.title'),
hotkey: 'Shift+D', desc: t('hotkeys.downloadImage.desc'),
}, hotkey: 'Shift+D',
{ },
title: t('hotkeys.undoStroke.title'), {
desc: t('hotkeys.undoStroke.desc'), title: t('hotkeys.undoStroke.title'),
hotkey: 'Ctrl+Z', desc: t('hotkeys.undoStroke.desc'),
}, hotkey: 'Ctrl+Z',
{ },
title: t('hotkeys.redoStroke.title'), {
desc: t('hotkeys.redoStroke.desc'), title: t('hotkeys.redoStroke.title'),
hotkey: 'Ctrl+Shift+Z, Ctrl+Y', desc: t('hotkeys.redoStroke.desc'),
}, hotkey: 'Ctrl+Shift+Z, Ctrl+Y',
{ },
title: t('hotkeys.resetView.title'), {
desc: t('hotkeys.resetView.desc'), title: t('hotkeys.resetView.title'),
hotkey: 'R', desc: t('hotkeys.resetView.desc'),
}, hotkey: 'R',
{ },
title: t('hotkeys.previousStagingImage.title'), {
desc: t('hotkeys.previousStagingImage.desc'), title: t('hotkeys.previousStagingImage.title'),
hotkey: 'Arrow Left', desc: t('hotkeys.previousStagingImage.desc'),
}, hotkey: 'Arrow Left',
{ },
title: t('hotkeys.nextStagingImage.title'), {
desc: t('hotkeys.nextStagingImage.desc'), title: t('hotkeys.nextStagingImage.title'),
hotkey: 'Arrow Right', desc: t('hotkeys.nextStagingImage.desc'),
}, hotkey: 'Arrow Right',
{ },
title: t('hotkeys.acceptStagingImage.title'), {
desc: t('hotkeys.acceptStagingImage.desc'), title: t('hotkeys.acceptStagingImage.title'),
hotkey: 'Enter', desc: t('hotkeys.acceptStagingImage.desc'),
}, hotkey: 'Enter',
]; },
],
[t]
);
const nodesHotkeys = [ const nodesHotkeys = useMemo(
{ () => [
title: t('hotkeys.addNodes.title'), {
desc: t('hotkeys.addNodes.desc'), title: t('hotkeys.addNodes.title'),
hotkey: 'Shift + A / Space', desc: t('hotkeys.addNodes.desc'),
}, hotkey: 'Shift + A / Space',
]; },
],
const renderHotkeyModalItems = (hotkeys: HotkeyList[]) => ( [t]
<Flex flexDir="column" gap={4}>
{hotkeys.map((hotkey, i) => (
<Flex flexDir="column" px={2} gap={4} key={i}>
<HotkeysModalItem
title={hotkey.title}
description={hotkey.desc}
hotkey={hotkey.hotkey}
/>
{i < hotkeys.length - 1 && <Divider />}
</Flex>
))}
</Flex>
); );
return ( return (