mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): misc perf/rerender improvements
More efficient selectors, memoized/stable references to objects, lazy popover/menu rendering.
This commit is contained in:
committed by
Kent Keirsey
parent
2ba505cce9
commit
539887b215
@ -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
|
||||||
|
@ -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 (
|
||||||
|
@ -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],
|
|
||||||
({ canvas }) => {
|
|
||||||
const { cursorPosition } = 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 (
|
||||||
|
@ -68,7 +68,7 @@ const GallerySettingsPopover = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvPopover>
|
<InvPopover isLazy>
|
||||||
<InvPopoverTrigger>
|
<InvPopoverTrigger>
|
||||||
<InvIconButton
|
<InvIconButton
|
||||||
tooltip={t('gallery.gallerySettings')}
|
tooltip={t('gallery.gallerySettings')}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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,7 +56,8 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const appHotkeys = [
|
const appHotkeys = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
title: t('hotkeys.invoke.title'),
|
title: t('hotkeys.invoke.title'),
|
||||||
desc: t('hotkeys.invoke.desc'),
|
desc: t('hotkeys.invoke.desc'),
|
||||||
@ -77,9 +93,12 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
desc: t('hotkeys.changeTabs.desc'),
|
desc: t('hotkeys.changeTabs.desc'),
|
||||||
hotkey: '1-5',
|
hotkey: '1-5',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const generalHotkeys = [
|
const generalHotkeys = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
title: t('hotkeys.setPrompt.title'),
|
title: t('hotkeys.setPrompt.title'),
|
||||||
desc: t('hotkeys.setPrompt.desc'),
|
desc: t('hotkeys.setPrompt.desc'),
|
||||||
@ -120,9 +139,12 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
desc: t('hotkeys.closePanels.desc'),
|
desc: t('hotkeys.closePanels.desc'),
|
||||||
hotkey: 'Esc',
|
hotkey: 'Esc',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const galleryHotkeys = [
|
const galleryHotkeys = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
title: t('hotkeys.previousImage.title'),
|
title: t('hotkeys.previousImage.title'),
|
||||||
desc: t('hotkeys.previousImage.desc'),
|
desc: t('hotkeys.previousImage.desc'),
|
||||||
@ -143,9 +165,12 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
desc: t('hotkeys.decreaseGalleryThumbSize.desc'),
|
desc: t('hotkeys.decreaseGalleryThumbSize.desc'),
|
||||||
hotkey: 'Shift+Down',
|
hotkey: 'Shift+Down',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const unifiedCanvasHotkeys = [
|
const unifiedCanvasHotkeys = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
title: t('hotkeys.selectBrush.title'),
|
title: t('hotkeys.selectBrush.title'),
|
||||||
desc: t('hotkeys.selectBrush.desc'),
|
desc: t('hotkeys.selectBrush.desc'),
|
||||||
@ -276,29 +301,19 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
desc: t('hotkeys.acceptStagingImage.desc'),
|
desc: t('hotkeys.acceptStagingImage.desc'),
|
||||||
hotkey: 'Enter',
|
hotkey: 'Enter',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const nodesHotkeys = [
|
const nodesHotkeys = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
title: t('hotkeys.addNodes.title'),
|
title: t('hotkeys.addNodes.title'),
|
||||||
desc: t('hotkeys.addNodes.desc'),
|
desc: t('hotkeys.addNodes.desc'),
|
||||||
hotkey: 'Shift + A / Space',
|
hotkey: 'Shift + A / Space',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
[t]
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user