feat(ui): add upload to IAIDndImage

Add uploading to IAIDndImage
- add `postUploadAction` arg to `imageUploaded` thunk, with several current valid options (set control image, set init, set nodes image, set canvas, or toast)
- updated IAIDndImage to optionally allow click to upload
This commit is contained in:
psychedelicious 2023-06-07 00:38:22 +10:00
parent f223ad7776
commit 58fec84858
12 changed files with 159 additions and 129 deletions

View File

@ -8,7 +8,6 @@ import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png';
export const addCanvasMergedListener = () => {
startAppListening({
@ -49,12 +48,15 @@ export const addCanvasMergedListener = () => {
const imageUploadedRequest = dispatch(
imageUploaded({
formData: {
file: new File([blob], MERGED_CANVAS_FILENAME, {
file: new File([blob], 'mergedCanvas.png', {
type: 'image/png',
}),
},
imageCategory: 'general',
isIntermediate: true,
postUploadAction: {
type: 'TOAST_CANVAS_MERGED',
},
})
);

View File

@ -6,8 +6,6 @@ import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { imageUpserted } from 'features/gallery/store/imagesSlice';
export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasSavedToGalleryListener = () => {
@ -33,12 +31,15 @@ export const addCanvasSavedToGalleryListener = () => {
const imageUploadedRequest = dispatch(
imageUploaded({
formData: {
file: new File([blob], SAVED_CANVAS_FILENAME, {
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
},
imageCategory: 'general',
isIntermediate: false,
postUploadAction: {
type: 'TOAST_CANVAS_SAVED_TO_GALLERY',
},
})
);

View File

@ -3,8 +3,10 @@ import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
import { log } from 'app/logging/useLogger';
import { imageUpserted } from 'features/gallery/store/imagesSlice';
import { SAVED_CANVAS_FILENAME } from './canvasSavedToGallery';
import { MERGED_CANVAS_FILENAME } from './canvasMerged';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
const moduleLog = log.child({ namespace: 'image' });
@ -21,23 +23,48 @@ export const addImageUploadedFulfilledListener = () => {
return;
}
const originalFileName = action.meta.arg.formData.file.name;
dispatch(imageUpserted(image));
if (originalFileName === SAVED_CANVAS_FILENAME) {
const { postUploadAction } = action.meta.arg;
if (postUploadAction?.type === 'TOAST_CANVAS_SAVED_TO_GALLERY') {
dispatch(
addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
);
return;
}
if (originalFileName === MERGED_CANVAS_FILENAME) {
if (postUploadAction?.type === 'TOAST_CANVAS_MERGED') {
dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
return;
}
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(image));
return;
}
if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') {
const { controlNetId } = postUploadAction;
dispatch(controlNetImageChanged({ controlNetId, controlImage: image }));
return;
}
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
dispatch(initialImageChanged(image));
return;
}
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction;
dispatch(fieldValueChanged({ nodeId, fieldName, value: image }));
return;
}
if (postUploadAction?.type === 'TOAST_UPLOADED') {
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
return;
}
},
});
};

View File

@ -5,12 +5,18 @@ import IAIIconButton from 'common/components/IAIIconButton';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { AnimatePresence } from 'framer-motion';
import { ReactElement, SyntheticEvent } from 'react';
import { ReactElement, SyntheticEvent, useCallback } from 'react';
import { memo, useRef } from 'react';
import { FaImage, FaTimes } from 'react-icons/fa';
import { FaImage, FaTimes, FaUpload } from 'react-icons/fa';
import { ImageDTO } from 'services/api';
import { v4 as uuidv4 } from 'uuid';
import IAIDropOverlay from './IAIDropOverlay';
import { PostUploadAction, imageUploaded } from 'services/thunks/image';
import { FileRejection, useDropzone } from 'react-dropzone';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppToaster } from 'app/components/Toaster';
import { useTranslation } from 'react-i18next';
import { reject } from 'lodash-es';
type IAIDndImageProps = {
image: ImageDTO | null | undefined;
@ -23,9 +29,11 @@ type IAIDndImageProps = {
withMetadataOverlay?: boolean;
isDragDisabled?: boolean;
isDropDisabled?: boolean;
isUploadDisabled?: boolean;
fallback?: ReactElement;
payloadImage?: ImageDTO | null | undefined;
minSize?: number;
postUploadAction?: PostUploadAction;
};
const IAIDndImage = (props: IAIDndImageProps) => {
@ -39,11 +47,15 @@ const IAIDndImage = (props: IAIDndImageProps) => {
withMetadataOverlay = false,
isDropDisabled = false,
isDragDisabled = false,
isUploadDisabled = false,
fallback = <IAIImageFallback />,
payloadImage,
minSize = 24,
postUploadAction,
} = props;
const dispatch = useAppDispatch();
const dndId = useRef(uuidv4());
const {
isOver,
setNodeRef: setDroppableRef,
@ -65,7 +77,36 @@ const IAIDndImage = (props: IAIDndImageProps) => {
data: {
image: payloadImage ? payloadImage : image,
},
disabled: isDragDisabled,
disabled: isDragDisabled || !image,
});
const handleOnDropAccepted = useCallback(
(files: Array<File>) => {
const file = files[0];
if (!file) {
return;
}
console.log(postUploadAction);
dispatch(
imageUploaded({
formData: { file },
imageCategory: 'user',
isIntermediate: false,
postUploadAction,
})
);
},
[dispatch, postUploadAction]
);
const { getRootProps, getInputProps } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
onDropAccepted: handleOnDropAccepted,
noDrag: true,
multiple: false,
disabled: isUploadDisabled,
});
const setNodeRef = useCombinedRefs(setDroppableRef, setDraggableRef);
@ -139,19 +180,28 @@ const IAIDndImage = (props: IAIDndImageProps) => {
<Flex
sx={{
minH: minSize,
bg: 'base.850',
bg: 'base.800',
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
cursor: 'pointer',
transitionProperty: 'common',
transitionDuration: '0.1s',
color: 'base.500',
_hover: {
bg: 'base.750',
color: 'base.300',
},
}}
{...getRootProps()}
>
<input {...getInputProps()} />
<Icon
as={FaImage}
as={isUploadDisabled ? FaImage : FaUpload}
sx={{
boxSize: 24,
color: 'base.500',
boxSize: 12,
}}
/>
</Flex>

View File

@ -30,7 +30,7 @@ export const IAIDropOverlay = (props: Props) => {
sx={{
position: 'absolute',
top: 0,
left: 0,
insetInlineStart: 0,
w: 'full',
h: 'full',
}}
@ -39,7 +39,7 @@ export const IAIDropOverlay = (props: Props) => {
sx={{
position: 'absolute',
top: 0,
left: 0,
insetInlineStart: 0,
w: 'full',
h: 'full',
bg: 'base.900',
@ -56,7 +56,7 @@ export const IAIDropOverlay = (props: Props) => {
sx={{
position: 'absolute',
top: 0,
left: 0,
insetInlineStart: 0,
w: 'full',
h: 'full',
opacity: 1,

View File

@ -1,17 +1,13 @@
import { Box } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { ResourceKey } from 'i18next';
import {
KeyboardEvent,
memo,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
@ -19,10 +15,8 @@ import { useTranslation } from 'react-i18next';
import { imageUploaded } from 'services/thunks/image';
import ImageUploadOverlay from './ImageUploadOverlay';
import { useAppToaster } from 'app/components/Toaster';
import { filter, map, some } from 'lodash-es';
import { createSelector } from '@reduxjs/toolkit';
import { systemSelector } from 'features/system/store/systemSelectors';
import { ErrorCode } from 'react-dropzone';
const selector = createSelector(
[systemSelector, activeTabNameSelector],
@ -71,6 +65,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
formData: { file },
imageCategory: 'user',
isIntermediate: false,
postUploadAction: { type: 'TOAST_UPLOADED' },
})
);
},

View File

@ -193,66 +193,6 @@ const ControlNet = (props: ControlNetProps) => {
)}
</Flex>
);
return (
<Flex sx={{ flexDir: 'column', gap: 3 }}>
<ControlNetImagePreview controlNet={props.controlNet} />
<ParamControlNetModel controlNetId={controlNetId} model={model} />
<Tabs
isFitted
orientation="horizontal"
variant="enclosed"
size="sm"
colorScheme="accent"
>
<TabList>
<Tab
sx={{ 'button&': { _selected: { borderBottomColor: 'base.800' } } }}
>
Model Config
</Tab>
<Tab
sx={{ 'button&': { _selected: { borderBottomColor: 'base.800' } } }}
>
Preprocess
</Tab>
</TabList>
<TabPanels sx={{ pt: 2 }}>
<TabPanel sx={{ p: 0 }}>
<ParamControlNetWeight
controlNetId={controlNetId}
weight={weight}
/>
<ParamControlNetBeginEnd
controlNetId={controlNetId}
beginStepPct={beginStepPct}
endStepPct={endStepPct}
/>
</TabPanel>
<TabPanel sx={{ p: 0 }}>
<ParamControlNetProcessorSelect
controlNetId={controlNetId}
processorNode={processorNode}
/>
<ControlNetProcessorComponent
controlNetId={controlNetId}
processorNode={processorNode}
/>
<ControlNetPreprocessButton controlNet={props.controlNet} />
{/* <IAIButton
size="sm"
leftIcon={<FaUndo />}
onClick={handleReset}
isDisabled={Boolean(!processedControlImage)}
>
Reset Processing
</IAIButton> */}
</TabPanel>
</TabPanels>
</Tabs>
<IAIButton onClick={handleDelete}>Remove ControlNet</IAIButton>
</Flex>
);
};
export default memo(ControlNet);

View File

@ -70,6 +70,8 @@ const ControlNetImagePreview = (props: Props) => {
isDropDisabled={Boolean(
processedControlImage && processorType !== 'none'
)}
isUploadDisabled={Boolean(controlImage)}
postUploadAction={{ type: 'SET_CONTROLNET_IMAGE', controlNetId }}
/>
<AnimatePresence>
{shouldShowProcessedImage && (
@ -118,6 +120,7 @@ const ControlNetImagePreview = (props: Props) => {
image={processedControlImage}
onDrop={handleDrop}
payloadImage={controlImage}
isUploadDisabled={true}
/>
</Box>
</Box>

View File

@ -50,21 +50,8 @@ const CurrentImagePreview = () => {
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
const { shouldFetchImages } = useAppSelector(configSelector);
const toaster = useAppToaster();
const dispatch = useAppDispatch();
const handleError = useCallback(() => {
dispatch(imageSelected());
if (shouldFetchImages) {
toaster({
title: 'Something went wrong, please refresh',
status: 'error',
isClosable: true,
});
}
}, [dispatch, toaster, shouldFetchImages]);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.image_name === image?.image_name) {
@ -112,8 +99,8 @@ const CurrentImagePreview = () => {
<IAIDndImage
image={image}
onDrop={handleDrop}
onError={handleError}
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
isUploadDisabled={true}
/>
</Flex>
)}

View File

@ -60,6 +60,11 @@ const ImageInputFieldComponent = (
onDrop={handleDrop}
onReset={handleReset}
resetIconSize="sm"
postUploadAction={{
type: 'SET_NODES_IMAGE',
nodeId,
fieldName: field.name,
}}
/>
</Flex>
);

View File

@ -6,11 +6,8 @@ import {
initialImageChanged,
} from 'features/parameters/store/generationSlice';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { configSelector } from '../../../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
@ -28,28 +25,7 @@ const selector = createSelector(
const InitialImagePreview = () => {
const { initialImage } = useAppSelector(selector);
const { shouldFetchImages } = useAppSelector(configSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const toaster = useAppToaster();
const handleError = useCallback(() => {
dispatch(clearInitialImage());
if (shouldFetchImages) {
toaster({
title: 'Something went wrong, please refresh',
status: 'error',
isClosable: true,
});
} else {
toaster({
title: t('toast.parametersFailed'),
description: t('toast.parametersFailedDesc'),
status: 'error',
isClosable: true,
});
}
}, [dispatch, t, toaster, shouldFetchImages]);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
@ -81,6 +57,7 @@ const InitialImagePreview = () => {
onDrop={handleDrop}
onReset={handleReset}
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
/>
</Flex>
);

View File

@ -32,7 +32,49 @@ export const imageMetadataReceived = createAppAsyncThunk(
}
);
type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
type ControlNetAction = {
type: 'SET_CONTROLNET_IMAGE';
controlNetId: string;
};
type InitialImageAction = {
type: 'SET_INITIAL_IMAGE';
};
type NodesAction = {
type: 'SET_NODES_IMAGE';
nodeId: string;
fieldName: string;
};
type CanvasInitialImageAction = {
type: 'SET_CANVAS_INITIAL_IMAGE';
};
type CanvasMergedAction = {
type: 'TOAST_CANVAS_MERGED';
};
type CanvasSavedToGalleryAction = {
type: 'TOAST_CANVAS_SAVED_TO_GALLERY';
};
type UploadedToastAction = {
type: 'TOAST_UPLOADED';
};
export type PostUploadAction =
| ControlNetAction
| InitialImageAction
| NodesAction
| CanvasInitialImageAction
| CanvasMergedAction
| CanvasSavedToGalleryAction
| UploadedToastAction;
type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0] & {
postUploadAction?: PostUploadAction;
};
/**
* `ImagesService.uploadImage()` thunk
@ -40,8 +82,9 @@ type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
export const imageUploaded = createAppAsyncThunk(
'api/imageUploaded',
async (arg: ImageUploadedArg) => {
// strip out `activeTabName` from arg - the route does not need it
const response = await ImagesService.uploadImage(arg);
// `postUploadAction` is only used by the listener middleware - destructure it out
const { postUploadAction, ...rest } = arg;
const response = await ImagesService.uploadImage(rest);
return response;
}
);