From e1e5266fc3133612273a41028ef4d07c7fba63b2 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 15 May 2023 17:45:05 +1000
Subject: [PATCH] feat(ui): refactor base image uploading logic
---
invokeai/frontend/web/public/locales/en.json | 2 +-
.../frontend/web/src/app/components/App.tsx | 3 +
.../components/AuxiliaryProgressIndicator.tsx | 44 +++++
.../listeners/imageUploaded.ts | 3 +
.../web/src/common/components/IAIInput.tsx | 3 +-
.../src/common/components/IAINumberInput.tsx | 2 +
.../web/src/common/components/IAITextarea.tsx | 9 ++
.../common/components/ImageToImageButtons.tsx | 8 +-
.../src/common/components/ImageUploader.tsx | 152 +++++++++---------
.../common/components/ImageUploaderButton.tsx | 11 +-
.../components/ImageUploaderIconButton.tsx | 7 +-
.../src/common/components/Loading/Loading.tsx | 1 -
.../web/src/common/hooks/useImageUploader.ts | 21 ++-
.../src/common/util/stopPastePropagation.ts | 5 +
.../canvas/components/IAICanvasResizer.tsx | 2 +-
.../components/GalleryProgressImage.tsx | 5 +-
.../Core/ParamNegativeConditioning.tsx | 5 +-
.../Core/ParamPositiveConditioning.tsx | 5 +-
.../system/store/systemPersistDenylist.ts | 23 +--
.../src/features/system/store/systemSlice.ts | 24 +++
.../src/features/ui/components/InvokeTabs.tsx | 4 +
21 files changed, 213 insertions(+), 126 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/components/AuxiliaryProgressIndicator.tsx
create mode 100644 invokeai/frontend/web/src/common/components/IAITextarea.tsx
create mode 100644 invokeai/frontend/web/src/common/util/stopPastePropagation.ts
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index f82b3af677..319a920025 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -552,8 +552,8 @@
"canceled": "Processing Canceled",
"tempFoldersEmptied": "Temp Folder Emptied",
"uploadFailed": "Upload failed",
- "uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
"uploadFailedUnableToLoadDesc": "Unable to load file",
+ "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
"downloadImageStarted": "Image Download Started",
"imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied",
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index e819c04352..929c64edbb 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -22,6 +22,7 @@ import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
import Toaster from './Toaster';
import GlobalHotkeys from './GlobalHotkeys';
+import AuxiliaryProgressIndicator from './AuxiliaryProgressIndicator';
const DEFAULT_CONFIG = {};
@@ -99,6 +100,8 @@ const App = ({
+ {/* */}
+
{!isApplicationReady && !loadingOverridden && (
{
+ const { isUploading } = system;
+
+ let tooltip = '';
+
+ if (isUploading) {
+ tooltip = 'Uploading...';
+ }
+
+ return {
+ tooltip,
+ shouldShow: isUploading,
+ };
+});
+
+export const AuxiliaryProgressIndicator = () => {
+ const { shouldShow, tooltip } = useAppSelector(selector);
+
+ if (!shouldShow) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default memo(AuxiliaryProgressIndicator);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index c32da2e710..5b67be418f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -3,6 +3,7 @@ import { startAppListening } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image';
+import { addToast } from 'features/system/store/systemSlice';
export const addImageUploadedListener = () => {
startAppListening({
@@ -17,6 +18,8 @@ export const addImageUploadedListener = () => {
dispatch(uploadAdded(image));
+ dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
+
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
diff --git a/invokeai/frontend/web/src/common/components/IAIInput.tsx b/invokeai/frontend/web/src/common/components/IAIInput.tsx
index 3e90dca83a..3cba36d2c9 100644
--- a/invokeai/frontend/web/src/common/components/IAIInput.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIInput.tsx
@@ -5,6 +5,7 @@ import {
Input,
InputProps,
} from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { ChangeEvent, memo } from 'react';
interface IAIInputProps extends InputProps {
@@ -31,7 +32,7 @@ const IAIInput = (props: IAIInputProps) => {
{...formControlProps}
>
{label !== '' && {label}}
-
+
);
};
diff --git a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
index 762182eb47..bf598f3b12 100644
--- a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
+++ b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx
@@ -14,6 +14,7 @@ import {
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es';
import { FocusEvent, memo, useEffect, useState } from 'react';
@@ -125,6 +126,7 @@ const IAINumberInput = (props: Props) => {
onChange={handleOnChange}
onBlur={handleBlur}
{...rest}
+ onPaste={stopPastePropagation}
>
{showStepper && (
diff --git a/invokeai/frontend/web/src/common/components/IAITextarea.tsx b/invokeai/frontend/web/src/common/components/IAITextarea.tsx
new file mode 100644
index 0000000000..b5247887bb
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAITextarea.tsx
@@ -0,0 +1,9 @@
+import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react';
+import { stopPastePropagation } from 'common/util/stopPastePropagation';
+import { memo } from 'react';
+
+const IAITextarea = forwardRef((props: TextareaProps, ref) => {
+ return ;
+});
+
+export default memo(IAITextarea);
diff --git a/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
index 315571b6e7..469cd1695c 100644
--- a/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx
@@ -6,10 +6,12 @@ import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
+import useImageUploader from 'common/hooks/useImageUploader';
const InitialImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
+ const { openUploader } = useImageUploader();
const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage());
@@ -27,7 +29,11 @@ const InitialImageButtons = () => {
aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage}
/>
- } aria-label={t('common.upload')} />
+ }
+ onClick={openUploader}
+ aria-label={t('common.upload')}
+ />
);
diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
index c773fb85ed..a070b69bbc 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
@@ -10,6 +10,8 @@ import {
ReactNode,
useCallback,
useEffect,
+ useMemo,
+ useRef,
useState,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
@@ -17,6 +19,24 @@ 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],
+ (system, activeTabName) => {
+ const { isConnected, isUploading } = system;
+
+ const isUploaderDisabled = !isConnected || isUploading;
+
+ return {
+ isUploaderDisabled,
+ activeTabName,
+ };
+ }
+);
type ImageUploaderProps = {
children: ReactNode;
@@ -25,24 +45,20 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
const dispatch = useAppDispatch();
- const activeTabName = useAppSelector(activeTabNameSelector);
+ const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
const toaster = useAppToaster();
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState(false);
- const { setOpenUploader } = useImageUploader();
+ const { setOpenUploaderFunction } = useImageUploader();
const fileRejectionCallback = useCallback(
(rejection: FileRejection) => {
setIsHandlingUpload(true);
- const msg = rejection.errors.reduce(
- (acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
- ''
- );
+
toaster({
title: t('toast.uploadFailed'),
- description: msg,
+ description: rejection.errors.map((error) => error.message).join('\n'),
status: 'error',
- isClosable: true,
});
},
[t, toaster]
@@ -57,6 +73,15 @@ const ImageUploader = (props: ImageUploaderProps) => {
const onDrop = useCallback(
(acceptedFiles: Array, fileRejections: Array) => {
+ if (fileRejections.length > 1) {
+ toaster({
+ title: t('toast.uploadFailed'),
+ description: t('toast.uploadFailedInvalidUploadDesc'),
+ status: 'error',
+ });
+ return;
+ }
+
fileRejections.forEach((rejection: FileRejection) => {
fileRejectionCallback(rejection);
});
@@ -65,7 +90,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
fileAcceptedCallback(file);
});
},
- [fileAcceptedCallback, fileRejectionCallback]
+ [t, toaster, fileAcceptedCallback, fileRejectionCallback]
);
const {
@@ -74,92 +99,67 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragAccept,
isDragReject,
isDragActive,
+ inputRef,
open,
} = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true,
onDrop,
onDragOver: () => setIsHandlingUpload(true),
- maxFiles: 1,
+ disabled: isUploaderDisabled,
+ multiple: false,
});
- setOpenUploader(open);
-
useEffect(() => {
- const pasteImageListener = (e: ClipboardEvent) => {
- const dataTransferItemList = e.clipboardData?.items;
- if (!dataTransferItemList) return;
-
- const imageItems: Array = [];
-
- for (const item of dataTransferItemList) {
- if (
- item.kind === 'file' &&
- ['image/png', 'image/jpg'].includes(item.type)
- ) {
- imageItems.push(item);
- }
- }
-
- if (!imageItems.length) return;
-
- e.stopImmediatePropagation();
-
- if (imageItems.length > 1) {
- toaster({
- description: t('toast.uploadFailedMultipleImagesDesc'),
- status: 'error',
- isClosable: true,
- });
+ const handlePaste = async (e: ClipboardEvent) => {
+ if (!inputRef.current) {
return;
}
- const file = imageItems[0].getAsFile();
-
- if (!file) {
- toaster({
- description: t('toast.uploadFailedUnableToLoadDesc'),
- status: 'error',
- isClosable: true,
- });
- return;
+ if (e.clipboardData?.files) {
+ inputRef.current.files = e.clipboardData.files;
+ inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
}
-
- dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
};
- document.addEventListener('paste', pasteImageListener);
+
+ setOpenUploaderFunction(open);
+ document.addEventListener('paste', handlePaste);
+
return () => {
- document.removeEventListener('paste', pasteImageListener);
+ document.removeEventListener('paste', handlePaste);
+ setOpenUploaderFunction(() => {
+ return;
+ });
};
- }, [t, dispatch, toaster, activeTabName]);
+ }, [inputRef, open, setOpenUploaderFunction]);
- const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
- activeTabName
- )
- ? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`
- : ``;
+ const overlaySecondaryText = useMemo(() => {
+ if (['img2img', 'unifiedCanvas'].includes(activeTabName)) {
+ return ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`;
+ }
+
+ return '';
+ }, [t, activeTabName]);
return (
-
- {
- // Bail out if user hits spacebar - do not open the uploader
- if (e.key === ' ') return;
- }}
- >
-
- {children}
- {isDragActive && isHandlingUpload && (
-
- )}
-
-
+ {
+ // Bail out if user hits spacebar - do not open the uploader
+ if (e.key === ' ') return;
+ }}
+ >
+
+ {children}
+ {isDragActive && isHandlingUpload && (
+
+ )}
+
);
};
diff --git a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx b/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
index 6179efe5e6..bb24ce6e18 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
@@ -1,6 +1,5 @@
import { Flex, Heading, Icon } from '@chakra-ui/react';
-import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
-import { useContext } from 'react';
+import useImageUploader from 'common/hooks/useImageUploader';
import { FaUpload } from 'react-icons/fa';
type ImageUploaderButtonProps = {
@@ -9,11 +8,7 @@ type ImageUploaderButtonProps = {
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
const { styleClass } = props;
- const open = useContext(ImageUploaderTriggerContext);
-
- const handleClickUpload = () => {
- open && open();
- };
+ const { openUploader } = useImageUploader();
return (
{
className={styleClass}
>
{
const { t } = useTranslation();
- const openImageUploader = useContext(ImageUploaderTriggerContext);
+ const { openUploader } = useImageUploader();
return (
}
- onClick={openImageUploader || undefined}
+ onClick={openUploader}
/>
);
};
diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
index 8625e5b49b..591668f0c2 100644
--- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
+++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
@@ -24,7 +24,6 @@ const Loading = () => {
height="24px !important"
right="1.5rem"
bottom="1.5rem"
- speed="1.2s"
/>
);
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts b/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
index 10c81c4dfd..2b04ac9530 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
@@ -1,13 +1,22 @@
-let openFunction: () => void;
+import { useCallback } from 'react';
+
+let openUploader = () => {
+ return;
+};
const useImageUploader = () => {
- return {
- setOpenUploader: (open?: () => void) => {
- if (open) {
- openFunction = open;
+ const setOpenUploaderFunction = useCallback(
+ (openUploaderFunction?: () => void) => {
+ if (openUploaderFunction) {
+ openUploader = openUploaderFunction;
}
},
- openUploader: openFunction,
+ []
+ );
+
+ return {
+ setOpenUploaderFunction,
+ openUploader,
};
};
diff --git a/invokeai/frontend/web/src/common/util/stopPastePropagation.ts b/invokeai/frontend/web/src/common/util/stopPastePropagation.ts
new file mode 100644
index 0000000000..b6b237387d
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/stopPastePropagation.ts
@@ -0,0 +1,5 @@
+import { ClipboardEvent } from 'react';
+
+export const stopPastePropagation = (e: ClipboardEvent) => {
+ e.stopPropagation();
+};
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
index 013d689182..d16a5dab87 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasResizer.tsx
@@ -81,7 +81,7 @@ const IAICanvasResizer = () => {
height: '100%',
}}
>
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
index a2103eb8e2..98f7db2726 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx
@@ -62,7 +62,10 @@ const GalleryProgressImage = () => {
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}}
/>
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
index d3790d4c24..28ab50ff82 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
@@ -1,6 +1,7 @@
-import { FormControl, Textarea } from '@chakra-ui/react';
+import { FormControl } from '@chakra-ui/react';
import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import IAITextarea from 'common/components/IAITextarea';
import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
@@ -14,7 +15,7 @@ const ParamNegativeConditioning = () => {
return (
-