mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-01 03:01:13 -04:00
refactor(ui): canvas flow (wip)
This commit is contained in:
@@ -2018,6 +2018,7 @@
|
|||||||
"replaceCurrent": "Replace Current",
|
"replaceCurrent": "Replace Current",
|
||||||
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
||||||
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, or <PullBboxButton>pull the bounding box into this layer</PullBboxButton> to get started.",
|
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, or <PullBboxButton>pull the bounding box into this layer</PullBboxButton> to get started.",
|
||||||
|
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
|
||||||
"imageNoise": "Image Noise",
|
"imageNoise": "Image Noise",
|
||||||
"denoiseLimit": "Denoise Limit",
|
"denoiseLimit": "Denoise Limit",
|
||||||
"warnings": {
|
"warnings": {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
|
|||||||
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
||||||
import {
|
import {
|
||||||
canvasStagingAreaPersistConfig,
|
|
||||||
canvasSessionSlice,
|
canvasSessionSlice,
|
||||||
|
canvasStagingAreaPersistConfig,
|
||||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
||||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { Button, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||||
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import type { FileRejection } from 'react-dropzone';
|
import type { FileRejection } from 'react-dropzone';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -163,32 +163,63 @@ const sx = {
|
|||||||
},
|
},
|
||||||
} satisfies SystemStyleObject;
|
} satisfies SystemStyleObject;
|
||||||
|
|
||||||
export const UploadImageButton = ({
|
export const UploadImageIconButton = memo(
|
||||||
isDisabled = false,
|
({
|
||||||
onUpload,
|
isDisabled = false,
|
||||||
isError = false,
|
onUpload,
|
||||||
...rest
|
isError = false,
|
||||||
}: {
|
...rest
|
||||||
|
}: {
|
||||||
|
onUpload?: (imageDTO: ImageDTO) => void;
|
||||||
|
isError?: boolean;
|
||||||
|
} & SetOptional<IconButtonProps, 'aria-label'>) => {
|
||||||
|
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Upload image"
|
||||||
|
variant="outline"
|
||||||
|
sx={sx}
|
||||||
|
data-error={isError}
|
||||||
|
icon={<PiUploadBold />}
|
||||||
|
isLoading={uploadApi.request.isLoading}
|
||||||
|
{...rest}
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
/>
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
UploadImageIconButton.displayName = 'UploadImageIconButton';
|
||||||
|
|
||||||
|
type UploadImageButtonProps = {
|
||||||
onUpload?: (imageDTO: ImageDTO) => void;
|
onUpload?: (imageDTO: ImageDTO) => void;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
} & SetOptional<IconButtonProps, 'aria-label'>) => {
|
} & ButtonProps;
|
||||||
|
|
||||||
|
export const UploadImageButton = memo((props: UploadImageButtonProps) => {
|
||||||
|
const { children, isDisabled = false, onUpload, isError = false, ...rest } = props;
|
||||||
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
|
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<Button
|
||||||
aria-label="Upload image"
|
aria-label="Upload image"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
sx={sx}
|
sx={sx}
|
||||||
data-error={isError}
|
data-error={isError}
|
||||||
icon={<PiUploadBold />}
|
rightIcon={<PiUploadBold />}
|
||||||
isLoading={uploadApi.request.isLoading}
|
isLoading={uploadApi.request.isLoading}
|
||||||
{...rest}
|
{...rest}
|
||||||
{...uploadApi.getUploadButtonProps()}
|
{...uploadApi.getUploadButtonProps()}
|
||||||
/>
|
>
|
||||||
|
{children ?? 'Upload'}
|
||||||
|
</Button>
|
||||||
<input {...uploadApi.getUploadInputProps()} />
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
UploadImageButton.displayName = 'UploadImageButton';
|
||||||
|
|
||||||
export const UploadMultipleImageButton = ({
|
export const UploadMultipleImageButton = ({
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { selectIsLocal } from 'features/system/store/configSlice';
|
|||||||
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
|
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { $invocationProgressMessage } from 'services/events/stores';
|
import { $lastProgressMessage } from 'services/events/stores';
|
||||||
|
|
||||||
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
|
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const invocationProgressMessage = useStore($invocationProgressMessage);
|
const invocationProgressMessage = useStore($lastProgressMessage);
|
||||||
|
|
||||||
if (!invocationProgressMessage) {
|
if (!invocationProgressMessage) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
Button,
|
||||||
|
ContextMenu,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Image,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
Text,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||||
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||||
@@ -32,13 +45,17 @@ import {
|
|||||||
stagingAreaNextStagedImageSelected,
|
stagingAreaNextStagedImageSelected,
|
||||||
stagingAreaPrevStagedImageSelected,
|
stagingAreaPrevStagedImageSelected,
|
||||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
|
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||||
import type { ProgressImage } from 'features/nodes/types/common';
|
import type { ProgressImage } from 'features/nodes/types/common';
|
||||||
import { memo, useCallback, useEffect } from 'react';
|
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
|
||||||
import type { ImageDTO, S } from 'services/api/types';
|
import type { ImageDTO, S } from 'services/api/types';
|
||||||
import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
|
import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
|
||||||
import type { Equals } from 'tsafe';
|
import type { Equals, Param0 } from 'tsafe';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
|
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||||
@@ -80,45 +97,190 @@ export const CanvasMainPanelContent = memo(() => {
|
|||||||
|
|
||||||
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
||||||
|
|
||||||
|
const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||||
|
type: 'raster_layer',
|
||||||
|
withResize: true,
|
||||||
|
});
|
||||||
|
const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||||
|
type: 'raster_layer',
|
||||||
|
withInpaintMask: true,
|
||||||
|
});
|
||||||
|
const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||||
|
type: 'control_layer',
|
||||||
|
withResize: true,
|
||||||
|
});
|
||||||
|
|
||||||
const NoActiveSession = memo(() => {
|
const NoActiveSession = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const newSesh = useCallback(() => {
|
const newSesh = useCallback(() => {
|
||||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
<Text fontSize="lg" fontWeight="bold">
|
<Heading>Get Started with Invoke</Heading>
|
||||||
No Active Session
|
<Button variant="ghost" onClick={newSesh}>
|
||||||
</Text>
|
Start a new Canvas Session
|
||||||
<Button display="flex" flexDir="column" gap={2} p={8} minH={0} minW={0} onClick={newSesh}>
|
|
||||||
<Text>New Canvas Session</Text>
|
|
||||||
<Text>- New Canvas Session</Text>
|
|
||||||
<Text>- 1 Inpaint mask layer added</Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
<Text>or</Text>
|
||||||
<Text>Generate with Starting Image</Text>
|
<Flex flexDir="column" maxW={512}>
|
||||||
<Text>- New Canvas Session</Text>
|
<GenerateWithStartingImage />
|
||||||
<Text>- Dropped image as raster layer</Text>
|
<GenerateWithControlImage />
|
||||||
<Text>- Bbox resized</Text>
|
<GenerateWithStartingImageAndInpaintMask />
|
||||||
</Flex>
|
|
||||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
|
||||||
<Text>Generate with Control Image</Text>
|
|
||||||
<Text>- New Canvas Session</Text>
|
|
||||||
<Text>- Dropped image as control layer</Text>
|
|
||||||
<Text>- Bbox resized</Text>
|
|
||||||
</Flex>
|
|
||||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
|
||||||
<Text>Edit Image</Text>
|
|
||||||
<Text>- New Canvas Session</Text>
|
|
||||||
<Text>- Dropped image as raster layer</Text>
|
|
||||||
<Text>- Bbox resized</Text>
|
|
||||||
<Text>- 1 Inpaint mask layer added</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
NoActiveSession.displayName = 'NoActiveSession';
|
NoActiveSession.displayName = 'NoActiveSession';
|
||||||
|
|
||||||
|
const GenerateWithStartingImage = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||||
|
() => ({
|
||||||
|
onUpload: (imageDTO: ImageDTO) => {
|
||||||
|
newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
|
||||||
|
},
|
||||||
|
allowMultiple: false,
|
||||||
|
}),
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||||
|
const components = useMemo(
|
||||||
|
() => ({
|
||||||
|
UploadButton: (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
color="base.300"
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
rightIcon={<PiUploadBold />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[uploadApi]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex position="relative" flexDir="column">
|
||||||
|
<Text fontSize="lg" fontWeight="semibold">
|
||||||
|
Generate with a Starting Image
|
||||||
|
</Text>
|
||||||
|
<Text color="base.300">Regenerate the starting image using the model (Image to Image).</Text>
|
||||||
|
<Text color="base.300">
|
||||||
|
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Text>
|
||||||
|
<DndDropTarget
|
||||||
|
dndTarget={newCanvasFromImageDndTarget}
|
||||||
|
dndTargetData={generateWithStartingImageDndTargetData}
|
||||||
|
label="Drop"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
|
||||||
|
|
||||||
|
const GenerateWithControlImage = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||||
|
() => ({
|
||||||
|
onUpload: (imageDTO: ImageDTO) => {
|
||||||
|
newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
|
||||||
|
},
|
||||||
|
allowMultiple: false,
|
||||||
|
}),
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||||
|
const components = useMemo(
|
||||||
|
() => ({
|
||||||
|
UploadButton: (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
color="base.300"
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
rightIcon={<PiUploadBold />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[uploadApi]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex position="relative" flexDir="column">
|
||||||
|
<Text fontSize="lg" fontWeight="semibold">
|
||||||
|
Generate with a Control Image
|
||||||
|
</Text>
|
||||||
|
<Text color="base.300">
|
||||||
|
Generate a new image using the control image to guide the structure and composition (Text to Image with
|
||||||
|
Control).
|
||||||
|
</Text>
|
||||||
|
<Text color="base.300">
|
||||||
|
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Text>
|
||||||
|
<DndDropTarget
|
||||||
|
dndTarget={newCanvasFromImageDndTarget}
|
||||||
|
dndTargetData={generateWithControlImageDndTargetData}
|
||||||
|
label="Drop"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
GenerateWithControlImage.displayName = 'GenerateWithControlImage';
|
||||||
|
|
||||||
|
const GenerateWithStartingImageAndInpaintMask = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||||
|
() => ({
|
||||||
|
onUpload: (imageDTO: ImageDTO) => {
|
||||||
|
newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
|
||||||
|
},
|
||||||
|
allowMultiple: false,
|
||||||
|
}),
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||||
|
const components = useMemo(
|
||||||
|
() => ({
|
||||||
|
UploadButton: (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
color="base.300"
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
rightIcon={<PiUploadBold />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[uploadApi]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex position="relative" flexDir="column">
|
||||||
|
<Text fontSize="lg" fontWeight="semibold">
|
||||||
|
Edit Image
|
||||||
|
</Text>
|
||||||
|
<Text color="base.300">Edit the image by regenerating parts of it (Inpaint).</Text>
|
||||||
|
<Text color="base.300">
|
||||||
|
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Text>
|
||||||
|
<DndDropTarget
|
||||||
|
dndTarget={newCanvasFromImageDndTarget}
|
||||||
|
dndTargetData={generateWithStartingImageAndInpaintMaskDndTargetData}
|
||||||
|
label="Drop"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
|
||||||
|
|
||||||
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
||||||
|
|
||||||
const SimpleActiveSession = memo(() => {
|
const SimpleActiveSession = memo(() => {
|
||||||
@@ -213,6 +375,9 @@ const SelectedImage = memo(() => {
|
|||||||
src={selectedImage.imageDTO.image_url}
|
src={selectedImage.imageDTO.image_url}
|
||||||
width={selectedImage.imageDTO.width}
|
width={selectedImage.imageDTO.width}
|
||||||
height={selectedImage.imageDTO.height}
|
height={selectedImage.imageDTO.height}
|
||||||
|
onLoad={() => {
|
||||||
|
console.log('onload');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||||
|
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||||
|
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||||
|
import { Box, Tab } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||||
|
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||||
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
||||||
|
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||||
|
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||||
|
import type { DndTargetState } from 'features/dnd/types';
|
||||||
|
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||||
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
|
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||||
|
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
|
export const CanvasRightPanelStacked = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||||
|
const imageViewer = useImageViewer();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const tabIndex = useMemo(() => {
|
||||||
|
if (activeTab === 'gallery') {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
const onClickViewerToggleButton = useCallback(() => {
|
||||||
|
imageViewer.open();
|
||||||
|
}, [imageViewer]);
|
||||||
|
|
||||||
|
const onChangeTab = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (index === 0) {
|
||||||
|
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||||
|
} else {
|
||||||
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelGroup direction="vertical">
|
||||||
|
<Panel>
|
||||||
|
<GalleryPanelContent />
|
||||||
|
</Panel>
|
||||||
|
<PanelResizeHandle />
|
||||||
|
<Panel>
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<CanvasLayersPanelContent />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
</Panel>
|
||||||
|
</PanelGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasRightPanelStacked.displayName = 'CanvasRightPanelStacked';
|
||||||
|
|
||||||
|
const PanelTabs = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const store = useAppStore();
|
||||||
|
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||||
|
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||||
|
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||||
|
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||||
|
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const layersTabLabel = useMemo(() => {
|
||||||
|
if (activeEntityCount === 0) {
|
||||||
|
return t('controlLayers.layer_other');
|
||||||
|
}
|
||||||
|
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
||||||
|
}, [activeEntityCount, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!layersTabRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
||||||
|
|
||||||
|
const onDragEnter = () => {
|
||||||
|
// If we are already on the layers tab, do nothing
|
||||||
|
if (getIsOnLayersTab()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else set the state to active and switch to the layers tab after a timeout
|
||||||
|
setLayersTabDndState('over');
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
timeoutRef.current = null;
|
||||||
|
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||||
|
// When we switch tabs, the other tab should be pending
|
||||||
|
setLayersTabDndState('idle');
|
||||||
|
setGalleryTabDndState('potential');
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
const onDragLeave = () => {
|
||||||
|
// Set the state to idle or pending depending on the current tab
|
||||||
|
if (getIsOnLayersTab()) {
|
||||||
|
setLayersTabDndState('idle');
|
||||||
|
} else {
|
||||||
|
setLayersTabDndState('potential');
|
||||||
|
}
|
||||||
|
// Abort the tab switch if it hasn't happened yet
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onDragStart = () => {
|
||||||
|
// Set the state to pending when a drag starts
|
||||||
|
setLayersTabDndState('potential');
|
||||||
|
};
|
||||||
|
return combine(
|
||||||
|
dropTargetForElements({
|
||||||
|
element: layersTabRef.current,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
}),
|
||||||
|
monitorForElements({
|
||||||
|
canMonitor: ({ source }) => {
|
||||||
|
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Only monitor if we are not already on the gallery tab
|
||||||
|
return !getIsOnLayersTab();
|
||||||
|
},
|
||||||
|
onDragStart,
|
||||||
|
}),
|
||||||
|
dropTargetForExternal({
|
||||||
|
element: layersTabRef.current,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
}),
|
||||||
|
monitorForExternal({
|
||||||
|
canMonitor: () => !getIsOnLayersTab(),
|
||||||
|
onDragStart,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!galleryTabRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
||||||
|
|
||||||
|
const onDragEnter = () => {
|
||||||
|
// If we are already on the gallery tab, do nothing
|
||||||
|
if (getIsOnGalleryTab()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else set the state to active and switch to the gallery tab after a timeout
|
||||||
|
setGalleryTabDndState('over');
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
timeoutRef.current = null;
|
||||||
|
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
|
// When we switch tabs, the other tab should be pending
|
||||||
|
setGalleryTabDndState('idle');
|
||||||
|
setLayersTabDndState('potential');
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragLeave = () => {
|
||||||
|
// Set the state to idle or pending depending on the current tab
|
||||||
|
if (getIsOnGalleryTab()) {
|
||||||
|
setGalleryTabDndState('idle');
|
||||||
|
} else {
|
||||||
|
setGalleryTabDndState('potential');
|
||||||
|
}
|
||||||
|
// Abort the tab switch if it hasn't happened yet
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
// Set the state to pending when a drag starts
|
||||||
|
setGalleryTabDndState('potential');
|
||||||
|
};
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
dropTargetForElements({
|
||||||
|
element: galleryTabRef.current,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
}),
|
||||||
|
monitorForElements({
|
||||||
|
canMonitor: ({ source }) => {
|
||||||
|
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Only monitor if we are not already on the gallery tab
|
||||||
|
return !getIsOnGalleryTab();
|
||||||
|
},
|
||||||
|
onDragStart,
|
||||||
|
}),
|
||||||
|
dropTargetForExternal({
|
||||||
|
element: galleryTabRef.current,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
}),
|
||||||
|
monitorForExternal({
|
||||||
|
canMonitor: () => !getIsOnGalleryTab(),
|
||||||
|
onDragStart,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onDrop = () => {
|
||||||
|
// Reset the dnd state when a drop happens
|
||||||
|
setGalleryTabDndState('idle');
|
||||||
|
setLayersTabDndState('idle');
|
||||||
|
};
|
||||||
|
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tab ref={layersTabRef} position="relative" w={32}>
|
||||||
|
<Box as="span" w="full">
|
||||||
|
{layersTabLabel}
|
||||||
|
</Box>
|
||||||
|
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
||||||
|
</Tab>
|
||||||
|
<Tab ref={galleryTabRef} position="relative" w={32}>
|
||||||
|
<Box as="span" w="full">
|
||||||
|
{t('gallery.gallery')}
|
||||||
|
</Box>
|
||||||
|
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
||||||
|
</Tab>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PanelTabs.displayName = 'PanelTabs';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
@@ -51,7 +51,7 @@ export const IPAdapterImagePreview = memo(
|
|||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||||
{!imageDTO && (
|
{!imageDTO && (
|
||||||
<UploadImageButton
|
<UploadImageIconButton
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
h="full"
|
||||||
isError={!imageDTO && !image?.image_name}
|
isError={!imageDTO && !image?.image_name}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
|
|||||||
const newSessionDialog = useNewGallerySessionDialog();
|
const newSessionDialog = useNewGallerySessionDialog();
|
||||||
|
|
||||||
const newGallerySessionImmediate = useCallback(() => {
|
const newGallerySessionImmediate = useCallback(() => {
|
||||||
dispatch(canvasSessionStarted({ sessionType: 'simple' }));
|
dispatch(canvasSessionStarted({ sessionType: null }));
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { $invocationProgressMessage } from 'services/events/stores';
|
import { $lastProgressMessage } from 'services/events/stores';
|
||||||
|
|
||||||
export const useDeferredModelLoadingInvocationProgressMessage = () => {
|
export const useDeferredModelLoadingInvocationProgressMessage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const invocationProgressMessage = useStore($invocationProgressMessage);
|
const invocationProgressMessage = useStore($lastProgressMessage);
|
||||||
const [delayedMessage, setDelayedMessage] = useState<string | null>(null);
|
const [delayedMessage, setDelayedMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||||
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
|
import type { ProgressImage} from 'features/nodes/types/common';
|
||||||
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
|
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import {
|
import {
|
||||||
@@ -437,10 +438,12 @@ export type LoRA = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type StagingAreaImage = {
|
export type StagingAreaImage = {
|
||||||
|
sessionId: string;
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
};
|
};
|
||||||
|
export type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
||||||
|
|
||||||
export const zAspectRatioID = z.enum(['Free', '21:9', '9:21', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16']);
|
export const zAspectRatioID = z.enum(['Free', '21:9', '9:21', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16']);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { BoardId } from 'features/gallery/store/types';
|
|||||||
import {
|
import {
|
||||||
addImagesToBoard,
|
addImagesToBoard,
|
||||||
createNewCanvasEntityFromImage,
|
createNewCanvasEntityFromImage,
|
||||||
|
newCanvasFromImage,
|
||||||
removeImagesFromBoard,
|
removeImagesFromBoard,
|
||||||
replaceCanvasEntityObjectsWithImage,
|
replaceCanvasEntityObjectsWithImage,
|
||||||
setComparisonImage,
|
setComparisonImage,
|
||||||
@@ -343,7 +344,35 @@ export const newCanvasEntityFromImageDndTarget: DndTarget<
|
|||||||
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
|
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region New Canvas from Image
|
||||||
|
const _newCanvas = buildTypeAndKey('new-canvas-entity-from-image');
|
||||||
|
type NewCanvasFromImageDndTargetData = DndData<
|
||||||
|
typeof _newCanvas.type,
|
||||||
|
typeof _newCanvas.key,
|
||||||
|
{
|
||||||
|
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||||
|
withResize?: boolean;
|
||||||
|
withInpaintMask?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export const newCanvasFromImageDndTarget: DndTarget<NewCanvasFromImageDndTargetData, SingleImageDndSourceData> = {
|
||||||
|
..._newCanvas,
|
||||||
|
typeGuard: buildTypeGuard(_newCanvas.key),
|
||||||
|
getData: buildGetData(_newCanvas.key, _newCanvas.type),
|
||||||
|
isValid: ({ sourceData }) => {
|
||||||
|
if (!singleImageDndSource.typeGuard(sourceData)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
handler: ({ sourceData, targetData, dispatch, getState }) => {
|
||||||
|
const { type, withResize } = targetData.payload;
|
||||||
|
const { imageDTO } = sourceData.payload;
|
||||||
|
newCanvasFromImage({ type, imageDTO, dispatch, getState, withResize });
|
||||||
|
},
|
||||||
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Replace Canvas Entity Objects With Image
|
//#region Replace Canvas Entity Objects With Image
|
||||||
@@ -471,6 +500,7 @@ export const dndTargets = [
|
|||||||
replaceCanvasEntityObjectsWithImageDndTarget,
|
replaceCanvasEntityObjectsWithImageDndTarget,
|
||||||
addImageToBoardDndTarget,
|
addImageToBoardDndTarget,
|
||||||
removeImageFromBoardDndTarget,
|
removeImageFromBoardDndTarget,
|
||||||
|
newCanvasFromImageDndTarget,
|
||||||
// Single or Multiple Image
|
// Single or Multiple Image
|
||||||
addImageToBoardDndTarget,
|
addImageToBoardDndTarget,
|
||||||
removeImageFromBoardDndTarget,
|
removeImageFromBoardDndTarget,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||||
|
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||||
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||||
@@ -43,6 +46,7 @@ const GalleryPanelContent = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||||
|
|
||||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -56,6 +60,30 @@ const GalleryPanelContent = () => {
|
|||||||
);
|
);
|
||||||
const boardsListPanel = usePanel(boardsListPanelOptions);
|
const boardsListPanel = usePanel(boardsListPanelOptions);
|
||||||
|
|
||||||
|
const galleryPanelOptions = useMemo<UsePanelOptions>(
|
||||||
|
() => ({
|
||||||
|
id: 'gallery-panel',
|
||||||
|
minSizePx: 128,
|
||||||
|
defaultSizePx: 256,
|
||||||
|
imperativePanelGroupRef,
|
||||||
|
panelGroupDirection: 'vertical',
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const galleryPanel = usePanel(galleryPanelOptions);
|
||||||
|
|
||||||
|
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
|
||||||
|
() => ({
|
||||||
|
id: 'canvas-layers-panel',
|
||||||
|
minSizePx: 128,
|
||||||
|
defaultSizePx: 256,
|
||||||
|
imperativePanelGroupRef,
|
||||||
|
panelGroupDirection: 'vertical',
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
|
||||||
|
|
||||||
const handleClickBoardSearch = useCallback(() => {
|
const handleClickBoardSearch = useCallback(() => {
|
||||||
if (boardSearchText.length) {
|
if (boardSearchText.length) {
|
||||||
dispatch(boardSearchTextChanged(''));
|
dispatch(boardSearchTextChanged(''));
|
||||||
@@ -98,7 +126,7 @@ const GalleryPanelContent = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
|
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
|
||||||
<Panel collapsible {...boardsListPanel.panelProps}>
|
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
|
||||||
<Flex flexDir="column" w="full" h="full">
|
<Flex flexDir="column" w="full" h="full">
|
||||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||||
<Box w="full" pt={2}>
|
<Box w="full" pt={2}>
|
||||||
@@ -109,10 +137,20 @@ const GalleryPanelContent = () => {
|
|||||||
<BoardsListWrapper />
|
<BoardsListWrapper />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
<HorizontalResizeHandle id="gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
||||||
<Panel id="gallery-wrapper-panel" minSize={20}>
|
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
|
||||||
<Gallery />
|
<Gallery />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
{sessionType === 'advanced' && (
|
||||||
|
<>
|
||||||
|
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
|
||||||
|
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<CanvasLayersPanelContent />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
</FocusRegionWrapper>
|
</FocusRegionWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { AnimationProps } from 'framer-motion';
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { memo, useCallback, useRef, useState } from 'react';
|
import { memo, useCallback, useRef, useState } from 'react';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { $hasProgressImage } from 'services/events/stores';
|
import { $hasLastProgressImage } from 'services/events/stores';
|
||||||
|
|
||||||
import { NoContentForViewer } from './NoContentForViewer';
|
import { NoContentForViewer } from './NoContentForViewer';
|
||||||
import ProgressImage from './ProgressImage';
|
import ProgressImage from './ProgressImage';
|
||||||
@@ -86,7 +86,7 @@ const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
|||||||
export default memo(CurrentImagePreview);
|
export default memo(CurrentImagePreview);
|
||||||
|
|
||||||
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
||||||
const hasProgressImage = useStore($hasProgressImage);
|
const hasProgressImage = useStore($hasLastProgressImage);
|
||||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||||
|
|
||||||
if (hasProgressImage && shouldShowProgressInViewer) {
|
if (hasProgressImage && shouldShowProgressInViewer) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { $progressImage } from 'services/events/stores';
|
import { $lastProgressImage } from 'services/events/stores';
|
||||||
|
|
||||||
const selectShouldAntialiasProgressImage = createSelector(
|
const selectShouldAntialiasProgressImage = createSelector(
|
||||||
selectSystemSlice,
|
selectSystemSlice,
|
||||||
@@ -13,7 +13,7 @@ const selectShouldAntialiasProgressImage = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const CurrentImagePreview = () => {
|
const CurrentImagePreview = () => {
|
||||||
const progressImage = useStore($progressImage);
|
const progressImage = useStore($lastProgressImage);
|
||||||
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
|
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
|
||||||
|
|
||||||
const sx = useMemo<SystemStyleObject>(
|
const sx = useMemo<SystemStyleObject>(
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
|
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
|
||||||
import {
|
import {
|
||||||
bboxChangedFromCanvas,
|
bboxChangedFromCanvas,
|
||||||
|
canvasClearHistory,
|
||||||
controlLayerAdded,
|
controlLayerAdded,
|
||||||
entityRasterized,
|
entityRasterized,
|
||||||
inpaintMaskAdded,
|
inpaintMaskAdded,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
rgAdded,
|
rgAdded,
|
||||||
rgIPAdapterImageChanged,
|
rgIPAdapterImageChanged,
|
||||||
} from 'features/controlLayers/store/canvasSlice';
|
} from 'features/controlLayers/store/canvasSlice';
|
||||||
|
import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
|
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||||
import type {
|
import type {
|
||||||
CanvasControlLayerState,
|
CanvasControlLayerState,
|
||||||
@@ -147,10 +148,11 @@ export const newCanvasFromImage = async (arg: {
|
|||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||||
withResize?: boolean;
|
withResize?: boolean;
|
||||||
|
withInpaintMask?: boolean;
|
||||||
dispatch: AppDispatch;
|
dispatch: AppDispatch;
|
||||||
getState: () => RootState;
|
getState: () => RootState;
|
||||||
}) => {
|
}) => {
|
||||||
const { type, imageDTO, withResize = false, dispatch, getState } = arg;
|
const { type, imageDTO, withResize = false, withInpaintMask = false, dispatch, getState } = arg;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const base = selectBboxModelBase(state);
|
const base = selectBboxModelBase(state);
|
||||||
@@ -192,10 +194,14 @@ export const newCanvasFromImage = async (arg: {
|
|||||||
objects: [imageObject],
|
objects: [imageObject],
|
||||||
} satisfies Partial<CanvasRasterLayerState>;
|
} satisfies Partial<CanvasRasterLayerState>;
|
||||||
addFitOnLayerInitCallback(overrides.id);
|
addFitOnLayerInitCallback(overrides.id);
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'control_layer': {
|
case 'control_layer': {
|
||||||
@@ -205,10 +211,14 @@ export const newCanvasFromImage = async (arg: {
|
|||||||
controlAdapter: deepClone(initialControlNet),
|
controlAdapter: deepClone(initialControlNet),
|
||||||
} satisfies Partial<CanvasControlLayerState>;
|
} satisfies Partial<CanvasControlLayerState>;
|
||||||
addFitOnLayerInitCallback(overrides.id);
|
addFitOnLayerInitCallback(overrides.id);
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'inpaint_mask': {
|
case 'inpaint_mask': {
|
||||||
@@ -217,10 +227,14 @@ export const newCanvasFromImage = async (arg: {
|
|||||||
objects: [imageObject],
|
objects: [imageObject],
|
||||||
} satisfies Partial<CanvasInpaintMaskState>;
|
} satisfies Partial<CanvasInpaintMaskState>;
|
||||||
addFitOnLayerInitCallback(overrides.id);
|
addFitOnLayerInitCallback(overrides.id);
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'regional_guidance': {
|
case 'regional_guidance': {
|
||||||
@@ -229,25 +243,37 @@ export const newCanvasFromImage = async (arg: {
|
|||||||
objects: [imageObject],
|
objects: [imageObject],
|
||||||
} satisfies Partial<CanvasRegionalGuidanceState>;
|
} satisfies Partial<CanvasRegionalGuidanceState>;
|
||||||
addFitOnLayerInitCallback(overrides.id);
|
addFitOnLayerInitCallback(overrides.id);
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'reference_image': {
|
case 'reference_image': {
|
||||||
const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
|
const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
|
||||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
|
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'regional_guidance_with_reference_image': {
|
case 'regional_guidance_with_reference_image': {
|
||||||
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
|
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
|
||||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||||
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
|
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
|
||||||
dispatch(canvasReset());
|
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||||
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
|
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
|
||||||
|
if (withInpaintMask) {
|
||||||
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
|
}
|
||||||
|
dispatch(canvasClearHistory());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Flex, Text } from '@invoke-ai/ui-library';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||||
import type { SetNodeImageFieldImageDndTargetData } from 'features/dnd/dnd';
|
import type { SetNodeImageFieldImageDndTargetData } from 'features/dnd/dnd';
|
||||||
import { setNodeImageFieldImageDndTarget } from 'features/dnd/dnd';
|
import { setNodeImageFieldImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
@@ -66,7 +66,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
|||||||
return (
|
return (
|
||||||
<Flex position="relative" className={NO_DRAG_CLASS} w="full" h={32} alignItems="stretch">
|
<Flex position="relative" className={NO_DRAG_CLASS} w="full" h={32} alignItems="stretch">
|
||||||
{!imageDTO && (
|
{!imageDTO && (
|
||||||
<UploadImageButton
|
<UploadImageIconButton
|
||||||
w="full"
|
w="full"
|
||||||
h="auto"
|
h="auto"
|
||||||
isError={fieldTemplate.required && !field.value}
|
isError={fieldTemplate.required && !field.value}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||||
import type { SetUpscaleInitialImageDndTargetData } from 'features/dnd/dnd';
|
import type { SetUpscaleInitialImageDndTargetData } from 'features/dnd/dnd';
|
||||||
import { setUpscaleInitialImageDndTarget } from 'features/dnd/dnd';
|
import { setUpscaleInitialImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
@@ -34,7 +34,7 @@ export const UpscaleInitialImage = () => {
|
|||||||
return (
|
return (
|
||||||
<Flex justifyContent="flex-start">
|
<Flex justifyContent="flex-start">
|
||||||
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
||||||
{!imageDTO && <UploadImageButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
{!imageDTO && <UploadImageIconButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||||
{imageDTO && (
|
{imageDTO && (
|
||||||
<>
|
<>
|
||||||
<DndImage imageDTO={imageDTO} />
|
<DndImage imageDTO={imageDTO} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||||
import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightPanel';
|
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
@@ -161,11 +161,9 @@ AppContent.displayName = 'AppContent';
|
|||||||
|
|
||||||
const RightPanelContent = memo(() => {
|
const RightPanelContent = memo(() => {
|
||||||
const tab = useAppSelector(selectActiveTab);
|
const tab = useAppSelector(selectActiveTab);
|
||||||
|
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||||
|
|
||||||
if (tab === 'canvas') {
|
if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
|
||||||
return <CanvasRightPanel />;
|
|
||||||
}
|
|
||||||
if (tab === 'upscaling' || tab === 'workflows') {
|
|
||||||
return <GalleryPanelContent />;
|
return <GalleryPanelContent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||||
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
|
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
|
||||||
@@ -30,10 +31,11 @@ const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tab = useAppSelector(selectActiveTab);
|
const tab = useAppSelector(selectActiveTab);
|
||||||
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
|
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
|
||||||
|
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
|
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
|
||||||
{tab === 'canvas' && (
|
{tab === 'canvas' && sessionType === 'advanced' && (
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<ToolChooser />
|
<ToolChooser />
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
|
|||||||
@@ -30,7 +30,15 @@ import type { ClientToServerEvents, ServerToClientEvents } from 'services/events
|
|||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
import type { JsonObject } from 'type-fest';
|
import type { JsonObject } from 'type-fest';
|
||||||
|
|
||||||
import { $lastCanvasProgressEvent, $lastProgressEvent } from './stores';
|
import {
|
||||||
|
$lastCanvasProgressEvent,
|
||||||
|
$lastCanvasProgressImage,
|
||||||
|
$lastProgressEvent,
|
||||||
|
$lastUpscalingProgressEvent,
|
||||||
|
$lastUpscalingProgressImage,
|
||||||
|
$lastWorkflowsProgressEvent,
|
||||||
|
$lastWorkflowsProgressImage,
|
||||||
|
} from './stores';
|
||||||
|
|
||||||
const log = logger('events');
|
const log = logger('events');
|
||||||
|
|
||||||
@@ -92,7 +100,7 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('invocation_progress', (data) => {
|
socket.on('invocation_progress', (data) => {
|
||||||
const { invocation_source_id, invocation, image, origin, percentage, message } = data;
|
const { invocation_source_id, invocation, session_id, image, origin, percentage, message } = data;
|
||||||
|
|
||||||
let _message = 'Invocation progress';
|
let _message = 'Invocation progress';
|
||||||
if (message) {
|
if (message) {
|
||||||
@@ -107,7 +115,27 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
|
|||||||
|
|
||||||
$lastProgressEvent.set(data);
|
$lastProgressEvent.set(data);
|
||||||
|
|
||||||
|
if (origin === 'canvas') {
|
||||||
|
$lastCanvasProgressEvent.set(data);
|
||||||
|
if (image) {
|
||||||
|
$lastCanvasProgressImage.set({ sessionId: session_id, image });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin === 'upscaling') {
|
||||||
|
$lastUpscalingProgressEvent.set(data);
|
||||||
|
if (image) {
|
||||||
|
$lastUpscalingProgressImage.set({ sessionId: session_id, image });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (origin === 'workflows') {
|
if (origin === 'workflows') {
|
||||||
|
$lastWorkflowsProgressEvent.set(data);
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
$lastWorkflowsProgressImage.set({ sessionId: session_id, image });
|
||||||
|
}
|
||||||
|
|
||||||
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
||||||
if (nes) {
|
if (nes) {
|
||||||
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProgressImage } from 'features/nodes/types/common';
|
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
|
||||||
import { round } from 'lodash-es';
|
import { round } from 'lodash-es';
|
||||||
import { atom, computed, map } from 'nanostores';
|
import { atom, computed, map } from 'nanostores';
|
||||||
import type { S } from 'services/api/types';
|
import type { S } from 'services/api/types';
|
||||||
@@ -9,33 +9,6 @@ export const $socket = atom<AppSocket | null>(null);
|
|||||||
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
|
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
|
||||||
export const $isConnected = atom<boolean>(false);
|
export const $isConnected = atom<boolean>(false);
|
||||||
export const $lastProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
export const $lastProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||||
$lastProgressEvent.subscribe((event) => {
|
|
||||||
if (!event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (event.destination) {
|
|
||||||
case 'workflows':
|
|
||||||
$lastWorkflowsProgressEvent.set(event);
|
|
||||||
if (event.image) {
|
|
||||||
$lastWorkflowsProgressImage.set({ sessionId: event.session_id, image: event.image });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'upscaling':
|
|
||||||
$lastUpscalingProgressEvent.set(event);
|
|
||||||
if (event.image) {
|
|
||||||
$lastUpscalingProgressImage.set({ sessionId: event.session_id, image: event.image });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'canvas':
|
|
||||||
$lastCanvasProgressEvent.set(event);
|
|
||||||
if (event.image) {
|
|
||||||
$lastCanvasProgressImage.set({ sessionId: event.session_id, image: event.image });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
|
||||||
|
|
||||||
export const $lastCanvasProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
export const $lastCanvasProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||||
export const $lastCanvasProgressImage = atom<EphemeralProgressImage | null>(null);
|
export const $lastCanvasProgressImage = atom<EphemeralProgressImage | null>(null);
|
||||||
@@ -44,9 +17,9 @@ export const $lastWorkflowsProgressImage = atom<EphemeralProgressImage | null>(n
|
|||||||
export const $lastUpscalingProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
export const $lastUpscalingProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||||
export const $lastUpscalingProgressImage = atom<EphemeralProgressImage | null>(null);
|
export const $lastUpscalingProgressImage = atom<EphemeralProgressImage | null>(null);
|
||||||
|
|
||||||
export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
|
export const $lastProgressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
|
||||||
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
|
export const $hasLastProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
|
||||||
export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {
|
export const $lastProgressMessage = computed($lastProgressEvent, (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user