mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
merge with main
This commit is contained in:
@ -40,13 +40,13 @@ from .widgets import (
|
||||
TextBox,
|
||||
set_min_terminal_size,
|
||||
)
|
||||
from invokeai.app.services.config import get_invokeai_config
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
|
||||
# minimum size for the UI
|
||||
MIN_COLS = 120
|
||||
MIN_LINES = 45
|
||||
|
||||
config = get_invokeai_config()
|
||||
config = InvokeAIAppConfig.get_config()
|
||||
|
||||
class addModelsForm(npyscreen.FormMultiPage):
|
||||
# for responsive resizing - disabled
|
||||
|
@ -20,12 +20,12 @@ from npyscreen import widget
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
import invokeai.backend.util.logging as logger
|
||||
from invokeai.services.config import get_invokeai_config
|
||||
from invokeai.services.config import InvokeAIAppConfig
|
||||
from ...backend.model_management import ModelManager
|
||||
from ...frontend.install.widgets import FloatTitleSlider
|
||||
|
||||
DEST_MERGED_MODEL_DIR = "merged_models"
|
||||
config = get_invokeai_config()
|
||||
config = InvokeAIAppConfig.get_config()
|
||||
|
||||
def merge_diffusion_models(
|
||||
model_ids_or_paths: List[Union[str, Path]],
|
||||
|
@ -22,7 +22,7 @@ from omegaconf import OmegaConf
|
||||
|
||||
import invokeai.backend.util.logging as logger
|
||||
|
||||
from invokeai.app.services.config import get_invokeai_config
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
from ...backend.training import (
|
||||
do_textual_inversion_training,
|
||||
parse_args
|
||||
@ -423,7 +423,7 @@ def do_front_end(args: Namespace):
|
||||
save_args(args)
|
||||
|
||||
try:
|
||||
do_textual_inversion_training(get_invokeai_config(),**args)
|
||||
do_textual_inversion_training(InvokeAIAppConfig.get_config(),**args)
|
||||
copy_to_embeddings_folder(args)
|
||||
except Exception as e:
|
||||
logger.error("An exception occurred during training. The exception was:")
|
||||
@ -436,7 +436,7 @@ def main():
|
||||
global config
|
||||
|
||||
args = parse_args()
|
||||
config = get_invokeai_config(argv=[])
|
||||
config = InvokeAIAppConfig.get_config()
|
||||
|
||||
# change root if needed
|
||||
if args.root_dir:
|
||||
|
@ -26,10 +26,10 @@ We need to start the nodes web server, which serves the OpenAPI schema to the ge
|
||||
|
||||
```bash
|
||||
# from the repo root
|
||||
python scripts/invoke-new.py --web
|
||||
python scripts/invokeai-web.py
|
||||
```
|
||||
|
||||
2. Generate the API client.
|
||||
2. Generate the API client.
|
||||
|
||||
```bash
|
||||
# from invokeai/frontend/web/
|
||||
|
@ -12,7 +12,14 @@ Code in `invokeai/frontend/web/` if you want to have a look.
|
||||
|
||||
## Stack
|
||||
|
||||
State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a custom redux middleware to help).
|
||||
State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). We lean heavily on RTK:
|
||||
- `createAsyncThunk` for HTTP requests
|
||||
- `createEntityAdapter` for fetching images and models
|
||||
- `createListenerMiddleware` for workflows
|
||||
|
||||
The API client and associated types are generated from the OpenAPI schema. See API_CLIENT.md.
|
||||
|
||||
Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a simple socket.io redux middleware to help).
|
||||
|
||||
[Chakra-UI](https://github.com/chakra-ui/chakra-ui) for components and styling.
|
||||
|
||||
@ -37,9 +44,15 @@ From `invokeai/frontend/web/` run `yarn install` to get everything set up.
|
||||
Start everything in dev mode:
|
||||
|
||||
1. Start the dev server: `yarn dev`
|
||||
2. Start the InvokeAI Nodes backend: `python scripts/invokeai-new.py --web # run from the repo root`
|
||||
2. Start the InvokeAI Nodes backend: `python scripts/invokeai-web.py # run from the repo root`
|
||||
3. Point your browser to the dev server address e.g. <http://localhost:5173/>
|
||||
|
||||
#### VSCode Remote Dev
|
||||
|
||||
We've noticed an intermittent issue with the VSCode Remote Dev port forwarding. If you use this feature of VSCode, you may intermittently click the Invoke button and then get nothing until the request times out. Suggest disabling the IDE's port forwarding feature and doing it manually via SSH:
|
||||
|
||||
`ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host`
|
||||
|
||||
### Production builds
|
||||
|
||||
For a number of technical and logistical reasons, we need to commit UI build artefacts to the repo.
|
||||
|
@ -60,6 +60,8 @@
|
||||
"@chakra-ui/styled-system": "^2.9.0",
|
||||
"@chakra-ui/theme-tools": "^2.0.16",
|
||||
"@dagrejs/graphlib": "^2.1.12",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
@ -87,7 +89,7 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hotkeys-hook": "4.4.0",
|
||||
"react-i18next": "^12.2.2",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-konva": "^18.2.7",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-resizable-panels": "^0.0.42",
|
||||
|
@ -21,6 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react';
|
||||
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
import Toaster from './Toaster';
|
||||
import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
|
||||
|
||||
const DEFAULT_CONFIG = {};
|
||||
|
||||
@ -76,18 +77,21 @@ const App = ({
|
||||
{isLightboxEnabled && <Lightbox />}
|
||||
<ImageUploader>
|
||||
<Grid
|
||||
gap={4}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
sx={{
|
||||
gap: 4,
|
||||
p: 4,
|
||||
gridAutoRows: 'min-content auto',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{headerComponent || <SiteHeader />}
|
||||
<Flex
|
||||
gap={4}
|
||||
w={{ base: '100vw', xl: 'full' }}
|
||||
h="full"
|
||||
flexDir={{ base: 'column', xl: 'row' }}
|
||||
sx={{
|
||||
gap: 4,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<InvokeTabs />
|
||||
</Flex>
|
||||
@ -130,6 +134,7 @@ const App = ({
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
<DeleteImageModal />
|
||||
<Toaster />
|
||||
<GlobalHotkeys />
|
||||
</>
|
||||
|
@ -0,0 +1,71 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
pointerWithin,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||
import OverlayDragImage from './OverlayDragImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { isImageDTO } from 'services/types/guards';
|
||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
|
||||
type ImageDndContextProps = PropsWithChildren;
|
||||
|
||||
const ImageDndContext = (props: ImageDndContextProps) => {
|
||||
const [draggedImage, setDraggedImage] = useState<ImageDTO | null>(null);
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
const dragData = event.active.data.current;
|
||||
if (dragData && 'image' in dragData && isImageDTO(dragData.image)) {
|
||||
setDraggedImage(dragData.image);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const handleDrop = event.over?.data.current?.handleDrop;
|
||||
if (handleDrop && typeof handleDrop === 'function' && draggedImage) {
|
||||
handleDrop(draggedImage);
|
||||
}
|
||||
setDraggedImage(null);
|
||||
},
|
||||
[draggedImage]
|
||||
);
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { distance: 15 },
|
||||
});
|
||||
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { distance: 15 },
|
||||
});
|
||||
// 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 (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={pointerWithin}
|
||||
>
|
||||
{props.children}
|
||||
<DragOverlay dropAnimation={null} modifiers={[snapCenterToCursor]}>
|
||||
{draggedImage && <OverlayDragImage image={draggedImage} />}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ImageDndContext);
|
@ -0,0 +1,36 @@
|
||||
import { Box, Image } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
type OverlayDragImageProps = {
|
||||
image: ImageDTO;
|
||||
};
|
||||
|
||||
const OverlayDragImage = (props: OverlayDragImageProps) => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
userSelect: 'none',
|
||||
cursor: 'grabbing',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
sx={{
|
||||
maxW: 36,
|
||||
maxH: 36,
|
||||
borderRadius: 'base',
|
||||
shadow: 'dark-lg',
|
||||
}}
|
||||
src={props.image.thumbnail_url}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OverlayDragImage);
|
@ -16,6 +16,11 @@ import { PartialAppConfig } from 'app/types/invokeai';
|
||||
import '../../i18n';
|
||||
import { socketMiddleware } from 'services/events/middleware';
|
||||
import { Middleware } from '@reduxjs/toolkit';
|
||||
import ImageDndContext from './ImageDnd/ImageDndContext';
|
||||
import {
|
||||
DeleteImageContext,
|
||||
DeleteImageContextProvider,
|
||||
} from 'app/contexts/DeleteImageContext';
|
||||
|
||||
const App = lazy(() => import('./App'));
|
||||
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
|
||||
@ -69,11 +74,15 @@ const InvokeAIUI = ({
|
||||
<Provider store={store}>
|
||||
<React.Suspense fallback={<Loading />}>
|
||||
<ThemeLocaleProvider>
|
||||
<App
|
||||
config={config}
|
||||
headerComponent={headerComponent}
|
||||
setIsReady={setIsReady}
|
||||
/>
|
||||
<ImageDndContext>
|
||||
<DeleteImageContextProvider>
|
||||
<App
|
||||
config={config}
|
||||
headerComponent={headerComponent}
|
||||
setIsReady={setIsReady}
|
||||
/>
|
||||
</DeleteImageContextProvider>
|
||||
</ImageDndContext>
|
||||
</ThemeLocaleProvider>
|
||||
</React.Suspense>
|
||||
</Provider>
|
||||
|
203
invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
Normal file
203
invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { requestedImageDeletion } from 'features/gallery/store/actions';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
|
||||
import { nodesSelecter } from 'features/nodes/store/nodesSlice';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { some } from 'lodash-es';
|
||||
|
||||
export type ImageUsage = {
|
||||
isInitialImage: boolean;
|
||||
isCanvasImage: boolean;
|
||||
isNodesImage: boolean;
|
||||
isControlNetImage: boolean;
|
||||
};
|
||||
|
||||
export const selectImageUsage = createSelector(
|
||||
[
|
||||
generationSelector,
|
||||
canvasSelector,
|
||||
nodesSelecter,
|
||||
controlNetSelector,
|
||||
(state: RootState, image_name?: string) => image_name,
|
||||
],
|
||||
(generation, canvas, nodes, controlNet, image_name) => {
|
||||
const isInitialImage = generation.initialImage?.image_name === image_name;
|
||||
|
||||
const isCanvasImage = canvas.layerState.objects.some(
|
||||
(obj) => obj.kind === 'image' && obj.image.image_name === image_name
|
||||
);
|
||||
|
||||
const isNodesImage = nodes.nodes.some((node) => {
|
||||
return some(
|
||||
node.data.inputs,
|
||||
(input) =>
|
||||
input.type === 'image' && input.value?.image_name === image_name
|
||||
);
|
||||
});
|
||||
|
||||
const isControlNetImage = some(
|
||||
controlNet.controlNets,
|
||||
(c) =>
|
||||
c.controlImage?.image_name === image_name ||
|
||||
c.processedControlImage?.image_name === image_name
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isInitialImage,
|
||||
isCanvasImage,
|
||||
isNodesImage,
|
||||
isControlNetImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type DeleteImageContextValue = {
|
||||
/**
|
||||
* Whether the delete image dialog is open.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
/**
|
||||
* Closes the delete image dialog.
|
||||
*/
|
||||
onClose: () => void;
|
||||
/**
|
||||
* Opens the delete image dialog and handles all deletion-related checks.
|
||||
*/
|
||||
onDelete: (image?: ImageDTO) => void;
|
||||
/**
|
||||
* The image pending deletion
|
||||
*/
|
||||
image?: ImageDTO;
|
||||
/**
|
||||
* The features in which this image is used
|
||||
*/
|
||||
imageUsage?: ImageUsage;
|
||||
/**
|
||||
* Immediately deletes an image.
|
||||
*
|
||||
* You probably don't want to use this - use `onDelete` instead.
|
||||
*/
|
||||
onImmediatelyDelete: () => void;
|
||||
};
|
||||
|
||||
export const DeleteImageContext = createContext<DeleteImageContextValue>({
|
||||
isOpen: false,
|
||||
onClose: () => undefined,
|
||||
onImmediatelyDelete: () => undefined,
|
||||
onDelete: () => undefined,
|
||||
});
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector],
|
||||
(system) => {
|
||||
const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
|
||||
|
||||
return {
|
||||
canDeleteImage: isConnected && !isProcessing,
|
||||
shouldConfirmOnDelete,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type Props = PropsWithChildren;
|
||||
|
||||
export const DeleteImageContextProvider = (props: Props) => {
|
||||
const { canDeleteImage, shouldConfirmOnDelete } = useAppSelector(selector);
|
||||
const [imageToDelete, setImageToDelete] = useState<ImageDTO>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// Check where the image to be deleted is used (eg init image, controlnet, etc.)
|
||||
const imageUsage = useAppSelector((state) =>
|
||||
selectImageUsage(state, imageToDelete?.image_name)
|
||||
);
|
||||
|
||||
// Clean up after deleting or dismissing the modal
|
||||
const closeAndClearImageToDelete = useCallback(() => {
|
||||
setImageToDelete(undefined);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
// Dispatch the actual deletion action, to be handled by listener middleware
|
||||
const handleActualDeletion = useCallback(
|
||||
(image: ImageDTO) => {
|
||||
dispatch(requestedImageDeletion({ image, imageUsage }));
|
||||
closeAndClearImageToDelete();
|
||||
},
|
||||
[closeAndClearImageToDelete, dispatch, imageUsage]
|
||||
);
|
||||
|
||||
// This is intended to be called by the delete button in the dialog
|
||||
const onImmediatelyDelete = useCallback(() => {
|
||||
if (canDeleteImage && imageToDelete) {
|
||||
handleActualDeletion(imageToDelete);
|
||||
}
|
||||
closeAndClearImageToDelete();
|
||||
}, [
|
||||
canDeleteImage,
|
||||
imageToDelete,
|
||||
closeAndClearImageToDelete,
|
||||
handleActualDeletion,
|
||||
]);
|
||||
|
||||
const handleGatedDeletion = useCallback(
|
||||
(image: ImageDTO) => {
|
||||
if (shouldConfirmOnDelete || some(imageUsage)) {
|
||||
// If we should confirm on delete, or if the image is in use, open the dialog
|
||||
onOpen();
|
||||
} else {
|
||||
handleActualDeletion(image);
|
||||
}
|
||||
},
|
||||
[imageUsage, shouldConfirmOnDelete, onOpen, handleActualDeletion]
|
||||
);
|
||||
|
||||
// Consumers of the context call this to delete an image
|
||||
const onDelete = useCallback((image?: ImageDTO) => {
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
// Set the image to delete, then let the effect call the actual deletion
|
||||
setImageToDelete(image);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// We need to use an effect here to trigger the image usage selector, else we get a stale value
|
||||
if (imageToDelete) {
|
||||
handleGatedDeletion(imageToDelete);
|
||||
}
|
||||
}, [handleGatedDeletion, imageToDelete]);
|
||||
|
||||
return (
|
||||
<DeleteImageContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
image: imageToDelete,
|
||||
onClose: closeAndClearImageToDelete,
|
||||
onDelete,
|
||||
onImmediatelyDelete,
|
||||
imageUsage,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</DeleteImageContext.Provider>
|
||||
);
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||
import { controlNetDenylist } from 'features/controlNet/store/controlNetDenylist';
|
||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
||||
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
|
||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||
@ -23,6 +24,7 @@ const serializationDenylist: {
|
||||
system: systemPersistDenylist,
|
||||
// config: configPersistDenyList,
|
||||
ui: uiPersistDenylist,
|
||||
controlNet: controlNetDenylist,
|
||||
// hotkeys: hotkeysPersistDenylist,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||
import { initialControlNetState } from 'features/controlNet/store/controlNetSlice';
|
||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { initialImagesState } from 'features/gallery/store/imagesSlice';
|
||||
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
|
||||
@ -28,6 +29,7 @@ const initialStates: {
|
||||
ui: initialUIState,
|
||||
hotkeys: initialHotkeysState,
|
||||
images: initialImagesState,
|
||||
controlNet: initialControlNetState,
|
||||
};
|
||||
|
||||
export const unserialize: UnserializeFunction = (data, key) => {
|
||||
|
@ -70,6 +70,9 @@ import {
|
||||
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
|
||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -173,3 +176,10 @@ addReceivedPageOfImagesRejectedListener();
|
||||
|
||||
// Gallery
|
||||
addImageCategoriesChangedListener();
|
||||
|
||||
// ControlNet
|
||||
addControlNetImageProcessedListener();
|
||||
addControlNetAutoProcessListener();
|
||||
|
||||
// Update image URLs on connect
|
||||
addUpdateImageUrlsOnConnectListener();
|
||||
|
@ -28,6 +28,13 @@ export const addCanvasCopiedToClipboardListener = () => {
|
||||
}
|
||||
|
||||
copyBlobToClipboard(blob);
|
||||
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Canvas Copied to Clipboard',
|
||||
status: 'success',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -27,7 +27,8 @@ export const addCanvasDownloadedAsImageListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, 'mergedCanvas.png');
|
||||
downloadBlob(blob, 'canvas.png');
|
||||
dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,22 +1,20 @@
|
||||
import { canvasMerged } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||
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({
|
||||
actionCreator: canvasMerged,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state, true);
|
||||
const blob = await getFullBaseLayerBlob();
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
@ -48,12 +46,12 @@ export const addCanvasMergedListener = () => {
|
||||
relativeTo: canvasBaseLayer.getParent(),
|
||||
});
|
||||
|
||||
const filename = `mergedCanvas_${uuidv4()}.png`;
|
||||
|
||||
dispatch(
|
||||
const imageUploadedRequest = dispatch(
|
||||
imageUploaded({
|
||||
formData: {
|
||||
file: new File([blob], filename, { type: 'image/png' }),
|
||||
file: new File([blob], MERGED_CANVAS_FILENAME, {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
imageCategory: 'general',
|
||||
isIntermediate: true,
|
||||
@ -61,9 +59,11 @@ export const addCanvasMergedListener = () => {
|
||||
);
|
||||
|
||||
const [{ payload }] = await take(
|
||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(action) &&
|
||||
action.meta.arg.formData.file.name === filename
|
||||
(
|
||||
uploadedImageAction
|
||||
): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||
);
|
||||
|
||||
const mergedCanvasImage = payload;
|
||||
|
@ -4,9 +4,10 @@ import { log } from 'app/logging/useLogger';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
export const addCanvasSavedToGalleryListener = () => {
|
||||
@ -15,7 +16,7 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state, true);
|
||||
const blob = await getBaseLayerBlob(state);
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
@ -29,12 +30,12 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = `mergedCanvas_${uuidv4()}.png`;
|
||||
|
||||
dispatch(
|
||||
const imageUploadedRequest = dispatch(
|
||||
imageUploaded({
|
||||
formData: {
|
||||
file: new File([blob], filename, { type: 'image/png' }),
|
||||
file: new File([blob], SAVED_CANVAS_FILENAME, {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
imageCategory: 'general',
|
||||
isIntermediate: false,
|
||||
@ -42,9 +43,11 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
);
|
||||
|
||||
const [{ payload: uploadedImageDTO }] = await take(
|
||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(action) &&
|
||||
action.meta.arg.formData.file.name === filename
|
||||
(
|
||||
uploadedImageAction
|
||||
): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||
);
|
||||
|
||||
dispatch(imageUpserted(uploadedImageDTO));
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||
import {
|
||||
controlNetImageChanged,
|
||||
controlNetProcessorParamsChanged,
|
||||
controlNetProcessorTypeChanged,
|
||||
} from 'features/controlNet/store/controlNetSlice';
|
||||
import { RootState } from 'app/store/store';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'controlNet' });
|
||||
|
||||
const predicate = (action: AnyAction, state: RootState) => {
|
||||
const isActionMatched =
|
||||
controlNetProcessorParamsChanged.match(action) ||
|
||||
controlNetImageChanged.match(action) ||
|
||||
controlNetProcessorTypeChanged.match(action);
|
||||
|
||||
if (!isActionMatched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { controlImage, processorType } =
|
||||
state.controlNet.controlNets[action.payload.controlNetId];
|
||||
|
||||
const isProcessorSelected = processorType !== 'none';
|
||||
|
||||
const isBusy = state.system.isProcessing;
|
||||
|
||||
const hasControlImage = Boolean(controlImage);
|
||||
|
||||
return isProcessorSelected && !isBusy && hasControlImage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listener that automatically processes a ControlNet image when its processor parameters are changed.
|
||||
*
|
||||
* The network request is debounced by 1 second.
|
||||
*/
|
||||
export const addControlNetAutoProcessListener = () => {
|
||||
startAppListening({
|
||||
predicate,
|
||||
effect: async (
|
||||
action,
|
||||
{ dispatch, getState, cancelActiveListeners, delay }
|
||||
) => {
|
||||
const { controlNetId } = action.payload;
|
||||
|
||||
// Cancel any in-progress instances of this listener
|
||||
cancelActiveListeners();
|
||||
|
||||
// Delay before starting actual work
|
||||
await delay(300);
|
||||
|
||||
dispatch(controlNetImageProcessed({ controlNetId }));
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/thunks/image';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||
import { Graph } from 'services/api';
|
||||
import { sessionCreated } from 'services/thunks/session';
|
||||
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'controlNet' });
|
||||
|
||||
export const addControlNetImageProcessedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: controlNetImageProcessed,
|
||||
effect: async (
|
||||
action,
|
||||
{ dispatch, getState, take, unsubscribe, subscribe }
|
||||
) => {
|
||||
const { controlNetId } = action.payload;
|
||||
const controlNet = getState().controlNet.controlNets[controlNetId];
|
||||
|
||||
if (!controlNet.controlImage) {
|
||||
moduleLog.error('Unable to process ControlNet image');
|
||||
return;
|
||||
}
|
||||
|
||||
// ControlNet one-off procressing graph is just the processor node, no edges.
|
||||
// Also we need to grab the image.
|
||||
const graph: Graph = {
|
||||
nodes: {
|
||||
[controlNet.processorNode.id]: {
|
||||
...controlNet.processorNode,
|
||||
is_intermediate: true,
|
||||
image: pick(controlNet.controlImage, [
|
||||
'image_name',
|
||||
'image_origin',
|
||||
]),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Create a session to run the graph & wait til it's ready to invoke
|
||||
const sessionCreatedAction = dispatch(sessionCreated({ graph }));
|
||||
const [sessionCreatedFulfilledAction] = await take(
|
||||
(action): action is ReturnType<typeof sessionCreated.fulfilled> =>
|
||||
sessionCreated.fulfilled.match(action) &&
|
||||
action.meta.requestId === sessionCreatedAction.requestId
|
||||
);
|
||||
|
||||
const sessionId = sessionCreatedFulfilledAction.payload.id;
|
||||
|
||||
// Invoke the session & wait til it's complete
|
||||
dispatch(sessionReadyToInvoke());
|
||||
const [invocationCompleteAction] = await take(
|
||||
(action): action is ReturnType<typeof socketInvocationComplete> =>
|
||||
socketInvocationComplete.match(action) &&
|
||||
action.payload.data.graph_execution_state_id === sessionId
|
||||
);
|
||||
|
||||
// We still have to check the output type
|
||||
if (isImageOutput(invocationCompleteAction.payload.data.result)) {
|
||||
const { image_name } =
|
||||
invocationCompleteAction.payload.data.result.image;
|
||||
|
||||
// Wait for the ImageDTO to be received
|
||||
const [imageMetadataReceivedAction] = await take(
|
||||
(
|
||||
action
|
||||
): action is ReturnType<typeof imageMetadataReceived.fulfilled> =>
|
||||
imageMetadataReceived.fulfilled.match(action) &&
|
||||
action.payload.image_name === image_name
|
||||
);
|
||||
const processedControlImage = imageMetadataReceivedAction.payload;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { arg: action.payload, processedControlImage } },
|
||||
'ControlNet image processed'
|
||||
);
|
||||
|
||||
// Update the processed image in the store
|
||||
dispatch(
|
||||
controlNetProcessedImageChanged({
|
||||
controlNetId,
|
||||
processedControlImage,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -6,10 +6,13 @@ import { clamp } from 'lodash-es';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
imageRemoved,
|
||||
imagesAdapter,
|
||||
selectImagesEntities,
|
||||
selectImagesIds,
|
||||
} from 'features/gallery/store/imagesSlice';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||
|
||||
@ -20,11 +23,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: requestedImageDeletion,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const image = action.payload;
|
||||
if (!image) {
|
||||
moduleLog.warn('No image provided');
|
||||
return;
|
||||
}
|
||||
const { image, imageUsage } = action.payload;
|
||||
|
||||
const { image_name, image_origin } = image;
|
||||
|
||||
@ -58,8 +57,28 @@ export const addRequestedImageDeletionListener = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
|
||||
if (imageUsage.isCanvasImage) {
|
||||
dispatch(resetCanvas());
|
||||
}
|
||||
|
||||
if (imageUsage.isControlNetImage) {
|
||||
dispatch(controlNetReset());
|
||||
}
|
||||
|
||||
if (imageUsage.isInitialImage) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
||||
if (imageUsage.isNodesImage) {
|
||||
dispatch(nodeEditorReset());
|
||||
}
|
||||
|
||||
// Preemptively remove from gallery
|
||||
dispatch(imageRemoved(image_name));
|
||||
|
||||
// Delete from server
|
||||
dispatch(
|
||||
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
||||
);
|
||||
@ -74,9 +93,7 @@ export const addImageDeletedPendingListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageDeleted.pending,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { imageName, imageOrigin } = action.meta.arg;
|
||||
// Preemptively remove the image from the gallery
|
||||
imagesAdapter.removeOne(getState().images, imageName);
|
||||
//
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/thunks/image';
|
||||
import { imageMetadataReceived, imageUpdated } from 'services/thunks/image';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
@ -10,10 +10,29 @@ export const addImageMetadataReceivedFulfilledListener = () => {
|
||||
actionCreator: imageMetadataReceived.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const image = action.payload;
|
||||
if (image.is_intermediate) {
|
||||
|
||||
const state = getState();
|
||||
|
||||
if (
|
||||
image.session_id === state.canvas.layerState.stagingArea.sessionId &&
|
||||
state.canvas.shouldAutoSave
|
||||
) {
|
||||
dispatch(
|
||||
imageUpdated({
|
||||
imageName: image.image_name,
|
||||
imageOrigin: image.image_origin,
|
||||
requestBody: { is_intermediate: false },
|
||||
})
|
||||
);
|
||||
} else if (image.is_intermediate) {
|
||||
// No further actions needed for intermediate images
|
||||
moduleLog.trace(
|
||||
{ data: { image } },
|
||||
'Image metadata received (intermediate), skipping'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
||||
dispatch(imageUpserted(image));
|
||||
},
|
||||
|
@ -3,6 +3,8 @@ 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';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -19,9 +21,22 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const originalFileName = action.meta.arg.formData.file.name;
|
||||
|
||||
dispatch(imageUpserted(image));
|
||||
|
||||
if (originalFileName === SAVED_CANVAS_FILENAME) {
|
||||
dispatch(
|
||||
addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (originalFileName === MERGED_CANVAS_FILENAME) {
|
||||
dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
},
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
import { imagesAdapter } from 'features/gallery/store/imagesSlice';
|
||||
import { imageUpdatedOne } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -14,13 +14,12 @@ export const addImageUrlsReceivedFulfilledListener = () => {
|
||||
|
||||
const { image_name, image_url, thumbnail_url } = image;
|
||||
|
||||
imagesAdapter.updateOne(getState().images, {
|
||||
id: image_name,
|
||||
changes: {
|
||||
image_url,
|
||||
thumbnail_url,
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
imageUpdatedOne({
|
||||
id: image_name,
|
||||
changes: { image_url, thumbnail_url },
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -2,12 +2,10 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { t } from 'i18next';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { startAppListening } from '..';
|
||||
import {
|
||||
initialImageSelected,
|
||||
isImageDTO,
|
||||
} from 'features/parameters/store/actions';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { makeToast } from 'app/components/Toaster';
|
||||
import { selectImagesById } from 'features/gallery/store/imagesSlice';
|
||||
import { isImageDTO } from 'services/types/guards';
|
||||
|
||||
export const addInitialImageSelectedListener = () => {
|
||||
startAppListening({
|
||||
|
@ -0,0 +1,93 @@
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { nodesSelecter } from 'features/nodes/store/nodesSlice';
|
||||
import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { forEach, uniqBy } from 'lodash-es';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { selectImagesEntities } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'images' });
|
||||
|
||||
const selectAllUsedImages = createSelector(
|
||||
[
|
||||
generationSelector,
|
||||
canvasSelector,
|
||||
nodesSelecter,
|
||||
controlNetSelector,
|
||||
selectImagesEntities,
|
||||
],
|
||||
(generation, canvas, nodes, controlNet, imageEntities) => {
|
||||
const allUsedImages: ImageDTO[] = [];
|
||||
|
||||
if (generation.initialImage) {
|
||||
allUsedImages.push(generation.initialImage);
|
||||
}
|
||||
|
||||
canvas.layerState.objects.forEach((obj) => {
|
||||
if (obj.kind === 'image') {
|
||||
allUsedImages.push(obj.image);
|
||||
}
|
||||
});
|
||||
|
||||
nodes.nodes.forEach((node) => {
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (input.type === 'image' && input.value) {
|
||||
allUsedImages.push(input.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
forEach(controlNet.controlNets, (c) => {
|
||||
if (c.controlImage) {
|
||||
allUsedImages.push(c.controlImage);
|
||||
}
|
||||
if (c.processedControlImage) {
|
||||
allUsedImages.push(c.processedControlImage);
|
||||
}
|
||||
});
|
||||
|
||||
forEach(imageEntities, (image) => {
|
||||
if (image) {
|
||||
allUsedImages.push(image);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueImages = uniqBy(allUsedImages, 'image_name');
|
||||
|
||||
return uniqueImages;
|
||||
}
|
||||
);
|
||||
|
||||
export const addUpdateImageUrlsOnConnectListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketConnected,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const state = getState();
|
||||
|
||||
if (!state.config.shouldUpdateImagesOnConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allUsedImages = selectAllUsedImages(state);
|
||||
|
||||
moduleLog.trace(
|
||||
{ data: allUsedImages },
|
||||
`Fetching new image URLs for ${allUsedImages.length} images`
|
||||
);
|
||||
|
||||
allUsedImages.forEach(({ image_name, image_origin }) => {
|
||||
dispatch(
|
||||
imageUrlsReceived({
|
||||
imageName: image_name,
|
||||
imageOrigin: image_origin,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -13,6 +13,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||
import imagesReducer from 'features/gallery/store/imagesSlice';
|
||||
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
||||
import generationReducer from 'features/parameters/store/generationSlice';
|
||||
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
|
||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
||||
import systemReducer from 'features/system/store/systemSlice';
|
||||
// import sessionReducer from 'features/system/store/sessionSlice';
|
||||
@ -45,6 +46,7 @@ const allReducers = {
|
||||
ui: uiReducer,
|
||||
hotkeys: hotkeysReducer,
|
||||
images: imagesReducer,
|
||||
controlNet: controlNetReducer,
|
||||
// session: sessionReducer,
|
||||
};
|
||||
|
||||
@ -62,6 +64,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
|
||||
'postprocessing',
|
||||
'system',
|
||||
'ui',
|
||||
'controlNet',
|
||||
// 'hotkeys',
|
||||
// 'config',
|
||||
];
|
||||
|
@ -95,6 +95,7 @@ export type AppFeature =
|
||||
* A disable-able Stable Diffusion feature
|
||||
*/
|
||||
export type SDFeature =
|
||||
| 'controlNet'
|
||||
| 'noise'
|
||||
| 'variation'
|
||||
| 'symmetry'
|
||||
@ -107,12 +108,9 @@ export type SDFeature =
|
||||
*/
|
||||
export type AppConfig = {
|
||||
/**
|
||||
* Whether or not URLs should be transformed to use a different host
|
||||
*/
|
||||
shouldTransformUrls: boolean;
|
||||
/**
|
||||
* Whether or not we need to re-fetch images
|
||||
* Whether or not we should update image urls when image loading errors
|
||||
*/
|
||||
shouldUpdateImagesOnConnect: boolean;
|
||||
disabledTabs: InvokeTabName[];
|
||||
disabledFeatures: AppFeature[];
|
||||
disabledSDFeatures: SDFeature[];
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
type IAICheckboxProps = CheckboxProps & {
|
||||
label: string | ReactNode;
|
||||
};
|
||||
|
||||
const IAICheckbox = (props: IAICheckboxProps) => {
|
||||
const { label, ...rest } = props;
|
||||
return (
|
||||
<Checkbox colorScheme="accent" {...rest}>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAICheckbox);
|
@ -49,7 +49,7 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}>
|
||||
<Box sx={{ p: 4, borderBottomRadius: 'base', bg: 'base.800' }}>
|
||||
{children}
|
||||
</Box>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CheckIcon } from '@chakra-ui/icons';
|
||||
import { CheckIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@ -10,7 +10,6 @@ import {
|
||||
GridItem,
|
||||
List,
|
||||
ListItem,
|
||||
Select,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
@ -19,7 +18,8 @@ import { autoUpdate, offset, shift, useFloating } from '@floating-ui/react-dom';
|
||||
import { useSelect } from 'downshift';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { getInputOutlineStyles } from 'theme/util/getInputOutlineStyles';
|
||||
|
||||
export type ItemTooltips = { [key: string]: string };
|
||||
|
||||
@ -34,6 +34,7 @@ type IAICustomSelectProps = {
|
||||
buttonProps?: FlexProps;
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
ellipsisPosition?: 'start' | 'end';
|
||||
};
|
||||
|
||||
const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
@ -48,6 +49,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
tooltip,
|
||||
buttonProps,
|
||||
tooltipProps,
|
||||
ellipsisPosition = 'end',
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@ -69,6 +71,14 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
middleware: [offset(4), shift({ crossAxis: true, padding: 8 })],
|
||||
});
|
||||
|
||||
const labelTextDirection = useMemo(() => {
|
||||
if (ellipsisPosition === 'start') {
|
||||
return document.dir === 'rtl' ? 'ltr' : 'rtl';
|
||||
}
|
||||
|
||||
return document.dir;
|
||||
}, [ellipsisPosition]);
|
||||
|
||||
return (
|
||||
<FormControl sx={{ w: 'full' }} {...formControlProps}>
|
||||
{label && (
|
||||
@ -82,20 +92,44 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
</FormLabel>
|
||||
)}
|
||||
<Tooltip label={tooltip} {...tooltipProps}>
|
||||
<Select
|
||||
<Flex
|
||||
{...getToggleButtonProps({ ref: refs.setReference })}
|
||||
{...buttonProps}
|
||||
as={Flex}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
width: 'full',
|
||||
py: 1,
|
||||
px: 2,
|
||||
gap: 2,
|
||||
justifyContent: 'space-between',
|
||||
...getInputOutlineStyles(),
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontSize: 'sm', fontWeight: 500, color: 'base.100' }}>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
fontWeight: 500,
|
||||
color: 'base.100',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
direction: labelTextDirection,
|
||||
}}
|
||||
>
|
||||
{selectedItem}
|
||||
</Text>
|
||||
</Select>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
color: 'base.300',
|
||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Box {...getMenuProps()}>
|
||||
{isOpen && (
|
||||
@ -104,11 +138,10 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
ref={refs.setFloating}
|
||||
sx={{
|
||||
...floatingStyles,
|
||||
width: 'max-content',
|
||||
top: 0,
|
||||
left: 0,
|
||||
insetInlineStart: 0,
|
||||
flexDirection: 'column',
|
||||
zIndex: 1,
|
||||
zIndex: 2,
|
||||
bg: 'base.800',
|
||||
borderRadius: 'base',
|
||||
border: '1px',
|
||||
@ -118,61 +151,72 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
|
||||
px: 0,
|
||||
h: 'fit-content',
|
||||
maxH: 64,
|
||||
minW: 48,
|
||||
}}
|
||||
>
|
||||
<OverlayScrollbarsComponent>
|
||||
{items.map((item, index) => (
|
||||
<Tooltip
|
||||
isDisabled={!itemTooltips}
|
||||
key={`${item}${index}`}
|
||||
label={itemTooltips?.[item]}
|
||||
hasArrow
|
||||
placement="right"
|
||||
>
|
||||
<ListItem
|
||||
sx={{
|
||||
bg: highlightedIndex === index ? 'base.700' : undefined,
|
||||
py: 1,
|
||||
paddingInlineStart: 3,
|
||||
paddingInlineEnd: 6,
|
||||
cursor: 'pointer',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.15s',
|
||||
}}
|
||||
{items.map((item, index) => {
|
||||
const isSelected = selectedItem === item;
|
||||
const isHighlighted = highlightedIndex === index;
|
||||
const fontWeight = isSelected ? 700 : 500;
|
||||
const bg = isHighlighted
|
||||
? 'base.700'
|
||||
: isSelected
|
||||
? 'base.750'
|
||||
: undefined;
|
||||
return (
|
||||
<Tooltip
|
||||
isDisabled={!itemTooltips}
|
||||
key={`${item}${index}`}
|
||||
{...getItemProps({ item, index })}
|
||||
label={itemTooltips?.[item]}
|
||||
hasArrow
|
||||
placement="right"
|
||||
>
|
||||
{withCheckIcon ? (
|
||||
<Grid gridTemplateColumns="1.25rem auto">
|
||||
<GridItem>
|
||||
{selectedItem === item && <CheckIcon boxSize={2} />}
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
color: 'base.100',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
) : (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
color: 'base.100',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
)}
|
||||
</ListItem>
|
||||
</Tooltip>
|
||||
))}
|
||||
<ListItem
|
||||
sx={{
|
||||
bg,
|
||||
py: 1,
|
||||
paddingInlineStart: 3,
|
||||
paddingInlineEnd: 6,
|
||||
cursor: 'pointer',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.15s',
|
||||
}}
|
||||
key={`${item}${index}`}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
{withCheckIcon ? (
|
||||
<Grid gridTemplateColumns="1.25rem auto">
|
||||
<GridItem>
|
||||
{isSelected && <CheckIcon boxSize={2} />}
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
color: 'base.100',
|
||||
fontWeight,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
) : (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
color: 'base.50',
|
||||
fontWeight,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
)}
|
||||
</ListItem>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</OverlayScrollbarsComponent>
|
||||
</List>
|
||||
)}
|
||||
|
167
invokeai/frontend/web/src/common/components/IAIDndImage.tsx
Normal file
167
invokeai/frontend/web/src/common/components/IAIDndImage.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { Box, Flex, Icon, IconButtonProps, Image } from '@chakra-ui/react';
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { useCombinedRefs } from '@dnd-kit/utilities';
|
||||
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 { memo, useRef } from 'react';
|
||||
import { FaImage, FaTimes } from 'react-icons/fa';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import IAIDropOverlay from './IAIDropOverlay';
|
||||
|
||||
type IAIDndImageProps = {
|
||||
image: ImageDTO | null | undefined;
|
||||
onDrop: (droppedImage: ImageDTO) => void;
|
||||
onReset?: () => void;
|
||||
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
||||
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
||||
resetIconSize?: IconButtonProps['size'];
|
||||
withResetIcon?: boolean;
|
||||
withMetadataOverlay?: boolean;
|
||||
isDragDisabled?: boolean;
|
||||
isDropDisabled?: boolean;
|
||||
fallback?: ReactElement;
|
||||
payloadImage?: ImageDTO | null | undefined;
|
||||
minSize?: number;
|
||||
};
|
||||
|
||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
const {
|
||||
image,
|
||||
onDrop,
|
||||
onReset,
|
||||
onError,
|
||||
resetIconSize = 'md',
|
||||
withResetIcon = false,
|
||||
withMetadataOverlay = false,
|
||||
isDropDisabled = false,
|
||||
isDragDisabled = false,
|
||||
fallback = <IAIImageFallback />,
|
||||
payloadImage,
|
||||
minSize = 24,
|
||||
} = props;
|
||||
const dndId = useRef(uuidv4());
|
||||
const {
|
||||
isOver,
|
||||
setNodeRef: setDroppableRef,
|
||||
active,
|
||||
} = useDroppable({
|
||||
id: dndId.current,
|
||||
disabled: isDropDisabled,
|
||||
data: {
|
||||
handleDrop: onDrop,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef: setDraggableRef,
|
||||
} = useDraggable({
|
||||
id: dndId.current,
|
||||
data: {
|
||||
image: payloadImage ? payloadImage : image,
|
||||
},
|
||||
disabled: isDragDisabled,
|
||||
});
|
||||
|
||||
const setNodeRef = useCombinedRefs(setDroppableRef, setDraggableRef);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
minW: minSize,
|
||||
minH: minSize,
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{image && (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={image.image_url}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallback={fallback}
|
||||
onError={onError}
|
||||
objectFit="contain"
|
||||
draggable={false}
|
||||
sx={{
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay image={image} />}
|
||||
{onReset && withResetIcon && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<IAIIconButton
|
||||
size={resetIconSize}
|
||||
tooltip="Reset Image"
|
||||
aria-label="Reset Image"
|
||||
icon={<FaTimes />}
|
||||
onClick={onReset}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{active && <IAIDropOverlay isOver={isOver} />}
|
||||
</AnimatePresence>
|
||||
</Flex>
|
||||
)}
|
||||
{!image && (
|
||||
<>
|
||||
<Flex
|
||||
sx={{
|
||||
minH: minSize,
|
||||
bg: 'base.850',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
as={FaImage}
|
||||
sx={{
|
||||
boxSize: 24,
|
||||
color: 'base.500',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<AnimatePresence>
|
||||
{active && <IAIDropOverlay isOver={isOver} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAIDndImage);
|
@ -0,0 +1,91 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
type Props = {
|
||||
isOver: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const IAIDropOverlay = (props: Props) => {
|
||||
const { isOver, label = 'Drop' } = props;
|
||||
const motionId = useRef(uuidv4());
|
||||
return (
|
||||
<motion.div
|
||||
key={motionId.current}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.900',
|
||||
opacity: 0.7,
|
||||
borderRadius: 'base',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
opacity: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: isOver ? 'base.200' : 'base.500',
|
||||
borderRadius: 'base',
|
||||
borderStyle: 'dashed',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: '2xl',
|
||||
fontWeight: 600,
|
||||
transform: isOver ? 'scale(1.1)' : 'scale(1)',
|
||||
color: isOver ? 'base.100' : 'base.500',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAIDropOverlay);
|
@ -0,0 +1,25 @@
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxProps,
|
||||
FormControl,
|
||||
FormControlProps,
|
||||
FormLabel,
|
||||
} from '@chakra-ui/react';
|
||||
import { memo, ReactNode } from 'react';
|
||||
|
||||
type IAIFullCheckboxProps = CheckboxProps & {
|
||||
label: string | ReactNode;
|
||||
formControlProps?: FormControlProps;
|
||||
};
|
||||
|
||||
const IAIFullCheckbox = (props: IAIFullCheckboxProps) => {
|
||||
const { label, formControlProps, ...rest } = props;
|
||||
return (
|
||||
<FormControl {...formControlProps}>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Checkbox colorScheme="accent" {...rest} />
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAIFullCheckbox);
|
@ -0,0 +1,27 @@
|
||||
import { Flex, FlexProps, Spinner, SpinnerProps } from '@chakra-ui/react';
|
||||
|
||||
type Props = FlexProps & {
|
||||
spinnerProps?: SpinnerProps;
|
||||
};
|
||||
|
||||
export const IAIImageFallback = (props: Props) => {
|
||||
const { spinnerProps, ...rest } = props;
|
||||
const { sx, ...restFlexProps } = rest;
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
bg: 'base.900',
|
||||
opacity: 0.7,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
...sx,
|
||||
}}
|
||||
{...restFlexProps}
|
||||
>
|
||||
<Spinner size="xl" {...spinnerProps} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react';
|
||||
import { memo, ReactElement } from 'react';
|
||||
|
||||
type IAISimpleCheckboxProps = CheckboxProps & {
|
||||
label: string | ReactElement;
|
||||
};
|
||||
|
||||
const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => {
|
||||
const { label, ...rest } = props;
|
||||
return (
|
||||
<Checkbox colorScheme="accent" {...rest}>
|
||||
<Text color="base.200" fontSize="md">
|
||||
{label}
|
||||
</Text>
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAISimpleCheckbox);
|
@ -40,7 +40,7 @@ import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
|
||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||
|
||||
export type IAIFullSliderProps = {
|
||||
label: string;
|
||||
label?: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
@ -178,9 +178,11 @@ const IAISlider = (props: IAIFullSliderProps) => {
|
||||
isDisabled={isDisabled}
|
||||
{...sliderFormControlProps}
|
||||
>
|
||||
<FormLabel {...sliderFormLabelProps} mb={-1}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
{label && (
|
||||
<FormLabel {...sliderFormLabelProps} mb={-1}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<HStack w="100%" gap={2} alignItems="center">
|
||||
<Slider
|
||||
@ -203,6 +205,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
|
||||
sx={{
|
||||
insetInlineStart: '0 !important',
|
||||
insetInlineEnd: 'unset !important',
|
||||
mt: 1.5,
|
||||
}}
|
||||
{...sliderMarkProps}
|
||||
>
|
||||
@ -213,6 +216,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
|
||||
sx={{
|
||||
insetInlineStart: 'unset !important',
|
||||
insetInlineEnd: '0 !important',
|
||||
mt: 1.5,
|
||||
}}
|
||||
{...sliderMarkProps}
|
||||
>
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
FormLabelProps,
|
||||
Switch,
|
||||
SwitchProps,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
|
||||
@ -13,6 +14,7 @@ interface Props extends SwitchProps {
|
||||
width?: string | number;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -25,22 +27,27 @@ const IAISwitch = (props: Props) => {
|
||||
width = 'auto',
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
tooltip,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
width={width}
|
||||
display="flex"
|
||||
gap={4}
|
||||
alignItems="center"
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel my={1} flexGrow={1} {...formLabelProps}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<Switch {...rest} />
|
||||
</FormControl>
|
||||
<Tooltip label={tooltip} hasArrow placement="top" isDisabled={!tooltip}>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
width={width}
|
||||
display="flex"
|
||||
gap={4}
|
||||
alignItems="center"
|
||||
{...formControlProps}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel my={1} flexGrow={1} {...formLabelProps}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Switch {...rest} />
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Badge, Flex } from '@chakra-ui/react';
|
||||
import { isNumber, isString } from 'lodash-es';
|
||||
import { isString } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
@ -8,14 +8,6 @@ type ImageMetadataOverlayProps = {
|
||||
};
|
||||
|
||||
const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
|
||||
const dimensions = useMemo(() => {
|
||||
if (!isNumber(image.metadata?.width) || isNumber(!image.metadata?.height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `${image.metadata?.width} × ${image.metadata?.height}`;
|
||||
}, [image.metadata]);
|
||||
|
||||
const model = useMemo(() => {
|
||||
if (!isString(image.metadata?.model)) {
|
||||
return;
|
||||
@ -31,17 +23,15 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
|
||||
flexDirection: 'column',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
insetInlineStart: 0,
|
||||
p: 2,
|
||||
alignItems: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{dimensions && (
|
||||
<Badge variant="solid" colorScheme="base">
|
||||
{dimensions}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="solid" colorScheme="base">
|
||||
{image.width} × {image.height}
|
||||
</Badge>
|
||||
{model && (
|
||||
<Badge variant="solid" colorScheme="base">
|
||||
{model}
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { ButtonGroup, Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex w="full" alignItems="center">
|
||||
<Text size="sm" fontWeight={500} color="base.300">
|
||||
{t('parameters.initialImage')}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<ButtonGroup>
|
||||
<IAIIconButton
|
||||
icon={<FaUndo />}
|
||||
aria-label={t('accessibility.reset')}
|
||||
onClick={handleResetInitialImage}
|
||||
/>
|
||||
<IAIIconButton
|
||||
icon={<FaUpload />}
|
||||
onClick={openUploader}
|
||||
aria-label={t('common.upload')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitialImageButtons;
|
@ -1,12 +1,12 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export const readinessSelector = createSelector(
|
||||
const readinessSelector = createSelector(
|
||||
[generationSelector, systemSelector, activeTabNameSelector],
|
||||
(generation, system, activeTabName) => {
|
||||
const {
|
||||
@ -60,3 +60,8 @@ export const readinessSelector = createSelector(
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const useIsReadyToInvoke = () => {
|
||||
const { isReady } = useAppSelector(readinessSelector);
|
||||
return isReady;
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCallback } from 'react';
|
||||
import { OpenAPI } from 'services/api';
|
||||
|
||||
export const getUrlAlt = (url: string, shouldTransformUrls: boolean) => {
|
||||
if (OpenAPI.BASE && shouldTransformUrls) {
|
||||
return [OpenAPI.BASE, url].join('/');
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
export const useGetUrl = () => {
|
||||
const shouldTransformUrls = useAppSelector(
|
||||
(state: RootState) => state.config.shouldTransformUrls
|
||||
);
|
||||
|
||||
const getUrl = useCallback(
|
||||
(url?: string) => {
|
||||
if (OpenAPI.BASE && shouldTransformUrls) {
|
||||
return [OpenAPI.BASE, url].join('/');
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
[shouldTransformUrls]
|
||||
);
|
||||
|
||||
return {
|
||||
shouldTransformUrls,
|
||||
getUrl,
|
||||
};
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
import { isEqual } from 'lodash-es';
|
||||
@ -33,7 +32,6 @@ const selector = createSelector(
|
||||
|
||||
const IAICanvasObjectRenderer = () => {
|
||||
const { objects } = useAppSelector(selector);
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
if (!objects) return null;
|
||||
|
||||
@ -46,7 +44,7 @@ const IAICanvasObjectRenderer = () => {
|
||||
key={i}
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
url={getUrl(obj.image.image_url)}
|
||||
url={obj.image.image_url}
|
||||
/>
|
||||
);
|
||||
} else if (isCanvasBaseLine(obj)) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { GroupConfig } from 'konva/lib/Group';
|
||||
import { isEqual } from 'lodash-es';
|
||||
@ -56,13 +55,12 @@ const IAICanvasStagingArea = (props: Props) => {
|
||||
width,
|
||||
height,
|
||||
} = useAppSelector(selector);
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
return (
|
||||
<Group {...rest}>
|
||||
{shouldShowStagingImage && currentStagingAreaImage && (
|
||||
<IAICanvasImage
|
||||
url={getUrl(currentStagingAreaImage.image.image_url) ?? ''}
|
||||
url={currentStagingAreaImage.image.image_url}
|
||||
x={x}
|
||||
y={y}
|
||||
/>
|
||||
|
@ -2,7 +2,7 @@ import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
@ -117,12 +117,12 @@ const IAICanvasMaskOptions = () => {
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={`${t('unifiedCanvas.enableMask')} (H)`}
|
||||
isChecked={isMaskEnabled}
|
||||
onChange={handleToggleEnableMask}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.preserveMaskedArea')}
|
||||
isChecked={shouldPreserveMaskedArea}
|
||||
onChange={(e) =>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
@ -102,50 +102,50 @@ const IAICanvasSettingsButtonPopover = () => {
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.showIntermediates')}
|
||||
isChecked={shouldShowIntermediates}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldShowIntermediates(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.showGrid')}
|
||||
isChecked={shouldShowGrid}
|
||||
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.snapToGrid')}
|
||||
isChecked={shouldSnapToGrid}
|
||||
onChange={handleChangeShouldSnapToGrid}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.darkenOutsideSelection')}
|
||||
isChecked={shouldDarkenOutsideBoundingBox}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.autoSaveToGallery')}
|
||||
isChecked={shouldAutoSave}
|
||||
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.saveBoxRegionOnly')}
|
||||
isChecked={shouldCropToBoundingBoxOnSave}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.limitStrokesToBox')}
|
||||
isChecked={shouldRestrictStrokesToBox}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldRestrictStrokesToBox(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.showCanvasDebugInfo')}
|
||||
isChecked={shouldShowCanvasDebugInfo}
|
||||
onChange={(e) =>
|
||||
@ -153,7 +153,7 @@ const IAICanvasSettingsButtonPopover = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('unifiedCanvas.antialiasing')}
|
||||
isChecked={shouldAntialias}
|
||||
onChange={(e) => dispatch(setShouldAntialias(e.target.checked))}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import { Box, ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
@ -210,16 +210,19 @@ const IAICanvasToolbar = () => {
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<IAISelect
|
||||
tooltip={`${t('unifiedCanvas.layer')} (Q)`}
|
||||
tooltipProps={{ hasArrow: true, placement: 'top' }}
|
||||
value={layer}
|
||||
validValues={LAYER_NAMES_DICT}
|
||||
onChange={handleChangeLayer}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
<Box w={24}>
|
||||
<IAISelect
|
||||
tooltip={`${t('unifiedCanvas.layer')} (Q)`}
|
||||
tooltipProps={{ hasArrow: true, placement: 'top' }}
|
||||
value={layer}
|
||||
validValues={LAYER_NAMES_DICT}
|
||||
onChange={handleChangeLayer}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<IAICanvasMaskOptions />
|
||||
<IAICanvasToolChooserOptions />
|
||||
|
@ -30,6 +30,8 @@ import {
|
||||
} from './canvasTypes';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { sessionCanceled } from 'services/thunks/session';
|
||||
import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
|
||||
export const initialLayerState: CanvasLayerState = {
|
||||
objects: [],
|
||||
@ -851,6 +853,30 @@ export const canvasSlice = createSlice({
|
||||
state.layerState.stagingArea = initialLayerState.stagingArea;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => {
|
||||
state.doesCanvasNeedScaling = true;
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||
action.payload;
|
||||
|
||||
state.layerState.objects.forEach((object) => {
|
||||
if (object.kind === 'image') {
|
||||
if (object.image.image_name === image_name) {
|
||||
object.image.image_url = image_url;
|
||||
object.image.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.layerState.stagingArea.images.forEach((stagedImage) => {
|
||||
if (stagedImage.image.image_name === image_name) {
|
||||
stagedImage.image.image_url = image_url;
|
||||
stagedImage.image.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2,10 +2,10 @@ import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
export const getBaseLayerBlob = async (
|
||||
state: RootState,
|
||||
withoutBoundingBox?: boolean
|
||||
) => {
|
||||
/**
|
||||
* Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave`
|
||||
*/
|
||||
export const getBaseLayerBlob = async (state: RootState) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
@ -24,15 +24,14 @@ export const getBaseLayerBlob = async (
|
||||
|
||||
const absPos = clonedBaseLayer.getAbsolutePosition();
|
||||
|
||||
const boundingBox =
|
||||
shouldCropToBoundingBoxOnSave && !withoutBoundingBox
|
||||
? {
|
||||
x: boundingBoxCoordinates.x + absPos.x,
|
||||
y: boundingBoxCoordinates.y + absPos.y,
|
||||
width: boundingBoxDimensions.width,
|
||||
height: boundingBoxDimensions.height,
|
||||
}
|
||||
: clonedBaseLayer.getClientRect();
|
||||
const boundingBox = shouldCropToBoundingBoxOnSave
|
||||
? {
|
||||
x: boundingBoxCoordinates.x + absPos.x,
|
||||
y: boundingBoxCoordinates.y + absPos.y,
|
||||
width: boundingBoxDimensions.width,
|
||||
height: boundingBoxDimensions.height,
|
||||
}
|
||||
: clonedBaseLayer.getClientRect();
|
||||
|
||||
return konvaNodeToBlob(clonedBaseLayer, boundingBox);
|
||||
};
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
/**
|
||||
* Gets the canvas base layer blob, without bounding box
|
||||
*/
|
||||
export const getFullBaseLayerBlob = async () => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedBaseLayer = canvasBaseLayer.clone();
|
||||
|
||||
clonedBaseLayer.scale({ x: 1, y: 1 });
|
||||
|
||||
return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect());
|
||||
};
|
@ -0,0 +1,258 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import {
|
||||
ControlNetConfig,
|
||||
controlNetAdded,
|
||||
controlNetRemoved,
|
||||
controlNetToggled,
|
||||
} from '../store/controlNetSlice';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import ParamControlNetModel from './parameters/ParamControlNetModel';
|
||||
import ParamControlNetWeight from './parameters/ParamControlNetWeight';
|
||||
import {
|
||||
Checkbox,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaCopy, FaPlus, FaTrash, FaWrench } from 'react-icons/fa';
|
||||
|
||||
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
|
||||
import ControlNetImagePreview from './ControlNetImagePreview';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useToggle } from 'react-use';
|
||||
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
|
||||
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
|
||||
import ControlNetPreprocessButton from './ControlNetPreprocessButton';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||||
|
||||
type ControlNetProps = {
|
||||
controlNet: ControlNetConfig;
|
||||
};
|
||||
|
||||
const ControlNet = (props: ControlNetProps) => {
|
||||
const {
|
||||
controlNetId,
|
||||
isEnabled,
|
||||
model,
|
||||
weight,
|
||||
beginStepPct,
|
||||
endStepPct,
|
||||
controlImage,
|
||||
processedControlImage,
|
||||
processorNode,
|
||||
processorType,
|
||||
} = props.controlNet;
|
||||
const dispatch = useAppDispatch();
|
||||
const [shouldShowAdvanced, onToggleAdvanced] = useToggle(false);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dispatch(controlNetRemoved({ controlNetId }));
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
dispatch(
|
||||
controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet })
|
||||
);
|
||||
}, [dispatch, props.controlNet]);
|
||||
|
||||
const handleToggleIsEnabled = useCallback(() => {
|
||||
dispatch(controlNetToggled({ controlNetId }));
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
p: 3,
|
||||
bg: 'base.850',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2 }}>
|
||||
<IAISwitch
|
||||
tooltip="Toggle"
|
||||
aria-label="Toggle"
|
||||
isChecked={isEnabled}
|
||||
onChange={handleToggleIsEnabled}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
w: 'full',
|
||||
minW: 0,
|
||||
opacity: isEnabled ? 1 : 0.5,
|
||||
pointerEvents: isEnabled ? 'auto' : 'none',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
>
|
||||
<ParamControlNetModel controlNetId={controlNetId} model={model} />
|
||||
</Box>
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
tooltip="Duplicate"
|
||||
aria-label="Duplicate"
|
||||
onClick={handleDuplicate}
|
||||
icon={<FaCopy />}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
tooltip="Delete"
|
||||
aria-label="Delete"
|
||||
colorScheme="error"
|
||||
onClick={handleDelete}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label="Expand"
|
||||
onClick={onToggleAdvanced}
|
||||
variant="link"
|
||||
icon={
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
boxSize: 4,
|
||||
color: 'base.300',
|
||||
transform: shouldShowAdvanced
|
||||
? 'rotate(0deg)'
|
||||
: 'rotate(180deg)',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<Flex sx={{ gap: 4 }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
w: 'full',
|
||||
h: 24,
|
||||
paddingInlineStart: 1,
|
||||
paddingInlineEnd: shouldShowAdvanced ? 1 : 0,
|
||||
pb: 2,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<ParamControlNetWeight
|
||||
controlNetId={controlNetId}
|
||||
weight={weight}
|
||||
mini
|
||||
/>
|
||||
<ParamControlNetBeginEnd
|
||||
controlNetId={controlNetId}
|
||||
beginStepPct={beginStepPct}
|
||||
endStepPct={endStepPct}
|
||||
mini
|
||||
/>
|
||||
</Flex>
|
||||
{!shouldShowAdvanced && (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
h: 24,
|
||||
w: 24,
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
>
|
||||
<ControlNetImagePreview controlNet={props.controlNet} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{shouldShowAdvanced && (
|
||||
<>
|
||||
<Box pt={2}>
|
||||
<ControlNetImagePreview controlNet={props.controlNet} />
|
||||
</Box>
|
||||
<ParamControlNetProcessorSelect
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
<ControlNetProcessorComponent
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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);
|
@ -0,0 +1,144 @@
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import {
|
||||
ControlNetConfig,
|
||||
controlNetImageChanged,
|
||||
controlNetSelector,
|
||||
} from '../store/controlNetSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { useHoverDirty } from 'react-use';
|
||||
|
||||
const selector = createSelector(
|
||||
controlNetSelector,
|
||||
(controlNet) => {
|
||||
const { isProcessingControlImage } = controlNet;
|
||||
return { isProcessingControlImage };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type Props = {
|
||||
controlNet: ControlNetConfig;
|
||||
};
|
||||
|
||||
const ControlNetImagePreview = (props: Props) => {
|
||||
const { controlNetId, controlImage, processedControlImage, processorType } =
|
||||
props.controlNet;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessingControlImage } = useAppSelector(selector);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isMouseOverImage = useHoverDirty(containerRef);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (controlImage?.image_name === droppedImage.image_name) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
controlNetImageChanged({ controlNetId, controlImage: droppedImage })
|
||||
);
|
||||
},
|
||||
[controlImage, controlNetId, dispatch]
|
||||
);
|
||||
|
||||
const shouldShowProcessedImageBackdrop =
|
||||
Number(controlImage?.width) > Number(processedControlImage?.width) ||
|
||||
Number(controlImage?.height) > Number(processedControlImage?.height);
|
||||
|
||||
const shouldShowProcessedImage =
|
||||
controlImage &&
|
||||
processedControlImage &&
|
||||
!isMouseOverImage &&
|
||||
!isProcessingControlImage &&
|
||||
processorType !== 'none';
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{ position: 'relative', w: 'full', h: 'full', aspectRatio: '1/1' }}
|
||||
>
|
||||
<IAIDndImage
|
||||
image={controlImage}
|
||||
onDrop={handleDrop}
|
||||
isDropDisabled={Boolean(
|
||||
processedControlImage && processorType !== 'none'
|
||||
)}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{shouldShowProcessedImage && (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
}}
|
||||
>
|
||||
{shouldShowProcessedImageBackdrop && (
|
||||
<Box
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.900',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<IAIDndImage
|
||||
image={processedControlImage}
|
||||
onDrop={handleDrop}
|
||||
payloadImage={controlImage}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isProcessingControlImage && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<IAIImageFallback />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ControlNetImagePreview);
|
@ -0,0 +1,36 @@
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { ControlNetConfig } from '../store/controlNetSlice';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { controlNetImageProcessed } from '../store/actions';
|
||||
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
||||
|
||||
type Props = {
|
||||
controlNet: ControlNetConfig;
|
||||
};
|
||||
|
||||
const ControlNetPreprocessButton = (props: Props) => {
|
||||
const { controlNetId, controlImage } = props.controlNet;
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useIsReadyToInvoke();
|
||||
|
||||
const handleProcess = useCallback(() => {
|
||||
dispatch(
|
||||
controlNetImageProcessed({
|
||||
controlNetId,
|
||||
})
|
||||
);
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
return (
|
||||
<IAIButton
|
||||
size="sm"
|
||||
onClick={handleProcess}
|
||||
isDisabled={Boolean(!controlImage) || !isReady}
|
||||
>
|
||||
Preprocess
|
||||
</IAIButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ControlNetPreprocessButton);
|
@ -0,0 +1,131 @@
|
||||
import { memo } from 'react';
|
||||
import { RequiredControlNetProcessorNode } from '../store/types';
|
||||
import CannyProcessor from './processors/CannyProcessor';
|
||||
import HedProcessor from './processors/HedProcessor';
|
||||
import LineartProcessor from './processors/LineartProcessor';
|
||||
import LineartAnimeProcessor from './processors/LineartAnimeProcessor';
|
||||
import ContentShuffleProcessor from './processors/ContentShuffleProcessor';
|
||||
import MediapipeFaceProcessor from './processors/MediapipeFaceProcessor';
|
||||
import MidasDepthProcessor from './processors/MidasDepthProcessor';
|
||||
import MlsdImageProcessor from './processors/MlsdImageProcessor';
|
||||
import NormalBaeProcessor from './processors/NormalBaeProcessor';
|
||||
import OpenposeProcessor from './processors/OpenposeProcessor';
|
||||
import PidiProcessor from './processors/PidiProcessor';
|
||||
import ZoeDepthProcessor from './processors/ZoeDepthProcessor';
|
||||
|
||||
export type ControlNetProcessorProps = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredControlNetProcessorNode;
|
||||
};
|
||||
|
||||
const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
if (processorNode.type === 'canny_image_processor') {
|
||||
return (
|
||||
<CannyProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'hed_image_processor') {
|
||||
return (
|
||||
<HedProcessor controlNetId={controlNetId} processorNode={processorNode} />
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'lineart_image_processor') {
|
||||
return (
|
||||
<LineartProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'content_shuffle_image_processor') {
|
||||
return (
|
||||
<ContentShuffleProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'lineart_anime_image_processor') {
|
||||
return (
|
||||
<LineartAnimeProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'mediapipe_face_processor') {
|
||||
return (
|
||||
<MediapipeFaceProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'midas_depth_image_processor') {
|
||||
return (
|
||||
<MidasDepthProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'mlsd_image_processor') {
|
||||
return (
|
||||
<MlsdImageProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'normalbae_image_processor') {
|
||||
return (
|
||||
<NormalBaeProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'openpose_image_processor') {
|
||||
return (
|
||||
<OpenposeProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'pidi_image_processor') {
|
||||
return (
|
||||
<PidiProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (processorNode.type === 'zoe_depth_image_processor') {
|
||||
return (
|
||||
<ZoeDepthProcessor
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(ControlNetProcessorComponent);
|
@ -0,0 +1,20 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { controlNetProcessorParamsChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { ControlNetProcessorNode } from 'features/controlNet/store/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useProcessorNodeChanged = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleProcessorNodeChanged = useCallback(
|
||||
(controlNetId: string, changes: Partial<ControlNetProcessorNode>) => {
|
||||
dispatch(
|
||||
controlNetProcessorParamsChanged({
|
||||
controlNetId,
|
||||
changes,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return handleProcessorNodeChanged;
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
RangeSlider,
|
||||
RangeSliderFilledTrack,
|
||||
RangeSliderMark,
|
||||
RangeSliderThumb,
|
||||
RangeSliderTrack,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import {
|
||||
controlNetBeginStepPctChanged,
|
||||
controlNetEndStepPctChanged,
|
||||
} from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
beginStepPct: number;
|
||||
endStepPct: number;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
||||
|
||||
const ParamControlNetBeginEnd = (props: Props) => {
|
||||
const { controlNetId, beginStepPct, endStepPct, mini = false } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleStepPctChanged = useCallback(
|
||||
(v: number[]) => {
|
||||
dispatch(
|
||||
controlNetBeginStepPctChanged({ controlNetId, beginStepPct: v[0] })
|
||||
);
|
||||
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: v[1] }));
|
||||
},
|
||||
[controlNetId, dispatch]
|
||||
);
|
||||
|
||||
const handleStepPctReset = useCallback(() => {
|
||||
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
|
||||
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 1 }));
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel>Begin / End Step Percentage</FormLabel>
|
||||
<HStack w="100%" gap={2} alignItems="center">
|
||||
<RangeSlider
|
||||
aria-label={['Begin Step %', 'End Step %']}
|
||||
value={[beginStepPct, endStepPct]}
|
||||
onChange={handleStepPctChanged}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
minStepsBetweenThumbs={5}
|
||||
>
|
||||
<RangeSliderTrack>
|
||||
<RangeSliderFilledTrack />
|
||||
</RangeSliderTrack>
|
||||
<Tooltip label={formatPct(beginStepPct)} placement="top" hasArrow>
|
||||
<RangeSliderThumb index={0} />
|
||||
</Tooltip>
|
||||
<Tooltip label={formatPct(endStepPct)} placement="top" hasArrow>
|
||||
<RangeSliderThumb index={1} />
|
||||
</Tooltip>
|
||||
{!mini && (
|
||||
<>
|
||||
<RangeSliderMark
|
||||
value={0}
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
fontWeight: '500',
|
||||
color: 'base.200',
|
||||
insetInlineStart: '0 !important',
|
||||
insetInlineEnd: 'unset !important',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
0%
|
||||
</RangeSliderMark>
|
||||
<RangeSliderMark
|
||||
value={0.5}
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
fontWeight: '500',
|
||||
color: 'base.200',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
50%
|
||||
</RangeSliderMark>
|
||||
<RangeSliderMark
|
||||
value={1}
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
fontWeight: '500',
|
||||
color: 'base.200',
|
||||
insetInlineStart: 'unset !important',
|
||||
insetInlineEnd: '0 !important',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
100%
|
||||
</RangeSliderMark>
|
||||
</>
|
||||
)}
|
||||
</RangeSlider>
|
||||
|
||||
{!mini && (
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('accessibility.reset')}
|
||||
tooltip={t('accessibility.reset')}
|
||||
icon={<BiReset />}
|
||||
onClick={handleStepPctReset}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetBeginEnd);
|
@ -0,0 +1,28 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { controlNetToggled } from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type ParamControlNetIsEnabledProps = {
|
||||
controlNetId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => {
|
||||
const { controlNetId, isEnabled } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleIsEnabledChanged = useCallback(() => {
|
||||
dispatch(controlNetToggled({ controlNetId }));
|
||||
}, [dispatch, controlNetId]);
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Enabled"
|
||||
isChecked={isEnabled}
|
||||
onChange={handleIsEnabledChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetIsEnabled);
|
@ -0,0 +1,36 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIFullCheckbox from 'common/components/IAIFullCheckbox';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import {
|
||||
controlNetToggled,
|
||||
isControlNetImagePreprocessedToggled,
|
||||
} from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type ParamControlNetIsEnabledProps = {
|
||||
controlNetId: string;
|
||||
isControlImageProcessed: boolean;
|
||||
};
|
||||
|
||||
const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => {
|
||||
const { controlNetId, isControlImageProcessed } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleIsControlImageProcessedToggled = useCallback(() => {
|
||||
dispatch(
|
||||
isControlNetImagePreprocessedToggled({
|
||||
controlNetId,
|
||||
})
|
||||
);
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Preprocess"
|
||||
isChecked={isControlImageProcessed}
|
||||
onChange={handleIsControlImageProcessedToggled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetIsEnabled);
|
@ -0,0 +1,41 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAICustomSelect from 'common/components/IAICustomSelect';
|
||||
import {
|
||||
CONTROLNET_MODELS,
|
||||
ControlNetModel,
|
||||
} from 'features/controlNet/store/constants';
|
||||
import { controlNetModelChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type ParamIsControlNetModelProps = {
|
||||
controlNetId: string;
|
||||
model: ControlNetModel;
|
||||
};
|
||||
|
||||
const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => {
|
||||
const { controlNetId, model } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleModelChanged = useCallback(
|
||||
(val: string | null | undefined) => {
|
||||
// TODO: do not cast
|
||||
const model = val as ControlNetModel;
|
||||
dispatch(controlNetModelChanged({ controlNetId, model }));
|
||||
},
|
||||
[controlNetId, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAICustomSelect
|
||||
tooltip={model}
|
||||
tooltipProps={{ placement: 'top', hasArrow: true }}
|
||||
items={CONTROLNET_MODELS}
|
||||
selectedItem={model}
|
||||
setSelectedItem={handleModelChanged}
|
||||
ellipsisPosition="start"
|
||||
withCheckIcon
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamIsControlNetModel);
|
@ -0,0 +1,47 @@
|
||||
import IAICustomSelect from 'common/components/IAICustomSelect';
|
||||
import { memo, useCallback } from 'react';
|
||||
import {
|
||||
ControlNetProcessorNode,
|
||||
ControlNetProcessorType,
|
||||
} from '../../store/types';
|
||||
import { controlNetProcessorTypeChanged } from '../../store/controlNetSlice';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { CONTROLNET_PROCESSORS } from '../../store/constants';
|
||||
|
||||
type ParamControlNetProcessorSelectProps = {
|
||||
controlNetId: string;
|
||||
processorNode: ControlNetProcessorNode;
|
||||
};
|
||||
|
||||
const CONTROLNET_PROCESSOR_TYPES = Object.keys(
|
||||
CONTROLNET_PROCESSORS
|
||||
) as ControlNetProcessorType[];
|
||||
|
||||
const ParamControlNetProcessorSelect = (
|
||||
props: ParamControlNetProcessorSelectProps
|
||||
) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const handleProcessorTypeChanged = useCallback(
|
||||
(v: string | null | undefined) => {
|
||||
dispatch(
|
||||
controlNetProcessorTypeChanged({
|
||||
controlNetId,
|
||||
processorType: v as ControlNetProcessorType,
|
||||
})
|
||||
);
|
||||
},
|
||||
[controlNetId, dispatch]
|
||||
);
|
||||
return (
|
||||
<IAICustomSelect
|
||||
label="Processor"
|
||||
items={CONTROLNET_PROCESSOR_TYPES}
|
||||
selectedItem={processorNode.type ?? 'canny_image_processor'}
|
||||
setSelectedItem={handleProcessorTypeChanged}
|
||||
withCheckIcon
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetProcessorSelect);
|
@ -0,0 +1,57 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { controlNetWeightChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type ParamControlNetWeightProps = {
|
||||
controlNetId: string;
|
||||
weight: number;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
|
||||
const { controlNetId, weight, mini = false } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleWeightChanged = useCallback(
|
||||
(weight: number) => {
|
||||
dispatch(controlNetWeightChanged({ controlNetId, weight }));
|
||||
},
|
||||
[controlNetId, dispatch]
|
||||
);
|
||||
|
||||
const handleWeightReset = () => {
|
||||
dispatch(controlNetWeightChanged({ controlNetId, weight: 1 }));
|
||||
};
|
||||
|
||||
if (mini) {
|
||||
return (
|
||||
<IAISlider
|
||||
label={'Weight'}
|
||||
sliderFormLabelProps={{ pb: 1 }}
|
||||
value={weight}
|
||||
onChange={handleWeightChanged}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IAISlider
|
||||
label="Weight"
|
||||
value={weight}
|
||||
onChange={handleWeightChanged}
|
||||
withInput
|
||||
withReset
|
||||
handleReset={handleWeightReset}
|
||||
withSliderMarks
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetWeight);
|
@ -0,0 +1,72 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor.default;
|
||||
|
||||
type CannyProcessorProps = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredCannyImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const CannyProcessor = (props: CannyProcessorProps) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { low_threshold, high_threshold } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleLowThresholdChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { low_threshold: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleLowThresholdReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
low_threshold: DEFAULTS.low_threshold,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleHighThresholdChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { high_threshold: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleHighThresholdReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
high_threshold: DEFAULTS.high_threshold,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Low Threshold"
|
||||
value={low_threshold}
|
||||
onChange={handleLowThresholdChanged}
|
||||
handleReset={handleLowThresholdReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={255}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="High Threshold"
|
||||
value={high_threshold}
|
||||
onChange={handleHighThresholdChanged}
|
||||
handleReset={handleHighThresholdReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={255}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CannyProcessor);
|
@ -0,0 +1,141 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredContentShuffleImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const ContentShuffleProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution, w, h, f } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleWChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { w: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleWReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
w: DEFAULTS.w,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleHChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { h: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleHReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
h: DEFAULTS.h,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleFChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { f: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleFReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
f: DEFAULTS.f,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="W"
|
||||
value={w}
|
||||
onChange={handleWChanged}
|
||||
handleReset={handleWReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="H"
|
||||
value={h}
|
||||
onChange={handleHChanged}
|
||||
handleReset={handleHReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="F"
|
||||
value={f}
|
||||
onChange={handleFChanged}
|
||||
handleReset={handleFReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ContentShuffleProcessor);
|
@ -0,0 +1,88 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor.default;
|
||||
|
||||
type HedProcessorProps = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredHedImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const HedPreprocessor = (props: HedProcessorProps) => {
|
||||
const {
|
||||
controlNetId,
|
||||
processorNode: { detect_resolution, image_resolution, scribble },
|
||||
} = props;
|
||||
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleScribbleChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
processorChanged(controlNetId, { scribble: e.target.checked });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISwitch
|
||||
label="Scribble"
|
||||
isChecked={scribble}
|
||||
onChange={handleScribbleChanged}
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(HedPreprocessor);
|
@ -0,0 +1,72 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredLineartAnimeImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const LineartAnimeProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LineartAnimeProcessor);
|
@ -0,0 +1,85 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor.default;
|
||||
|
||||
type LineartProcessorProps = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredLineartImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const LineartProcessor = (props: LineartProcessorProps) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution, coarse } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleCoarseChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
processorChanged(controlNetId, { coarse: e.target.checked });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISwitch
|
||||
label="Coarse"
|
||||
isChecked={coarse}
|
||||
onChange={handleCoarseChanged}
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LineartProcessor);
|
@ -0,0 +1,69 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredMediapipeFaceProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredMediapipeFaceProcessorInvocation;
|
||||
};
|
||||
|
||||
const MediapipeFaceProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { max_faces, min_confidence } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleMaxFacesChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { max_faces: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleMinConfidenceChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { min_confidence: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleMaxFacesReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { max_faces: DEFAULTS.max_faces });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleMinConfidenceReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { min_confidence: DEFAULTS.min_confidence });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Max Faces"
|
||||
value={max_faces}
|
||||
onChange={handleMaxFacesChanged}
|
||||
handleReset={handleMaxFacesReset}
|
||||
withReset
|
||||
min={1}
|
||||
max={20}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Min Confidence"
|
||||
value={min_confidence}
|
||||
onChange={handleMinConfidenceChanged}
|
||||
handleReset={handleMinConfidenceReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MediapipeFaceProcessor);
|
@ -0,0 +1,70 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredMidasDepthImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const MidasDepthProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { a_mult, bg_th } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleAMultChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { a_mult: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleBgThChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { bg_th: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleAMultReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { a_mult: DEFAULTS.a_mult });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleBgThReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { bg_th: DEFAULTS.bg_th });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="a_mult"
|
||||
value={a_mult}
|
||||
onChange={handleAMultChanged}
|
||||
handleReset={handleAMultReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={20}
|
||||
step={0.01}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="bg_th"
|
||||
value={bg_th}
|
||||
onChange={handleBgThChanged}
|
||||
handleReset={handleBgThReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={20}
|
||||
step={0.01}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MidasDepthProcessor);
|
@ -0,0 +1,116 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredMlsdImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const MlsdImageProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleThrDChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { thr_d: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleThrVChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { thr_v: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleThrDReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { thr_d: DEFAULTS.thr_d });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleThrVReset = useCallback(() => {
|
||||
processorChanged(controlNetId, { thr_v: DEFAULTS.thr_v });
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="W"
|
||||
value={thr_d}
|
||||
onChange={handleThrDChanged}
|
||||
handleReset={handleThrDReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="H"
|
||||
value={thr_v}
|
||||
onChange={handleThrVChanged}
|
||||
handleReset={handleThrVReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MlsdImageProcessor);
|
@ -0,0 +1,72 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredNormalbaeImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const NormalBaeProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NormalBaeProcessor);
|
@ -0,0 +1,85 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredOpenposeImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const OpenposeProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution, hand_and_face } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleHandAndFaceChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
processorChanged(controlNetId, { hand_and_face: e.target.checked });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISwitch
|
||||
label="Hand and Face"
|
||||
isChecked={hand_and_face}
|
||||
onChange={handleHandAndFaceChanged}
|
||||
/>
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OpenposeProcessor);
|
@ -0,0 +1,93 @@
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
|
||||
import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
|
||||
import ProcessorWrapper from './common/ProcessorWrapper';
|
||||
|
||||
const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor.default;
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredPidiImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const PidiProcessor = (props: Props) => {
|
||||
const { controlNetId, processorNode } = props;
|
||||
const { image_resolution, detect_resolution, scribble, safe } = processorNode;
|
||||
const processorChanged = useProcessorNodeChanged();
|
||||
|
||||
const handleDetectResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { detect_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleImageResolutionChanged = useCallback(
|
||||
(v: number) => {
|
||||
processorChanged(controlNetId, { image_resolution: v });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleDetectResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
detect_resolution: DEFAULTS.detect_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleImageResolutionReset = useCallback(() => {
|
||||
processorChanged(controlNetId, {
|
||||
image_resolution: DEFAULTS.image_resolution,
|
||||
});
|
||||
}, [controlNetId, processorChanged]);
|
||||
|
||||
const handleScribbleChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
processorChanged(controlNetId, { scribble: e.target.checked });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
const handleSafeChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
processorChanged(controlNetId, { safe: e.target.checked });
|
||||
},
|
||||
[controlNetId, processorChanged]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessorWrapper>
|
||||
<IAISlider
|
||||
label="Detect Resolution"
|
||||
value={detect_resolution}
|
||||
onChange={handleDetectResolutionChanged}
|
||||
handleReset={handleDetectResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISlider
|
||||
label="Image Resolution"
|
||||
value={image_resolution}
|
||||
onChange={handleImageResolutionChanged}
|
||||
handleReset={handleImageResolutionReset}
|
||||
withReset
|
||||
min={0}
|
||||
max={4096}
|
||||
withInput
|
||||
/>
|
||||
<IAISwitch
|
||||
label="Scribble"
|
||||
isChecked={scribble}
|
||||
onChange={handleScribbleChanged}
|
||||
/>
|
||||
<IAISwitch label="Safe" isChecked={safe} onChange={handleSafeChanged} />
|
||||
</ProcessorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PidiProcessor);
|
@ -0,0 +1,14 @@
|
||||
import { RequiredZoeDepthImageProcessorInvocation } from 'features/controlNet/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
processorNode: RequiredZoeDepthImageProcessorInvocation;
|
||||
};
|
||||
|
||||
const ZoeDepthProcessor = (props: Props) => {
|
||||
// Has no parameters?
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(ZoeDepthProcessor);
|
@ -0,0 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type Props = PropsWithChildren;
|
||||
|
||||
export default function ProcessorWrapper(props: Props) {
|
||||
return <Flex sx={{ flexDirection: 'column', gap: 2 }}>{props.children}</Flex>;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const controlNetImageProcessed = createAction<{
|
||||
controlNetId: string;
|
||||
}>('controlNet/imageProcessed');
|
212
invokeai/frontend/web/src/features/controlNet/store/constants.ts
Normal file
212
invokeai/frontend/web/src/features/controlNet/store/constants.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import {
|
||||
ControlNetProcessorType,
|
||||
RequiredCannyImageProcessorInvocation,
|
||||
RequiredControlNetProcessorNode,
|
||||
} from './types';
|
||||
|
||||
type ControlNetProcessorsDict = Record<
|
||||
ControlNetProcessorType,
|
||||
{
|
||||
type: ControlNetProcessorType;
|
||||
label: string;
|
||||
description: string;
|
||||
default: RequiredControlNetProcessorNode;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* A dict of ControlNet processors, including:
|
||||
* - type
|
||||
* - label
|
||||
* - description
|
||||
* - default values
|
||||
*
|
||||
* TODO: Generate from the OpenAPI schema
|
||||
*/
|
||||
export const CONTROLNET_PROCESSORS = {
|
||||
none: {
|
||||
type: 'none',
|
||||
label: 'None',
|
||||
description: '',
|
||||
default: {
|
||||
type: 'none',
|
||||
},
|
||||
},
|
||||
canny_image_processor: {
|
||||
type: 'canny_image_processor',
|
||||
label: 'Canny',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'canny_image_processor',
|
||||
type: 'canny_image_processor',
|
||||
low_threshold: 100,
|
||||
high_threshold: 200,
|
||||
},
|
||||
},
|
||||
content_shuffle_image_processor: {
|
||||
type: 'content_shuffle_image_processor',
|
||||
label: 'Content Shuffle',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'content_shuffle_image_processor',
|
||||
type: 'content_shuffle_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
h: 512,
|
||||
w: 512,
|
||||
f: 256,
|
||||
},
|
||||
},
|
||||
hed_image_processor: {
|
||||
type: 'hed_image_processor',
|
||||
label: 'HED',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'hed_image_processor',
|
||||
type: 'hed_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
scribble: false,
|
||||
},
|
||||
},
|
||||
lineart_anime_image_processor: {
|
||||
type: 'lineart_anime_image_processor',
|
||||
label: 'Lineart Anime',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'lineart_anime_image_processor',
|
||||
type: 'lineart_anime_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
},
|
||||
},
|
||||
lineart_image_processor: {
|
||||
type: 'lineart_image_processor',
|
||||
label: 'Lineart',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'lineart_image_processor',
|
||||
type: 'lineart_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
coarse: false,
|
||||
},
|
||||
},
|
||||
mediapipe_face_processor: {
|
||||
type: 'mediapipe_face_processor',
|
||||
label: 'Mediapipe Face',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'mediapipe_face_processor',
|
||||
type: 'mediapipe_face_processor',
|
||||
max_faces: 1,
|
||||
min_confidence: 0.5,
|
||||
},
|
||||
},
|
||||
midas_depth_image_processor: {
|
||||
type: 'midas_depth_image_processor',
|
||||
label: 'Depth (Midas)',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'midas_depth_image_processor',
|
||||
type: 'midas_depth_image_processor',
|
||||
a_mult: 2,
|
||||
bg_th: 0.1,
|
||||
},
|
||||
},
|
||||
mlsd_image_processor: {
|
||||
type: 'mlsd_image_processor',
|
||||
label: 'MLSD',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'mlsd_image_processor',
|
||||
type: 'mlsd_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
thr_d: 0.1,
|
||||
thr_v: 0.1,
|
||||
},
|
||||
},
|
||||
normalbae_image_processor: {
|
||||
type: 'normalbae_image_processor',
|
||||
label: 'NormalBae',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'normalbae_image_processor',
|
||||
type: 'normalbae_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
},
|
||||
},
|
||||
openpose_image_processor: {
|
||||
type: 'openpose_image_processor',
|
||||
label: 'Openpose',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'openpose_image_processor',
|
||||
type: 'openpose_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
hand_and_face: false,
|
||||
},
|
||||
},
|
||||
pidi_image_processor: {
|
||||
type: 'pidi_image_processor',
|
||||
label: 'PIDI',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'pidi_image_processor',
|
||||
type: 'pidi_image_processor',
|
||||
detect_resolution: 512,
|
||||
image_resolution: 512,
|
||||
scribble: false,
|
||||
safe: false,
|
||||
},
|
||||
},
|
||||
zoe_depth_image_processor: {
|
||||
type: 'zoe_depth_image_processor',
|
||||
label: 'Depth (Zoe)',
|
||||
description: '',
|
||||
default: {
|
||||
id: 'zoe_depth_image_processor',
|
||||
type: 'zoe_depth_image_processor',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CONTROLNET_MODELS = [
|
||||
'lllyasviel/control_v11p_sd15_canny',
|
||||
'lllyasviel/control_v11p_sd15_inpaint',
|
||||
'lllyasviel/control_v11p_sd15_mlsd',
|
||||
'lllyasviel/control_v11f1p_sd15_depth',
|
||||
'lllyasviel/control_v11p_sd15_normalbae',
|
||||
'lllyasviel/control_v11p_sd15_seg',
|
||||
'lllyasviel/control_v11p_sd15_lineart',
|
||||
'lllyasviel/control_v11p_sd15s2_lineart_anime',
|
||||
'lllyasviel/control_v11p_sd15_scribble',
|
||||
'lllyasviel/control_v11p_sd15_softedge',
|
||||
'lllyasviel/control_v11e_sd15_shuffle',
|
||||
'lllyasviel/control_v11p_sd15_openpose',
|
||||
'lllyasviel/control_v11f1e_sd15_tile',
|
||||
'lllyasviel/control_v11e_sd15_ip2p',
|
||||
'CrucibleAI/ControlNetMediaPipeFace',
|
||||
];
|
||||
|
||||
export type ControlNetModel = (typeof CONTROLNET_MODELS)[number];
|
||||
|
||||
export const CONTROLNET_MODEL_MAP: Record<
|
||||
ControlNetModel,
|
||||
ControlNetProcessorType
|
||||
> = {
|
||||
'lllyasviel/control_v11p_sd15_canny': 'canny_image_processor',
|
||||
'lllyasviel/control_v11p_sd15_mlsd': 'mlsd_image_processor',
|
||||
'lllyasviel/control_v11f1p_sd15_depth': 'midas_depth_image_processor',
|
||||
'lllyasviel/control_v11p_sd15_normalbae': 'normalbae_image_processor',
|
||||
'lllyasviel/control_v11p_sd15_lineart': 'lineart_image_processor',
|
||||
'lllyasviel/control_v11p_sd15s2_lineart_anime':
|
||||
'lineart_anime_image_processor',
|
||||
'lllyasviel/control_v11p_sd15_softedge': 'hed_image_processor',
|
||||
'lllyasviel/control_v11e_sd15_shuffle': 'content_shuffle_image_processor',
|
||||
'lllyasviel/control_v11p_sd15_openpose': 'openpose_image_processor',
|
||||
'CrucibleAI/ControlNetMediaPipeFace': 'mediapipe_face_processor',
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { ControlNetState } from './controlNetSlice';
|
||||
|
||||
/**
|
||||
* ControlNet slice persist denylist
|
||||
*/
|
||||
export const controlNetDenylist: (keyof ControlNetState)[] = [
|
||||
'isProcessingControlImage',
|
||||
];
|
@ -0,0 +1,254 @@
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import {
|
||||
ControlNetProcessorType,
|
||||
RequiredCannyImageProcessorInvocation,
|
||||
RequiredControlNetProcessorNode,
|
||||
} from './types';
|
||||
import {
|
||||
CONTROLNET_MODELS,
|
||||
CONTROLNET_PROCESSORS,
|
||||
ControlNetModel,
|
||||
} from './constants';
|
||||
import { controlNetImageProcessed } from './actions';
|
||||
import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
|
||||
import { forEach } from 'lodash-es';
|
||||
|
||||
export const initialControlNet: Omit<ControlNetConfig, 'controlNetId'> = {
|
||||
isEnabled: true,
|
||||
model: CONTROLNET_MODELS[0],
|
||||
weight: 1,
|
||||
beginStepPct: 0,
|
||||
endStepPct: 1,
|
||||
controlImage: null,
|
||||
processedControlImage: null,
|
||||
processorType: 'canny_image_processor',
|
||||
processorNode: CONTROLNET_PROCESSORS.canny_image_processor
|
||||
.default as RequiredCannyImageProcessorInvocation,
|
||||
};
|
||||
|
||||
export type ControlNetConfig = {
|
||||
controlNetId: string;
|
||||
isEnabled: boolean;
|
||||
model: ControlNetModel;
|
||||
weight: number;
|
||||
beginStepPct: number;
|
||||
endStepPct: number;
|
||||
controlImage: ImageDTO | null;
|
||||
processedControlImage: ImageDTO | null;
|
||||
processorType: ControlNetProcessorType;
|
||||
processorNode: RequiredControlNetProcessorNode;
|
||||
};
|
||||
|
||||
export type ControlNetState = {
|
||||
controlNets: Record<string, ControlNetConfig>;
|
||||
isEnabled: boolean;
|
||||
isProcessingControlImage: boolean;
|
||||
};
|
||||
|
||||
export const initialControlNetState: ControlNetState = {
|
||||
controlNets: {},
|
||||
isEnabled: false,
|
||||
isProcessingControlImage: false,
|
||||
};
|
||||
|
||||
export const controlNetSlice = createSlice({
|
||||
name: 'controlNet',
|
||||
initialState: initialControlNetState,
|
||||
reducers: {
|
||||
isControlNetEnabledToggled: (state) => {
|
||||
state.isEnabled = !state.isEnabled;
|
||||
},
|
||||
controlNetAdded: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
controlNet?: ControlNetConfig;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, controlNet } = action.payload;
|
||||
state.controlNets[controlNetId] = {
|
||||
...(controlNet ?? initialControlNet),
|
||||
controlNetId,
|
||||
};
|
||||
},
|
||||
controlNetAddedFromImage: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }>
|
||||
) => {
|
||||
const { controlNetId, controlImage } = action.payload;
|
||||
state.controlNets[controlNetId] = {
|
||||
...initialControlNet,
|
||||
controlNetId,
|
||||
controlImage,
|
||||
};
|
||||
},
|
||||
controlNetRemoved: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string }>
|
||||
) => {
|
||||
const { controlNetId } = action.payload;
|
||||
delete state.controlNets[controlNetId];
|
||||
},
|
||||
controlNetToggled: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string }>
|
||||
) => {
|
||||
const { controlNetId } = action.payload;
|
||||
state.controlNets[controlNetId].isEnabled =
|
||||
!state.controlNets[controlNetId].isEnabled;
|
||||
},
|
||||
controlNetImageChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
controlImage: ImageDTO | null;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, controlImage } = action.payload;
|
||||
state.controlNets[controlNetId].controlImage = controlImage;
|
||||
state.controlNets[controlNetId].processedControlImage = null;
|
||||
if (
|
||||
controlImage !== null &&
|
||||
state.controlNets[controlNetId].processorType !== 'none'
|
||||
) {
|
||||
state.isProcessingControlImage = true;
|
||||
}
|
||||
},
|
||||
controlNetProcessedImageChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
processedControlImage: ImageDTO | null;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, processedControlImage } = action.payload;
|
||||
state.controlNets[controlNetId].processedControlImage =
|
||||
processedControlImage;
|
||||
state.isProcessingControlImage = false;
|
||||
},
|
||||
controlNetModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string; model: ControlNetModel }>
|
||||
) => {
|
||||
const { controlNetId, model } = action.payload;
|
||||
state.controlNets[controlNetId].model = model;
|
||||
},
|
||||
controlNetWeightChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string; weight: number }>
|
||||
) => {
|
||||
const { controlNetId, weight } = action.payload;
|
||||
state.controlNets[controlNetId].weight = weight;
|
||||
},
|
||||
controlNetBeginStepPctChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string; beginStepPct: number }>
|
||||
) => {
|
||||
const { controlNetId, beginStepPct } = action.payload;
|
||||
state.controlNets[controlNetId].beginStepPct = beginStepPct;
|
||||
},
|
||||
controlNetEndStepPctChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ controlNetId: string; endStepPct: number }>
|
||||
) => {
|
||||
const { controlNetId, endStepPct } = action.payload;
|
||||
state.controlNets[controlNetId].endStepPct = endStepPct;
|
||||
},
|
||||
controlNetProcessorParamsChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
changes: Omit<
|
||||
Partial<RequiredControlNetProcessorNode>,
|
||||
'id' | 'type' | 'is_intermediate'
|
||||
>;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, changes } = action.payload;
|
||||
const processorNode = state.controlNets[controlNetId].processorNode;
|
||||
state.controlNets[controlNetId].processorNode = {
|
||||
...processorNode,
|
||||
...changes,
|
||||
};
|
||||
},
|
||||
controlNetProcessorTypeChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
processorType: ControlNetProcessorType;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, processorType } = action.payload;
|
||||
state.controlNets[controlNetId].processorType = processorType;
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
||||
processorType
|
||||
].default as RequiredControlNetProcessorNode;
|
||||
},
|
||||
controlNetReset: () => {
|
||||
return { ...initialControlNetState };
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(controlNetImageProcessed, (state, action) => {
|
||||
if (
|
||||
state.controlNets[action.payload.controlNetId].controlImage !== null
|
||||
) {
|
||||
state.isProcessingControlImage = true;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
||||
// Preemptively remove the image from the gallery
|
||||
const { imageName } = action.meta.arg;
|
||||
forEach(state.controlNets, (c) => {
|
||||
if (c.controlImage?.image_name === imageName) {
|
||||
c.controlImage = null;
|
||||
c.processedControlImage = null;
|
||||
}
|
||||
if (c.processedControlImage?.image_name === imageName) {
|
||||
c.processedControlImage = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||
action.payload;
|
||||
|
||||
forEach(state.controlNets, (c) => {
|
||||
if (c.controlImage?.image_name === image_name) {
|
||||
c.controlImage.image_url = image_url;
|
||||
c.controlImage.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
if (c.processedControlImage?.image_name === image_name) {
|
||||
c.processedControlImage.image_url = image_url;
|
||||
c.processedControlImage.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
isControlNetEnabledToggled,
|
||||
controlNetAdded,
|
||||
controlNetAddedFromImage,
|
||||
controlNetRemoved,
|
||||
controlNetImageChanged,
|
||||
controlNetProcessedImageChanged,
|
||||
controlNetToggled,
|
||||
controlNetModelChanged,
|
||||
controlNetWeightChanged,
|
||||
controlNetBeginStepPctChanged,
|
||||
controlNetEndStepPctChanged,
|
||||
controlNetProcessorParamsChanged,
|
||||
controlNetProcessorTypeChanged,
|
||||
controlNetReset,
|
||||
} = controlNetSlice.actions;
|
||||
|
||||
export default controlNetSlice.reducer;
|
||||
|
||||
export const controlNetSelector = (state: RootState) => state.controlNet;
|
329
invokeai/frontend/web/src/features/controlNet/store/types.ts
Normal file
329
invokeai/frontend/web/src/features/controlNet/store/types.ts
Normal file
@ -0,0 +1,329 @@
|
||||
import { isObject } from 'lodash-es';
|
||||
import {
|
||||
CannyImageProcessorInvocation,
|
||||
ContentShuffleImageProcessorInvocation,
|
||||
HedImageProcessorInvocation,
|
||||
LineartAnimeImageProcessorInvocation,
|
||||
LineartImageProcessorInvocation,
|
||||
MediapipeFaceProcessorInvocation,
|
||||
MidasDepthImageProcessorInvocation,
|
||||
MlsdImageProcessorInvocation,
|
||||
NormalbaeImageProcessorInvocation,
|
||||
OpenposeImageProcessorInvocation,
|
||||
PidiImageProcessorInvocation,
|
||||
ZoeDepthImageProcessorInvocation,
|
||||
} from 'services/api';
|
||||
import { O } from 'ts-toolbelt';
|
||||
|
||||
/**
|
||||
* Any ControlNet processor node
|
||||
*/
|
||||
export type ControlNetProcessorNode =
|
||||
| CannyImageProcessorInvocation
|
||||
| ContentShuffleImageProcessorInvocation
|
||||
| HedImageProcessorInvocation
|
||||
| LineartAnimeImageProcessorInvocation
|
||||
| LineartImageProcessorInvocation
|
||||
| MediapipeFaceProcessorInvocation
|
||||
| MidasDepthImageProcessorInvocation
|
||||
| MlsdImageProcessorInvocation
|
||||
| NormalbaeImageProcessorInvocation
|
||||
| OpenposeImageProcessorInvocation
|
||||
| PidiImageProcessorInvocation
|
||||
| ZoeDepthImageProcessorInvocation;
|
||||
|
||||
/**
|
||||
* Any ControlNet processor type
|
||||
*/
|
||||
export type ControlNetProcessorType = NonNullable<
|
||||
ControlNetProcessorNode['type'] | 'none'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Canny processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredCannyImageProcessorInvocation = O.Required<
|
||||
CannyImageProcessorInvocation,
|
||||
'type' | 'low_threshold' | 'high_threshold'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The ContentShuffle processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredContentShuffleImageProcessorInvocation = O.Required<
|
||||
ContentShuffleImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The HED processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredHedImageProcessorInvocation = O.Required<
|
||||
HedImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'scribble'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Lineart Anime processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredLineartAnimeImageProcessorInvocation = O.Required<
|
||||
LineartAnimeImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Lineart processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredLineartImageProcessorInvocation = O.Required<
|
||||
LineartImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'coarse'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The MediapipeFace processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredMediapipeFaceProcessorInvocation = O.Required<
|
||||
MediapipeFaceProcessorInvocation,
|
||||
'type' | 'max_faces' | 'min_confidence'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The MidasDepth processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredMidasDepthImageProcessorInvocation = O.Required<
|
||||
MidasDepthImageProcessorInvocation,
|
||||
'type' | 'a_mult' | 'bg_th'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The MLSD processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredMlsdImageProcessorInvocation = O.Required<
|
||||
MlsdImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The NormalBae processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredNormalbaeImageProcessorInvocation = O.Required<
|
||||
NormalbaeImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Openpose processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredOpenposeImageProcessorInvocation = O.Required<
|
||||
OpenposeImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'hand_and_face'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The Pidi processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredPidiImageProcessorInvocation = O.Required<
|
||||
PidiImageProcessorInvocation,
|
||||
'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble'
|
||||
>;
|
||||
|
||||
/**
|
||||
* The ZoeDepth processor node, with parameters flagged as required
|
||||
*/
|
||||
export type RequiredZoeDepthImageProcessorInvocation = O.Required<
|
||||
ZoeDepthImageProcessorInvocation,
|
||||
'type'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Any ControlNet Processor node, with its parameters flagged as required
|
||||
*/
|
||||
export type RequiredControlNetProcessorNode =
|
||||
| RequiredCannyImageProcessorInvocation
|
||||
| RequiredContentShuffleImageProcessorInvocation
|
||||
| RequiredHedImageProcessorInvocation
|
||||
| RequiredLineartAnimeImageProcessorInvocation
|
||||
| RequiredLineartImageProcessorInvocation
|
||||
| RequiredMediapipeFaceProcessorInvocation
|
||||
| RequiredMidasDepthImageProcessorInvocation
|
||||
| RequiredMlsdImageProcessorInvocation
|
||||
| RequiredNormalbaeImageProcessorInvocation
|
||||
| RequiredOpenposeImageProcessorInvocation
|
||||
| RequiredPidiImageProcessorInvocation
|
||||
| RequiredZoeDepthImageProcessorInvocation;
|
||||
|
||||
/**
|
||||
* Type guard for CannyImageProcessorInvocation
|
||||
*/
|
||||
export const isCannyImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is CannyImageProcessorInvocation => {
|
||||
if (isObject(obj) && 'type' in obj && obj.type === 'canny_image_processor') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for ContentShuffleImageProcessorInvocation
|
||||
*/
|
||||
export const isContentShuffleImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is ContentShuffleImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'content_shuffle_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for HedImageprocessorInvocation
|
||||
*/
|
||||
export const isHedImageprocessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is HedImageProcessorInvocation => {
|
||||
if (isObject(obj) && 'type' in obj && obj.type === 'hed_image_processor') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for LineartAnimeImageProcessorInvocation
|
||||
*/
|
||||
export const isLineartAnimeImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is LineartAnimeImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'lineart_anime_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for LineartImageProcessorInvocation
|
||||
*/
|
||||
export const isLineartImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is LineartImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'lineart_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for MediapipeFaceProcessorInvocation
|
||||
*/
|
||||
export const isMediapipeFaceProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is MediapipeFaceProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'mediapipe_face_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for MidasDepthImageProcessorInvocation
|
||||
*/
|
||||
export const isMidasDepthImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is MidasDepthImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'midas_depth_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for MlsdImageProcessorInvocation
|
||||
*/
|
||||
export const isMlsdImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is MlsdImageProcessorInvocation => {
|
||||
if (isObject(obj) && 'type' in obj && obj.type === 'mlsd_image_processor') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for NormalbaeImageProcessorInvocation
|
||||
*/
|
||||
export const isNormalbaeImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is NormalbaeImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'normalbae_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for OpenposeImageProcessorInvocation
|
||||
*/
|
||||
export const isOpenposeImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is OpenposeImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'openpose_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for PidiImageProcessorInvocation
|
||||
*/
|
||||
export const isPidiImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is PidiImageProcessorInvocation => {
|
||||
if (isObject(obj) && 'type' in obj && obj.type === 'pidi_image_processor') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for ZoeDepthImageProcessorInvocation
|
||||
*/
|
||||
export const isZoeDepthImageProcessorInvocation = (
|
||||
obj: unknown
|
||||
): obj is ZoeDepthImageProcessorInvocation => {
|
||||
if (
|
||||
isObject(obj) &&
|
||||
'type' in obj &&
|
||||
obj.type === 'zoe_depth_image_processor'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
@ -1,13 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Link,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
|
||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
@ -45,22 +39,18 @@ import {
|
||||
FaShareAlt,
|
||||
} from 'react-icons/fa';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import {
|
||||
requestedImageDeletion,
|
||||
sentImageToCanvas,
|
||||
sentImageToImg2Img,
|
||||
} from '../store/actions';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
||||
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
|
||||
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
|
||||
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
|
||||
import { DeleteImageButton } from './DeleteImageModal';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[
|
||||
@ -123,10 +113,6 @@ const currentImageButtonsSelector = createSelector(
|
||||
|
||||
type CurrentImageButtonsProps = FlexProps;
|
||||
|
||||
/**
|
||||
* Row of buttons for common actions:
|
||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||
*/
|
||||
const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
@ -138,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
facetoolStrength,
|
||||
shouldDisableToolbarButtons,
|
||||
shouldShowImageDetails,
|
||||
// currentImage,
|
||||
isLightboxOpen,
|
||||
activeTabName,
|
||||
shouldHidePreview,
|
||||
image,
|
||||
canDeleteImage,
|
||||
shouldConfirmOnDelete,
|
||||
shouldShowProgressInViewer,
|
||||
} = useAppSelector(currentImageButtonsSelector);
|
||||
|
||||
@ -153,20 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
|
||||
|
||||
const { getUrl, shouldTransformUrls } = useGetUrl();
|
||||
|
||||
const {
|
||||
isOpen: isDeleteDialogOpen,
|
||||
onOpen: onDeleteDialogOpen,
|
||||
onClose: onDeleteDialogClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||
useRecallParameters();
|
||||
|
||||
const { onDelete } = useContext(DeleteImageContext);
|
||||
|
||||
// const handleCopyImage = useCallback(async () => {
|
||||
// if (!image?.url) {
|
||||
// return;
|
||||
@ -197,10 +174,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldTransformUrls) {
|
||||
return getUrl(image.image_url);
|
||||
}
|
||||
|
||||
if (image.image_url.startsWith('http')) {
|
||||
return image.image_url;
|
||||
}
|
||||
@ -229,7 +202,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
}, [toaster, shouldTransformUrls, getUrl, t, image]);
|
||||
}, [toaster, t, image]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(image);
|
||||
@ -269,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
// selectedImage && dispatch(runESRGAN(selectedImage));
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onDelete(image);
|
||||
}, [image, onDelete]);
|
||||
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
() => {
|
||||
@ -370,31 +347,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
[image, shouldShowImageDetails, toaster]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (canDeleteImage && image) {
|
||||
dispatch(requestedImageDeletion(image));
|
||||
}
|
||||
}, [image, canDeleteImage, dispatch]);
|
||||
|
||||
const handleInitiateDelete = useCallback(() => {
|
||||
if (shouldConfirmOnDelete) {
|
||||
onDeleteDialogOpen();
|
||||
} else {
|
||||
handleDelete();
|
||||
}
|
||||
}, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
|
||||
|
||||
const handleClickProgressImagesToggle = useCallback(() => {
|
||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||
}, [dispatch, shouldShowProgressInViewer]);
|
||||
|
||||
useHotkeys('delete', handleInitiateDelete, [
|
||||
image,
|
||||
shouldConfirmOnDelete,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
]);
|
||||
|
||||
const handleLightBox = useCallback(() => {
|
||||
dispatch(setIsLightboxOpen(!isLightboxOpen));
|
||||
}, [dispatch, isLightboxOpen]);
|
||||
@ -461,11 +417,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
{t('parameters.copyImageToLink')}
|
||||
</IAIButton>
|
||||
|
||||
<Link
|
||||
download={true}
|
||||
href={getUrl(image?.image_url ?? '')}
|
||||
target="_blank"
|
||||
>
|
||||
<Link download={true} href={image?.image_url} target="_blank">
|
||||
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
||||
{t('parameters.downloadImage')}
|
||||
</IAIButton>
|
||||
@ -607,7 +559,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isAttached={true}>
|
||||
<DeleteImageButton image={image} />
|
||||
<DeleteImageButton onClick={handleDelete} />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, Icon } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
@ -7,7 +7,7 @@ import { isEqual } from 'lodash-es';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
|
||||
export const currentImageDisplaySelector = createSelector(
|
||||
[systemSelector, gallerySelector],
|
||||
@ -15,21 +15,20 @@ export const currentImageDisplaySelector = createSelector(
|
||||
const { progressImage } = system;
|
||||
|
||||
return {
|
||||
hasAnImageToDisplay: gallery.selectedImage || progressImage,
|
||||
hasSelectedImage: Boolean(gallery.selectedImage),
|
||||
hasProgressImage: Boolean(progressImage),
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const { hasAnImageToDisplay } = useAppSelector(currentImageDisplaySelector);
|
||||
const { hasSelectedImage, hasProgressImage } = useAppSelector(
|
||||
currentImageDisplaySelector
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -52,23 +51,16 @@ const CurrentImageDisplay = () => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
{hasAnImageToDisplay ? (
|
||||
<>
|
||||
<CurrentImageButtons />
|
||||
<CurrentImagePreview />
|
||||
</>
|
||||
) : (
|
||||
<Icon
|
||||
as={FaImage}
|
||||
sx={{
|
||||
boxSize: 24,
|
||||
color: 'base.500',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CurrentImagePreview />
|
||||
</Flex>
|
||||
{hasSelectedImage && (
|
||||
<Box sx={{ position: 'absolute', top: 0 }}>
|
||||
<CurrentImageButtons />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
||||
import { DragEvent, memo, useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import ImageFallbackSpinner from './ImageFallbackSpinner';
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { configSelector } from '../../system/store/configSelectors';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { imageSelected } from '../store/gallerySlice';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
|
||||
export const imagesSelector = createSelector(
|
||||
[uiSelector, gallerySelector, systemSelector],
|
||||
@ -46,27 +46,14 @@ const CurrentImagePreview = () => {
|
||||
const {
|
||||
shouldShowImageDetails,
|
||||
image,
|
||||
shouldHidePreview,
|
||||
progressImage,
|
||||
shouldShowProgressInViewer,
|
||||
shouldAntialiasProgressImage,
|
||||
} = useAppSelector(imagesSelector);
|
||||
const { shouldFetchImages } = useAppSelector(configSelector);
|
||||
const { getUrl } = useGetUrl();
|
||||
const toaster = useAppToaster();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
[image]
|
||||
);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
dispatch(imageSelected());
|
||||
if (shouldFetchImages) {
|
||||
@ -78,11 +65,21 @@ const CurrentImagePreview = () => {
|
||||
}
|
||||
}, [dispatch, toaster, shouldFetchImages]);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (droppedImage.image_name === image?.image_name) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageSelected(droppedImage));
|
||||
},
|
||||
[dispatch, image?.image_name]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@ -95,8 +92,8 @@ const CurrentImagePreview = () => {
|
||||
height={progressImage.height}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
maxWidth: 'full',
|
||||
maxHeight: 'full',
|
||||
height: 'auto',
|
||||
position: 'absolute',
|
||||
borderRadius: 'base',
|
||||
@ -104,34 +101,29 @@ const CurrentImagePreview = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
image && (
|
||||
<>
|
||||
<Image
|
||||
src={getUrl(image.image_url)}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallback={<ImageFallbackSpinner />}
|
||||
onDragStart={handleDragStart}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: 'auto',
|
||||
position: 'absolute',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
<ImageMetadataOverlay image={image} />
|
||||
</>
|
||||
)
|
||||
<Flex
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IAIDndImage
|
||||
image={image}
|
||||
onDrop={handleDrop}
|
||||
onError={handleError}
|
||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{shouldShowImageDetails && image && 'metadata' in image && (
|
||||
{shouldShowImageDetails && image && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
borderRadius: 'base',
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
@ -139,7 +131,19 @@ const CurrentImagePreview = () => {
|
||||
<ImageMetadataViewer image={image} />
|
||||
</Box>
|
||||
)}
|
||||
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
||||
{!shouldShowImageDetails && image && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<NextPrevImageButtons />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -5,51 +5,81 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
Divider,
|
||||
Flex,
|
||||
ListItem,
|
||||
Text,
|
||||
UnorderedList,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
DeleteImageContext,
|
||||
ImageUsage,
|
||||
} from 'app/contexts/DeleteImageContext';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { configSelector } from 'features/system/store/configSelectors';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { some } from 'lodash-es';
|
||||
|
||||
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
||||
import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector, configSelector],
|
||||
(system, config) => {
|
||||
const { shouldConfirmOnDelete } = system;
|
||||
const { canRestoreDeletedImagesFromBin } = config;
|
||||
return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin };
|
||||
|
||||
return {
|
||||
shouldConfirmOnDelete,
|
||||
canRestoreDeletedImagesFromBin,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
interface DeleteImageModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete: () => void;
|
||||
}
|
||||
const ImageInUseMessage = (props: { imageUsage?: ImageUsage }) => {
|
||||
const { imageUsage } = props;
|
||||
|
||||
const DeleteImageModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
handleDelete,
|
||||
}: DeleteImageModalProps) => {
|
||||
if (!imageUsage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!some(imageUsage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>This image is currently in use in the following features:</Text>
|
||||
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
||||
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
||||
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
||||
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
||||
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
||||
</UnorderedList>
|
||||
<Text>
|
||||
If you delete this image, those features will immediately be reset.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteImageModal = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isOpen, onClose, onImmediatelyDelete, image, imageUsage } =
|
||||
useContext(DeleteImageContext);
|
||||
|
||||
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
|
||||
useAppSelector(selector);
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) =>
|
||||
@ -57,10 +87,7 @@ const DeleteImageModal = ({
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleClickDelete = useCallback(() => {
|
||||
handleDelete();
|
||||
onClose();
|
||||
}, [handleDelete, onClose]);
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
@ -76,15 +103,15 @@ const DeleteImageModal = ({
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Flex direction="column" gap={5}>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<Text>
|
||||
{canRestoreDeletedImagesFromBin
|
||||
? t('gallery.deleteImageBin')
|
||||
: t('gallery.deleteImagePermanent')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction="column" gap={3}>
|
||||
<ImageInUseMessage imageUsage={imageUsage} />
|
||||
<Divider />
|
||||
<Text>
|
||||
{canRestoreDeletedImagesFromBin
|
||||
? t('gallery.deleteImageBin')
|
||||
: t('gallery.deleteImagePermanent')}
|
||||
</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<IAISwitch
|
||||
label={t('common.dontAskMeAgain')}
|
||||
isChecked={!shouldConfirmOnDelete}
|
||||
@ -96,7 +123,7 @@ const DeleteImageModal = ({
|
||||
<IAIButton ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</IAIButton>
|
||||
<IAIButton colorScheme="error" onClick={handleClickDelete} ml={3}>
|
||||
<IAIButton colorScheme="error" onClick={onImmediatelyDelete} ml={3}>
|
||||
Delete
|
||||
</IAIButton>
|
||||
</AlertDialogFooter>
|
||||
@ -107,3 +134,33 @@ const DeleteImageModal = ({
|
||||
};
|
||||
|
||||
export default memo(DeleteImageModal);
|
||||
|
||||
const deleteImageButtonsSelector = createSelector(
|
||||
[systemSelector],
|
||||
(system) => {
|
||||
const { isProcessing, isConnected } = system;
|
||||
|
||||
return isConnected && !isProcessing;
|
||||
}
|
||||
);
|
||||
|
||||
type DeleteImageButtonProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const DeleteImageButton = (props: DeleteImageButtonProps) => {
|
||||
const { onClick } = props;
|
||||
const { t } = useTranslation();
|
||||
const canDeleteImage = useAppSelector(deleteImageButtonsSelector);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
onClick={onClick}
|
||||
icon={<FaTrash />}
|
||||
tooltip={`${t('gallery.deleteImage')} (Del)`}
|
||||
aria-label={`${t('gallery.deleteImage')} (Del)`}
|
||||
isDisabled={!canDeleteImage}
|
||||
colorScheme="error"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,8 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
Image,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react';
|
||||
import { memo, useCallback, useContext, useState } from 'react';
|
||||
import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||
import {
|
||||
resizeAndScaleCanvas,
|
||||
@ -21,7 +12,6 @@ import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
@ -32,13 +22,11 @@ import { isEqual } from 'lodash-es';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import {
|
||||
requestedImageDeletion,
|
||||
sentImageToCanvas,
|
||||
sentImageToImg2Img,
|
||||
} from '../store/actions';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
|
||||
|
||||
export const selector = createSelector(
|
||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||
@ -92,66 +80,39 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
galleryImageMinimumWidth,
|
||||
canDeleteImage,
|
||||
shouldUseSingleGalleryColumn,
|
||||
shouldConfirmOnDelete,
|
||||
} = useAppSelector(selector);
|
||||
|
||||
const {
|
||||
isOpen: isDeleteDialogOpen,
|
||||
onOpen: onDeleteDialogOpen,
|
||||
onClose: onDeleteDialogClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { image_url, thumbnail_url, image_name } = image;
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||
|
||||
const { onDelete } = useContext(DeleteImageContext);
|
||||
const handleDelete = useCallback(() => {
|
||||
onDelete(image);
|
||||
}, [image, onDelete]);
|
||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||
useRecallParameters();
|
||||
|
||||
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||
id: `galleryImage_${image_name}`,
|
||||
data: {
|
||||
image,
|
||||
},
|
||||
});
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
|
||||
// Immediately deletes an image
|
||||
const handleDelete = useCallback(() => {
|
||||
if (canDeleteImage && image) {
|
||||
dispatch(requestedImageDeletion(image));
|
||||
}
|
||||
}, [dispatch, image, canDeleteImage]);
|
||||
|
||||
// Opens the alert dialog to check if user is sure they want to delete
|
||||
const handleInitiateDelete = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (shouldConfirmOnDelete) {
|
||||
onDeleteDialogOpen();
|
||||
} else {
|
||||
handleDelete();
|
||||
}
|
||||
},
|
||||
[handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]
|
||||
);
|
||||
|
||||
const handleSelectImage = useCallback(() => {
|
||||
dispatch(imageSelected(image));
|
||||
}, [image, dispatch]);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
[image]
|
||||
);
|
||||
|
||||
// Recall parameters handlers
|
||||
const handleRecallPrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
@ -208,11 +169,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
};
|
||||
|
||||
const handleOpenInNewTab = () => {
|
||||
window.open(getUrl(image.image_url), '_blank');
|
||||
window.open(image.image_url, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
sx={{ w: 'full', h: 'full', touchAction: 'none' }}
|
||||
>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
renderMenu={() => (
|
||||
@ -278,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
{t('parameters.sendToUnifiedCanvas')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.300' }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDelete}
|
||||
>
|
||||
{t('gallery.deleteImage')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
@ -291,8 +261,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
userSelect="none"
|
||||
draggable={true}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleSelectImage}
|
||||
ref={ref}
|
||||
sx={{
|
||||
@ -312,7 +280,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
|
||||
}
|
||||
rounded="md"
|
||||
src={getUrl(thumbnail_url || image_url)}
|
||||
src={thumbnail_url || image_url}
|
||||
fallback={<FaImage />}
|
||||
sx={{
|
||||
width: '100%',
|
||||
@ -356,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
}}
|
||||
>
|
||||
<IAIIconButton
|
||||
onClickCapture={handleInitiateDelete}
|
||||
onClickCapture={handleDelete}
|
||||
aria-label={t('gallery.deleteImage')}
|
||||
icon={<FaTrash />}
|
||||
size="xs"
|
||||
@ -368,12 +336,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
</Box>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<DeleteImageModal
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onClose={onDeleteDialogClose}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
|
@ -1,92 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import DeleteImageModal from '../DeleteImageModal';
|
||||
import { requestedImageDeletion } from 'features/gallery/store/actions';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector],
|
||||
(system) => {
|
||||
const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
|
||||
|
||||
return {
|
||||
canDeleteImage: isConnected && !isProcessing,
|
||||
shouldConfirmOnDelete,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type DeleteImageButtonProps = {
|
||||
image: ImageDTO | undefined;
|
||||
};
|
||||
|
||||
const DeleteImageButton = (props: DeleteImageButtonProps) => {
|
||||
const { image } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const {
|
||||
isOpen: isDeleteDialogOpen,
|
||||
onOpen: onDeleteDialogOpen,
|
||||
onClose: onDeleteDialogClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (canDeleteImage && image) {
|
||||
dispatch(requestedImageDeletion(image));
|
||||
}
|
||||
}, [image, canDeleteImage, dispatch]);
|
||||
|
||||
const handleInitiateDelete = useCallback(() => {
|
||||
if (shouldConfirmOnDelete) {
|
||||
onDeleteDialogOpen();
|
||||
} else {
|
||||
handleDelete();
|
||||
}
|
||||
}, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
|
||||
|
||||
useHotkeys('delete', handleInitiateDelete, [
|
||||
image,
|
||||
shouldConfirmOnDelete,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIIconButton
|
||||
onClick={handleInitiateDelete}
|
||||
icon={<FaTrash />}
|
||||
tooltip={`${t('gallery.deleteImage')} (Del)`}
|
||||
aria-label={`${t('gallery.deleteImage')} (Del)`}
|
||||
isDisabled={!image || !isConnected}
|
||||
colorScheme="error"
|
||||
/>
|
||||
{image && (
|
||||
<DeleteImageModal
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onClose={onDeleteDialogClose}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeleteImageButton);
|
@ -14,6 +14,8 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
color: 'base.400',
|
||||
minH: 36,
|
||||
minW: 36,
|
||||
}}
|
||||
>
|
||||
<Spinner size={size} {...rest} />
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
@ -233,7 +233,7 @@ const ImageGalleryContent = () => {
|
||||
withReset
|
||||
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('gallery.maintainAspectRatio')}
|
||||
isChecked={galleryImageObjectFit === 'contain'}
|
||||
onChange={() =>
|
||||
@ -244,14 +244,14 @@ const ImageGalleryContent = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('gallery.autoSwitchNewImages')}
|
||||
isChecked={shouldAutoSwitchToNewImages}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<IAICheckbox
|
||||
<IAISimpleCheckbox
|
||||
label={t('gallery.singleColumnLayout')}
|
||||
isChecked={shouldUseSingleGalleryColumn}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
|
@ -9,19 +9,6 @@ import {
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import promptToString from 'common/util/promptToString';
|
||||
import {
|
||||
setCfgScale,
|
||||
setHeight,
|
||||
setImg2imgStrength,
|
||||
setNegativePrompt,
|
||||
setPositivePrompt,
|
||||
setScheduler,
|
||||
setSeed,
|
||||
setSteps,
|
||||
setWidth,
|
||||
} from 'features/parameters/store/generationSlice';
|
||||
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
|
||||
import { memo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -30,7 +17,6 @@ import { FaCopy } from 'react-icons/fa';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { Scheduler } from 'app/constants';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
|
||||
type MetadataItemProps = {
|
||||
@ -146,7 +132,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const metadata = image?.metadata;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
const metadataJSON = JSON.stringify(image, null, 2);
|
||||
|
||||
@ -168,11 +153,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight="semibold">File:</Text>
|
||||
<Link
|
||||
href={getUrl(image.image_url)}
|
||||
isExternal
|
||||
maxW="calc(100% - 3rem)"
|
||||
>
|
||||
<Link href={image.image_url} isExternal maxW="calc(100% - 3rem)">
|
||||
{image.image_name}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ImageNameAndOrigin } from 'features/parameters/store/actions';
|
||||
import { ImageUsage } from 'app/contexts/DeleteImageContext';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
export const requestedImageDeletion = createAction<
|
||||
ImageDTO | ImageNameAndOrigin | undefined
|
||||
>('gallery/requestedImageDeletion');
|
||||
export type RequestedImageDeletionArg = {
|
||||
image: ImageDTO;
|
||||
imageUsage: ImageUsage;
|
||||
};
|
||||
|
||||
export const requestedImageDeletion = createAction<RequestedImageDeletionArg>(
|
||||
'gallery/requestedImageDeletion'
|
||||
);
|
||||
|
||||
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
||||
|
||||
|
@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { imageUpserted } from './imagesSlice';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
@ -50,10 +51,22 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(imageUpserted, (state, action) => {
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
if (
|
||||
state.shouldAutoSwitchToNewImages &&
|
||||
action.payload.image_category === 'general'
|
||||
) {
|
||||
state.selectedImage = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||
action.payload;
|
||||
|
||||
if (state.selectedImage?.image_name === image_name) {
|
||||
state.selectedImage.image_url = image_url;
|
||||
state.selectedImage.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
PayloadAction,
|
||||
Update,
|
||||
createEntityAdapter,
|
||||
createSelector,
|
||||
createSlice,
|
||||
@ -7,12 +8,17 @@ import {
|
||||
import { RootState } from 'app/store/store';
|
||||
import { ImageCategory, ImageDTO } from 'services/api';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import { isString, keyBy } from 'lodash-es';
|
||||
import { receivedPageOfImages } from 'services/thunks/image';
|
||||
import { keyBy } from 'lodash-es';
|
||||
import {
|
||||
imageDeleted,
|
||||
imageMetadataReceived,
|
||||
imageUrlsReceived,
|
||||
receivedPageOfImages,
|
||||
} from 'services/thunks/image';
|
||||
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
|
||||
sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
|
||||
});
|
||||
|
||||
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
||||
@ -49,13 +55,11 @@ const imagesSlice = createSlice({
|
||||
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||
imagesAdapter.upsertOne(state, action.payload);
|
||||
},
|
||||
imageRemoved: (state, action: PayloadAction<string | ImageDTO>) => {
|
||||
if (isString(action.payload)) {
|
||||
imagesAdapter.removeOne(state, action.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
imagesAdapter.removeOne(state, action.payload.image_name);
|
||||
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
||||
imagesAdapter.updateOne(state, action.payload);
|
||||
},
|
||||
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||
imagesAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
||||
state.categories = action.payload;
|
||||
@ -76,6 +80,20 @@ const imagesSlice = createSlice({
|
||||
state.total = total;
|
||||
imagesAdapter.upsertMany(state, items);
|
||||
});
|
||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
||||
// Image deleted
|
||||
const { imageName } = action.meta.arg;
|
||||
imagesAdapter.removeOne(state, imageName);
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||
action.payload;
|
||||
|
||||
imagesAdapter.updateOne(state, {
|
||||
id: image_name,
|
||||
changes: { image_url, thumbnail_url },
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -87,8 +105,12 @@ export const {
|
||||
selectTotal: selectImagesTotal,
|
||||
} = imagesAdapter.getSelectors<RootState>((state) => state.images);
|
||||
|
||||
export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
|
||||
imagesSlice.actions;
|
||||
export const {
|
||||
imageUpserted,
|
||||
imageUpdatedOne,
|
||||
imageRemoved,
|
||||
imageCategoriesChanged,
|
||||
} = imagesSlice.actions;
|
||||
|
||||
export default imagesSlice.reducer;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
type ReactPanZoomProps = {
|
||||
@ -23,7 +22,6 @@ export default function ReactPanZoomImage({
|
||||
scaleY,
|
||||
}: ReactPanZoomProps) {
|
||||
const { centerView } = useTransformContext();
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
return (
|
||||
<TransformComponent
|
||||
@ -37,7 +35,7 @@ export default function ReactPanZoomImage({
|
||||
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
width: '100%',
|
||||
}}
|
||||
src={getUrl(image.image_url)}
|
||||
src={image.image_url}
|
||||
alt={alt}
|
||||
ref={ref}
|
||||
className={styleClass ? styleClass : ''}
|
||||
|
@ -6,7 +6,7 @@ import { memo } from 'react';
|
||||
|
||||
const FieldTypeLegend = () => {
|
||||
return (
|
||||
<Flex gap={2} flexDirection={{ base: 'column', xl: 'row' }}>
|
||||
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||
{map(FIELDS, ({ title, description, color }, key) => (
|
||||
<Tooltip key={key} label={description}>
|
||||
<Badge
|
||||
|
@ -11,7 +11,7 @@ const NodeEditor = () => {
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 'full',
|
||||
height: { base: '100vh', xl: 'full' },
|
||||
height: 'full',
|
||||
borderRadius: 'md',
|
||||
bg: 'base.850',
|
||||
}}
|
||||
|
@ -1,54 +1,67 @@
|
||||
import { Box, Image } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import useGetImageByName from 'features/gallery/hooks/useGetImageByName';
|
||||
|
||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
ImageInputFieldTemplate,
|
||||
ImageInputFieldValue,
|
||||
} from 'features/nodes/types/types';
|
||||
import { DragEvent, memo, useCallback, useState } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { FieldComponentProps } from './types';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
const ImageInputFieldComponent = (
|
||||
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeId, field } = props;
|
||||
|
||||
const getImageByName = useGetImageByName();
|
||||
const dispatch = useAppDispatch();
|
||||
const [url, setUrl] = useState<string | undefined>(field.value?.image_url);
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
const name = e.dataTransfer.getData('invokeai/imageName');
|
||||
const image = getImageByName(name);
|
||||
|
||||
if (!image) {
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (field.value?.image_name === droppedImage.image_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUrl(image.image_url);
|
||||
|
||||
dispatch(
|
||||
fieldValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value: image,
|
||||
value: droppedImage,
|
||||
})
|
||||
);
|
||||
},
|
||||
[getImageByName, dispatch, field.name, nodeId]
|
||||
[dispatch, field.name, field.value?.image_name, nodeId]
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
dispatch(
|
||||
fieldValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value: undefined,
|
||||
})
|
||||
);
|
||||
}, [dispatch, field.name, nodeId]);
|
||||
|
||||
return (
|
||||
<Box onDrop={handleDrop}>
|
||||
<Image src={getUrl(url)} fallback={<SelectImagePlaceholder />} />
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IAIDndImage
|
||||
image={field.value}
|
||||
onDrop={handleDrop}
|
||||
onReset={handleReset}
|
||||
resetIconSize="sm"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { readinessSelector } from 'app/selectors/readinessSelector';
|
||||
import { userInvoked } from 'app/store/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
} from 'common/components/IAIIconButton';
|
||||
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
@ -21,9 +21,8 @@ interface InvokeButton
|
||||
export default function NodeInvokeButton(props: InvokeButton) {
|
||||
const { iconButton = false, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isReady } = useAppSelector(readinessSelector);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
|
||||
const isReady = useIsReadyToInvoke();
|
||||
const handleInvoke = useCallback(() => {
|
||||
dispatch(userInvoked('nodes'));
|
||||
}, [dispatch]);
|
||||
|
@ -16,9 +16,10 @@ import { receivedOpenAPISchema } from 'services/thunks/schema';
|
||||
import { InvocationTemplate, InvocationValue } from '../types/types';
|
||||
import { parseSchema } from '../util/parseSchema';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { size } from 'lodash-es';
|
||||
import { isAnyGraphBuilt } from './actions';
|
||||
import { forEach, size } from 'lodash-es';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
import { RootState } from 'app/store/store';
|
||||
|
||||
export type NodesState = {
|
||||
nodes: Node<InvocationValue>[];
|
||||
@ -92,15 +93,29 @@ const nodesSlice = createSlice({
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
nodeEditorReset: () => {
|
||||
return { ...initialNodesState };
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
||||
state.schema = action.payload;
|
||||
});
|
||||
|
||||
builder.addMatcher(isAnyGraphBuilt, (state, action) => {
|
||||
// TODO: Achtung! Side effect in a reducer!
|
||||
log.info({ namespace: 'nodes', data: action.payload }, 'Graph built');
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||
action.payload;
|
||||
|
||||
state.nodes.forEach((node) => {
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (input.type === 'image') {
|
||||
if (input.value?.image_name === image_name) {
|
||||
input.value.image_url = image_url;
|
||||
input.value.thumbnail_url = thumbnail_url;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -115,6 +130,9 @@ export const {
|
||||
connectionEnded,
|
||||
shouldShowGraphOverlayChanged,
|
||||
parsedOpenAPISchema,
|
||||
nodeEditorReset,
|
||||
} = nodesSlice.actions;
|
||||
|
||||
export default nodesSlice.reducer;
|
||||
|
||||
export const nodesSelecter = (state: RootState) => state.nodes;
|
||||
|
@ -0,0 +1,99 @@
|
||||
import { RootState } from 'app/store/store';
|
||||
import { forEach, size } from 'lodash-es';
|
||||
import { CollectInvocation, ControlNetInvocation } from 'services/api';
|
||||
import { NonNullableGraph } from '../types/types';
|
||||
|
||||
const CONTROL_NET_COLLECT = 'control_net_collect';
|
||||
|
||||
export const addControlNetToLinearGraph = (
|
||||
graph: NonNullableGraph,
|
||||
baseNodeId: string,
|
||||
state: RootState
|
||||
): void => {
|
||||
const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet;
|
||||
|
||||
// Add ControlNet
|
||||
if (isControlNetEnabled) {
|
||||
if (size(controlNets) > 1) {
|
||||
const controlNetIterateNode: CollectInvocation = {
|
||||
id: CONTROL_NET_COLLECT,
|
||||
type: 'collect',
|
||||
};
|
||||
graph.nodes[controlNetIterateNode.id] = controlNetIterateNode;
|
||||
graph.edges.push({
|
||||
source: { node_id: controlNetIterateNode.id, field: 'collection' },
|
||||
destination: {
|
||||
node_id: baseNodeId,
|
||||
field: 'control',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
forEach(controlNets, (controlNet, index) => {
|
||||
const {
|
||||
controlNetId,
|
||||
isEnabled,
|
||||
controlImage,
|
||||
processedControlImage,
|
||||
beginStepPct,
|
||||
endStepPct,
|
||||
model,
|
||||
processorType,
|
||||
weight,
|
||||
} = controlNet;
|
||||
|
||||
if (!isEnabled) {
|
||||
// Skip disabled ControlNets
|
||||
return;
|
||||
}
|
||||
|
||||
const controlNetNode: ControlNetInvocation = {
|
||||
id: `control_net_${controlNetId}`,
|
||||
type: 'controlnet',
|
||||
begin_step_percent: beginStepPct,
|
||||
end_step_percent: endStepPct,
|
||||
control_model: model as ControlNetInvocation['control_model'],
|
||||
control_weight: weight,
|
||||
};
|
||||
|
||||
if (processedControlImage && processorType !== 'none') {
|
||||
// We've already processed the image in the app, so we can just use the processed image
|
||||
const { image_name, image_origin } = processedControlImage;
|
||||
controlNetNode.image = {
|
||||
image_name,
|
||||
image_origin,
|
||||
};
|
||||
} else if (controlImage && processorType !== 'none') {
|
||||
// The control image is preprocessed
|
||||
const { image_name, image_origin } = controlImage;
|
||||
controlNetNode.image = {
|
||||
image_name,
|
||||
image_origin,
|
||||
};
|
||||
} else {
|
||||
// Skip ControlNets without an unprocessed image - should never happen if everything is working correctly
|
||||
return;
|
||||
}
|
||||
|
||||
graph.nodes[controlNetNode.id] = controlNetNode;
|
||||
|
||||
if (size(controlNets) > 1) {
|
||||
graph.edges.push({
|
||||
source: { node_id: controlNetNode.id, field: 'control' },
|
||||
destination: {
|
||||
node_id: CONTROL_NET_COLLECT,
|
||||
field: 'item',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
graph.edges.push({
|
||||
source: { node_id: controlNetNode.id, field: 'control' },
|
||||
destination: {
|
||||
node_id: baseNodeId,
|
||||
field: 'control',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -14,6 +14,7 @@ import {
|
||||
import { NonNullableGraph } from 'features/nodes/types/types';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { set } from 'lodash-es';
|
||||
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'nodes' });
|
||||
|
||||
@ -408,5 +409,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
|
||||
});
|
||||
}
|
||||
|
||||
addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state);
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
TextToLatentsInvocation,
|
||||
} from 'services/api';
|
||||
import { NonNullableGraph } from 'features/nodes/types/types';
|
||||
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
|
||||
|
||||
const POSITIVE_CONDITIONING = 'positive_conditioning';
|
||||
const NEGATIVE_CONDITIONING = 'negative_conditioning';
|
||||
@ -308,5 +309,8 @@ export const buildTextToImageGraph = (state: RootState): Graph => {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state);
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user