mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
tidy(ui): clean up old dnd stuff
This commit is contained in:
@@ -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",
|
||||
|
||||
52
invokeai/frontend/web/pnpm-lock.yaml
generated
52
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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')}
|
||||
|
||||
@@ -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);
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user