mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): change multi image drop to not have selection as payload
This caused a lot of re-rendering whenever the selection changed, which caused a huge performance hit. It also made changing the current image lag a bit. Instead of providing an array of image names as a multi-select dnd payload, there is now no multi-select dnd payload at all - instead, the payload types are used by the `imageDropped` listener to pull the selection out of redux. Now, the only big re-renders are when the selectionCount changes. In the future I'll figure out a good way to do image names as payload without incurring re-renders.
This commit is contained in:
parent
1358c5eb7d
commit
f155887b7d
@ -1,4 +1,8 @@
|
|||||||
import { Box, ChakraProps, Flex, Heading, Image } from '@chakra-ui/react';
|
import { Box, ChakraProps, Flex, Heading, Image } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { TypesafeDraggableData } from './typesafeDnd';
|
import { TypesafeDraggableData } from './typesafeDnd';
|
||||||
|
|
||||||
@ -28,7 +32,24 @@ const STYLES: ChakraProps['sx'] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
stateSelector,
|
||||||
|
(state) => {
|
||||||
|
const gallerySelectionCount = state.gallery.selection.length;
|
||||||
|
const batchSelectionCount = state.batch.selection.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
gallerySelectionCount,
|
||||||
|
batchSelectionCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
const DragPreview = (props: OverlayDragImageProps) => {
|
const DragPreview = (props: OverlayDragImageProps) => {
|
||||||
|
const { gallerySelectionCount, batchSelectionCount } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
if (!props.dragData) {
|
if (!props.dragData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -57,7 +78,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.dragData.payloadType === 'IMAGE_NAMES') {
|
if (props.dragData.payloadType === 'BATCH_SELECTION') {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -70,7 +91,26 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
...STYLES,
|
...STYLES,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading>{props.dragData.payload.imageNames.length}</Heading>
|
<Heading>{batchSelectionCount}</Heading>
|
||||||
|
<Heading size="sm">Images</Heading>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.dragData.payloadType === 'GALLERY_SELECTION') {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
cursor: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDir: 'column',
|
||||||
|
...STYLES,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading>{gallerySelectionCount}</Heading>
|
||||||
<Heading size="sm">Images</Heading>
|
<Heading size="sm">Images</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -77,14 +77,18 @@ export type ImageDraggableData = BaseDragData & {
|
|||||||
payload: { imageDTO: ImageDTO };
|
payload: { imageDTO: ImageDTO };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImageNamesDraggableData = BaseDragData & {
|
export type GallerySelectionDraggableData = BaseDragData & {
|
||||||
payloadType: 'IMAGE_NAMES';
|
payloadType: 'GALLERY_SELECTION';
|
||||||
payload: { imageNames: string[] };
|
};
|
||||||
|
|
||||||
|
export type BatchSelectionDraggableData = BaseDragData & {
|
||||||
|
payloadType: 'BATCH_SELECTION';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDraggableData =
|
export type TypesafeDraggableData =
|
||||||
| ImageDraggableData
|
| ImageDraggableData
|
||||||
| ImageNamesDraggableData;
|
| GallerySelectionDraggableData
|
||||||
|
| BatchSelectionDraggableData;
|
||||||
|
|
||||||
interface UseDroppableTypesafeArguments
|
interface UseDroppableTypesafeArguments
|
||||||
extends Omit<UseDroppableArguments, 'data'> {
|
extends Omit<UseDroppableArguments, 'data'> {
|
||||||
@ -155,11 +159,13 @@ export const isValidDrop = (
|
|||||||
case 'SET_NODES_IMAGE':
|
case 'SET_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'SET_MULTI_NODES_IMAGE':
|
case 'SET_MULTI_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
||||||
case 'ADD_TO_BATCH':
|
case 'ADD_TO_BATCH':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
||||||
case 'MOVE_BOARD':
|
case 'MOVE_BOARD':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return (
|
||||||
|
payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION' || 'BATCH_SELECTION'
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { startAppListening } from '../';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
|
||||||
import {
|
import {
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
|
||||||
import {
|
import {
|
||||||
imageAddedToBatch,
|
imageAddedToBatch,
|
||||||
imagesAddedToBatch,
|
imagesAddedToBatch,
|
||||||
} from 'features/batch/store/batchSlice';
|
} from 'features/batch/store/batchSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
fieldValueChanged,
|
fieldValueChanged,
|
||||||
imageCollectionFieldValueChanged,
|
imageCollectionFieldValueChanged,
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||||
|
import { startAppListening } from '../';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'dnd' });
|
const moduleLog = log.child({ namespace: 'dnd' });
|
||||||
|
|
||||||
@ -33,6 +32,7 @@ export const addImageDroppedListener = () => {
|
|||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const { activeData, overData } = action.payload;
|
const { activeData, overData } = action.payload;
|
||||||
const { actionType } = overData;
|
const { actionType } = overData;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
// set current image
|
// set current image
|
||||||
if (
|
if (
|
||||||
@ -64,9 +64,9 @@ export const addImageDroppedListener = () => {
|
|||||||
// add multiple images to batch
|
// add multiple images to batch
|
||||||
if (
|
if (
|
||||||
actionType === 'ADD_TO_BATCH' &&
|
actionType === 'ADD_TO_BATCH' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES'
|
activeData.payloadType === 'GALLERY_SELECTION'
|
||||||
) {
|
) {
|
||||||
dispatch(imagesAddedToBatch(activeData.payload.imageNames));
|
dispatch(imagesAddedToBatch(state.gallery.selection));
|
||||||
}
|
}
|
||||||
|
|
||||||
// set control image
|
// set control image
|
||||||
@ -128,14 +128,14 @@ export const addImageDroppedListener = () => {
|
|||||||
// set multiple nodes images (multiple images handler)
|
// set multiple nodes images (multiple images handler)
|
||||||
if (
|
if (
|
||||||
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES'
|
activeData.payloadType === 'GALLERY_SELECTION'
|
||||||
) {
|
) {
|
||||||
const { fieldName, nodeId } = overData.context;
|
const { fieldName, nodeId } = overData.context;
|
||||||
dispatch(
|
dispatch(
|
||||||
imageCollectionFieldValueChanged({
|
imageCollectionFieldValueChanged({
|
||||||
nodeId,
|
nodeId,
|
||||||
fieldName,
|
fieldName,
|
||||||
value: activeData.payload.imageNames.map((image_name) => ({
|
value: state.gallery.selection.map((image_name) => ({
|
||||||
image_name,
|
image_name,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
@ -19,7 +19,7 @@ const makeSelector = (image_name: string) =>
|
|||||||
createSelector(
|
createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => ({
|
(state) => ({
|
||||||
selection: state.batch.selection,
|
selectionCount: state.batch.selection.length,
|
||||||
isSelected: state.batch.selection.includes(image_name),
|
isSelected: state.batch.selection.includes(image_name),
|
||||||
}),
|
}),
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -43,7 +43,7 @@ const BatchImage = (props: BatchImageProps) => {
|
|||||||
[props.imageName]
|
[props.imageName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isSelected, selection } = useAppSelector(selector);
|
const { isSelected, selectionCount } = useAppSelector(selector);
|
||||||
|
|
||||||
const handleClickRemove = useCallback(() => {
|
const handleClickRemove = useCallback(() => {
|
||||||
dispatch(imageRemovedFromBatch(props.imageName));
|
dispatch(imageRemovedFromBatch(props.imageName));
|
||||||
@ -63,13 +63,10 @@ const BatchImage = (props: BatchImageProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||||
if (selection.length > 1) {
|
if (selectionCount > 1) {
|
||||||
return {
|
return {
|
||||||
id: 'batch',
|
id: 'batch',
|
||||||
payloadType: 'IMAGE_NAMES',
|
payloadType: 'BATCH_SELECTION',
|
||||||
payload: {
|
|
||||||
imageNames: selection,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +77,7 @@ const BatchImage = (props: BatchImageProps) => {
|
|||||||
payload: { imageDTO },
|
payload: { imageDTO },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [imageDTO, selection]);
|
}, [imageDTO, selectionCount]);
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <Icon as={FaExclamationCircle} />;
|
return <Icon as={FaExclamationCircle} />;
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import {
|
|
||||||
ControlNetConfig,
|
|
||||||
controlNetImageChanged,
|
|
||||||
controlNetSelector,
|
|
||||||
} from '../store/controlNetSlice';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { Box, Flex, SystemStyleObject } from '@chakra-ui/react';
|
import { Box, Flex, SystemStyleObject } from '@chakra-ui/react';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { IAILoadingImageFallback } from 'common/components/IAIImageFallback';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { FaUndo } from 'react-icons/fa';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import {
|
import {
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import { IAILoadingImageFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
import { PostUploadAction } from 'services/api/thunks/image';
|
||||||
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
|
controlNetImageChanged,
|
||||||
|
controlNetSelector,
|
||||||
|
} from '../store/controlNetSlice';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
controlNetSelector,
|
controlNetSelector,
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
|
||||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
|
|
||||||
import {
|
import {
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||||
|
import NextPrevImageButtons from './NextPrevImageButtons';
|
||||||
|
|
||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
[stateSelector, selectLastSelectedImage],
|
[stateSelector, selectLastSelectedImage],
|
||||||
|
@ -22,10 +22,10 @@ export const makeSelector = (image_name: string) =>
|
|||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ gallery }) => {
|
({ gallery }) => {
|
||||||
const isSelected = gallery.selection.includes(image_name);
|
const isSelected = gallery.selection.includes(image_name);
|
||||||
const selection = gallery.selection;
|
const selectionCount = gallery.selection.length;
|
||||||
return {
|
return {
|
||||||
isSelected,
|
isSelected,
|
||||||
selection,
|
selectionCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -44,7 +44,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
|
|
||||||
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
|
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
|
||||||
|
|
||||||
const { isSelected, selection } = useAppSelector(localSelector);
|
const { isSelected, selectionCount } = useAppSelector(localSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -75,11 +75,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||||
if (selection.length > 1) {
|
if (selectionCount > 1) {
|
||||||
return {
|
return {
|
||||||
id: 'gallery-image',
|
id: 'gallery-image',
|
||||||
payloadType: 'IMAGE_NAMES',
|
payloadType: 'GALLERY_SELECTION',
|
||||||
payload: { imageNames: selection },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +89,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
payload: { imageDTO },
|
payload: { imageDTO },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [imageDTO, selection]);
|
}, [imageDTO, selectionCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||||
|
@ -7,18 +7,17 @@ import {
|
|||||||
} from 'features/nodes/types/types';
|
} from 'features/nodes/types/types';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { FieldComponentProps } from './types';
|
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import {
|
import {
|
||||||
NodesImageDropData,
|
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
import { PostUploadAction } from 'services/api/thunks/image';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { FieldComponentProps } from './types';
|
||||||
|
|
||||||
const ImageInputFieldComponent = (
|
const ImageInputFieldComponent = (
|
||||||
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
||||||
|
Loading…
Reference in New Issue
Block a user