mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Adds full-app image drag and drop, also image paste
This commit is contained in:
parent
2e9463089d
commit
101fe9efa9
517
frontend/dist/assets/index.01c74d27.js
vendored
517
frontend/dist/assets/index.01c74d27.js
vendored
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.18143ecd.js
vendored
Normal file
517
frontend/dist/assets/index.18143ecd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.61df3eff.css
vendored
1
frontend/dist/assets/index.61df3eff.css
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.ff30f6a0.css
vendored
Normal file
1
frontend/dist/assets/index.ff30f6a0.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.01c74d27.js"></script>
|
<script type="module" crossorigin src="./assets/index.18143ecd.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.61df3eff.css">
|
<link rel="stylesheet" href="./assets/index.ff30f6a0.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -17,5 +17,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-console {
|
.app-console {
|
||||||
z-index: 9999;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ import { useAppDispatch } from './store';
|
|||||||
import { requestSystemConfig } from './socketio/actions';
|
import { requestSystemConfig } from './socketio/actions';
|
||||||
import { keepGUIAlive } from './utils';
|
import { keepGUIAlive } from './utils';
|
||||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||||
|
import ImageUploader from '../common/components/ImageUploader';
|
||||||
|
|
||||||
keepGUIAlive();
|
keepGUIAlive();
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [isReady, setIsReady] = useState<boolean>(false);
|
const [isReady, setIsReady] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,6 +23,7 @@ const App = () => {
|
|||||||
|
|
||||||
return isReady ? (
|
return isReady ? (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
<ImageUploader>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
<div className="app-content">
|
<div className="app-content">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
@ -29,6 +32,7 @@ const App = () => {
|
|||||||
<div className="app-console">
|
<div className="app-console">
|
||||||
<Console />
|
<Console />
|
||||||
</div>
|
</div>
|
||||||
|
</ImageUploader>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Loading />
|
<Loading />
|
||||||
|
8
frontend/src/app/contexts/ImageUploaderTriggerContext.ts
Normal file
8
frontend/src/app/contexts/ImageUploaderTriggerContext.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
type VoidFunc = () => void;
|
||||||
|
|
||||||
|
type ImageUploaderTriggerContextType = VoidFunc | null;
|
||||||
|
|
||||||
|
export const ImageUploaderTriggerContext =
|
||||||
|
createContext<ImageUploaderTriggerContextType>(null);
|
4
frontend/src/app/invokeai.d.ts
vendored
4
frontend/src/app/invokeai.d.ts
vendored
@ -197,7 +197,9 @@ export declare type ImageUrlResponse = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export declare type ImageUploadDestination = 'img2img' | 'inpainting';
|
||||||
|
|
||||||
export declare type UploadImagePayload = {
|
export declare type UploadImagePayload = {
|
||||||
file: File;
|
file: File;
|
||||||
destination: 'img2img' | 'inpainting';
|
destination?: ImageUploadDestination;
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
clearIntermediateImage,
|
clearIntermediateImage,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
removeImage,
|
removeImage,
|
||||||
|
setCurrentImage,
|
||||||
setIntermediateImage,
|
setIntermediateImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from '../../features/gallery/gallerySlice';
|
||||||
|
|
||||||
@ -305,6 +306,10 @@ const makeSocketIOListeners = (
|
|||||||
dispatch(setImageToInpaint(image));
|
dispatch(setImageToInpaint(image));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
dispatch(setCurrentImage(image));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
|
72
frontend/src/common/components/ImageUploader.scss
Normal file
72
frontend/src/common/components/ImageUploader.scss
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
.dropzone-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 999;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.dropzone-overlay {
|
||||||
|
opacity: 0.5;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.is-drag-accept {
|
||||||
|
box-shadow: inset 0 0 20rem 1rem var(--status-good-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-drag-reject {
|
||||||
|
box-shadow: inset 0 0 20rem 1rem var(--status-bad-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-handling-upload {
|
||||||
|
box-shadow: inset 0 0 20rem 1rem var(--status-working-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-uploader-button-outer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--subtext-color-bright);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--inpaint-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-button-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 4rem !important;
|
||||||
|
height: 4rem !important;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem !important;
|
||||||
|
}
|
||||||
|
}
|
173
frontend/src/common/components/ImageUploader.tsx
Normal file
173
frontend/src/common/components/ImageUploader.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||||
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
|
import { Heading, Spinner, useToast } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
|
import { uploadImage } from '../../app/socketio/actions';
|
||||||
|
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||||
|
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||||
|
|
||||||
|
const appSelector = createSelector(
|
||||||
|
(state: RootState) => state.options,
|
||||||
|
(options: OptionsState) => {
|
||||||
|
const { activeTab } = options;
|
||||||
|
return {
|
||||||
|
activeTabName: tabMap[activeTab],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ImageUploaderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageUploader = (props: ImageUploaderProps) => {
|
||||||
|
const { children } = props;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { activeTabName } = useAppSelector(appSelector);
|
||||||
|
const toast = useToast({});
|
||||||
|
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fileRejectionCallback = useCallback(
|
||||||
|
(rejection: FileRejection) => {
|
||||||
|
setIsHandlingUpload(true);
|
||||||
|
const msg = rejection.errors.reduce(
|
||||||
|
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: 'Upload failed',
|
||||||
|
description: msg,
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileAcceptedCallback = useCallback(
|
||||||
|
(file: File) => {
|
||||||
|
setIsHandlingUpload(true);
|
||||||
|
const payload: UploadImagePayload = { file };
|
||||||
|
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||||
|
payload.destination = activeTabName as ImageUploadDestination;
|
||||||
|
}
|
||||||
|
dispatch(uploadImage(payload));
|
||||||
|
},
|
||||||
|
[dispatch, activeTabName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||||
|
fileRejections.forEach((rejection: FileRejection) => {
|
||||||
|
fileRejectionCallback(rejection);
|
||||||
|
});
|
||||||
|
|
||||||
|
acceptedFiles.forEach((file: File) => {
|
||||||
|
fileAcceptedCallback(file);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[fileAcceptedCallback, fileRejectionCallback]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
isDragAccept,
|
||||||
|
isDragReject,
|
||||||
|
isDragActive,
|
||||||
|
open,
|
||||||
|
} = useDropzone({
|
||||||
|
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||||
|
noClick: true,
|
||||||
|
onDrop,
|
||||||
|
maxFiles: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pasteImageListener = (e: ClipboardEvent) => {
|
||||||
|
const dataTransferItemList = e.clipboardData?.items;
|
||||||
|
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) {
|
||||||
|
toast({
|
||||||
|
description:
|
||||||
|
'Multiple images pasted, may only upload one image at a time',
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = imageItems[0].getAsFile();
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
toast({
|
||||||
|
description: 'Unable to load file',
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: UploadImagePayload = { file };
|
||||||
|
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||||
|
payload.destination = activeTabName as ImageUploadDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(uploadImage(payload));
|
||||||
|
};
|
||||||
|
document.addEventListener('paste', pasteImageListener);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('paste', pasteImageListener);
|
||||||
|
};
|
||||||
|
}, [dispatch, toast, activeTabName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageUploaderTriggerContext.Provider value={open}>
|
||||||
|
<div {...getRootProps({ style: {} })}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{children}
|
||||||
|
{isDragActive && (
|
||||||
|
<div className="dropzone-container">
|
||||||
|
{isDragAccept && (
|
||||||
|
<div className="dropzone-overlay is-drag-accept">
|
||||||
|
<Heading size={'lg'}>Drop Images</Heading>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isDragReject && (
|
||||||
|
<div className="dropzone-overlay is-drag-reject">
|
||||||
|
<Heading size={'lg'}>Invalid Upload</Heading>
|
||||||
|
<Heading size={'md'}>Must be single JPEG or PNG image</Heading>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isHandlingUpload && (
|
||||||
|
<div className="dropzone-overlay is-handling-upload">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ImageUploaderTriggerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageUploader;
|
31
frontend/src/common/components/ImageUploaderButton.tsx
Normal file
31
frontend/src/common/components/ImageUploaderButton.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Heading } from '@chakra-ui/react';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { FaUpload } from 'react-icons/fa';
|
||||||
|
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||||
|
|
||||||
|
type ImageUploaderButtonProps = {
|
||||||
|
styleClass?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
|
||||||
|
const { styleClass } = props;
|
||||||
|
const open = useContext(ImageUploaderTriggerContext);
|
||||||
|
|
||||||
|
const handleClickUpload = () => {
|
||||||
|
open && open();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`image-uploader-button-outer ${styleClass}`}
|
||||||
|
onClick={handleClickUpload}
|
||||||
|
>
|
||||||
|
<div className="image-upload-button">
|
||||||
|
<FaUpload />
|
||||||
|
<Heading size={'lg'}>Click or Drag and Drop</Heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageUploaderButton;
|
@ -1,39 +0,0 @@
|
|||||||
.image-upload-zone {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
color: var(--subtext-color-bright);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--inpaint-bg-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-upload-child-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-upload-child {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 2rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 4rem !important;
|
|
||||||
height: 4rem !important;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import { Heading, useToast } from '@chakra-ui/react';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { FileRejection } from 'react-dropzone';
|
|
||||||
import { FaUpload } from 'react-icons/fa';
|
|
||||||
import ImageUploader from '../../features/options/ImageUploader';
|
|
||||||
|
|
||||||
interface InvokeImageUploaderProps {
|
|
||||||
handleFile: (file: File) => void;
|
|
||||||
styleClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
|
||||||
const { handleFile, styleClass } = props;
|
|
||||||
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
// Callbacks to for handling file upload attempts
|
|
||||||
const fileAcceptedCallback = useCallback(
|
|
||||||
(file: File) => handleFile(file),
|
|
||||||
[handleFile]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileRejectionCallback = useCallback(
|
|
||||||
(rejection: FileRejection) => {
|
|
||||||
const msg = rejection.errors.reduce(
|
|
||||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Upload failed',
|
|
||||||
description: msg,
|
|
||||||
status: 'error',
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[toast]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-upload-zone">
|
|
||||||
<ImageUploader
|
|
||||||
fileAcceptedCallback={fileAcceptedCallback}
|
|
||||||
fileRejectionCallback={fileRejectionCallback}
|
|
||||||
styleClass={
|
|
||||||
styleClass
|
|
||||||
? `${styleClass} image-upload-child-wrapper`
|
|
||||||
: `image-upload-child-wrapper`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="image-upload-child">
|
|
||||||
<FaUpload />
|
|
||||||
<Heading size={'lg'}>Upload or Drop Image Here</Heading>
|
|
||||||
</div>
|
|
||||||
</ImageUploader>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hoverable-image-context-menu {
|
.hoverable-image-context-menu {
|
||||||
z-index: 999;
|
z-index: 15;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: var(--context-menu-bg-color);
|
background-color: var(--context-menu-bg-color);
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
cloneElement,
|
|
||||||
ReactElement,
|
|
||||||
ReactNode,
|
|
||||||
SyntheticEvent,
|
|
||||||
useCallback,
|
|
||||||
} from 'react';
|
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
|
||||||
/**
|
|
||||||
* Component which, on click, should open the upload interface.
|
|
||||||
*/
|
|
||||||
children: ReactElement;
|
|
||||||
/**
|
|
||||||
* Callback to handle uploading the selected file.
|
|
||||||
*/
|
|
||||||
fileAcceptedCallback: (file: File) => void;
|
|
||||||
/**
|
|
||||||
* Callback to handle a file being rejected.
|
|
||||||
*/
|
|
||||||
fileRejectionCallback: (rejection: FileRejection) => void;
|
|
||||||
// Styling
|
|
||||||
styleClass?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File upload using react-dropzone.
|
|
||||||
* Needs a child to be the button to activate the upload interface.
|
|
||||||
*/
|
|
||||||
const ImageUploader = ({
|
|
||||||
children,
|
|
||||||
fileAcceptedCallback,
|
|
||||||
fileRejectionCallback,
|
|
||||||
styleClass,
|
|
||||||
}: ImageUploaderProps) => {
|
|
||||||
const onDrop = useCallback(
|
|
||||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
|
||||||
fileRejections.forEach((rejection: FileRejection) => {
|
|
||||||
fileRejectionCallback(rejection);
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptedFiles.forEach((file: File) => {
|
|
||||||
fileAcceptedCallback(file);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[fileAcceptedCallback, fileRejectionCallback]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getRootProps, getInputProps, open } = useDropzone({
|
|
||||||
onDrop,
|
|
||||||
accept: {
|
|
||||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
open();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box {...getRootProps()} className={styleClass}>
|
|
||||||
<input {...getInputProps({ multiple: false })} />
|
|
||||||
{cloneElement(children, {
|
|
||||||
onClick: handleClickUploadIcon,
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageUploader;
|
|
@ -1,12 +1,9 @@
|
|||||||
import { uploadImage } from '../../../app/socketio/actions';
|
import { RootState, useAppSelector } from '../../../app/store';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import ImageUploadButton from '../../../common/components/ImageUploaderButton';
|
||||||
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
|
||||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
import InitImagePreview from './InitImagePreview';
|
import InitImagePreview from './InitImagePreview';
|
||||||
|
|
||||||
const ImageToImageDisplay = () => {
|
const ImageToImageDisplay = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const initialImage = useAppSelector(
|
const initialImage = useAppSelector(
|
||||||
(state: RootState) => state.options.initialImage
|
(state: RootState) => state.options.initialImage
|
||||||
);
|
);
|
||||||
@ -18,11 +15,7 @@ const ImageToImageDisplay = () => {
|
|||||||
<InitImagePreview />
|
<InitImagePreview />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<InvokeImageUploader
|
<ImageUploadButton />
|
||||||
handleFile={(file: File) =>
|
|
||||||
dispatch(uploadImage({ file, destination: 'img2img' }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,6 +16,7 @@ import { useAppDispatch, useAppSelector } from '../../../app/store';
|
|||||||
import {
|
import {
|
||||||
addLine,
|
addLine,
|
||||||
addPointToCurrentLine,
|
addPointToCurrentLine,
|
||||||
|
clearImageToInpaint,
|
||||||
setCursorPosition,
|
setCursorPosition,
|
||||||
setIsDrawing,
|
setIsDrawing,
|
||||||
} from './inpaintingSlice';
|
} from './inpaintingSlice';
|
||||||
@ -33,6 +34,7 @@ import InpaintingBoundingBoxPreview, {
|
|||||||
} from './components/InpaintingBoundingBoxPreview';
|
} from './components/InpaintingBoundingBoxPreview';
|
||||||
import { KonvaEventObject } from 'konva/lib/Node';
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import KeyboardEventManager from './components/KeyboardEventManager';
|
import KeyboardEventManager from './components/KeyboardEventManager';
|
||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
|
||||||
// Use a closure allow other components to use these things... not ideal...
|
// Use a closure allow other components to use these things... not ideal...
|
||||||
export let stageRef: MutableRefObject<StageType | null>;
|
export let stageRef: MutableRefObject<StageType | null>;
|
||||||
@ -57,6 +59,8 @@ const InpaintingCanvas = () => {
|
|||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
} = useAppSelector(inpaintingCanvasSelector);
|
} = useAppSelector(inpaintingCanvasSelector);
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
// set the closure'd refs
|
// set the closure'd refs
|
||||||
stageRef = useRef<StageType>(null);
|
stageRef = useRef<StageType>(null);
|
||||||
maskLayerRef = useRef<Konva.Layer>(null);
|
maskLayerRef = useRef<Konva.Layer>(null);
|
||||||
@ -80,9 +84,18 @@ const InpaintingCanvas = () => {
|
|||||||
inpaintingImageElementRef.current = image;
|
inpaintingImageElementRef.current = image;
|
||||||
setCanvasBgImage(image);
|
setCanvasBgImage(image);
|
||||||
};
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
toast({
|
||||||
|
title: 'Unable to Load Image',
|
||||||
|
description: `Image ${imageToInpaint.url} failed to load`,
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
dispatch(clearImageToInpaint());
|
||||||
|
};
|
||||||
image.src = imageToInpaint.url;
|
image.src = imageToInpaint.url;
|
||||||
}
|
}
|
||||||
}, [imageToInpaint, dispatch, stageScale]);
|
}, [imageToInpaint, dispatch, stageScale, toast]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import { uploadImage } from '../../../app/socketio/actions';
|
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
import ImageUploadButton from '../../../common/components/ImageUploaderButton';
|
||||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
import { OptionsState } from '../../options/optionsSlice';
|
import { OptionsState } from '../../options/optionsSlice';
|
||||||
import InpaintingCanvas from './InpaintingCanvas';
|
import InpaintingCanvas from './InpaintingCanvas';
|
||||||
@ -36,10 +35,7 @@ const InpaintingDisplay = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const resizeCallback = _.debounce(
|
const resizeCallback = _.debounce(() => dispatch(setNeedsCache(true)), 250);
|
||||||
() => dispatch(setNeedsCache(true)),
|
|
||||||
250
|
|
||||||
);
|
|
||||||
window.addEventListener('resize', resizeCallback);
|
window.addEventListener('resize', resizeCallback);
|
||||||
return () => window.removeEventListener('resize', resizeCallback);
|
return () => window.removeEventListener('resize', resizeCallback);
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -52,11 +48,7 @@ const InpaintingDisplay = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<InvokeImageUploader
|
<ImageUploadButton />
|
||||||
handleFile={(file: File) =>
|
|
||||||
dispatch(uploadImage({ file, destination: 'inpainting' }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
@use '../common/components/IAICheckbox.scss';
|
@use '../common/components/IAICheckbox.scss';
|
||||||
@use '../common/components/IAIPopover.scss';
|
@use '../common/components/IAIPopover.scss';
|
||||||
@use '../common/components/IAIColorPicker.scss';
|
@use '../common/components/IAIColorPicker.scss';
|
||||||
@use '../common/components/InvokeImageUploader.scss';
|
@use '../common/components/ImageUploader.scss';
|
||||||
@use '../common/components/WorkInProgress/WorkInProgress.scss';
|
@use '../common/components/WorkInProgress/WorkInProgress.scss';
|
||||||
@use '../common/components/GuidePopover.scss';
|
@use '../common/components/GuidePopover.scss';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user