feat(ui): migrate to pragmatic-drag-and-drop (wip 3)

This commit is contained in:
psychedelicious
2024-10-27 22:26:09 +10:00
parent d4a95af14f
commit cf67d084fd
20 changed files with 348 additions and 283 deletions

View File

@@ -12,7 +12,7 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { replaceLayerWithImageDndTarget, type ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
import { memo, useMemo } from 'react';
import { memo, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
@@ -25,9 +25,10 @@ export const ControlLayer = memo(({ id }: Props) => {
() => ({ id, type: 'control_layer' }),
[id]
);
const dndId = useId();
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }),
[entityIdentifier]
() => replaceLayerWithImageDndTarget.getData({ dndId, entityIdentifier }),
[dndId, entityIdentifier]
);
return (

View File

@@ -1,80 +1,69 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { useNanoid } from 'common/hooks/useNanoid';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import type { DndTargetData } from 'features/dnd2/types';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
const sx = {
position: 'relative',
w: 'full',
h: 'full',
alignItems: 'center',
borderColor: 'error.500',
borderStyle: 'solid',
borderWidth: 0,
borderRadius: 'base',
'&[data-error=true]': {
borderWidth: 1,
},
} satisfies SystemStyleObject;
type Props = {
image: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
targetData: DndTargetData;
postUploadAction: PostUploadAction;
};
export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData, postUploadAction }: Props) => {
const { t } = useTranslation();
const isConnected = useStore($isConnected);
const dndId = useNanoid('ip_adapter_image_preview');
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
image?.image_name ?? skipToken
);
const imageDTOQueryResult = useGetImageDTOQuery(image?.image_name ?? skipToken);
const handleResetControlImage = useCallback(() => {
onChangeImage(null);
}, [onChangeImage]);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (controlImage) {
return {
id: dndId,
payloadType: 'IMAGE_DTO',
payload: { imageDTO: controlImage },
};
}
}, [controlImage, dndId]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
if (isConnected && imageDTOQueryResult.isError) {
handleResetControlImage();
}
}, [handleResetControlImage, isConnected, isErrorControlImage]);
}, [handleResetControlImage, imageDTOQueryResult.isError, isConnected]);
return (
<Flex
position="relative"
w="full"
h="full"
alignItems="center"
borderColor="error.500"
borderStyle="solid"
borderWidth={controlImage ? 0 : 1}
borderRadius="base"
>
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={controlImage}
postUploadAction={postUploadAction}
/>
{controlImage && (
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}
/>
</Flex>
<Flex sx={sx} data-error={!imageDTOQueryResult.currentData && !image?.image_name}>
{imageDTOQueryResult.currentData && (
<>
<DndImage dndId={targetData.dndId} imageDTO={imageDTOQueryResult.currentData} />
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}
/>
</Flex>
</>
)}
<DndDropTarget targetData={targetData} label={t('gallery.drop')} />
</Flex>
);
});

View File

@@ -19,8 +19,8 @@ import {
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { IPAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { setGlobalReferenceImageDndTarget, type SetGlobalReferenceImageDndTargetData } from 'features/dnd2/types';
import { memo, useCallback, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
@@ -80,14 +80,16 @@ export const IPAdapterSettings = memo(() => {
[dispatch, entityIdentifier]
);
const droppableData = useMemo<IPAImageDropData>(
() => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }),
[entityIdentifier.id]
);
const dndId = useId();
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
[entityIdentifier.id]
);
const targetData = useMemo<SetGlobalReferenceImageDndTargetData>(
() => setGlobalReferenceImageDndTarget.getData({ dndId, globalReferenceImageId: entityIdentifier.id }),
[dndId, entityIdentifier.id]
);
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
const isBusy = useCanvasIsBusy();
@@ -122,9 +124,9 @@ export const IPAdapterSettings = memo(() => {
</Flex>
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
<IPAdapterImagePreview
image={ipAdapter.image ?? null}
image={ipAdapter.image}
onChangeImage={onChangeImage}
droppableData={droppableData}
targetData={targetData}
postUploadAction={postUploadAction}
/>
</Flex>

View File

@@ -10,7 +10,7 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
import { replaceLayerWithImageDndTarget } from 'features/dnd2/types';
import { memo, useMemo } from 'react';
import { memo, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
@@ -19,10 +19,11 @@ type Props = {
export const RasterLayer = memo(({ id }: Props) => {
const { t } = useTranslation();
const dndId = useId();
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }),
[entityIdentifier]
() => replaceLayerWithImageDndTarget.getData({ dndId, entityIdentifier }),
[dndId, entityIdentifier]
);
return (

View File

@@ -20,8 +20,9 @@ import {
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd2/types';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd2/types';
import { memo, useCallback, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
@@ -34,6 +35,7 @@ type Props = {
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dndId = useId();
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
@@ -91,14 +93,16 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
[dispatch, entityIdentifier, referenceImageId]
);
const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({
actionType: 'SET_RG_IP_ADAPTER_IMAGE',
context: { id: entityIdentifier.id, referenceImageId: referenceImageId },
id: entityIdentifier.id,
}),
[entityIdentifier.id, referenceImageId]
const targetData = useMemo<SetRegionalGuidanceReferenceImageDndTargetData>(
() =>
setRegionalGuidanceReferenceImageDndTarget.getData({
dndId,
regionalGuidanceId: entityIdentifier.id,
referenceImageId,
}),
[dndId, entityIdentifier.id, referenceImageId]
);
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, referenceImageId: referenceImageId }),
[entityIdentifier.id, referenceImageId]
@@ -151,9 +155,9 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
</Flex>
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
<IPAdapterImagePreview
image={ipAdapter.image ?? null}
image={ipAdapter.image}
onChangeImage={onChangeImage}
droppableData={droppableData}
targetData={targetData}
postUploadAction={postUploadAction}
/>
</Flex>

View File

@@ -40,7 +40,7 @@ export const DndDropOverlay = memo((props: Props) => {
left={0.5}
opacity={1}
borderWidth={1.5}
borderColor={dndState === 'over' ? 'invokeYellow.300' : 'base.500'}
borderColor={dndState === 'over' ? 'invokeYellow.300' : 'base.300'}
borderRadius="base"
borderStyle="dashed"
transitionProperty="common"
@@ -52,7 +52,7 @@ export const DndDropOverlay = memo((props: Props) => {
<Text
fontSize="lg"
fontWeight="semibold"
color={dndState === 'over' ? 'invokeYellow.300' : 'base.500'}
color={dndState === 'over' ? 'invokeYellow.300' : 'base.300'}
transitionProperty="common"
transitionDuration="0.1s"
textAlign="center"

View File

@@ -3,9 +3,11 @@ import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-d
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/dnd';
import { useAppDispatch } from 'app/store/storeHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { DndDropOverlay } from 'features/dnd2/DndDropOverlay';
import type { DndState, DndTargetData } from 'features/dnd2/types';
import { isDndSourceData, isValidDrop, singleImageDndSource } from 'features/dnd2/types';
@@ -22,6 +24,20 @@ const sizeInMB = (sizeInBytes: number, decimalsNum = 2) => {
return +result.toFixed(decimalsNum);
};
const sx = {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
w: 'full',
h: 'full',
// We must disable pointer events when idle to prevent the overlay from blocking clicks
'&[data-dnd-state="idle"]': {
pointerEvents: 'none',
},
} satisfies SystemStyleObject;
const zUploadFile = z
.custom<File>()
.refine(
@@ -76,6 +92,9 @@ export const DndDropTarget = memo((props: Props) => {
if (!isDndSourceData(sourceData)) {
return false;
}
if (targetData.dndId === sourceData.dndId) {
return false;
}
return isValidDrop(sourceData, targetData);
},
onDragEnter: () => {
@@ -99,6 +118,9 @@ export const DndDropTarget = memo((props: Props) => {
if (!isDndSourceData(sourceData)) {
return false;
}
if (targetData.dndId === sourceData.dndId) {
return false;
}
return isValidDrop(sourceData, targetData);
},
onDragStart: () => {
@@ -153,7 +175,12 @@ export const DndDropTarget = memo((props: Props) => {
image_category: 'user',
is_intermediate: false,
});
dispatch(dndDropped({ sourceData: singleImageDndSource.getData({ imageDTO }), targetData }));
dispatch(
dndDropped({
sourceData: singleImageDndSource.getData({ dndId: getPrefixedId('random-dnd-id'), imageDTO }),
targetData,
})
);
}
},
}),
@@ -177,18 +204,7 @@ export const DndDropTarget = memo((props: Props) => {
}, [targetData, dispatch, externalDropEnabled]);
return (
<Box
ref={ref}
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
w="full"
h="full"
// We must disable pointer events when idle to prevent the overlay from blocking clicks
pointerEvents={dndState === 'idle' ? 'none' : 'auto'}
>
<Box ref={ref} sx={sx} data-dnd-state={dndState}>
<DndDropOverlay dndState={dndState} label={label} />
</Box>
);

View File

@@ -0,0 +1,65 @@
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { singleImageDndSource } from 'features/dnd2/types';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { memo, useEffect, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
const sx = {
objectFit: 'contain',
maxW: 'full',
maxH: 'full',
borderRadius: 'base',
cursor: 'grab',
'&[data-is-dragging=true]': {
opacity: 0.3,
},
} satisfies SystemStyleObject;
type Props = ImageProps & {
imageDTO: ImageDTO;
dndId: string;
};
export const DndImage = memo(({ imageDTO, dndId, ...rest }: Props) => {
const store = useAppStore();
const [isDragging, setIsDragging] = useState(false);
const [element, ref] = useState<HTMLImageElement | null>(null);
useEffect(() => {
if (!element) {
return;
}
return draggable({
element,
getInitialData: () => {
return singleImageDndSource.getData({ dndId, imageDTO });
},
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
});
}, [imageDTO, element, store, dndId]);
useImageContextMenu(imageDTO, element);
return (
<Image
role="button"
ref={ref}
src={imageDTO.image_url}
fallbackSrc={imageDTO.thumbnail_url}
w={imageDTO.width}
sx={sx}
data-is-dragging={isDragging}
{...rest}
/>
);
});
DndImage.displayName = 'DndImage';

View File

@@ -2,37 +2,47 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
import type { BoardId } from 'features/gallery/store/types';
import type { ImageDTO } from 'services/api/types';
export type DndData = Record<string | symbol, unknown>;
type AnyRecord = Record<string | symbol, unknown>;
export type BaseDndData = { dndId: string } & Record<string | symbol, unknown>;
const _buildDataTypeGuard =
<T extends DndData>(key: symbol) =>
(data: DndData): data is T => {
<T extends AnyRecord>(key: symbol) =>
(data: AnyRecord): data is T => {
return Boolean(data[key]);
};
const _buildDataGetter =
<T extends DndData>(key: symbol) =>
<T extends AnyRecord>(key: symbol) =>
(data: Omit<T, typeof key>): T => {
return {
[key]: true,
...data,
} as T;
};
const buildDndSourceApi = <T extends DndData>(key: symbol) =>
const buildDndSourceApi = <T extends AnyRecord>(key: symbol) =>
({ key, typeGuard: _buildDataTypeGuard<T>(key), getData: _buildDataGetter<T>(key) }) as const;
type WithDndId<T> = T & { dndId: string };
type DndData<PrivateKey extends symbol, Data extends Record<string | symbol, unknown> = Record<string, never>> = {
[k in PrivateKey]: true;
} & WithDndId<Data>;
//#region DndSourceData
const _SingleImageDndSourceDataKey = Symbol('SingleImageDndSourceData');
export type SingleImageDndSourceData = {
export type SingleImageDndSourceDataPrev = {
[_SingleImageDndSourceDataKey]: true;
imageDTO: ImageDTO;
};
export type SingleImageDndSourceData = DndData<typeof _SingleImageDndSourceDataKey, { imageDTO: ImageDTO }>;
export const singleImageDndSource = buildDndSourceApi<SingleImageDndSourceData>(_SingleImageDndSourceDataKey);
const _MultipleImageDndSourceDataKey = Symbol('MultipleImageDndSourceData');
export type MultipleImageDndSourceData = {
[_MultipleImageDndSourceDataKey]: true;
imageDTOs: ImageDTO[];
boardId: BoardId;
};
export type MultipleImageDndSourceData = DndData<
typeof _MultipleImageDndSourceDataKey,
{
imageDTOs: ImageDTO[];
boardId: BoardId;
}
>;
export const multipleImageDndSource = buildDndSourceApi<MultipleImageDndSourceData>(_MultipleImageDndSourceDataKey);
/**
@@ -40,7 +50,7 @@ export const multipleImageDndSource = buildDndSourceApi<MultipleImageDndSourceDa
*/
const sourceApis = [singleImageDndSource, multipleImageDndSource] as const;
export type DndSourceData = SingleImageDndSourceData | MultipleImageDndSourceData;
export const isDndSourceData = (data: DndData): data is DndSourceData => {
export const isDndSourceData = (data: AnyRecord): data is DndSourceData => {
for (const sourceApi of sourceApis) {
if (sourceApi.typeGuard(data)) {
return true;
@@ -52,27 +62,31 @@ export const isDndSourceData = (data: DndData): data is DndSourceData => {
//#endregion
//#region DndTargetData
const buildDndTargetApi = <T extends DndData>(
const buildDndTargetApi = <T extends BaseDndData>(
key: symbol,
validateDrop: (sourceData: DndSourceData, targetData: T) => boolean
) => ({ key, typeGuard: _buildDataTypeGuard<T>(key), getData: _buildDataGetter<T>(key), validateDrop }) as const;
const _SetGlobalReferenceImageDndTargetDataKey = Symbol('SetGlobalReferenceImageDndTargetData');
export type SetGlobalReferenceImageDndTargetData = {
[_SetGlobalReferenceImageDndTargetDataKey]: true;
globalReferenceImageId: string;
};
export type SetGlobalReferenceImageDndTargetData = DndData<
typeof _SetGlobalReferenceImageDndTargetDataKey,
{
globalReferenceImageId: string;
}
>;
export const setGlobalReferenceImageDndTarget = buildDndTargetApi<SetGlobalReferenceImageDndTargetData>(
_SetGlobalReferenceImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetRegionalGuidanceReferenceImageDndTargetDataKey = Symbol('SetRegionalGuidanceReferenceImageDndTargetData');
export type SetRegionalGuidanceReferenceImageDndTargetData = {
[_SetRegionalGuidanceReferenceImageDndTargetDataKey]: true;
regionalGuidanceId: string;
referenceImageId: string;
};
export type SetRegionalGuidanceReferenceImageDndTargetData = DndData<
typeof _SetRegionalGuidanceReferenceImageDndTargetDataKey,
{
regionalGuidanceId: string;
referenceImageId: string;
}
>;
export const setRegionalGuidanceReferenceImageDndTarget =
buildDndTargetApi<SetRegionalGuidanceReferenceImageDndTargetData>(
_SetRegionalGuidanceReferenceImageDndTargetDataKey,
@@ -80,36 +94,28 @@ export const setRegionalGuidanceReferenceImageDndTarget =
);
const _AddRasterLayerFromImageDndTargetDataKey = Symbol('AddRasterLayerFromImageDndTargetData');
export type AddRasterLayerFromImageDndTargetData = {
[_AddRasterLayerFromImageDndTargetDataKey]: true;
};
export type AddRasterLayerFromImageDndTargetData = DndData<typeof _AddRasterLayerFromImageDndTargetDataKey>;
export const addRasterLayerFromImageDndTarget = buildDndTargetApi<AddRasterLayerFromImageDndTargetData>(
_AddRasterLayerFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddControlLayerFromImageDndTargetDataKey = Symbol('AddControlLayerFromImageDndTargetData');
export type AddControlLayerFromImageDndTargetData = {
[_AddControlLayerFromImageDndTargetDataKey]: true;
};
export type AddControlLayerFromImageDndTargetData = DndData<typeof _AddControlLayerFromImageDndTargetDataKey>;
export const addControlLayerFromImageDndTarget = buildDndTargetApi<AddControlLayerFromImageDndTargetData>(
_AddControlLayerFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddInpaintMaskFromImageDndTargetDataKey = Symbol('AddInpaintMaskFromImageDndTargetData');
export type AddInpaintMaskFromImageDndTargetData = {
[_AddInpaintMaskFromImageDndTargetDataKey]: true;
};
export type AddInpaintMaskFromImageDndTargetData = DndData<typeof _AddInpaintMaskFromImageDndTargetDataKey>;
export const addInpaintMaskFromImageDndTarget = buildDndTargetApi<AddInpaintMaskFromImageDndTargetData>(
_AddInpaintMaskFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddRegionalGuidanceFromImageDndTargetDataKey = Symbol('AddRegionalGuidanceFromImageDndTargetData');
export type AddRegionalGuidanceFromImageDndTargetData = {
[_AddRegionalGuidanceFromImageDndTargetDataKey]: true;
};
export type AddRegionalGuidanceFromImageDndTargetData = DndData<typeof _AddRegionalGuidanceFromImageDndTargetDataKey>;
export const addRegionalGuidanceFromImageDndTarget = buildDndTargetApi<AddRegionalGuidanceFromImageDndTargetData>(
_AddRegionalGuidanceFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
@@ -118,9 +124,9 @@ export const addRegionalGuidanceFromImageDndTarget = buildDndTargetApi<AddRegion
const _AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey = Symbol(
'AddRegionalGuidanceReferenceImageFromImageDndTargetData'
);
export type AddRegionalGuidanceReferenceImageFromImageDndTargetData = {
[_AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey]: true;
};
export type AddRegionalGuidanceReferenceImageFromImageDndTargetData = DndData<
typeof _AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey
>;
export const addRegionalGuidanceReferenceImageFromImageDndTarget =
buildDndTargetApi<AddRegionalGuidanceReferenceImageFromImageDndTargetData>(
_AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey,
@@ -128,9 +134,9 @@ export const addRegionalGuidanceReferenceImageFromImageDndTarget =
);
const _AddGlobalReferenceImageFromImageDndTargetDataKey = Symbol('AddGlobalReferenceImageFromImageDndTargetData');
export type AddGlobalReferenceImageFromImageDndTargetData = {
[_AddGlobalReferenceImageFromImageDndTargetDataKey]: true;
};
export type AddGlobalReferenceImageFromImageDndTargetData = DndData<
typeof _AddGlobalReferenceImageFromImageDndTargetDataKey
>;
export const addGlobalReferenceImageFromImageDndTarget =
buildDndTargetApi<AddGlobalReferenceImageFromImageDndTargetData>(
_AddGlobalReferenceImageFromImageDndTargetDataKey,
@@ -138,51 +144,66 @@ export const addGlobalReferenceImageFromImageDndTarget =
);
const _ReplaceLayerWithImageDndTargetDataKey = Symbol('ReplaceLayerWithImageDndTargetData');
export type ReplaceLayerWithImageDndTargetData = {
[_ReplaceLayerWithImageDndTargetDataKey]: true;
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
};
export type ReplaceLayerWithImageDndTargetData = DndData<
typeof _ReplaceLayerWithImageDndTargetDataKey,
{
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
}
>;
export const replaceLayerWithImageDndTarget = buildDndTargetApi<ReplaceLayerWithImageDndTargetData>(
_ReplaceLayerWithImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetUpscaleInitialImageFromImageDndTargetDataKey = Symbol('SetUpscaleInitialImageFromImageDndTargetData');
export type SetUpscaleInitialImageFromImageDndTargetData = {
[_SetUpscaleInitialImageFromImageDndTargetDataKey]: true;
};
export type SetUpscaleInitialImageFromImageDndTargetData = DndData<
typeof _SetUpscaleInitialImageFromImageDndTargetDataKey
>;
export const setUpscaleInitialImageFromImageDndTarget = buildDndTargetApi<SetUpscaleInitialImageFromImageDndTargetData>(
_SetUpscaleInitialImageFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetNodeImageFieldDndTargetDataKey = Symbol('SetNodeImageFieldDndTargetData');
export type SetNodeImageFieldDndTargetData = {
[_SetNodeImageFieldDndTargetDataKey]: true;
nodeId: string;
fieldName: string;
};
export type SetNodeImageFieldDndTargetData = DndData<
typeof _SetNodeImageFieldDndTargetDataKey,
{
nodeId: string;
fieldName: string;
}
>;
export const setNodeImageFieldDndTarget = buildDndTargetApi<SetNodeImageFieldDndTargetData>(
_SetNodeImageFieldDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SelectForCompareDndTargetDataKey = Symbol('SelectForCompareDndTargetData');
export type SelectForCompareDndTargetData = {
[_SelectForCompareDndTargetDataKey]: true;
firstImageName?: string | null;
secondImageName?: string | null;
};
export type SelectForCompareDndTargetData = DndData<
typeof _SelectForCompareDndTargetDataKey,
{
firstImageName?: string | null;
secondImageName?: string | null;
}
>;
export const selectForCompareDndTarget = buildDndTargetApi<SelectForCompareDndTargetData>(
_SelectForCompareDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _ToastDndTargetDataKey = Symbol('ToastDndTargetData');
export type ToastDndTargetData = DndData<typeof _ToastDndTargetDataKey>;
export const ToastDndTarget = buildDndTargetApi<ToastDndTargetData>(
_ToastDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddToBoardDndTargetDataKey = Symbol('AddToBoardDndTargetData');
export type AddToBoardDndTargetData = {
[_AddToBoardDndTargetDataKey]: true;
boardId: string;
};
export type AddToBoardDndTargetData = DndData<
typeof _AddToBoardDndTargetDataKey,
{
boardId: string;
}
>;
export const addToBoardDndTarget = buildDndTargetApi<AddToBoardDndTargetData>(
_AddToBoardDndTargetDataKey,
(sourceData, targetData) => {
@@ -204,9 +225,7 @@ export const addToBoardDndTarget = buildDndTargetApi<AddToBoardDndTargetData>(
);
const _RemoveFromBoardDndTargetDataKey = Symbol('RemoveFromBoardDndTargetData');
export type RemoveFromBoardDndTargetData = {
[_RemoveFromBoardDndTargetDataKey]: true;
};
export type RemoveFromBoardDndTargetData = DndData<typeof _RemoveFromBoardDndTargetDataKey>;
export const removeFromBoardDndTarget = buildDndTargetApi<RemoveFromBoardDndTargetData>(
_RemoveFromBoardDndTargetDataKey,
(sourceData) => {
@@ -270,7 +289,7 @@ export type DndTargetData =
| RemoveFromBoardDndTargetData
| SelectForCompareDndTargetData;
export const isDndTargetData = (data: DndData): data is DndTargetData => {
export const isDndTargetData = (data: BaseDndData): data is DndTargetData => {
for (const targetApi of targetApis) {
if (targetApi.typeGuard(data)) {
return true;

View File

@@ -1,6 +1,3 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import { autoScrollForExternal } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/external';
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
@@ -11,7 +8,7 @@ import {
selectSelectedBoardId,
} from 'features/gallery/store/gallerySelectors';
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
import { useEffect, useMemo, useRef } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
@@ -26,7 +23,6 @@ type Props = {
export const BoardsList = ({ isPrivate }: Props) => {
const { t } = useTranslation();
const boardsListRef = useRef<HTMLDivElement>(null);
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const boardSearchText = useAppSelector(selectBoardSearchText);
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
@@ -75,14 +71,6 @@ export const BoardsList = ({ isPrivate }: Props) => {
}
}, [isPrivate, allowPrivateBoards, t]);
useEffect(() => {
const element = boardsListRef.current;
if (!element) {
return;
}
return combine(autoScrollForElements({ element }), autoScrollForExternal({ element }));
}, []);
return (
<Flex direction="column">
<Flex

View File

@@ -8,7 +8,7 @@ import { selectAllowPrivateBoards } from 'features/system/store/configSelectors'
import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useEffect, useRef } from 'react';
import { memo, useEffect, useState } from 'react';
import { BoardsList } from './BoardsList';
@@ -19,24 +19,20 @@ const overlayScrollbarsStyles: CSSProperties = {
const BoardsListWrapper = () => {
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const [os, osRef] = useState<OverlayScrollbarsComponentRef | null>(null);
useEffect(() => {
const elements = osRef.current?.osInstance()?.elements();
if (!elements) {
const element = os?.osInstance()?.elements().viewport;
if (!element) {
return;
}
return combine(
autoScrollForElements({ element: elements.viewport }),
autoScrollForExternal({ element: elements.viewport })
);
}, []);
return combine(autoScrollForElements({ element }), autoScrollForExternal({ element }));
}, [os]);
return (
<Box position="relative" w="full" h="full">
<Box position="absolute" top={0} right={0} bottom={0} left={0}>
<OverlayScrollbarsComponent
ref={osRef}
defer
style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options}
>

View File

@@ -15,7 +15,7 @@ import {
selectSelectedBoardId,
} from 'features/gallery/store/gallerySelectors';
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
@@ -36,7 +36,7 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const dndId = useId();
const onClick = useCallback(() => {
if (selectedBoardId !== board.board_id) {
dispatch(boardIdSelected({ boardId: board.board_id }));
@@ -47,8 +47,8 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
}, [selectedBoardId, board.board_id, autoAssignBoardOnClick, autoAddBoardId, dispatch]);
const targetData: AddToBoardDndTargetData = useMemo(
() => addToBoardDndTarget.getData({ boardId: board.board_id }),
[board.board_id]
() => addToBoardDndTarget.getData({ dndId, boardId: board.board_id }),
[board.board_id, dndId]
);
return (

View File

@@ -7,6 +7,7 @@ import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/lis
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd2/types';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
@@ -15,7 +16,7 @@ import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/Sized
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { MouseEventHandler } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
// This class name is used to calculate the number of images that fit in the gallery
@@ -83,6 +84,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
const store = useAppStore();
const [isDragging, setIsDragging] = useState(false);
const [element, ref] = useState<HTMLImageElement | null>(null);
const dndId = useId();
const selectIsSelectedForCompare = useMemo(
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name),
[imageDTO.image_name]
@@ -114,11 +116,15 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
// When we have multiple images selected, and the dragged image is part of the selection, initiate a
// multi-image drag.
if (gallery.selection.length > 1 && gallery.selection.includes(imageDTO)) {
return multipleImageDndSource.getData({ imageDTOs: gallery.selection, boardId: gallery.selectedBoardId });
return multipleImageDndSource.getData({
dndId: getPrefixedId('dnd-gallery-selection'),
imageDTOs: gallery.selection,
boardId: gallery.selectedBoardId,
});
}
// Otherwise, initiate a single-image drag
return singleImageDndSource.getData({ imageDTO });
return singleImageDndSource.getData({ dndId, imageDTO });
},
// This is a "local" drag start event, meaning that it is only called when this specific image is dragged.
onDragStart: (args) => {
@@ -145,7 +151,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
},
})
);
}, [imageDTO, element, store]);
}, [imageDTO, element, store, dndId]);
const isHovered = useBoolean(false);

View File

@@ -2,17 +2,17 @@ import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
import type { TypesafeDraggableData } from 'features/dnd/types';
import { DndImage } from 'features/dnd2/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { $hasProgressImage, $isProgressFromCanvas } from 'services/events/stores';
import { NoContentForViewer } from './NoContentForViewer';
@@ -21,22 +21,9 @@ import ProgressImage from './ProgressImage';
const CurrentImagePreview = () => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
const imageName = useAppSelector(selectLastSelectedImageName);
const hasProgressImage = useStore($hasProgressImage);
const isProgressFromCanvas = useStore($isProgressFromCanvas);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: 'current-image',
payloadType: 'IMAGE_DTO',
payload: { imageDTO },
};
}
}, [imageDTO]);
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
const timeoutId = useRef(0);
@@ -60,20 +47,7 @@ const CurrentImagePreview = () => {
justifyContent="center"
position="relative"
>
{hasProgressImage && !isProgressFromCanvas && shouldShowProgressInViewer ? (
<ProgressImage />
) : (
<IAIDndImage
imageDTO={imageDTO}
draggableData={draggableData}
isDropDisabled={true}
isUploadDisabled={true}
fitContainer
useThumbailFallback
noContentFallback={<NoContentForViewer />}
dataTestId="image-preview"
/>
)}
<ImageContent imageDTO={imageDTO} />
<Box position="absolute" top={0} insetInlineStart={0}>
<CanvasAlertsSendingToCanvas />
</Box>
@@ -107,6 +81,27 @@ const CurrentImagePreview = () => {
export default memo(CurrentImagePreview);
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
const hasProgressImage = useStore($hasProgressImage);
const isProgressFromCanvas = useStore($isProgressFromCanvas);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
if (hasProgressImage && !isProgressFromCanvas && shouldShowProgressInViewer) {
return <ProgressImage />;
}
if (!imageDTO) {
return <NoContentForViewer />;
}
return (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<DndImage dndId="current-image" imageDTO={imageDTO} />
</Flex>
);
});
ImageContent.displayName = 'ImageContent';
const initial: AnimationProps['initial'] = {
opacity: 0,
};

View File

@@ -1,4 +1,3 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/storeHooks';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { SelectForCompareDndTargetData } from 'features/dnd2/types';
@@ -14,16 +13,13 @@ export const ImageComparisonDroppable = memo(() => {
const targetData = useMemo<SelectForCompareDndTargetData>(() => {
const { firstImage, secondImage } = selectComparisonImages(store.getState());
return selectForCompareDndTarget.getData({
dndId: 'current-image',
firstImageName: firstImage?.image_name,
secondImageName: secondImage?.image_name,
});
}, [store]);
return (
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
<DndDropTarget targetData={targetData} label={t('gallery.selectForCompare')} />
</Flex>
);
return <DndDropTarget targetData={targetData} label={t('gallery.selectForCompare')} />;
});
ImageComparisonDroppable.displayName = 'ImageComparisonDroppable';

View File

@@ -7,12 +7,12 @@ import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallMode
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectIsLocal } from 'features/system/store/configSlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { PiImageBold } from 'react-icons/pi';
import { useMainModels } from 'services/api/hooks/modelsByType';
export const NoContentForViewer = () => {
export const NoContentForViewer = memo(() => {
const hasImages = useHasImages();
const [mainModels, { data }] = useMainModels();
const isLocal = useAppSelector(selectIsLocal);
@@ -113,4 +113,6 @@ export const NoContentForViewer = () => {
</Flex>
</Flex>
);
};
});
NoContentForViewer.displayName = 'NoContentForViewer';

View File

@@ -1,8 +1,8 @@
import { Flex, Image, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { DndImage } from 'features/dnd2/DndImage';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
@@ -10,7 +10,7 @@ import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { memo, useCallback, useId, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { NodeProps } from 'reactflow';
import { $lastProgressEvent } from 'services/events/stores';
@@ -18,6 +18,7 @@ import { $lastProgressEvent } from 'services/events/stores';
const CurrentImageNode = (props: NodeProps) => {
const imageDTO = useAppSelector(selectLastSelectedImage);
const lastProgressEvent = useStore($lastProgressEvent);
const dndId = useId();
if (lastProgressEvent?.image) {
return (
@@ -30,7 +31,7 @@ const CurrentImageNode = (props: NodeProps) => {
if (imageDTO) {
return (
<Wrapper nodeProps={props}>
<IAIDndImage imageDTO={imageDTO} isDragDisabled useThumbailFallback />
<DndImage dndId={dndId} imageDTO={imageDTO} />
</Wrapper>
);
}

View File

@@ -2,12 +2,14 @@ import { Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import type { SetNodeImageFieldDndTargetData } from 'features/dnd2/types';
import { setNodeImageFieldDndTarget } from 'features/dnd2/types';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageFieldInputInstance, ImageFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { memo, useCallback, useEffect, useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
@@ -17,11 +19,12 @@ import { $isConnected } from 'services/events/stores';
import type { FieldComponentProps } from './types';
const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInstance, ImageFieldInputTemplate>) => {
const { t } = useTranslation();
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const isConnected = useStore($isConnected);
const { currentData: imageDTO, isError } = useGetImageDTOQuery(field.value?.image_name ?? skipToken);
const dndId = useId();
const handleReset = useCallback(() => {
dispatch(
fieldImageValueChanged({
@@ -32,23 +35,9 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
);
}, [dispatch, field.name, nodeId]);
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: `node-${nodeId}-${field.name}`,
payloadType: 'IMAGE_DTO',
payload: { imageDTO },
};
}
}, [field.name, imageDTO, nodeId]);
const droppableData = useMemo<TypesafeDroppableData | undefined>(
() => ({
id: `node-${nodeId}-${field.name}`,
actionType: 'SET_NODES_IMAGE',
context: { nodeId, fieldName: field.name },
}),
[field.name, nodeId]
const targetData = useMemo<SetNodeImageFieldDndTargetData>(
() => setNodeImageFieldDndTarget.getData({ dndId, nodeId, fieldName: field.name }),
[dndId, field.name, nodeId]
);
const postUploadAction = useMemo<PostUploadAction>(
@@ -68,6 +57,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
return (
<Flex
position="relative"
className="nodrag"
w="full"
h="full"
@@ -78,21 +68,19 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
borderWidth={fieldTemplate.required && !field.value ? 1 : 0}
borderRadius="base"
>
<IAIDndImage
imageDTO={imageDTO}
droppableData={droppableData}
draggableData={draggableData}
postUploadAction={postUploadAction}
useThumbailFallback
uploadElement={<UploadElement />}
minSize={8}
>
<IAIDndImageIcon
onClick={handleReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold /> : undefined}
tooltip="Reset Image"
/>
</IAIDndImage>
{imageDTO && (
<>
<DndImage dndId={dndId} imageDTO={imageDTO} minW={8} minH={8} />
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={handleReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold /> : undefined}
tooltip="Reset Image"
/>
</Flex>
</>
)}
<DndDropTarget targetData={targetData} label={t('gallery.drop')} />
</Flex>
);
};

View File

@@ -1,5 +1,5 @@
import IAIDndImage from 'common/components/IAIDndImage';
import { memo } from 'react';
import { DndImage } from 'features/dnd2/DndImage';
import { memo, useId } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageOutput } from 'services/api/types';
@@ -9,9 +9,13 @@ type Props = {
const ImageOutputPreview = ({ output }: Props) => {
const { image } = output;
const { data: imageDTO } = useGetImageDTOQuery(image.image_name);
const { currentData: imageDTO } = useGetImageDTOQuery(image.image_name);
const dndId = useId();
if (!imageDTO) {
return null;
}
return <IAIDndImage imageDTO={imageDTO} />;
return <DndImage dndId={dndId} imageDTO={imageDTO} />;
};
export default memo(ImageOutputPreview);

View File

@@ -1,26 +1,21 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import type { TypesafeDroppableData } from 'features/dnd/types';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import { setUpscaleInitialImageFromImageDndTarget } from 'features/dnd2/types';
import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
import { useCallback, useId, useMemo } from 'react';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import type { PostUploadAction } from 'services/api/types';
const targetData = setUpscaleInitialImageFromImageDndTarget.getData({});
export const UpscaleInitialImage = () => {
const dispatch = useAppDispatch();
const imageDTO = useAppSelector(selectUpscaleInitialImage);
const droppableData = useMemo<TypesafeDroppableData | undefined>(
() => ({
actionType: 'SET_UPSCALE_INITIAL_IMAGE',
id: 'upscale-intial-image',
}),
[]
);
const dndId = useId();
const postUploadAction = useMemo<PostUploadAction>(
() => ({
type: 'SET_UPSCALE_INITIAL_IMAGE',
@@ -35,13 +30,9 @@ export const UpscaleInitialImage = () => {
return (
<Flex justifyContent="flex-start">
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
<IAIDndImage
droppableData={droppableData}
imageDTO={imageDTO || undefined}
postUploadAction={postUploadAction}
/>
{imageDTO && (
<>
<DndImage dndId={dndId} imageDTO={imageDTO} />
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={onReset}
@@ -66,6 +57,7 @@ export const UpscaleInitialImage = () => {
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
</>
)}
<DndDropTarget targetData={targetData} label={t('gallery.drop')} />
</Flex>
</Flex>
);