tidy(ui): clean up old dnd stuff

This commit is contained in:
psychedelicious
2024-10-28 15:53:34 +10:00
parent fb5e462300
commit 31c9acb1fa
30 changed files with 31 additions and 1766 deletions

View File

@@ -56,9 +56,6 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.4.0",
"@dagrejs/dagre": "^1.1.4",
"@dagrejs/graphlib": "^2.2.4",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.43",
"@nanostores/react": "^0.7.3",

View File

@@ -17,15 +17,6 @@ dependencies:
'@dagrejs/graphlib':
specifier: ^2.2.4
version: 2.2.4
'@dnd-kit/core':
specifier: ^6.1.0
version: 6.1.0(react-dom@18.3.1)(react@18.3.1)
'@dnd-kit/sortable':
specifier: ^8.0.0
version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@18.3.1)
'@fontsource-variable/inter':
specifier: ^5.1.0
version: 5.1.0
@@ -1001,49 +992,6 @@ packages:
engines: {node: '>17.0.0'}
dev: false
/@dnd-kit/accessibility@3.1.0(react@18.3.1):
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.3.1
tslib: 2.7.0
dev: false
/@dnd-kit/core@6.1.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@dnd-kit/accessibility': 3.1.0(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.7.0
dev: false
/@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1):
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
peerDependencies:
'@dnd-kit/core': ^6.1.0
react: '>=16.8.0'
dependencies:
'@dnd-kit/core': 6.1.0(react-dom@18.3.1)(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
tslib: 2.7.0
dev: false
/@dnd-kit/utilities@3.2.2(react@18.3.1):
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.3.1
tslib: 2.7.0
dev: false
/@emotion/babel-plugin@11.12.0:
resolution: {integrity: sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==}
dependencies:

View File

@@ -19,7 +19,6 @@ import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { createStore } from 'app/store/store';
import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import AppDndContext from 'features/dnd/components/AppDndContext';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useLayoutEffect, useMemo } from 'react';
@@ -237,9 +236,7 @@ const InvokeAIUI = ({
<Provider store={store}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<AppDndContext>
<App config={config} studioInitAction={studioInitAction} />
</AppDndContext>
<App config={config} studioInitAction={studioInitAction} />
</ThemeLocaleProvider>
</React.Suspense>
</Provider>

View File

@@ -17,7 +17,6 @@ import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMi
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
import { addImageDeletionListeners } from 'app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners';
import { addImageDroppedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
@@ -95,7 +94,6 @@ addWorkflowLoadRequestedListener(startAppListening);
addUpdateAllNodesRequestedListener(startAppListening);
// DND
addImageDroppedListener(startAppListening);
addDndDroppedListener(startAppListening);
// Models

View File

@@ -1,333 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { deepClone } from 'common/util/deepClone';
import { selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
controlLayerAdded,
entityRasterized,
entitySelected,
inpaintMaskAdded,
rasterLayerAdded,
referenceImageAdded,
referenceImageIPAdapterImageChanged,
rgAdded,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasReferenceImageState,
CanvasRegionalGuidanceState,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { imagesApi } from 'services/api/endpoints/images';
export const dndDropped = createAction<{
overData: TypesafeDroppableData;
activeData: TypesafeDraggableData;
}>('dnd/dndDropped');
const log = logger('system');
export const addImageDroppedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: dndDropped,
effect: (action, { dispatch, getState }) => {
const { activeData, overData } = action.payload;
if (!isValidDrop(overData, activeData)) {
return;
}
if (activeData.payloadType === 'IMAGE_DTO') {
log.debug({ activeData, overData }, 'Image dropped');
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`);
} else if (activeData.payloadType === 'NODE_FIELD') {
log.debug({ activeData, overData }, 'Node field dropped');
} else {
log.debug({ activeData, overData }, `Unknown payload dropped`);
}
/**
* Image dropped on IP Adapter Layer
*/
if (
overData.actionType === 'SET_IPA_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { id } = overData.context;
dispatch(
referenceImageIPAdapterImageChanged({
entityIdentifier: { id, type: 'reference_image' },
imageDTO: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image dropped on RG Layer IP Adapter
*/
if (
overData.actionType === 'SET_RG_IP_ADAPTER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { id, referenceImageId } = overData.context;
dispatch(
rgIPAdapterImageChanged({
entityIdentifier: { id, type: 'regional_guidance' },
referenceImageId,
imageDTO: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image dropped on Raster layer
*/
if (
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasRasterLayerState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
return;
}
/**
/**
* Image dropped on Inpaint Mask
*/
if (
overData.actionType === 'ADD_INPAINT_MASK_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasInpaintMaskState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
return;
}
/**
/**
* Image dropped on Regional Guidance
*/
if (
overData.actionType === 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasRegionalGuidanceState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rgAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Raster layer
*/
if (
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const state = getState();
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(state).bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },
controlAdapter: deepClone(initialControlNet),
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
return;
}
if (
overData.actionType === 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const state = getState();
const ipAdapter = deepClone(selectDefaultIPAdapter(state));
ipAdapter.image = imageDTOToImageWithDims(activeData.payload.imageDTO);
const overrides: Partial<CanvasRegionalGuidanceState> = {
referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }],
};
dispatch(rgAdded({ overrides, isSelected: true }));
return;
}
if (
overData.actionType === 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const state = getState();
const ipAdapter = deepClone(selectDefaultIPAdapter(state));
ipAdapter.image = imageDTOToImageWithDims(activeData.payload.imageDTO);
const overrides: Partial<CanvasReferenceImageState> = {
ipAdapter,
};
dispatch(referenceImageAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Raster layer
*/
if (overData.actionType === 'REPLACE_LAYER_WITH_IMAGE' && activeData.payloadType === 'IMAGE_DTO') {
const state = getState();
const { entityIdentifier } = overData.context;
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(state).bbox.rect;
dispatch(entityRasterized({ entityIdentifier, imageObject, position: { x, y }, replaceObjects: true }));
dispatch(entitySelected({ entityIdentifier }));
return;
}
/**
* Image dropped on node image field
*/
if (
overData.actionType === 'SET_NODES_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { fieldName, nodeId } = overData.context;
dispatch(
fieldImageValueChanged({
nodeId,
fieldName,
value: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image selected for compare
*/
if (
overData.actionType === 'SELECT_FOR_COMPARE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { imageDTO } = activeData.payload;
dispatch(imageToCompareChanged(imageDTO));
return;
}
/**
* Image dropped on user board
*/
if (
overData.actionType === 'ADD_TO_BOARD' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { imageDTO } = activeData.payload;
const { boardId } = overData.context;
dispatch(
imagesApi.endpoints.addImageToBoard.initiate({
imageDTO,
board_id: boardId,
})
);
dispatch(selectionChanged([]));
return;
}
/**
* Image dropped on 'none' board
*/
if (
overData.actionType === 'REMOVE_FROM_BOARD' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { imageDTO } = activeData.payload;
dispatch(
imagesApi.endpoints.removeImageFromBoard.initiate({
imageDTO,
})
);
dispatch(selectionChanged([]));
return;
}
/**
* Image dropped on upscale initial image
*/
if (
overData.actionType === 'SET_UPSCALE_INITIAL_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { imageDTO } = activeData.payload;
dispatch(upscaleInitialImageChanged(imageDTO));
return;
}
/**
* Multiple images dropped on user board
*/
if (overData.actionType === 'ADD_TO_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') {
const imageDTOs = getState().gallery.selection;
const { boardId } = overData.context;
dispatch(
imagesApi.endpoints.addImagesToBoard.initiate({
imageDTOs,
board_id: boardId,
})
);
dispatch(selectionChanged([]));
return;
}
/**
* Multiple images dropped on 'none' board
*/
if (overData.actionType === 'REMOVE_FROM_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') {
const imageDTOs = getState().gallery.selection;
dispatch(
imagesApi.endpoints.removeImagesFromBoard.initiate({
imageDTOs,
})
);
dispatch(selectionChanged([]));
return;
}
},
});
};

View File

@@ -1,251 +0,0 @@
import type { ChakraProps, FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { IAILoadingImageFallback, IAINoContentFallback } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable';
const defaultUploadElement = <Icon as={PiUploadSimpleBold} boxSize={16} />;
const defaultNoContentFallback = <IAINoContentFallback icon={PiImageBold} />;
const baseStyles: SystemStyleObject = {
touchAction: 'none',
userSelect: 'none',
webkitUserSelect: 'none',
};
const sx: SystemStyleObject = {
...baseStyles,
'.gallery-image-container::before': {
content: '""',
display: 'inline-block',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
borderRadius: 'base',
},
'&[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
'&:hover>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
};
type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
withMetadataOverlay?: boolean;
isDragDisabled?: boolean;
isDropDisabled?: boolean;
isUploadDisabled?: boolean;
minSize?: number;
postUploadAction?: PostUploadAction;
imageSx?: ChakraProps['sx'];
fitContainer?: boolean;
droppableData?: TypesafeDroppableData;
draggableData?: TypesafeDraggableData;
dropLabel?: string;
isSelected?: boolean;
isSelectedForCompare?: boolean;
thumbnail?: boolean;
noContentFallback?: ReactElement;
useThumbailFallback?: boolean;
withHoverOverlay?: boolean;
children?: JSX.Element;
uploadElement?: ReactNode;
dataTestId?: string;
};
const IAIDndImage = (props: IAIDndImageProps) => {
const {
imageDTO,
onError,
onClick,
withMetadataOverlay = false,
isDropDisabled = false,
isDragDisabled = false,
isUploadDisabled = false,
minSize = 24,
postUploadAction,
imageSx,
fitContainer = false,
droppableData,
draggableData,
dropLabel,
isSelected = false,
isSelectedForCompare = false,
thumbnail = false,
noContentFallback = defaultNoContentFallback,
uploadElement = defaultUploadElement,
useThumbailFallback,
withHoverOverlay = false,
children,
dataTestId,
...rest
} = props;
const openInNewTab = useCallback(
(e: MouseEvent) => {
if (!imageDTO) {
return;
}
if (e.button !== 1) {
return;
}
window.open(imageDTO.image_url, '_blank');
},
[imageDTO]
);
const ref = useRef<HTMLDivElement>(null);
useImageContextMenu(imageDTO, ref);
return (
<Flex
ref={ref}
width="full"
height="full"
alignItems="center"
justifyContent="center"
position="relative"
minW={minSize ? minSize : undefined}
minH={minSize ? minSize : undefined}
userSelect="none"
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
// sx={withHoverOverlay ? sx : baseStyles}
data-selected={isSelectedForCompare ? 'selectedForCompare' : isSelected ? 'selected' : undefined}
{...rest}
>
{imageDTO && (
<Flex
// className="gallery-image-container"
w="full"
h="full"
position={fitContainer ? 'absolute' : 'relative'}
alignItems="center"
justifyContent="center"
>
<Image
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
fallbackStrategy="beforeLoadOrError"
fallbackSrc={useThumbailFallback ? imageDTO.thumbnail_url : undefined}
fallback={useThumbailFallback ? undefined : <IAILoadingImageFallback image={imageDTO} />}
onError={onError}
draggable={false}
w={imageDTO.width}
objectFit="contain"
maxW="full"
maxH="full"
borderRadius="base"
sx={imageSx}
data-testid={dataTestId}
/>
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
</Flex>
)}
{!imageDTO && !isUploadDisabled && (
<UploadButton
isUploadDisabled={isUploadDisabled}
postUploadAction={postUploadAction}
uploadElement={uploadElement}
minSize={minSize}
/>
)}
{!imageDTO && isUploadDisabled && noContentFallback}
{imageDTO && !isDragDisabled && (
<IAIDraggable
data={draggableData}
disabled={isDragDisabled || !imageDTO}
onClick={onClick}
onAuxClick={openInNewTab}
/>
)}
{children}
{!isDropDisabled && <IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />}
</Flex>
);
};
export default memo(IAIDndImage);
const UploadButton = memo(
({
isUploadDisabled,
postUploadAction,
uploadElement,
minSize,
}: {
isUploadDisabled: boolean;
postUploadAction?: PostUploadAction;
uploadElement: ReactNode;
minSize: number;
}) => {
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
isDisabled: isUploadDisabled,
});
const uploadButtonStyles = useMemo<SystemStyleObject>(() => {
const styles: SystemStyleObject = {
minH: minSize,
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: '0.1s',
color: 'base.500',
};
if (!isUploadDisabled) {
Object.assign(styles, {
cursor: 'pointer',
bg: 'base.700',
_hover: {
bg: 'base.650',
color: 'base.300',
},
});
}
return styles;
}, [isUploadDisabled, minSize]);
return (
<Flex sx={uploadButtonStyles} {...getUploadButtonProps()}>
<input {...getUploadInputProps()} />
{uploadElement}
</Flex>
);
}
);
UploadButton.displayName = 'UploadButton';

View File

@@ -1,38 +0,0 @@
import type { BoxProps } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import { useDraggableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import type { TypesafeDraggableData } from 'features/dnd/types';
import { memo, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
type IAIDraggableProps = BoxProps & {
disabled?: boolean;
data?: TypesafeDraggableData;
};
const IAIDraggable = (props: IAIDraggableProps) => {
const { data, disabled, ...rest } = props;
const dndId = useRef(uuidv4());
const { attributes, listeners, setNodeRef } = useDraggableTypesafe({
id: dndId.current,
disabled,
data,
});
return (
<Box
ref={setNodeRef}
position="absolute"
w="full"
h="full"
top={0}
insetInlineStart={0}
{...attributes}
{...listeners}
{...rest}
/>
);
};
export default memo(IAIDraggable);

View File

@@ -1,64 +0,0 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { memo } from 'react';
type Props = {
isOver: boolean;
label?: string;
withBackdrop?: boolean;
};
const IAIDropOverlay = (props: Props) => {
const { isOver, label, withBackdrop = true } = props;
return (
<Flex position="absolute" top={0} right={0} bottom={0} left={0}>
<Flex
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
w="full"
h="full"
bg={withBackdrop ? 'base.900' : 'transparent'}
opacity={0.7}
borderRadius="base"
alignItems="center"
justifyContent="center"
transitionProperty="common"
transitionDuration="0.1s"
/>
<Flex
position="absolute"
top={0.5}
right={0.5}
bottom={0.5}
left={0.5}
opacity={1}
borderWidth={1.5}
borderColor={isOver ? 'invokeYellow.300' : 'base.500'}
borderRadius="base"
borderStyle="dashed"
transitionProperty="common"
transitionDuration="0.1s"
alignItems="center"
justifyContent="center"
>
{label && (
<Text
fontSize="lg"
fontWeight="semibold"
color={isOver ? 'invokeYellow.300' : 'base.500'}
transitionProperty="common"
transitionDuration="0.1s"
textAlign="center"
>
{label}
</Text>
)}
</Flex>
</Flex>
);
};
export default memo(IAIDropOverlay);

View File

@@ -1,14 +0,0 @@
import type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react';
type IAIDroppableProps = {
dropLabel?: string;
disabled?: boolean;
data?: TypesafeDroppableData;
};
const IAIDroppable = (props: IAIDroppableProps) => {
return null;
};
export default memo(IAIDroppable);

View File

@@ -2,11 +2,11 @@ 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 IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
@@ -55,7 +55,7 @@ export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData, p
<>
<DndImage imageDTO={imageDTO} />
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
<DndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}

View File

@@ -1,71 +0,0 @@
import { MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import { logger } from 'app/logging/logger';
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
import { useAppDispatch } from 'app/store/storeHooks';
import DndOverlay from 'features/dnd/components/DndOverlay';
import type { DragEndEvent, DragStartEvent, TypesafeDraggableData } from 'features/dnd/types';
import { customPointerWithin } from 'features/dnd/util/customPointerWithin';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { DndContextTypesafe } from './DndContextTypesafe';
const log = logger('system');
const AppDndContext = (props: PropsWithChildren) => {
const [activeDragData, setActiveDragData] = useState<TypesafeDraggableData | null>(null);
const dispatch = useAppDispatch();
const handleDragStart = useCallback((event: DragStartEvent) => {
log.trace({ dragData: event.active.data.current }, 'Drag started');
const activeData = event.active.data.current;
if (!activeData) {
return;
}
setActiveDragData(activeData);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
log.trace({ dragData: event.active.data.current }, 'Drag ended');
const overData = event.over?.data.current;
if (!activeDragData || !overData) {
return;
}
dispatch(dndDropped({ overData, activeData: activeDragData }));
setActiveDragData(null);
},
[activeDragData, dispatch]
);
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 10 },
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint: { distance: 10 },
});
// TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos
// Alternatively, fix `rectIntersection` collection detection to work with the drag overlay
// (currently the drag element collision rect is not correctly calculated)
// const keyboardSensor = useSensor(KeyboardSensor);
const sensors = useSensors(mouseSensor, touchSensor);
return (
<DndContextTypesafe
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={customPointerWithin}
autoScroll={false}
>
{props.children}
<DndOverlay activeDragData={activeDragData} />
</DndContextTypesafe>
);
};
export default memo(AppDndContext);

View File

@@ -1,6 +0,0 @@
import { DndContext } from '@dnd-kit/core';
import type { DndContextTypesafeProps } from 'features/dnd/types';
export function DndContextTypesafe(props: DndContextTypesafeProps) {
return <DndContext {...props} />;
}

View File

@@ -1,52 +0,0 @@
import { DragOverlay } from '@dnd-kit/core';
import { useScaledModifer } from 'features/dnd/hooks/useScaledCenteredModifer';
import type { TypesafeDraggableData } from 'features/dnd/types';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import DragPreview from './DragPreview';
type DndOverlayProps = {
activeDragData: TypesafeDraggableData | null;
};
const DndOverlay = (props: DndOverlayProps) => {
const scaledModifier = useScaledModifer();
const modifiers = useMemo(() => [scaledModifier], [scaledModifier]);
return (
<DragOverlay dropAnimation={null} modifiers={modifiers} style={dragOverlayStyles}>
<AnimatePresence>
{props.activeDragData && (
<motion.div layout key="overlay-drag-image" initial={initial} animate={animate}>
<DragPreview dragData={props.activeDragData} />
</motion.div>
)}
</AnimatePresence>
</DragOverlay>
);
};
export default memo(DndOverlay);
const dragOverlayStyles: CSSProperties = {
width: 'min-content',
height: 'min-content',
cursor: 'grabbing',
pointerEvents: 'none',
userSelect: 'none',
// expand overlay to prevent cursor from going outside it and displaying
padding: '10rem',
};
const initial: AnimationProps['initial'] = {
opacity: 0,
scale: 0.7,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
scale: 1,
transition: { duration: 0.1 },
};

View File

@@ -1,23 +0,0 @@
import type { DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { DndContextTypesafe } from './DndContextTypesafe';
type Props = PropsWithChildren & {
items: string[];
onDragEnd(event: DragEndEvent): void;
};
const DndSortable = (props: Props) => {
return (
<DndContextTypesafe onDragEnd={props.onDragEnd}>
<SortableContext items={props.items} strategy={verticalListSortingStrategy}>
{props.children}
</SortableContext>
</DndContextTypesafe>
);
};
export default memo(DndSortable);

View File

@@ -1,83 +0,0 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Heading, Image, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { TypesafeDraggableData } from 'features/dnd/types';
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type OverlayDragImageProps = {
dragData: TypesafeDraggableData | null;
};
const BOX_SIZE = 28;
const imageStyles: ChakraProps['sx'] = {
w: BOX_SIZE,
h: BOX_SIZE,
maxW: BOX_SIZE,
maxH: BOX_SIZE,
shadow: 'dark-lg',
borderRadius: 'lg',
opacity: 0.3,
borderColor: 'base.200',
bg: 'base.900',
color: 'base.100',
};
const multiImageStyles: ChakraProps['sx'] = {
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
flexDir: 'column',
...imageStyles,
};
const DragPreview = (props: OverlayDragImageProps) => {
const { t } = useTranslation();
const selectionCount = useAppSelector(selectSelectionCount);
if (!props.dragData) {
return null;
}
if (props.dragData.payloadType === 'NODE_FIELD') {
const { field, fieldTemplate } = props.dragData.payload;
return (
<Box
position="relative"
p={2}
px={3}
opacity={0.7}
bg="base.300"
borderRadius="base"
boxShadow="dark-lg"
whiteSpace="nowrap"
fontSize="sm"
>
<Text>{field.label || fieldTemplate.title}</Text>
</Box>
);
}
if (props.dragData.payloadType === 'IMAGE_DTO') {
const { thumbnail_url, width, height } = props.dragData.payload.imageDTO;
return (
<Box position="relative" width="full" height="full" display="flex" alignItems="center" justifyContent="center">
<Image sx={imageStyles} objectFit="contain" src={thumbnail_url} width={width} height={height} />
</Box>
);
}
if (props.dragData.payloadType === 'GALLERY_SELECTION') {
return (
<Flex sx={multiImageStyles}>
<Heading>{selectionCount}</Heading>
<Heading size="sm">{t('parameters.images')}</Heading>
</Flex>
);
}
return null;
};
export default memo(DragPreview);

View File

@@ -1,15 +0,0 @@
import { useDraggable, useDroppable } from '@dnd-kit/core';
import type {
UseDraggableTypesafeArguments,
UseDraggableTypesafeReturnValue,
UseDroppableTypesafeArguments,
UseDroppableTypesafeReturnValue,
} from 'features/dnd/types';
export function useDroppableTypesafe(props: UseDroppableTypesafeArguments) {
return useDroppable(props) as UseDroppableTypesafeReturnValue;
}
export function useDraggableTypesafe(props: UseDraggableTypesafeArguments) {
return useDraggable(props) as UseDraggableTypesafeReturnValue;
}

View File

@@ -1,47 +0,0 @@
import type { Modifier } from '@dnd-kit/core';
import { getEventCoordinates } from '@dnd-kit/utilities';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { $viewport } from 'features/nodes/store/nodesSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react';
/**
* Applies scaling to the drag transform (if on node editor tab) and centers it on cursor.
*/
export const useScaledModifer = () => {
const activeTabName = useAppSelector(selectActiveTab);
const workflowsViewport = useStore($viewport);
const modifier: Modifier = useCallback(
({ activatorEvent, draggingNodeRect, transform }) => {
if (draggingNodeRect && activatorEvent) {
const zoom = activeTabName === 'workflows' ? workflowsViewport.zoom : 1;
const activatorCoordinates = getEventCoordinates(activatorEvent);
if (!activatorCoordinates) {
return transform;
}
const offsetX = activatorCoordinates.x - draggingNodeRect.left;
const offsetY = activatorCoordinates.y - draggingNodeRect.top;
const x = transform.x + offsetX - draggingNodeRect.width / 2;
const y = transform.y + offsetY - draggingNodeRect.height / 2;
const scaleX = transform.scaleX * zoom;
const scaleY = transform.scaleY * zoom;
return {
x,
y,
scaleX,
scaleY,
};
}
return transform;
},
[activeTabName, workflowsViewport.zoom]
);
return modifier;
};

View File

@@ -1,185 +0,0 @@
// type-safe dnd from https://github.com/clauderic/dnd-kit/issues/935
import type {
Active,
Collision,
DndContextProps,
Over,
Translate,
useDraggable as useOriginalDraggable,
UseDraggableArguments,
useDroppable as useOriginalDroppable,
UseDroppableArguments,
} from '@dnd-kit/core';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { BoardId } from 'features/gallery/store/types';
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
import type { ImageDTO } from 'services/api/types';
type BaseDropData = {
id: string;
};
export type IPAImageDropData = BaseDropData & {
actionType: 'SET_IPA_IMAGE';
context: {
id: string;
};
};
export type RGIPAdapterImageDropData = BaseDropData & {
actionType: 'SET_RG_IP_ADAPTER_IMAGE';
context: {
id: string;
referenceImageId: string;
};
};
export type AddRasterLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE';
};
export type AddControlLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE';
};
type AddInpaintMaskFromImageDropData = BaseDropData & {
actionType: 'ADD_INPAINT_MASK_FROM_IMAGE';
};
type AddRegionalGuidanceFromImageDropData = BaseDropData & {
actionType: 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE';
};
export type AddRegionalReferenceImageFromImageDropData = BaseDropData & {
actionType: 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE';
};
export type AddGlobalReferenceImageFromImageDropData = BaseDropData & {
actionType: 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE';
};
export type ReplaceLayerImageDropData = BaseDropData & {
actionType: 'REPLACE_LAYER_WITH_IMAGE';
context: {
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
};
};
type UpscaleInitialImageDropData = BaseDropData & {
actionType: 'SET_UPSCALE_INITIAL_IMAGE';
};
type NodesImageDropData = BaseDropData & {
actionType: 'SET_NODES_IMAGE';
context: {
nodeId: string;
fieldName: string;
};
};
export type AddToBoardDropData = BaseDropData & {
actionType: 'ADD_TO_BOARD';
context: { boardId: string };
};
export type RemoveFromBoardDropData = BaseDropData & {
actionType: 'REMOVE_FROM_BOARD';
};
export type SelectForCompareDropData = BaseDropData & {
actionType: 'SELECT_FOR_COMPARE';
context: {
firstImageName?: string | null;
secondImageName?: string | null;
};
};
export type TypesafeDroppableData =
| NodesImageDropData
| AddToBoardDropData
| RemoveFromBoardDropData
| IPAImageDropData
| RGIPAdapterImageDropData
| SelectForCompareDropData
| UpscaleInitialImageDropData
| AddRasterLayerFromImageDropData
| AddControlLayerFromImageDropData
| ReplaceLayerImageDropData
| AddRegionalReferenceImageFromImageDropData
| AddGlobalReferenceImageFromImageDropData
| AddInpaintMaskFromImageDropData
| AddRegionalGuidanceFromImageDropData;
type BaseDragData = {
id: string;
};
type NodeFieldDraggableData = BaseDragData & {
payloadType: 'NODE_FIELD';
payload: {
nodeId: string;
field: FieldInputInstance;
fieldTemplate: FieldInputTemplate;
};
};
export type ImageDraggableData = BaseDragData & {
payloadType: 'IMAGE_DTO';
payload: { imageDTO: ImageDTO };
};
export type GallerySelectionDraggableData = BaseDragData & {
payloadType: 'GALLERY_SELECTION';
payload: { boardId: BoardId };
};
export type TypesafeDraggableData = NodeFieldDraggableData | ImageDraggableData | GallerySelectionDraggableData;
export interface UseDroppableTypesafeArguments extends Omit<UseDroppableArguments, 'data'> {
data?: TypesafeDroppableData;
}
export type UseDroppableTypesafeReturnValue = Omit<ReturnType<typeof useOriginalDroppable>, 'active' | 'over'> & {
active: TypesafeActive | null;
over: TypesafeOver | null;
};
export interface UseDraggableTypesafeArguments extends Omit<UseDraggableArguments, 'data'> {
data?: TypesafeDraggableData;
}
export type UseDraggableTypesafeReturnValue = Omit<ReturnType<typeof useOriginalDraggable>, 'active' | 'over'> & {
active: TypesafeActive | null;
over: TypesafeOver | null;
};
interface TypesafeActive extends Omit<Active, 'data'> {
data: React.MutableRefObject<TypesafeDraggableData | undefined>;
}
interface TypesafeOver extends Omit<Over, 'data'> {
data: React.MutableRefObject<TypesafeDroppableData | undefined>;
}
interface DragEvent {
activatorEvent: Event;
active: TypesafeActive;
collisions: Collision[] | null;
delta: Translate;
over: TypesafeOver | null;
}
export interface DragStartEvent extends Pick<DragEvent, 'active'> {}
interface DragMoveEvent extends DragEvent {}
interface DragOverEvent extends DragMoveEvent {}
export interface DragEndEvent extends DragEvent {}
interface DragCancelEvent extends DragEndEvent {}
export interface DndContextTypesafeProps
extends Omit<DndContextProps, 'onDragStart' | 'onDragMove' | 'onDragOver' | 'onDragEnd' | 'onDragCancel'> {
onDragStart?(event: DragStartEvent): void;
onDragMove?(event: DragMoveEvent): void;
onDragOver?(event: DragOverEvent): void;
onDragEnd?(event: DragEndEvent): void;
onDragCancel?(event: DragCancelEvent): void;
}

View File

@@ -1,34 +0,0 @@
import type { CollisionDetection } from '@dnd-kit/core';
import { pointerWithin } from '@dnd-kit/core';
/**
* Filters out droppable elements that are overflowed, then applies the pointerWithin collision detection.
*
* Fixes collision detection firing on droppables that are not visible, having been scrolled out of view.
*
* See https://github.com/clauderic/dnd-kit/issues/1198
*/
export const customPointerWithin: CollisionDetection = (arg) => {
if (!arg.pointerCoordinates) {
// sanity check
return [];
}
// Get all elements at the pointer coordinates. This excludes elements which are overflowed,
// so it won't include the droppable elements that are scrolled out of view.
const targetElements = document.elementsFromPoint(arg.pointerCoordinates.x, arg.pointerCoordinates.y);
const filteredDroppableContainers = arg.droppableContainers.filter((container) => {
if (!container.node.current) {
return false;
}
// Only include droppable elements that are in the list of elements at the pointer coordinates.
return targetElements.includes(container.node.current);
});
// Run the provided collision detection with the filtered droppable elements.
return pointerWithin({
...arg,
droppableContainers: filteredDroppableContainers,
});
};

View File

@@ -1,83 +0,0 @@
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?: TypesafeDraggableData | null) => {
if (!overData || !activeData) {
return false;
}
const { actionType } = overData;
const { payloadType } = activeData;
if (overData.id === activeData.id) {
return false;
}
switch (actionType) {
case 'SET_IPA_IMAGE':
case 'SET_RG_IP_ADAPTER_IMAGE':
case 'ADD_RASTER_LAYER_FROM_IMAGE':
case 'ADD_CONTROL_LAYER_FROM_IMAGE':
case 'ADD_INPAINT_MASK_FROM_IMAGE':
case 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE':
case 'SET_UPSCALE_INITIAL_IMAGE':
case 'SET_NODES_IMAGE':
case 'SELECT_FOR_COMPARE':
case 'REPLACE_LAYER_WITH_IMAGE':
case 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE':
case 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'ADD_TO_BOARD': {
// If the board is the same, don't allow the drop
// Check the payload types
const isPayloadValid = ['IMAGE_DTO', 'GALLERY_SELECTION'].includes(payloadType);
if (!isPayloadValid) {
return false;
}
// Check if the image's board is the board we are dragging onto
if (payloadType === 'IMAGE_DTO') {
const { imageDTO } = activeData.payload;
const currentBoard = imageDTO.board_id ?? 'none';
const destinationBoard = overData.context.boardId;
return currentBoard !== destinationBoard;
}
if (payloadType === 'GALLERY_SELECTION') {
// Assume all images are on the same board - this is true for the moment
const currentBoard = activeData.payload.boardId;
const destinationBoard = overData.context.boardId;
return currentBoard !== destinationBoard;
}
return false;
}
case 'REMOVE_FROM_BOARD': {
// If the board is the same, don't allow the drop
// Check the payload types
const isPayloadValid = ['IMAGE_DTO', 'GALLERY_SELECTION'].includes(payloadType);
if (!isPayloadValid) {
return false;
}
// Check if the image's board is the board we are dragging onto
if (payloadType === 'IMAGE_DTO') {
const { imageDTO } = activeData.payload;
const currentBoard = imageDTO.board_id ?? 'none';
return currentBoard !== 'none';
}
if (payloadType === 'GALLERY_SELECTION') {
const currentBoard = activeData.payload.boardId;
return currentBoard !== 'none';
}
return false;
}
default:
return false;
}
};

View File

@@ -21,7 +21,7 @@ type Props = Omit<IconButtonProps, 'aria-label' | 'onClick' | 'tooltip'> & {
tooltip: string;
};
const IAIDndImageIcon = (props: Props) => {
export const DndImageIcon = memo((props: Props) => {
const { onClick, tooltip, icon, ...rest } = props;
return (
@@ -35,6 +35,6 @@ const IAIDndImageIcon = (props: Props) => {
{...rest}
/>
);
};
});
export default memo(IAIDndImageIcon);
DndImageIcon.displayName = 'DndImageIcon';

View File

@@ -1,335 +0,0 @@
/**
* @jsxRuntime classic
* @jsx jsx
*/
import Button from '@atlaskit/button/new';
import ImageIcon from '@atlaskit/icon/core/migration/image';
import { easeInOut } from '@atlaskit/motion/curves';
import { largeDurationMs, mediumDurationMs } from '@atlaskit/motion/durations';
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
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 { token } from '@atlaskit/tokens';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css } from '@emotion/react';
import { bind } from 'bind-event-listener';
import { Fragment, memo, useCallback, useEffect, useRef, useState } from 'react';
import invariant from 'tiny-invariant';
import { GlobalStyles } from './util/global-styles';
const galleryStyles = css({
display: 'flex',
width: '70vw',
alignItems: 'center',
justifyContent: 'center',
gap: 'var(--grid)',
flexWrap: 'wrap',
});
const imageStyles = css({
display: 'block',
// borrowing values from pinterest
// ratio: 0.6378378378
width: '216px',
height: '340px',
objectFit: 'cover',
});
const uploadStyles = css({
// overflow: 'hidden',
position: 'relative',
// using these to hide the details
borderRadius: 'calc(var(--grid) * 2)',
overflow: 'hidden',
transition: `opacity ${largeDurationMs}ms ${easeInOut}, filter ${largeDurationMs}ms ${easeInOut}`,
});
const loadingStyles = css({
opacity: '0',
filter: 'blur(1.5rem)',
});
const readyStyles = css({
opacity: '1',
filter: 'blur(0)',
});
const uploadDetailStyles = css({
display: 'flex',
boxSizing: 'border-box',
width: '100%',
padding: 'var(--grid)',
position: 'absolute',
bottom: 0,
gap: 'var(--grid)',
flexDirection: 'row',
// background: token('color.background.sunken', fallbackColor),
backgroundColor: 'rgba(255,255,255,0.5)',
});
const uploadFilenameStyles = css({
flexGrow: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
type UserUpload = {
type: 'image';
dataUrl: string;
name: string;
size: number;
};
const Upload = memo(function Upload({ upload }: { upload: UserUpload }) {
const [state, setState] = useState<'loading' | 'ready'>('loading');
const clearTimeout = useRef<() => void>(() => {});
useEffect(function mount() {
return function unmount() {
clearTimeout.current();
};
}, []);
return (
<div css={[uploadStyles, state === 'loading' ? loadingStyles : readyStyles]}>
<img
src={upload.dataUrl}
css={imageStyles}
onLoad={() => {
// this is the _only_ way I could find to get the animation to run
// correctly every time in all browsers
// setTimeout(fn, 0) -> sometimes wouldn't work in chrome (event nesting two)
// requestAnimationFrame -> nope (event nesting two)
// requestIdleCallback -> nope (doesn't work in safari)
// I can find no reliable hook for applying the `ready` state,
// this is the best I could manage 😩
const timerId = setTimeout(() => setState('ready'), 100);
clearTimeout.current = () => window.clearTimeout(timerId);
}}
/>
<div css={uploadDetailStyles}>
<em css={uploadFilenameStyles}>{upload.name}</em>
<code>{Math.round(upload.size / 1000)}kB</code>
</div>
</div>
);
});
const Gallery = memo(function Gallery({ uploads: uploads }: { uploads: UserUpload[] }) {
if (!uploads.length) {
return null;
}
return (
<div css={galleryStyles}>
{uploads.map((upload, index) => (
<Upload upload={upload} key={index} />
))}
</div>
);
});
const fileStyles = css({
display: 'flex',
flexDirection: 'column',
padding: 'calc(var(--grid) * 6) calc(var(--grid) * 4)',
boxSizing: 'border-box',
alignItems: 'center',
justifyContent: 'center',
background: token('elevation.surface.sunken', '#091E4208'),
borderRadius: 'var(--border-radius)',
transition: `all ${mediumDurationMs}ms ${easeInOut}`,
border: '2px dashed transparent',
width: '100%',
gap: token('space.300', '24px'),
});
const textStyles = css({
color: token('color.text.disabled', '#091E424F'),
fontSize: '1.4rem',
display: 'flex',
alignItems: 'center',
gap: token('space.075'),
});
const overStyles = css({
background: token('color.background.selected.hovered', '#CCE0FF'),
color: token('color.text.selected', '#0C66E4'),
borderColor: token('color.border.brand', '#0C66E4'),
});
const potentialStyles = css({
borderColor: token('color.border.brand', '#0C66E4'),
});
const appStyles = css({
display: 'flex',
alignItems: 'center',
gap: 'calc(var(--grid) * 2)',
flexDirection: 'column',
});
const displayNoneStyles = css({ display: 'none' });
function Uploader() {
const ref = useRef<HTMLDivElement | null>(null);
const [state, setState] = useState<'idle' | 'potential' | 'over'>('idle');
const [uploads, setUploads] = useState<UserUpload[]>([]);
/**
* Creating a stable reference so that we can use it in our unmount effect.
*
* If we used uploads as a dependency in the second `useEffect` it would run
* every time the uploads changed, which is not desirable.
*/
const stableUploadsRef = useRef<UserUpload[]>(uploads);
useEffect(() => {
stableUploadsRef.current = uploads;
}, [uploads]);
useEffect(() => {
return () => {
/**
* MDN recommends explicitly releasing the object URLs when possible,
* instead of relying just on the browser's garbage collection.
*/
stableUploadsRef.current.forEach((upload) => {
URL.revokeObjectURL(upload.dataUrl);
});
};
}, []);
const addUpload = useCallback((file: File | null) => {
if (!file) {
return;
}
if (!file.type.startsWith('image/')) {
return;
}
const upload: UserUpload = {
type: 'image',
dataUrl: URL.createObjectURL(file),
name: file.name,
size: file.size,
};
setUploads((current) => [...current, upload]);
}, []);
const onFileInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.currentTarget.files ?? []);
files.forEach(addUpload);
},
[addUpload]
);
useEffect(() => {
const el = ref.current;
invariant(el);
return combine(
dropTargetForExternal({
element: el,
canDrop: containsFiles,
onDragEnter: () => setState('over'),
onDragLeave: () => setState('potential'),
onDrop: async ({ source }) => {
const files = await getFiles({ source });
files.forEach((file) => {
if (file == null) {
return;
}
if (!file.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.readAsDataURL(file);
// for simplicity:
// - not handling errors
// - not aborting the
// - not unbinding the event listener when the effect is removed
bind(reader, {
type: 'load',
listener(event) {
const result = reader.result;
if (typeof result === 'string') {
const upload: UserUpload = {
type: 'image',
dataUrl: result,
name: file.name,
size: file.size,
};
setUploads((current) => [...current, upload]);
}
},
});
});
},
}),
monitorForExternal({
canMonitor: containsFiles,
onDragStart: () => {
setState('potential');
preventUnhandled.start();
},
onDrop: () => {
setState('idle');
preventUnhandled.stop();
},
})
);
});
/**
* We trigger the file input manually when clicking the button. This also
* works when selecting the button using a keyboard.
*
* We do this for two reasons:
*
* 1. Styling file inputs is very limited.
* 2. Associating the button as a label for the input only gives us pointer
* support, but does not work for keyboard.
*/
const inputRef = useRef<HTMLInputElement>(null);
const onInputTriggerClick = useCallback(() => {
inputRef.current?.click();
}, []);
return (
<div css={appStyles}>
<div
ref={ref}
data-testid="drop-target"
css={[fileStyles, state === 'over' ? overStyles : state === 'potential' ? potentialStyles : undefined]}
>
<strong css={textStyles}>
Drop some images on me! <ImageIcon color="currentColor" spacing="spacious" label="" />
</strong>
<Button onClick={onInputTriggerClick}>Select images</Button>
<input
ref={inputRef}
css={displayNoneStyles}
id="file-input"
onChange={onFileInputChange}
type="file"
accept="image/*"
multiple
/>
</div>
<Gallery uploads={uploads} />
</div>
);
}
export default function Example() {
return (
<Fragment>
<GlobalStyles />
<Uploader />
</Fragment>
);
}

View File

@@ -1,7 +1,7 @@
import { useShiftModifier } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import type { MouseEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -32,7 +32,7 @@ export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
}
return (
<IAIDndImageIcon
<DndImageIcon
onClick={onClick}
icon={<PiTrashSimpleFill />}
tooltip={t('gallery.deleteImage_one')}

View File

@@ -1,4 +1,4 @@
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,7 +18,7 @@ export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) =>
}, [imageDTO, imageViewer]);
return (
<IAIDndImageIcon
<DndImageIcon
onClick={onClick}
icon={<PiArrowsOutBold />}
tooltip={t('gallery.openInViewer')}

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import { memo, useCallback } from 'react';
import { PiStarBold, PiStarFill } from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
@@ -25,7 +25,7 @@ export const GalleryImageStarIconButton = memo(({ imageDTO }: Props) => {
if (customStarUi) {
return (
<IAIDndImageIcon
<DndImageIcon
onClick={toggleStarredState}
icon={imageDTO.starred ? customStarUi.on.icon : customStarUi.off.icon}
tooltip={imageDTO.starred ? customStarUi.on.text : customStarUi.off.text}
@@ -37,7 +37,7 @@ export const GalleryImageStarIconButton = memo(({ imageDTO }: Props) => {
}
return (
<IAIDndImageIcon
<DndImageIcon
onClick={toggleStarredState}
icon={imageDTO.starred ? <PiStarFill /> : <PiStarBold />}
tooltip={imageDTO.starred ? 'Unstar' : 'Star'}

View File

@@ -1,5 +1,3 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
@@ -31,13 +29,6 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]);
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` });
const style = {
transform: CSS.Translate.toString(transform),
transition,
};
return (
<Flex
onMouseEnter={handleMouseOver}
@@ -49,15 +40,11 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
w="full"
p={4}
paddingLeft={0}
ref={setNodeRef}
style={style}
>
<IconButton
aria-label={t('nodes.reorderLinearView')}
variant="ghost"
icon={<PiDotsSixVerticalBold />}
{...listeners}
{...attributes}
mx={2}
height="full"
/>

View File

@@ -2,10 +2,10 @@ 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 IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageFieldInputInstance, ImageFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback, useEffect, useMemo } from 'react';
@@ -70,7 +70,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
<>
<DndImage imageDTO={imageDTO} minW={8} minH={8} />
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
<DndImageIcon
onClick={handleReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold /> : undefined}
tooltip="Reset Image"

View File

@@ -1,15 +1,11 @@
import { arrayMove } from '@dnd-kit/sortable';
import { Box, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import DndSortable from 'features/dnd/components/DndSortable';
import type { DragEndEvent } from 'features/dnd/types';
import LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { memo, useCallback, useMemo } from 'react';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -19,45 +15,21 @@ const WorkflowLinearTab = () => {
const fields = useAppSelector(selector);
const { isLoading } = useGetOpenAPISchemaQuery();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`);
if (over && active.id !== over.id) {
const oldIndex = fieldsStrings.indexOf(active.id as string);
const newIndex = fieldsStrings.indexOf(over.id as string);
const newFields = arrayMove(fieldsStrings, oldIndex, newIndex)
.map((field) => fields.find((obj) => `${obj.nodeId}.${obj.fieldName}` === field))
.filter((field) => field) as FieldIdentifier[];
dispatch(workflowExposedFieldsReordered(newFields));
}
},
[dispatch, fields]
);
const items = useMemo(() => fields.map((field) => `${field.nodeId}.${field.fieldName}`), [fields]);
return (
<Box position="relative" w="full" h="full">
<ScrollableContent>
<DndSortable onDragEnd={handleDragEnd} items={items}>
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
{isLoading ? (
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
) : fields.length ? (
fields.map(({ nodeId, fieldName }) => (
<LinearViewFieldInternal key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
))
) : (
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
)}
</Flex>
</DndSortable>
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
{isLoading ? (
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
) : fields.length ? (
fields.map(({ nodeId, fieldName }) => (
<LinearViewFieldInternal key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
))
) : (
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
)}
</Flex>
</ScrollableContent>
</Box>
);

View File

@@ -1,9 +1,9 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import { DndImageIcon } from 'features/dnd2/DndImageIcon';
import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
@@ -35,7 +35,7 @@ export const UpscaleInitialImage = () => {
<>
<DndImage imageDTO={imageDTO} />
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
<DndImageIcon
onClick={onReset}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}