From ed05bf2df3e5102a54ec8077aac387f1f135911b Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 16 Jun 2025 17:33:17 +1000
Subject: [PATCH] feat(ui): refine ref images UI
---
.../components/RefImage/RefImage.tsx | 29 ++---
.../components/RefImage/RefImageList.tsx | 108 ++++++++++--------
invokeai/frontend/web/src/features/dnd/dnd.ts | 32 ++++++
3 files changed, 107 insertions(+), 62 deletions(-)
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
index ca21d6a84a..f184f204a6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImage.tsx
@@ -1,5 +1,3 @@
-import type {
- SystemStyleObject} from '@invoke-ai/ui-library';
import {
Divider,
Flex,
@@ -10,7 +8,8 @@ import {
PopoverArrow,
PopoverBody,
PopoverContent,
- Portal
+ Portal,
+ Skeleton,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
@@ -80,24 +79,12 @@ export const RefImage = memo(() => {
});
RefImage.displayName = 'RefImage';
-const imageSx: SystemStyleObject = {
- opacity: 0.5,
- _hover: {
- opacity: 1,
- },
- "&[data-is-open='true']": {
- opacity: 1,
- },
- transitionProperty: 'opacity',
- transitionDuration: '0.2s',
-};
-
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
const id = useRefImageIdContext();
const entity = useRefImageEntity(id);
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
- if (!imageDTO || !entity.config.image) {
+ if (!entity.config.image) {
return (
{
icon={}
colorScheme="error"
onClick={disclosure.toggle}
+ flexShrink={0}
/>
);
@@ -120,14 +108,21 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
return (
}
maxW="full"
maxH="full"
borderRadius="base"
onClick={disclosure.toggle}
+ flexShrink={0}
// sx={imageSx}
// data-is-open={disclosure.isOpen}
/>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
index 723237fee5..d7efcb3d0b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx
@@ -1,17 +1,22 @@
/* eslint-disable i18next/no-literal-string */
import type { FlexProps } from '@invoke-ai/ui-library';
-import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
+import { Button, Flex } from '@invoke-ai/ui-library';
+import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
-import { useAddGlobalReferenceImage } from 'features/controlLayers/hooks/addLayerHooks';
-import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
-import { memo } from 'react';
-import { PiPlusBold } from 'react-icons/pi';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
+import { refImageAdded, selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
+import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { memo, useMemo } from 'react';
+import { PiUploadBold } from 'react-icons/pi';
+import type { ImageDTO } from 'services/api/types';
export const RefImageList = memo((props: FlexProps) => {
const ids = useAppSelector(selectRefImageEntityIds);
- const addRefImage = useAddGlobalReferenceImage();
return (
{ids.map((id) => (
@@ -19,59 +24,72 @@ export const RefImageList = memo((props: FlexProps) => {
))}
-
- }
- onClick={addRefImage}
- isDisabled={ids.length >= 5} // Limit to 5 reference images
- >
- Ref Image
-
+ {ids.length < 5 && }
+ {ids.length >= 5 && }
);
});
RefImageList.displayName = 'RefImageList';
-const AddRefImageIconButton = memo(() => {
- const addRefImage = useAddGlobalReferenceImage();
- return (
- }
- />
- );
-});
-AddRefImageIconButton.displayName = 'AddRefImageIconButton';
+const dndTargetData = addGlobalReferenceImageDndTarget.getData();
-const AddRefImageButton = memo((props) => {
- const addRefImage = useAddGlobalReferenceImage();
+const MaxRefImages = memo(() => {
return (
}
- onClick={addRefImage}
+ isDisabled
>
- Ref Image
+ Max Ref Images
);
});
-AddRefImageButton.displayName = 'AddRefImageButton';
+MaxRefImages.displayName = 'MaxRefImages';
+
+const AddRefImageDropTargetAndButton = memo(() => {
+ const { dispatch, getState } = useAppStore();
+
+ const uploadOptions = useMemo(
+ () =>
+ ({
+ onUpload: (imageDTO: ImageDTO) => {
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ },
+ allowMultiple: false,
+ }) as const,
+ [dispatch, getState]
+ );
+
+ const uploadApi = useImageUploadButton(uploadOptions);
+
+ return (
+ <>
+ }
+ {...uploadApi.getUploadButtonProps()}
+ >
+ Reference Image
+
+
+
+ >
+ );
+});
+AddRefImageDropTargetAndButton.displayName = 'AddRefImageDropTargetAndButton';
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index bb401727d3..df768f4ec6 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -1,7 +1,10 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, AppGetState } from 'app/store/store';
+import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
+import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
import type { BoardId } from 'features/gallery/store/types';
import {
@@ -152,6 +155,34 @@ export const setGlobalReferenceImageDndTarget: DndTarget<
};
//#endregion
+//#region Add Global Reference Image
+const _addGlobalReferenceImage = buildTypeAndKey('add-global-reference-image');
+export type AddGlobalReferenceImageDndTargetData = DndData<
+ typeof _addGlobalReferenceImage.type,
+ typeof _addGlobalReferenceImage.key
+>;
+export const addGlobalReferenceImageDndTarget: DndTarget<
+ AddGlobalReferenceImageDndTargetData,
+ SingleImageDndSourceData
+> = {
+ ..._addGlobalReferenceImage,
+ typeGuard: buildTypeGuard(_addGlobalReferenceImage.key),
+ getData: buildGetData(_addGlobalReferenceImage.key, _addGlobalReferenceImage.type),
+ isValid: ({ sourceData }) => {
+ if (singleImageDndSource.typeGuard(sourceData)) {
+ return true;
+ }
+ return false;
+ },
+ handler: ({ sourceData, dispatch, getState }) => {
+ const { imageDTO } = sourceData.payload;
+ const config = getDefaultRefImageConfig(getState);
+ config.image = imageDTOToImageWithDims(imageDTO);
+ dispatch(refImageAdded({ overrides: { config } }));
+ },
+};
+//#endregion
+
//#region Set Regional Guidance Reference Image
const _setRegionalGuidanceReferenceImage = buildTypeAndKey('set-regional-guidance-reference-image');
export type SetRegionalGuidanceReferenceImageDndTargetData = DndData<
@@ -496,6 +527,7 @@ export const dndTargets = [
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
newCanvasFromImageDndTarget,
+ addGlobalReferenceImageDndTarget,
// Single or Multiple Image
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,