feat(ui): refactor base image uploading logic

This commit is contained in:
psychedelicious 2023-05-15 17:45:05 +10:00
parent 5e4457445f
commit e1e5266fc3
21 changed files with 213 additions and 126 deletions

View File

@ -552,8 +552,8 @@
"canceled": "Processing Canceled", "canceled": "Processing Canceled",
"tempFoldersEmptied": "Temp Folder Emptied", "tempFoldersEmptied": "Temp Folder Emptied",
"uploadFailed": "Upload failed", "uploadFailed": "Upload failed",
"uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
"uploadFailedUnableToLoadDesc": "Unable to load file", "uploadFailedUnableToLoadDesc": "Unable to load file",
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
"downloadImageStarted": "Image Download Started", "downloadImageStarted": "Image Download Started",
"imageCopied": "Image Copied", "imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied", "imageLinkCopied": "Image Link Copied",

View File

@ -22,6 +22,7 @@ import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n'; import i18n from 'i18n';
import Toaster from './Toaster'; import Toaster from './Toaster';
import GlobalHotkeys from './GlobalHotkeys'; import GlobalHotkeys from './GlobalHotkeys';
import AuxiliaryProgressIndicator from './AuxiliaryProgressIndicator';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -99,6 +100,8 @@ const App = ({
<GalleryDrawer /> <GalleryDrawer />
<ParametersDrawer /> <ParametersDrawer />
{/* <AuxiliaryProgressIndicator /> */}
<AnimatePresence> <AnimatePresence>
{!isApplicationReady && !loadingOverridden && ( {!isApplicationReady && !loadingOverridden && (
<motion.div <motion.div

View File

@ -0,0 +1,44 @@
import { Flex, Spinner, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { memo } from 'react';
const selector = createSelector(systemSelector, (system) => {
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 (
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
color: 'base.600',
}}
>
<Tooltip label={tooltip} placement="right" hasArrow>
<Spinner />
</Tooltip>
</Flex>
);
};
export default memo(AuxiliaryProgressIndicator);

View File

@ -3,6 +3,7 @@ import { startAppListening } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice'; import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
export const addImageUploadedListener = () => { export const addImageUploadedListener = () => {
startAppListening({ startAppListening({
@ -17,6 +18,8 @@ export const addImageUploadedListener = () => {
dispatch(uploadAdded(image)); dispatch(uploadAdded(image));
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
if (state.gallery.shouldAutoSwitchToNewImages) { if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image)); dispatch(imageSelected(image));
} }

View File

@ -5,6 +5,7 @@ import {
Input, Input,
InputProps, InputProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { ChangeEvent, memo } from 'react'; import { ChangeEvent, memo } from 'react';
interface IAIInputProps extends InputProps { interface IAIInputProps extends InputProps {
@ -31,7 +32,7 @@ const IAIInput = (props: IAIInputProps) => {
{...formControlProps} {...formControlProps}
> >
{label !== '' && <FormLabel>{label}</FormLabel>} {label !== '' && <FormLabel>{label}</FormLabel>}
<Input {...rest} /> <Input {...rest} onPaste={stopPastePropagation} />
</FormControl> </FormControl>
); );
}; };

View File

@ -14,6 +14,7 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { FocusEvent, memo, useEffect, useState } from 'react'; import { FocusEvent, memo, useEffect, useState } from 'react';
@ -125,6 +126,7 @@ const IAINumberInput = (props: Props) => {
onChange={handleOnChange} onChange={handleOnChange}
onBlur={handleBlur} onBlur={handleBlur}
{...rest} {...rest}
onPaste={stopPastePropagation}
> >
<NumberInputField {...numberInputFieldProps} /> <NumberInputField {...numberInputFieldProps} />
{showStepper && ( {showStepper && (

View File

@ -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 <Textarea ref={ref} onPaste={stopPastePropagation} {...props} />;
});
export default memo(IAITextarea);

View File

@ -6,10 +6,12 @@ import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice';
import useImageUploader from 'common/hooks/useImageUploader';
const InitialImageButtons = () => { const InitialImageButtons = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { openUploader } = useImageUploader();
const handleResetInitialImage = useCallback(() => { const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage()); dispatch(clearInitialImage());
@ -27,7 +29,11 @@ const InitialImageButtons = () => {
aria-label={t('accessibility.reset')} aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage} onClick={handleResetInitialImage}
/> />
<IAIIconButton icon={<FaUpload />} aria-label={t('common.upload')} /> <IAIIconButton
icon={<FaUpload />}
onClick={openUploader}
aria-label={t('common.upload')}
/>
</ButtonGroup> </ButtonGroup>
</Flex> </Flex>
); );

View File

@ -10,6 +10,8 @@ import {
ReactNode, ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef,
useState, useState,
} from 'react'; } from 'react';
import { FileRejection, useDropzone } from 'react-dropzone'; import { FileRejection, useDropzone } from 'react-dropzone';
@ -17,6 +19,24 @@ import { useTranslation } from 'react-i18next';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import ImageUploadOverlay from './ImageUploadOverlay'; import ImageUploadOverlay from './ImageUploadOverlay';
import { useAppToaster } from 'app/components/Toaster'; 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 = { type ImageUploaderProps = {
children: ReactNode; children: ReactNode;
@ -25,24 +45,20 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => { const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props; const { children } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector); const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false); const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const { setOpenUploader } = useImageUploader(); const { setOpenUploaderFunction } = useImageUploader();
const fileRejectionCallback = useCallback( const fileRejectionCallback = useCallback(
(rejection: FileRejection) => { (rejection: FileRejection) => {
setIsHandlingUpload(true); setIsHandlingUpload(true);
const msg = rejection.errors.reduce(
(acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
''
);
toaster({ toaster({
title: t('toast.uploadFailed'), title: t('toast.uploadFailed'),
description: msg, description: rejection.errors.map((error) => error.message).join('\n'),
status: 'error', status: 'error',
isClosable: true,
}); });
}, },
[t, toaster] [t, toaster]
@ -57,6 +73,15 @@ const ImageUploader = (props: ImageUploaderProps) => {
const onDrop = useCallback( const onDrop = useCallback(
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => { (acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
if (fileRejections.length > 1) {
toaster({
title: t('toast.uploadFailed'),
description: t('toast.uploadFailedInvalidUploadDesc'),
status: 'error',
});
return;
}
fileRejections.forEach((rejection: FileRejection) => { fileRejections.forEach((rejection: FileRejection) => {
fileRejectionCallback(rejection); fileRejectionCallback(rejection);
}); });
@ -65,7 +90,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
fileAcceptedCallback(file); fileAcceptedCallback(file);
}); });
}, },
[fileAcceptedCallback, fileRejectionCallback] [t, toaster, fileAcceptedCallback, fileRejectionCallback]
); );
const { const {
@ -74,92 +99,67 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragAccept, isDragAccept,
isDragReject, isDragReject,
isDragActive, isDragActive,
inputRef,
open, open,
} = useDropzone({ } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true, noClick: true,
onDrop, onDrop,
onDragOver: () => setIsHandlingUpload(true), onDragOver: () => setIsHandlingUpload(true),
maxFiles: 1, disabled: isUploaderDisabled,
multiple: false,
}); });
setOpenUploader(open);
useEffect(() => { useEffect(() => {
const pasteImageListener = (e: ClipboardEvent) => { const handlePaste = async (e: ClipboardEvent) => {
const dataTransferItemList = e.clipboardData?.items; if (!inputRef.current) {
if (!dataTransferItemList) return;
const imageItems: Array<DataTransferItem> = [];
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,
});
return; return;
} }
const file = imageItems[0].getAsFile(); if (e.clipboardData?.files) {
inputRef.current.files = e.clipboardData.files;
if (!file) { inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
toaster({
description: t('toast.uploadFailedUnableToLoadDesc'),
status: 'error',
isClosable: true,
});
return;
} }
dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
}; };
document.addEventListener('paste', pasteImageListener);
setOpenUploaderFunction(open);
document.addEventListener('paste', handlePaste);
return () => { return () => {
document.removeEventListener('paste', pasteImageListener); document.removeEventListener('paste', handlePaste);
setOpenUploaderFunction(() => {
return;
});
}; };
}, [t, dispatch, toaster, activeTabName]); }, [inputRef, open, setOpenUploaderFunction]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes( const overlaySecondaryText = useMemo(() => {
activeTabName if (['img2img', 'unifiedCanvas'].includes(activeTabName)) {
) return ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`;
? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}` }
: ``;
return '';
}, [t, activeTabName]);
return ( return (
<ImageUploaderTriggerContext.Provider value={open}> <Box
<Box {...getRootProps({ style: {} })}
{...getRootProps({ style: {} })} onKeyDown={(e: KeyboardEvent) => {
onKeyDown={(e: KeyboardEvent) => { // Bail out if user hits spacebar - do not open the uploader
// Bail out if user hits spacebar - do not open the uploader if (e.key === ' ') return;
if (e.key === ' ') return; }}
}} >
> <input {...getInputProps()} />
<input {...getInputProps()} /> {children}
{children} {isDragActive && isHandlingUpload && (
{isDragActive && isHandlingUpload && ( <ImageUploadOverlay
<ImageUploadOverlay isDragAccept={isDragAccept}
isDragAccept={isDragAccept} isDragReject={isDragReject}
isDragReject={isDragReject} overlaySecondaryText={overlaySecondaryText}
overlaySecondaryText={overlaySecondaryText} setIsHandlingUpload={setIsHandlingUpload}
setIsHandlingUpload={setIsHandlingUpload} />
/> )}
)} </Box>
</Box>
</ImageUploaderTriggerContext.Provider>
); );
}; };

View File

@ -1,6 +1,5 @@
import { Flex, Heading, Icon } from '@chakra-ui/react'; import { Flex, Heading, Icon } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import useImageUploader from 'common/hooks/useImageUploader';
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa'; import { FaUpload } from 'react-icons/fa';
type ImageUploaderButtonProps = { type ImageUploaderButtonProps = {
@ -9,11 +8,7 @@ type ImageUploaderButtonProps = {
const ImageUploaderButton = (props: ImageUploaderButtonProps) => { const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
const { styleClass } = props; const { styleClass } = props;
const open = useContext(ImageUploaderTriggerContext); const { openUploader } = useImageUploader();
const handleClickUpload = () => {
open && open();
};
return ( return (
<Flex <Flex
@ -26,7 +21,7 @@ const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
className={styleClass} className={styleClass}
> >
<Flex <Flex
onClick={handleClickUpload} onClick={openUploader}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@ -1,19 +1,18 @@
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa'; import { FaUpload } from 'react-icons/fa';
import IAIIconButton from './IAIIconButton'; import IAIIconButton from './IAIIconButton';
import useImageUploader from 'common/hooks/useImageUploader';
const ImageUploaderIconButton = () => { const ImageUploaderIconButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const openImageUploader = useContext(ImageUploaderTriggerContext); const { openUploader } = useImageUploader();
return ( return (
<IAIIconButton <IAIIconButton
aria-label={t('accessibility.uploadImage')} aria-label={t('accessibility.uploadImage')}
tooltip="Upload Image" tooltip="Upload Image"
icon={<FaUpload />} icon={<FaUpload />}
onClick={openImageUploader || undefined} onClick={openUploader}
/> />
); );
}; };

View File

@ -24,7 +24,6 @@ const Loading = () => {
height="24px !important" height="24px !important"
right="1.5rem" right="1.5rem"
bottom="1.5rem" bottom="1.5rem"
speed="1.2s"
/> />
</Flex> </Flex>
); );

View File

@ -1,13 +1,22 @@
let openFunction: () => void; import { useCallback } from 'react';
let openUploader = () => {
return;
};
const useImageUploader = () => { const useImageUploader = () => {
return { const setOpenUploaderFunction = useCallback(
setOpenUploader: (open?: () => void) => { (openUploaderFunction?: () => void) => {
if (open) { if (openUploaderFunction) {
openFunction = open; openUploader = openUploaderFunction;
} }
}, },
openUploader: openFunction, []
);
return {
setOpenUploaderFunction,
openUploader,
}; };
}; };

View File

@ -0,0 +1,5 @@
import { ClipboardEvent } from 'react';
export const stopPastePropagation = (e: ClipboardEvent) => {
e.stopPropagation();
};

View File

@ -81,7 +81,7 @@ const IAICanvasResizer = () => {
height: '100%', height: '100%',
}} }}
> >
<Spinner thickness="2px" speed="1s" size="xl" /> <Spinner thickness="2px" size="xl" />
</Flex> </Flex>
); );
}; };

View File

@ -62,7 +62,10 @@ const GalleryProgressImage = () => {
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}} }}
/> />
<Spinner sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }} /> <Spinner
sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }}
speed="1.2s"
/>
</Flex> </Flex>
); );
}; };

View File

@ -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 type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import { setNegativePrompt } from 'features/parameters/store/generationSlice'; import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -14,7 +15,7 @@ const ParamNegativeConditioning = () => {
return ( return (
<FormControl> <FormControl>
<Textarea <IAITextarea
id="negativePrompt" id="negativePrompt"
name="negativePrompt" name="negativePrompt"
value={negativePrompt} value={negativePrompt}

View File

@ -1,4 +1,4 @@
import { Box, FormControl, Textarea } from '@chakra-ui/react'; import { Box, FormControl } from '@chakra-ui/react';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react';
@ -16,6 +16,7 @@ import { isEqual } from 'lodash-es';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import IAITextarea from 'common/components/IAITextarea';
const promptInputSelector = createSelector( const promptInputSelector = createSelector(
[(state: RootState) => state.generation, activeTabNameSelector], [(state: RootState) => state.generation, activeTabNameSelector],
@ -72,7 +73,7 @@ const ParamPositiveConditioning = () => {
<FormControl <FormControl
isInvalid={prompt.length === 0 || Boolean(prompt.match(/^[\s\r\n]+$/))} isInvalid={prompt.length === 0 || Boolean(prompt.match(/^[\s\r\n]+$/))}
> >
<Textarea <IAITextarea
id="prompt" id="prompt"
name="prompt" name="prompt"
placeholder={t('parameters.promptPlaceholder')} placeholder={t('parameters.promptPlaceholder')}

View File

@ -3,24 +3,6 @@ import { SystemState } from './systemSlice';
/** /**
* System slice persist denylist * System slice persist denylist
*/ */
const itemsToDenylist: (keyof SystemState)[] = [
'currentIteration',
'currentStatus',
'currentStep',
'isCancelable',
'isConnected',
'isESRGANAvailable',
'isGFPGANAvailable',
'isProcessing',
'socketId',
'totalIterations',
'totalSteps',
'openModel',
'isCancelScheduled',
'progressImage',
'wereModelsReceived',
'wasSchemaParsed',
];
export const systemPersistDenylist: (keyof SystemState)[] = [ export const systemPersistDenylist: (keyof SystemState)[] = [
'currentIteration', 'currentIteration',
'currentStatus', 'currentStatus',
@ -39,8 +21,5 @@ export const systemPersistDenylist: (keyof SystemState)[] = [
'wereModelsReceived', 'wereModelsReceived',
'wasSchemaParsed', 'wasSchemaParsed',
'isPersisted', 'isPersisted',
'isUploading',
]; ];
export const systemDenylist = itemsToDenylist.map(
(denylistItem) => `system.${denylistItem}`
);

View File

@ -25,6 +25,7 @@ import { TFuncKey } from 'i18next';
import { t } from 'i18next'; import { t } from 'i18next';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { LANGUAGES } from '../components/LanguagePicker'; import { LANGUAGES } from '../components/LanguagePicker';
import { imageUploaded } from 'services/thunks/image';
export type CancelStrategy = 'immediate' | 'scheduled'; export type CancelStrategy = 'immediate' | 'scheduled';
@ -93,6 +94,7 @@ export interface SystemState {
isPersisted: boolean; isPersisted: boolean;
shouldAntialiasProgressImage: boolean; shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES; language: keyof typeof LANGUAGES;
isUploading: boolean;
} }
export const initialSystemState: SystemState = { export const initialSystemState: SystemState = {
@ -128,6 +130,7 @@ export const initialSystemState: SystemState = {
infillMethods: ['tile', 'patchmatch'], infillMethods: ['tile', 'patchmatch'],
isPersisted: false, isPersisted: false,
language: 'en', language: 'en',
isUploading: false,
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@ -456,6 +459,27 @@ export const systemSlice = createSlice({
builder.addCase(parsedOpenAPISchema, (state) => { builder.addCase(parsedOpenAPISchema, (state) => {
state.wasSchemaParsed = true; state.wasSchemaParsed = true;
}); });
/**
* Image Uploading Started
*/
builder.addCase(imageUploaded.pending, (state) => {
state.isUploading = true;
});
/**
* Image Uploading Complete
*/
builder.addCase(imageUploaded.rejected, (state) => {
state.isUploading = false;
});
/**
* Image Uploading Complete
*/
builder.addCase(imageUploaded.fulfilled, (state) => {
state.isUploading = false;
});
}, },
}); });

View File

@ -1,5 +1,6 @@
import { import {
Icon, Icon,
Spacer,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
@ -35,6 +36,7 @@ import NodesTab from './tabs/Nodes/NodesTab';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
import ResizeHandle from './tabs/ResizeHandle'; import ResizeHandle from './tabs/ResizeHandle';
import ImageTab from './tabs/ImageToImage/ImageToImageTab'; import ImageTab from './tabs/ImageToImage/ImageToImageTab';
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
export interface InvokeTabInfo { export interface InvokeTabInfo {
id: InvokeTabName; id: InvokeTabName;
@ -162,6 +164,8 @@ const InvokeTabs = () => {
justifyContent={{ base: 'center', xl: 'start' }} justifyContent={{ base: 'center', xl: 'start' }}
> >
{tabs} {tabs}
<Spacer />
<AuxiliaryProgressIndicator />
</TabList> </TabList>
<PanelGroup <PanelGroup
autoSaveId="app" autoSaveId="app"