mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into release/make-web-dist-startable
This commit is contained in:
commit
49d29420c4
@ -21,6 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react';
|
|||||||
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
||||||
import GlobalHotkeys from './GlobalHotkeys';
|
import GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
|
import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {};
|
const DEFAULT_CONFIG = {};
|
||||||
|
|
||||||
@ -76,18 +77,21 @@ const App = ({
|
|||||||
{isLightboxEnabled && <Lightbox />}
|
{isLightboxEnabled && <Lightbox />}
|
||||||
<ImageUploader>
|
<ImageUploader>
|
||||||
<Grid
|
<Grid
|
||||||
gap={4}
|
sx={{
|
||||||
p={4}
|
gap: 4,
|
||||||
gridAutoRows="min-content auto"
|
p: 4,
|
||||||
w={APP_WIDTH}
|
gridAutoRows: 'min-content auto',
|
||||||
h={APP_HEIGHT}
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{headerComponent || <SiteHeader />}
|
{headerComponent || <SiteHeader />}
|
||||||
<Flex
|
<Flex
|
||||||
gap={4}
|
sx={{
|
||||||
w={{ base: '100vw', xl: 'full' }}
|
gap: 4,
|
||||||
h="full"
|
w: 'full',
|
||||||
flexDir={{ base: 'column', xl: 'row' }}
|
h: 'full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<InvokeTabs />
|
<InvokeTabs />
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -130,6 +134,7 @@ const App = ({
|
|||||||
<FloatingGalleryButton />
|
<FloatingGalleryButton />
|
||||||
</Portal>
|
</Portal>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<DeleteImageModal />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<GlobalHotkeys />
|
<GlobalHotkeys />
|
||||||
</>
|
</>
|
||||||
|
@ -17,6 +17,10 @@ import '../../i18n';
|
|||||||
import { socketMiddleware } from 'services/events/middleware';
|
import { socketMiddleware } from 'services/events/middleware';
|
||||||
import { Middleware } from '@reduxjs/toolkit';
|
import { Middleware } from '@reduxjs/toolkit';
|
||||||
import ImageDndContext from './ImageDnd/ImageDndContext';
|
import ImageDndContext from './ImageDnd/ImageDndContext';
|
||||||
|
import {
|
||||||
|
DeleteImageContext,
|
||||||
|
DeleteImageContextProvider,
|
||||||
|
} from 'app/contexts/DeleteImageContext';
|
||||||
|
|
||||||
const App = lazy(() => import('./App'));
|
const App = lazy(() => import('./App'));
|
||||||
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
|
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
|
||||||
@ -71,11 +75,13 @@ const InvokeAIUI = ({
|
|||||||
<React.Suspense fallback={<Loading />}>
|
<React.Suspense fallback={<Loading />}>
|
||||||
<ThemeLocaleProvider>
|
<ThemeLocaleProvider>
|
||||||
<ImageDndContext>
|
<ImageDndContext>
|
||||||
|
<DeleteImageContextProvider>
|
||||||
<App
|
<App
|
||||||
config={config}
|
config={config}
|
||||||
headerComponent={headerComponent}
|
headerComponent={headerComponent}
|
||||||
setIsReady={setIsReady}
|
setIsReady={setIsReady}
|
||||||
/>
|
/>
|
||||||
|
</DeleteImageContextProvider>
|
||||||
</ImageDndContext>
|
</ImageDndContext>
|
||||||
</ThemeLocaleProvider>
|
</ThemeLocaleProvider>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -72,6 +72,7 @@ import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingA
|
|||||||
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
||||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||||
|
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -179,3 +180,6 @@ addImageCategoriesChangedListener();
|
|||||||
// ControlNet
|
// ControlNet
|
||||||
addControlNetImageProcessedListener();
|
addControlNetImageProcessedListener();
|
||||||
addControlNetAutoProcessListener();
|
addControlNetAutoProcessListener();
|
||||||
|
|
||||||
|
// Update image URLs on connect
|
||||||
|
addUpdateImageUrlsOnConnectListener();
|
||||||
|
@ -28,6 +28,13 @@ export const addCanvasCopiedToClipboardListener = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyBlobToClipboard(blob);
|
copyBlobToClipboard(blob);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Canvas Copied to Clipboard',
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -27,7 +27,8 @@ export const addCanvasDownloadedAsImageListener = () => {
|
|||||||
return;
|
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 { canvasMerged } from 'features/canvas/store/actions';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { imageUploaded } from 'services/thunks/image';
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||||
|
export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png';
|
||||||
|
|
||||||
export const addCanvasMergedListener = () => {
|
export const addCanvasMergedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: canvasMerged,
|
actionCreator: canvasMerged,
|
||||||
effect: async (action, { dispatch, getState, take }) => {
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
const state = getState();
|
const blob = await getFullBaseLayerBlob();
|
||||||
|
|
||||||
const blob = await getBaseLayerBlob(state, true);
|
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
moduleLog.error('Problem getting base layer blob');
|
moduleLog.error('Problem getting base layer blob');
|
||||||
@ -48,12 +46,12 @@ export const addCanvasMergedListener = () => {
|
|||||||
relativeTo: canvasBaseLayer.getParent(),
|
relativeTo: canvasBaseLayer.getParent(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const filename = `mergedCanvas_${uuidv4()}.png`;
|
const imageUploadedRequest = dispatch(
|
||||||
|
|
||||||
dispatch(
|
|
||||||
imageUploaded({
|
imageUploaded({
|
||||||
formData: {
|
formData: {
|
||||||
file: new File([blob], filename, { type: 'image/png' }),
|
file: new File([blob], MERGED_CANVAS_FILENAME, {
|
||||||
|
type: 'image/png',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
imageCategory: 'general',
|
imageCategory: 'general',
|
||||||
isIntermediate: true,
|
isIntermediate: true,
|
||||||
@ -61,9 +59,11 @@ export const addCanvasMergedListener = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [{ payload }] = await take(
|
const [{ payload }] = await take(
|
||||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
(
|
||||||
imageUploaded.fulfilled.match(action) &&
|
uploadedImageAction
|
||||||
action.meta.arg.formData.file.name === filename
|
): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||||
|
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
||||||
|
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedCanvasImage = payload;
|
const mergedCanvasImage = payload;
|
||||||
|
@ -4,9 +4,10 @@ import { log } from 'app/logging/useLogger';
|
|||||||
import { imageUploaded } from 'services/thunks/image';
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||||
|
|
||||||
|
export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||||
|
|
||||||
export const addCanvasSavedToGalleryListener = () => {
|
export const addCanvasSavedToGalleryListener = () => {
|
||||||
@ -15,7 +16,7 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
effect: async (action, { dispatch, getState, take }) => {
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const blob = await getBaseLayerBlob(state, true);
|
const blob = await getBaseLayerBlob(state);
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
moduleLog.error('Problem getting base layer blob');
|
moduleLog.error('Problem getting base layer blob');
|
||||||
@ -29,12 +30,12 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = `mergedCanvas_${uuidv4()}.png`;
|
const imageUploadedRequest = dispatch(
|
||||||
|
|
||||||
dispatch(
|
|
||||||
imageUploaded({
|
imageUploaded({
|
||||||
formData: {
|
formData: {
|
||||||
file: new File([blob], filename, { type: 'image/png' }),
|
file: new File([blob], SAVED_CANVAS_FILENAME, {
|
||||||
|
type: 'image/png',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
imageCategory: 'general',
|
imageCategory: 'general',
|
||||||
isIntermediate: false,
|
isIntermediate: false,
|
||||||
@ -42,9 +43,11 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [{ payload: uploadedImageDTO }] = await take(
|
const [{ payload: uploadedImageDTO }] = await take(
|
||||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
(
|
||||||
imageUploaded.fulfilled.match(action) &&
|
uploadedImageAction
|
||||||
action.meta.arg.formData.file.name === filename
|
): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||||
|
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
||||||
|
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(imageUpserted(uploadedImageDTO));
|
dispatch(imageUpserted(uploadedImageDTO));
|
||||||
|
@ -6,10 +6,13 @@ import { clamp } from 'lodash-es';
|
|||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
imageRemoved,
|
imageRemoved,
|
||||||
imagesAdapter,
|
|
||||||
selectImagesEntities,
|
selectImagesEntities,
|
||||||
selectImagesIds,
|
selectImagesIds,
|
||||||
} from 'features/gallery/store/imagesSlice';
|
} 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' });
|
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||||
|
|
||||||
@ -20,11 +23,7 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: requestedImageDeletion,
|
actionCreator: requestedImageDeletion,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const image = action.payload;
|
const { image, imageUsage } = action.payload;
|
||||||
if (!image) {
|
|
||||||
moduleLog.warn('No image provided');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { image_name, image_origin } = image;
|
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));
|
dispatch(imageRemoved(image_name));
|
||||||
|
|
||||||
|
// Delete from server
|
||||||
dispatch(
|
dispatch(
|
||||||
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
||||||
);
|
);
|
||||||
@ -74,9 +93,7 @@ export const addImageDeletedPendingListener = () => {
|
|||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeleted.pending,
|
actionCreator: imageDeleted.pending,
|
||||||
effect: (action, { dispatch, getState }) => {
|
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 { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { imageMetadataReceived } from 'services/thunks/image';
|
import { imageMetadataReceived, imageUpdated } from 'services/thunks/image';
|
||||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
@ -10,10 +10,29 @@ export const addImageMetadataReceivedFulfilledListener = () => {
|
|||||||
actionCreator: imageMetadataReceived.fulfilled,
|
actionCreator: imageMetadataReceived.fulfilled,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const image = action.payload;
|
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
|
// No further actions needed for intermediate images
|
||||||
|
moduleLog.trace(
|
||||||
|
{ data: { image } },
|
||||||
|
'Image metadata received (intermediate), skipping'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
||||||
dispatch(imageUpserted(image));
|
dispatch(imageUpserted(image));
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,8 @@ import { imageUploaded } from 'services/thunks/image';
|
|||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
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' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -19,9 +21,22 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = getState();
|
const originalFileName = action.meta.arg.formData.file.name;
|
||||||
|
|
||||||
dispatch(imageUpserted(image));
|
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' }));
|
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { imageUrlsReceived } from 'services/thunks/image';
|
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' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -14,13 +14,12 @@ export const addImageUrlsReceivedFulfilledListener = () => {
|
|||||||
|
|
||||||
const { image_name, image_url, thumbnail_url } = image;
|
const { image_name, image_url, thumbnail_url } = image;
|
||||||
|
|
||||||
imagesAdapter.updateOne(getState().images, {
|
dispatch(
|
||||||
|
imageUpdatedOne({
|
||||||
id: image_name,
|
id: image_name,
|
||||||
changes: {
|
changes: { image_url, thumbnail_url },
|
||||||
image_url,
|
})
|
||||||
thumbnail_url,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -108,12 +108,9 @@ export type SDFeature =
|
|||||||
*/
|
*/
|
||||||
export type AppConfig = {
|
export type AppConfig = {
|
||||||
/**
|
/**
|
||||||
* Whether or not URLs should be transformed to use a different host
|
* Whether or not we should update image urls when image loading errors
|
||||||
*/
|
|
||||||
shouldTransformUrls: boolean;
|
|
||||||
/**
|
|
||||||
* Whether or not we need to re-fetch images
|
|
||||||
*/
|
*/
|
||||||
|
shouldUpdateImagesOnConnect: boolean;
|
||||||
disabledTabs: InvokeTabName[];
|
disabledTabs: InvokeTabName[];
|
||||||
disabledFeatures: AppFeature[];
|
disabledFeatures: AppFeature[];
|
||||||
disabledSDFeatures: SDFeature[];
|
disabledSDFeatures: SDFeature[];
|
||||||
|
@ -4,7 +4,6 @@ import { useCombinedRefs } from '@dnd-kit/utilities';
|
|||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { ReactElement, SyntheticEvent } from 'react';
|
import { ReactElement, SyntheticEvent } from 'react';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
@ -45,7 +44,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
minSize = 24,
|
minSize = 24,
|
||||||
} = props;
|
} = props;
|
||||||
const dndId = useRef(uuidv4());
|
const dndId = useRef(uuidv4());
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
const {
|
const {
|
||||||
isOver,
|
isOver,
|
||||||
setNodeRef: setDroppableRef,
|
setNodeRef: setDroppableRef,
|
||||||
@ -100,7 +98,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={getUrl(image.image_url)}
|
src={image.image_url}
|
||||||
fallbackStrategy="beforeLoadOrError"
|
fallbackStrategy="beforeLoadOrError"
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
|
@ -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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
@ -33,7 +32,6 @@ const selector = createSelector(
|
|||||||
|
|
||||||
const IAICanvasObjectRenderer = () => {
|
const IAICanvasObjectRenderer = () => {
|
||||||
const { objects } = useAppSelector(selector);
|
const { objects } = useAppSelector(selector);
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
|
|
||||||
if (!objects) return null;
|
if (!objects) return null;
|
||||||
|
|
||||||
@ -46,7 +44,7 @@ const IAICanvasObjectRenderer = () => {
|
|||||||
key={i}
|
key={i}
|
||||||
x={obj.x}
|
x={obj.x}
|
||||||
y={obj.y}
|
y={obj.y}
|
||||||
url={getUrl(obj.image.image_url)}
|
url={obj.image.image_url}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (isCanvasBaseLine(obj)) {
|
} else if (isCanvasBaseLine(obj)) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { GroupConfig } from 'konva/lib/Group';
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
@ -56,13 +55,12 @@ const IAICanvasStagingArea = (props: Props) => {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group {...rest}>
|
<Group {...rest}>
|
||||||
{shouldShowStagingImage && currentStagingAreaImage && (
|
{shouldShowStagingImage && currentStagingAreaImage && (
|
||||||
<IAICanvasImage
|
<IAICanvasImage
|
||||||
url={getUrl(currentStagingAreaImage.image.image_url) ?? ''}
|
url={currentStagingAreaImage.image.image_url}
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
/>
|
/>
|
||||||
|
@ -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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
@ -210,8 +210,10 @@ const IAICanvasToolbar = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box w={24}>
|
||||||
<IAISelect
|
<IAISelect
|
||||||
tooltip={`${t('unifiedCanvas.layer')} (Q)`}
|
tooltip={`${t('unifiedCanvas.layer')} (Q)`}
|
||||||
tooltipProps={{ hasArrow: true, placement: 'top' }}
|
tooltipProps={{ hasArrow: true, placement: 'top' }}
|
||||||
@ -220,6 +222,7 @@ const IAICanvasToolbar = () => {
|
|||||||
onChange={handleChangeLayer}
|
onChange={handleChangeLayer}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<IAICanvasMaskOptions />
|
<IAICanvasMaskOptions />
|
||||||
<IAICanvasToolChooserOptions />
|
<IAICanvasToolChooserOptions />
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { sessionCanceled } from 'services/thunks/session';
|
import { sessionCanceled } from 'services/thunks/session';
|
||||||
import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice';
|
import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice';
|
||||||
|
import { imageUrlsReceived } from 'services/thunks/image';
|
||||||
|
|
||||||
export const initialLayerState: CanvasLayerState = {
|
export const initialLayerState: CanvasLayerState = {
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -856,6 +857,26 @@ export const canvasSlice = createSlice({
|
|||||||
builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => {
|
builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => {
|
||||||
state.doesCanvasNeedScaling = true;
|
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 { RootState } from 'app/store/store';
|
||||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||||
|
|
||||||
export const getBaseLayerBlob = async (
|
/**
|
||||||
state: RootState,
|
* Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave`
|
||||||
withoutBoundingBox?: boolean
|
*/
|
||||||
) => {
|
export const getBaseLayerBlob = async (state: RootState) => {
|
||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
if (!canvasBaseLayer) {
|
if (!canvasBaseLayer) {
|
||||||
@ -24,8 +24,7 @@ export const getBaseLayerBlob = async (
|
|||||||
|
|
||||||
const absPos = clonedBaseLayer.getAbsolutePosition();
|
const absPos = clonedBaseLayer.getAbsolutePosition();
|
||||||
|
|
||||||
const boundingBox =
|
const boundingBox = shouldCropToBoundingBoxOnSave
|
||||||
shouldCropToBoundingBoxOnSave && !withoutBoundingBox
|
|
||||||
? {
|
? {
|
||||||
x: boundingBoxCoordinates.x + absPos.x,
|
x: boundingBoxCoordinates.x + absPos.x,
|
||||||
y: boundingBoxCoordinates.y + absPos.y,
|
y: boundingBoxCoordinates.y + absPos.y,
|
||||||
|
@ -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());
|
||||||
|
};
|
@ -13,6 +13,8 @@ import {
|
|||||||
ControlNetModel,
|
ControlNetModel,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { controlNetImageProcessed } from './actions';
|
import { controlNetImageProcessed } from './actions';
|
||||||
|
import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
|
||||||
|
import { forEach } from 'lodash-es';
|
||||||
|
|
||||||
export const initialControlNet: Omit<ControlNetConfig, 'controlNetId'> = {
|
export const initialControlNet: Omit<ControlNetConfig, 'controlNetId'> = {
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -185,6 +187,9 @@ export const controlNetSlice = createSlice({
|
|||||||
processorType
|
processorType
|
||||||
].default as RequiredControlNetProcessorNode;
|
].default as RequiredControlNetProcessorNode;
|
||||||
},
|
},
|
||||||
|
controlNetReset: () => {
|
||||||
|
return { ...initialControlNetState };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(controlNetImageProcessed, (state, action) => {
|
builder.addCase(controlNetImageProcessed, (state, action) => {
|
||||||
@ -194,6 +199,36 @@ export const controlNetSlice = createSlice({
|
|||||||
state.isProcessingControlImage = true;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,6 +246,7 @@ export const {
|
|||||||
controlNetEndStepPctChanged,
|
controlNetEndStepPctChanged,
|
||||||
controlNetProcessorParamsChanged,
|
controlNetProcessorParamsChanged,
|
||||||
controlNetProcessorTypeChanged,
|
controlNetProcessorTypeChanged,
|
||||||
|
controlNetReset,
|
||||||
} = controlNetSlice.actions;
|
} = controlNetSlice.actions;
|
||||||
|
|
||||||
export default controlNetSlice.reducer;
|
export default controlNetSlice.reducer;
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import {
|
import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
|
||||||
ButtonGroup,
|
|
||||||
Flex,
|
|
||||||
FlexProps,
|
|
||||||
Link,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -45,22 +39,18 @@ import {
|
|||||||
FaShareAlt,
|
FaShareAlt,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { gallerySelector } from '../store/gallerySelectors';
|
import { gallerySelector } from '../store/gallerySelectors';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import {
|
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
||||||
requestedImageDeletion,
|
|
||||||
sentImageToCanvas,
|
|
||||||
sentImageToImg2Img,
|
|
||||||
} from '../store/actions';
|
|
||||||
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
|
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
|
||||||
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
|
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
|
||||||
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
|
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
|
||||||
|
import { DeleteImageButton } from './DeleteImageModal';
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -123,10 +113,6 @@ const currentImageButtonsSelector = createSelector(
|
|||||||
|
|
||||||
type CurrentImageButtonsProps = FlexProps;
|
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 CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {
|
const {
|
||||||
@ -138,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
facetoolStrength,
|
facetoolStrength,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
// currentImage,
|
|
||||||
isLightboxOpen,
|
isLightboxOpen,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
shouldHidePreview,
|
shouldHidePreview,
|
||||||
image,
|
image,
|
||||||
canDeleteImage,
|
|
||||||
shouldConfirmOnDelete,
|
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
} = useAppSelector(currentImageButtonsSelector);
|
} = useAppSelector(currentImageButtonsSelector);
|
||||||
|
|
||||||
@ -153,20 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
|
||||||
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
|
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
|
||||||
|
|
||||||
const { getUrl, shouldTransformUrls } = useGetUrl();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isOpen: isDeleteDialogOpen,
|
|
||||||
onOpen: onDeleteDialogOpen,
|
|
||||||
onClose: onDeleteDialogClose,
|
|
||||||
} = useDisclosure();
|
|
||||||
|
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
|
const { onDelete } = useContext(DeleteImageContext);
|
||||||
|
|
||||||
// const handleCopyImage = useCallback(async () => {
|
// const handleCopyImage = useCallback(async () => {
|
||||||
// if (!image?.url) {
|
// if (!image?.url) {
|
||||||
// return;
|
// return;
|
||||||
@ -197,10 +174,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldTransformUrls) {
|
|
||||||
return getUrl(image.image_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image.image_url.startsWith('http')) {
|
if (image.image_url.startsWith('http')) {
|
||||||
return image.image_url;
|
return image.image_url;
|
||||||
}
|
}
|
||||||
@ -229,7 +202,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [toaster, shouldTransformUrls, getUrl, t, image]);
|
}, [toaster, t, image]);
|
||||||
|
|
||||||
const handleClickUseAllParameters = useCallback(() => {
|
const handleClickUseAllParameters = useCallback(() => {
|
||||||
recallAllParameters(image);
|
recallAllParameters(image);
|
||||||
@ -269,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
// selectedImage && dispatch(runESRGAN(selectedImage));
|
// selectedImage && dispatch(runESRGAN(selectedImage));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
onDelete(image);
|
||||||
|
}, [image, onDelete]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'Shift+U',
|
'Shift+U',
|
||||||
() => {
|
() => {
|
||||||
@ -370,31 +347,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
[image, shouldShowImageDetails, toaster]
|
[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(() => {
|
const handleClickProgressImagesToggle = useCallback(() => {
|
||||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||||
}, [dispatch, shouldShowProgressInViewer]);
|
}, [dispatch, shouldShowProgressInViewer]);
|
||||||
|
|
||||||
useHotkeys('delete', handleInitiateDelete, [
|
|
||||||
image,
|
|
||||||
shouldConfirmOnDelete,
|
|
||||||
isConnected,
|
|
||||||
isProcessing,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleLightBox = useCallback(() => {
|
const handleLightBox = useCallback(() => {
|
||||||
dispatch(setIsLightboxOpen(!isLightboxOpen));
|
dispatch(setIsLightboxOpen(!isLightboxOpen));
|
||||||
}, [dispatch, isLightboxOpen]);
|
}, [dispatch, isLightboxOpen]);
|
||||||
@ -461,11 +417,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
{t('parameters.copyImageToLink')}
|
{t('parameters.copyImageToLink')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
|
|
||||||
<Link
|
<Link download={true} href={image?.image_url} target="_blank">
|
||||||
download={true}
|
|
||||||
href={getUrl(image?.image_url ?? '')}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
||||||
{t('parameters.downloadImage')}
|
{t('parameters.downloadImage')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
@ -607,7 +559,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup isAttached={true}>
|
<ButtonGroup isAttached={true}>
|
||||||
<DeleteImageButton image={image} />
|
<DeleteImageButton onClick={handleDelete} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
@ -117,7 +117,7 @@ const CurrentImagePreview = () => {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{shouldShowImageDetails && image && image.metadata && (
|
{shouldShowImageDetails && image && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -5,51 +5,81 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
|
ListItem,
|
||||||
Text,
|
Text,
|
||||||
|
UnorderedList,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
DeleteImageContext,
|
||||||
|
ImageUsage,
|
||||||
|
} from 'app/contexts/DeleteImageContext';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import IAISwitch from 'common/components/IAISwitch';
|
import IAISwitch from 'common/components/IAISwitch';
|
||||||
import { configSelector } from 'features/system/store/configSelectors';
|
import { configSelector } from 'features/system/store/configSelectors';
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
import { FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[systemSelector, configSelector],
|
[systemSelector, configSelector],
|
||||||
(system, config) => {
|
(system, config) => {
|
||||||
const { shouldConfirmOnDelete } = system;
|
const { shouldConfirmOnDelete } = system;
|
||||||
const { canRestoreDeletedImagesFromBin } = config;
|
const { canRestoreDeletedImagesFromBin } = config;
|
||||||
return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin };
|
|
||||||
|
return {
|
||||||
|
shouldConfirmOnDelete,
|
||||||
|
canRestoreDeletedImagesFromBin,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
defaultSelectorOptions
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
interface DeleteImageModalProps {
|
const ImageInUseMessage = (props: { imageUsage?: ImageUsage }) => {
|
||||||
isOpen: boolean;
|
const { imageUsage } = props;
|
||||||
onClose: () => void;
|
|
||||||
handleDelete: () => void;
|
if (!imageUsage) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteImageModal = ({
|
if (!some(imageUsage)) {
|
||||||
isOpen,
|
return null;
|
||||||
onClose,
|
}
|
||||||
handleDelete,
|
|
||||||
}: DeleteImageModalProps) => {
|
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 dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { isOpen, onClose, onImmediatelyDelete, image, imageUsage } =
|
||||||
|
useContext(DeleteImageContext);
|
||||||
|
|
||||||
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
|
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) =>
|
(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
@ -57,10 +87,7 @@ const DeleteImageModal = ({
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickDelete = useCallback(() => {
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
handleDelete();
|
|
||||||
onClose();
|
|
||||||
}, [handleDelete, onClose]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
@ -76,15 +103,15 @@ const DeleteImageModal = ({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogBody>
|
<AlertDialogBody>
|
||||||
<Flex direction="column" gap={5}>
|
<Flex direction="column" gap={3}>
|
||||||
<Flex direction="column" gap={2}>
|
<ImageInUseMessage imageUsage={imageUsage} />
|
||||||
<Text>{t('common.areYouSure')}</Text>
|
<Divider />
|
||||||
<Text>
|
<Text>
|
||||||
{canRestoreDeletedImagesFromBin
|
{canRestoreDeletedImagesFromBin
|
||||||
? t('gallery.deleteImageBin')
|
? t('gallery.deleteImageBin')
|
||||||
: t('gallery.deleteImagePermanent')}
|
: t('gallery.deleteImagePermanent')}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
<Text>{t('common.areYouSure')}</Text>
|
||||||
<IAISwitch
|
<IAISwitch
|
||||||
label={t('common.dontAskMeAgain')}
|
label={t('common.dontAskMeAgain')}
|
||||||
isChecked={!shouldConfirmOnDelete}
|
isChecked={!shouldConfirmOnDelete}
|
||||||
@ -96,7 +123,7 @@ const DeleteImageModal = ({
|
|||||||
<IAIButton ref={cancelRef} onClick={onClose}>
|
<IAIButton ref={cancelRef} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
<IAIButton colorScheme="error" onClick={handleClickDelete} ml={3}>
|
<IAIButton colorScheme="error" onClick={onImmediatelyDelete} ml={3}>
|
||||||
Delete
|
Delete
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@ -107,3 +134,33 @@ const DeleteImageModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(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 {
|
import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Icon,
|
|
||||||
Image,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
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 { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa';
|
||||||
import DeleteImageModal from './DeleteImageModal';
|
|
||||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||||
import {
|
import {
|
||||||
resizeAndScaleCanvas,
|
resizeAndScaleCanvas,
|
||||||
@ -21,7 +12,6 @@ import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
|||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
@ -32,14 +22,11 @@ import { isEqual } from 'lodash-es';
|
|||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import {
|
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
||||||
requestedImageDeletion,
|
|
||||||
sentImageToCanvas,
|
|
||||||
sentImageToImg2Img,
|
|
||||||
} from '../store/actions';
|
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
|
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||||
@ -93,28 +80,22 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
canDeleteImage,
|
canDeleteImage,
|
||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
shouldConfirmOnDelete,
|
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const {
|
|
||||||
isOpen: isDeleteDialogOpen,
|
|
||||||
onOpen: onDeleteDialogOpen,
|
|
||||||
onClose: onDeleteDialogClose,
|
|
||||||
} = useDisclosure();
|
|
||||||
|
|
||||||
const { image, isSelected } = props;
|
const { image, isSelected } = props;
|
||||||
const { image_url, thumbnail_url, image_name } = image;
|
const { image_url, thumbnail_url, image_name } = image;
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||||
|
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
||||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||||
|
|
||||||
|
const { onDelete } = useContext(DeleteImageContext);
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
onDelete(image);
|
||||||
|
}, [image, onDelete]);
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
@ -128,26 +109,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
const handleMouseOver = () => setIsHovered(true);
|
const handleMouseOver = () => setIsHovered(true);
|
||||||
const handleMouseOut = () => setIsHovered(false);
|
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(() => {
|
const handleSelectImage = useCallback(() => {
|
||||||
dispatch(imageSelected(image));
|
dispatch(imageSelected(image));
|
||||||
}, [image, dispatch]);
|
}, [image, dispatch]);
|
||||||
@ -208,7 +169,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenInNewTab = () => {
|
const handleOpenInNewTab = () => {
|
||||||
window.open(getUrl(image.image_url), '_blank');
|
window.open(image.image_url, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -283,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
{t('parameters.sendToUnifiedCanvas')}
|
{t('parameters.sendToUnifiedCanvas')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}>
|
<MenuItem
|
||||||
|
sx={{ color: 'error.300' }}
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClickCapture={handleDelete}
|
||||||
|
>
|
||||||
{t('gallery.deleteImage')}
|
{t('gallery.deleteImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
@ -296,8 +261,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
onMouseOver={handleMouseOver}
|
onMouseOver={handleMouseOver}
|
||||||
onMouseOut={handleMouseOut}
|
onMouseOut={handleMouseOut}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
// draggable={true}
|
|
||||||
// onDragStart={handleDragStart}
|
|
||||||
onClick={handleSelectImage}
|
onClick={handleSelectImage}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sx={{
|
sx={{
|
||||||
@ -317,7 +280,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
|
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
|
||||||
}
|
}
|
||||||
rounded="md"
|
rounded="md"
|
||||||
src={getUrl(thumbnail_url || image_url)}
|
src={thumbnail_url || image_url}
|
||||||
fallback={<FaImage />}
|
fallback={<FaImage />}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -361,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
onClickCapture={handleInitiateDelete}
|
onClickCapture={handleDelete}
|
||||||
aria-label={t('gallery.deleteImage')}
|
aria-label={t('gallery.deleteImage')}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
size="xs"
|
size="xs"
|
||||||
@ -373,11 +336,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<DeleteImageModal
|
|
||||||
isOpen={isDeleteDialogOpen}
|
|
||||||
onClose={onDeleteDialogClose}
|
|
||||||
handleDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, memoEqualityCheck);
|
}, 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);
|
|
@ -9,19 +9,6 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
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 { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -30,7 +17,6 @@ import { FaCopy } from 'react-icons/fa';
|
|||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { Scheduler } from 'app/constants';
|
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
|
|
||||||
type MetadataItemProps = {
|
type MetadataItemProps = {
|
||||||
@ -146,7 +132,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
const metadata = image?.metadata;
|
const metadata = image?.metadata;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
|
|
||||||
const metadataJSON = JSON.stringify(image, null, 2);
|
const metadataJSON = JSON.stringify(image, null, 2);
|
||||||
|
|
||||||
@ -168,11 +153,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
>
|
>
|
||||||
<Flex gap={2}>
|
<Flex gap={2}>
|
||||||
<Text fontWeight="semibold">File:</Text>
|
<Text fontWeight="semibold">File:</Text>
|
||||||
<Link
|
<Link href={image.image_url} isExternal maxW="calc(100% - 3rem)">
|
||||||
href={getUrl(image.image_url)}
|
|
||||||
isExternal
|
|
||||||
maxW="calc(100% - 3rem)"
|
|
||||||
>
|
|
||||||
{image.image_name}
|
{image.image_name}
|
||||||
<ExternalLinkIcon mx="2px" />
|
<ExternalLinkIcon mx="2px" />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { ImageNameAndOrigin } from 'features/parameters/store/actions';
|
import { ImageUsage } from 'app/contexts/DeleteImageContext';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
|
|
||||||
export const requestedImageDeletion = createAction<
|
export type RequestedImageDeletionArg = {
|
||||||
ImageDTO | ImageNameAndOrigin | undefined
|
image: ImageDTO;
|
||||||
>('gallery/requestedImageDeletion');
|
imageUsage: ImageUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestedImageDeletion = createAction<RequestedImageDeletionArg>(
|
||||||
|
'gallery/requestedImageDeletion'
|
||||||
|
);
|
||||||
|
|
||||||
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { imageUpserted } from './imagesSlice';
|
import { imageUpserted } from './imagesSlice';
|
||||||
|
import { imageUrlsReceived } from 'services/thunks/image';
|
||||||
|
|
||||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
|
|
||||||
@ -57,6 +58,15 @@ export const gallerySlice = createSlice({
|
|||||||
state.selectedImage = action.payload;
|
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 {
|
import {
|
||||||
PayloadAction,
|
PayloadAction,
|
||||||
|
Update,
|
||||||
createEntityAdapter,
|
createEntityAdapter,
|
||||||
createSelector,
|
createSelector,
|
||||||
createSlice,
|
createSlice,
|
||||||
@ -7,12 +8,17 @@ import {
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { ImageCategory, ImageDTO } from 'services/api';
|
import { ImageCategory, ImageDTO } from 'services/api';
|
||||||
import { dateComparator } from 'common/util/dateComparator';
|
import { dateComparator } from 'common/util/dateComparator';
|
||||||
import { isString, keyBy } from 'lodash-es';
|
import { keyBy } from 'lodash-es';
|
||||||
import { receivedPageOfImages } from 'services/thunks/image';
|
import {
|
||||||
|
imageDeleted,
|
||||||
|
imageMetadataReceived,
|
||||||
|
imageUrlsReceived,
|
||||||
|
receivedPageOfImages,
|
||||||
|
} from 'services/thunks/image';
|
||||||
|
|
||||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||||
selectId: (image) => image.image_name,
|
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'];
|
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
||||||
@ -49,13 +55,11 @@ const imagesSlice = createSlice({
|
|||||||
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||||
imagesAdapter.upsertOne(state, action.payload);
|
imagesAdapter.upsertOne(state, action.payload);
|
||||||
},
|
},
|
||||||
imageRemoved: (state, action: PayloadAction<string | ImageDTO>) => {
|
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
||||||
if (isString(action.payload)) {
|
imagesAdapter.updateOne(state, action.payload);
|
||||||
|
},
|
||||||
|
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||||
imagesAdapter.removeOne(state, action.payload);
|
imagesAdapter.removeOne(state, action.payload);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
imagesAdapter.removeOne(state, action.payload.image_name);
|
|
||||||
},
|
},
|
||||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
||||||
state.categories = action.payload;
|
state.categories = action.payload;
|
||||||
@ -76,6 +80,20 @@ const imagesSlice = createSlice({
|
|||||||
state.total = total;
|
state.total = total;
|
||||||
imagesAdapter.upsertMany(state, items);
|
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,
|
selectTotal: selectImagesTotal,
|
||||||
} = imagesAdapter.getSelectors<RootState>((state) => state.images);
|
} = imagesAdapter.getSelectors<RootState>((state) => state.images);
|
||||||
|
|
||||||
export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
|
export const {
|
||||||
imagesSlice.actions;
|
imageUpserted,
|
||||||
|
imageUpdatedOne,
|
||||||
|
imageRemoved,
|
||||||
|
imageCategoriesChanged,
|
||||||
|
} = imagesSlice.actions;
|
||||||
|
|
||||||
export default imagesSlice.reducer;
|
export default imagesSlice.reducer;
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
|
|
||||||
type ReactPanZoomProps = {
|
type ReactPanZoomProps = {
|
||||||
@ -23,7 +22,6 @@ export default function ReactPanZoomImage({
|
|||||||
scaleY,
|
scaleY,
|
||||||
}: ReactPanZoomProps) {
|
}: ReactPanZoomProps) {
|
||||||
const { centerView } = useTransformContext();
|
const { centerView } = useTransformContext();
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
@ -37,7 +35,7 @@ export default function ReactPanZoomImage({
|
|||||||
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
|
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
src={getUrl(image.image_url)}
|
src={image.image_url}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={styleClass ? styleClass : ''}
|
className={styleClass ? styleClass : ''}
|
||||||
|
@ -6,7 +6,7 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
const FieldTypeLegend = () => {
|
const FieldTypeLegend = () => {
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} flexDirection={{ base: 'column', xl: 'row' }}>
|
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||||
{map(FIELDS, ({ title, description, color }, key) => (
|
{map(FIELDS, ({ title, description, color }, key) => (
|
||||||
<Tooltip key={key} label={description}>
|
<Tooltip key={key} label={description}>
|
||||||
<Badge
|
<Badge
|
||||||
|
@ -11,7 +11,7 @@ const NodeEditor = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: 'full',
|
width: 'full',
|
||||||
height: { base: '100vh', xl: 'full' },
|
height: 'full',
|
||||||
borderRadius: 'md',
|
borderRadius: 'md',
|
||||||
bg: 'base.850',
|
bg: 'base.850',
|
||||||
}}
|
}}
|
||||||
|
@ -16,9 +16,10 @@ import { receivedOpenAPISchema } from 'services/thunks/schema';
|
|||||||
import { InvocationTemplate, InvocationValue } from '../types/types';
|
import { InvocationTemplate, InvocationValue } from '../types/types';
|
||||||
import { parseSchema } from '../util/parseSchema';
|
import { parseSchema } from '../util/parseSchema';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { size } from 'lodash-es';
|
import { forEach, size } from 'lodash-es';
|
||||||
import { isAnyGraphBuilt } from './actions';
|
|
||||||
import { RgbaColor } from 'react-colorful';
|
import { RgbaColor } from 'react-colorful';
|
||||||
|
import { imageUrlsReceived } from 'services/thunks/image';
|
||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
|
||||||
export type NodesState = {
|
export type NodesState = {
|
||||||
nodes: Node<InvocationValue>[];
|
nodes: Node<InvocationValue>[];
|
||||||
@ -92,15 +93,29 @@ const nodesSlice = createSlice({
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
nodeEditorReset: () => {
|
||||||
|
return { ...initialNodesState };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
||||||
state.schema = action.payload;
|
state.schema = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addMatcher(isAnyGraphBuilt, (state, action) => {
|
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||||
// TODO: Achtung! Side effect in a reducer!
|
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||||
log.info({ namespace: 'nodes', data: action.payload }, 'Graph built');
|
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,
|
connectionEnded,
|
||||||
shouldShowGraphOverlayChanged,
|
shouldShowGraphOverlayChanged,
|
||||||
parsedOpenAPISchema,
|
parsedOpenAPISchema,
|
||||||
|
nodeEditorReset,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
|
||||||
|
export const nodesSelecter = (state: RootState) => state.nodes;
|
||||||
|
@ -25,6 +25,7 @@ const ParamNegativeConditioning = () => {
|
|||||||
borderColor: 'error.600',
|
borderColor: 'error.600',
|
||||||
}}
|
}}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
minH={16}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
|
@ -82,7 +82,7 @@ const ParamPositiveConditioning = () => {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
ref={promptRef}
|
ref={promptRef}
|
||||||
minH={{ base: 20, lg: 40 }}
|
minH={32}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
|
||||||
import {
|
import {
|
||||||
clearInitialImage,
|
clearInitialImage,
|
||||||
initialImageChanged,
|
initialImageChanged,
|
||||||
@ -30,7 +29,6 @@ const selector = createSelector(
|
|||||||
const InitialImagePreview = () => {
|
const InitialImagePreview = () => {
|
||||||
const { initialImage } = useAppSelector(selector);
|
const { initialImage } = useAppSelector(selector);
|
||||||
const { shouldFetchImages } = useAppSelector(configSelector);
|
const { shouldFetchImages } = useAppSelector(configSelector);
|
||||||
const { getUrl } = useGetUrl();
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
StrengthParam,
|
StrengthParam,
|
||||||
WidthParam,
|
WidthParam,
|
||||||
} from './parameterZodSchemas';
|
} from './parameterZodSchemas';
|
||||||
|
import { imageUrlsReceived } from 'services/thunks/image';
|
||||||
|
|
||||||
export interface GenerationState {
|
export interface GenerationState {
|
||||||
cfgScale: CfgScaleParam;
|
cfgScale: CfgScaleParam;
|
||||||
@ -231,6 +232,16 @@ export const generationSlice = createSlice({
|
|||||||
state.model = defaultModel;
|
state.model = defaultModel;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||||
|
const { image_name, image_origin, image_url, thumbnail_url } =
|
||||||
|
action.payload;
|
||||||
|
|
||||||
|
if (state.initialImage?.image_name === image_name) {
|
||||||
|
state.initialImage.image_url = image_url;
|
||||||
|
state.initialImage.thumbnail_url = thumbnail_url;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,22 +13,16 @@ const InvokeAILogoComponent = () => {
|
|||||||
<Image
|
<Image
|
||||||
src={InvokeAILogoImage}
|
src={InvokeAILogoImage}
|
||||||
alt="invoke-ai-logo"
|
alt="invoke-ai-logo"
|
||||||
w="32px"
|
sx={{
|
||||||
h="32px"
|
w: '32px',
|
||||||
minW="32px"
|
h: '32px',
|
||||||
minH="32px"
|
minW: '32px',
|
||||||
userSelect="none"
|
minH: '32px',
|
||||||
/>
|
|
||||||
<Flex
|
|
||||||
gap={3}
|
|
||||||
display={{
|
|
||||||
base: 'inherit',
|
|
||||||
sm: 'none',
|
|
||||||
md: 'inherit',
|
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Text fontSize="xl">
|
<Flex sx={{ gap: 3 }}>
|
||||||
|
<Text sx={{ fontSize: 'xl', userSelect: 'none' }}>
|
||||||
invoke <strong>ai</strong>
|
invoke <strong>ai</strong>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1,66 +1,138 @@
|
|||||||
import { Flex, Grid } from '@chakra-ui/react';
|
import { Flex, Spacer } from '@chakra-ui/react';
|
||||||
import { memo, useState } from 'react';
|
import { memo } from 'react';
|
||||||
import StatusIndicator from './StatusIndicator';
|
import StatusIndicator from './StatusIndicator';
|
||||||
|
|
||||||
import InvokeAILogoComponent from './InvokeAILogoComponent';
|
import { Link } from '@chakra-ui/react';
|
||||||
import SiteHeaderMenu from './SiteHeaderMenu';
|
|
||||||
import useResolution from 'common/hooks/useResolution';
|
|
||||||
import { FaBars } from 'react-icons/fa';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaBug, FaCube, FaDiscord, FaGithub, FaKeyboard } from 'react-icons/fa';
|
||||||
|
import { MdSettings } from 'react-icons/md';
|
||||||
|
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||||
|
import InvokeAILogoComponent from './InvokeAILogoComponent';
|
||||||
|
import LanguagePicker from './LanguagePicker';
|
||||||
|
import ModelManagerModal from './ModelManager/ModelManagerModal';
|
||||||
|
import SettingsModal from './SettingsModal/SettingsModal';
|
||||||
|
import ThemeChanger from './ThemeChanger';
|
||||||
|
import { useFeatureStatus } from '../hooks/useFeatureStatus';
|
||||||
|
|
||||||
/**
|
|
||||||
* Header, includes color mode toggle, settings button, status message.
|
|
||||||
*/
|
|
||||||
const SiteHeader = () => {
|
const SiteHeader = () => {
|
||||||
const [menuOpened, setMenuOpened] = useState(false);
|
|
||||||
const resolution = useResolution();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isModelManagerEnabled =
|
||||||
|
useFeatureStatus('modelManager').isFeatureEnabled;
|
||||||
|
const isLocalizationEnabled =
|
||||||
|
useFeatureStatus('localization').isFeatureEnabled;
|
||||||
|
const isBugLinkEnabled = useFeatureStatus('bugLink').isFeatureEnabled;
|
||||||
|
const isDiscordLinkEnabled = useFeatureStatus('discordLink').isFeatureEnabled;
|
||||||
|
const isGithubLinkEnabled = useFeatureStatus('githubLink').isFeatureEnabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
|
||||||
gridTemplateColumns={{ base: 'auto', sm: 'auto max-content' }}
|
|
||||||
paddingRight={{ base: 10, xl: 0 }}
|
|
||||||
gap={2}
|
|
||||||
>
|
|
||||||
<Flex justifyContent={{ base: 'center', sm: 'start' }}>
|
|
||||||
<InvokeAILogoComponent />
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
<Flex
|
||||||
alignItems="center"
|
sx={{
|
||||||
gap={2}
|
gap: 2,
|
||||||
justifyContent={{ base: 'center', sm: 'start' }}
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<InvokeAILogoComponent />
|
||||||
|
<Spacer />
|
||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
|
||||||
{resolution === 'desktop' ? (
|
{isModelManagerEnabled && (
|
||||||
<SiteHeaderMenu />
|
<ModelManagerModal>
|
||||||
) : (
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={<FaBars />}
|
aria-label={t('modelManager.modelManager')}
|
||||||
aria-label={t('accessibility.menu')}
|
tooltip={t('modelManager.modelManager')}
|
||||||
background={menuOpened ? 'base.800' : 'none'}
|
size="sm"
|
||||||
_hover={{ background: menuOpened ? 'base.800' : 'none' }}
|
variant="link"
|
||||||
onClick={() => setMenuOpened(!menuOpened)}
|
data-variant="link"
|
||||||
p={0}
|
fontSize={20}
|
||||||
></IAIIconButton>
|
icon={<FaCube />}
|
||||||
|
/>
|
||||||
|
</ModelManagerModal>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{resolution !== 'desktop' && menuOpened && (
|
<HotkeysModal>
|
||||||
<Flex
|
<IAIIconButton
|
||||||
position="absolute"
|
aria-label={t('common.hotkeysLabel')}
|
||||||
right={6}
|
tooltip={t('common.hotkeysLabel')}
|
||||||
top={{ base: 28, sm: 16 }}
|
size="sm"
|
||||||
backgroundColor="base.800"
|
variant="link"
|
||||||
padding={4}
|
data-variant="link"
|
||||||
borderRadius={4}
|
fontSize={20}
|
||||||
zIndex={{ base: 99, xl: 0 }}
|
icon={<FaKeyboard />}
|
||||||
|
/>
|
||||||
|
</HotkeysModal>
|
||||||
|
|
||||||
|
<ThemeChanger />
|
||||||
|
|
||||||
|
{isLocalizationEnabled && <LanguagePicker />}
|
||||||
|
|
||||||
|
{isBugLinkEnabled && (
|
||||||
|
<Link
|
||||||
|
isExternal
|
||||||
|
href="http://github.com/invoke-ai/InvokeAI/issues"
|
||||||
|
marginBottom="-0.25rem"
|
||||||
>
|
>
|
||||||
<SiteHeaderMenu />
|
<IAIIconButton
|
||||||
</Flex>
|
aria-label={t('common.reportBugLabel')}
|
||||||
|
tooltip={t('common.reportBugLabel')}
|
||||||
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
fontSize={20}
|
||||||
|
size="sm"
|
||||||
|
icon={<FaBug />}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
|
||||||
|
{isGithubLinkEnabled && (
|
||||||
|
<Link
|
||||||
|
isExternal
|
||||||
|
href="http://github.com/invoke-ai/InvokeAI"
|
||||||
|
marginBottom="-0.25rem"
|
||||||
|
>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label={t('common.githubLabel')}
|
||||||
|
tooltip={t('common.githubLabel')}
|
||||||
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
fontSize={20}
|
||||||
|
size="sm"
|
||||||
|
icon={<FaGithub />}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDiscordLinkEnabled && (
|
||||||
|
<Link
|
||||||
|
isExternal
|
||||||
|
href="https://discord.gg/ZmtBAhwWhy"
|
||||||
|
marginBottom="-0.25rem"
|
||||||
|
>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label={t('common.discordLabel')}
|
||||||
|
tooltip={t('common.discordLabel')}
|
||||||
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
fontSize={20}
|
||||||
|
size="sm"
|
||||||
|
icon={<FaDiscord />}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SettingsModal>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label={t('common.settingsLabel')}
|
||||||
|
tooltip={t('common.settingsLabel')}
|
||||||
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
fontSize={22}
|
||||||
|
size="sm"
|
||||||
|
icon={<MdSettings />}
|
||||||
|
/>
|
||||||
|
</SettingsModal>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { AppConfig, PartialAppConfig } from 'app/types/invokeai';
|
|||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
export const initialConfigState: AppConfig = {
|
export const initialConfigState: AppConfig = {
|
||||||
shouldTransformUrls: false,
|
shouldUpdateImagesOnConnect: false,
|
||||||
disabledTabs: [],
|
disabledTabs: [],
|
||||||
disabledFeatures: [],
|
disabledFeatures: [],
|
||||||
disabledSDFeatures: [],
|
disabledSDFeatures: [],
|
||||||
|
@ -152,16 +152,18 @@ const InvokeTabs = () => {
|
|||||||
onChange={(index: number) => {
|
onChange={(index: number) => {
|
||||||
dispatch(setActiveTab(index));
|
dispatch(setActiveTab(index));
|
||||||
}}
|
}}
|
||||||
flexGrow={1}
|
sx={{
|
||||||
flexDir={{ base: 'column', xl: 'row' }}
|
flexGrow: 1,
|
||||||
gap={{ base: 4 }}
|
gap: 4,
|
||||||
|
}}
|
||||||
isLazy
|
isLazy
|
||||||
>
|
>
|
||||||
<TabList
|
<TabList
|
||||||
pt={2}
|
sx={{
|
||||||
gap={4}
|
pt: 2,
|
||||||
flexDir={{ base: 'row', xl: 'column' }}
|
gap: 4,
|
||||||
justifyContent={{ base: 'center', xl: 'start' }}
|
flexDir: 'column',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tabs}
|
{tabs}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
@ -33,7 +33,6 @@ const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
|
|||||||
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
px={{ base: 10, xl: 0 }}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: 'base.700',
|
color: 'base.700',
|
||||||
_hover: {
|
_hover: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user