mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): migrate to pragmatic-drag-and-drop (wip 3)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
65
invokeai/frontend/web/src/features/dnd2/DndImage.tsx
Normal file
65
invokeai/frontend/web/src/features/dnd2/DndImage.tsx
Normal 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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user