mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-12 22:35:27 -05:00
refactor(ui): ref images (WIP)
This commit is contained in:
@@ -2017,7 +2017,8 @@
|
||||
"resetGenerationSettings": "Reset Generation Settings",
|
||||
"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.",
|
||||
"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.",
|
||||
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image or <PullBboxButton>pull the bounding box into this Reference Image</PullBboxButton> to get started.",
|
||||
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton> or drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image to get started.",
|
||||
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
|
||||
"imageNoise": "Image Noise",
|
||||
"denoiseLimit": "Denoise Limit",
|
||||
|
||||
@@ -73,7 +73,7 @@ export const useBoolean = (initialValue: boolean): UseBoolean => {
|
||||
};
|
||||
};
|
||||
|
||||
type UseDisclosure = {
|
||||
export type UseDisclosure = {
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
type FilterFunction = (el: HTMLElement | SVGElement) => boolean;
|
||||
|
||||
export function useCallbackRef<T extends (...args: any[]) => any>(
|
||||
callback: T | undefined,
|
||||
deps: React.DependencyList = []
|
||||
@@ -54,10 +56,17 @@ export interface UseOutsideClickProps {
|
||||
*
|
||||
* If omitted, a default filter function that ignores clicks in Chakra UI portals and react-select components is used.
|
||||
*/
|
||||
filter?: (el: HTMLElement) => boolean;
|
||||
filter?: FilterFunction;
|
||||
}
|
||||
|
||||
const DEFAULT_FILTER = (el: HTMLElement) => el.className.includes('chakra-portal') || el.id.includes('react-select');
|
||||
export const DEFAULT_FILTER: FilterFunction = (el) => {
|
||||
if (el instanceof SVGElement) {
|
||||
// SVGElement's type appears to be incorrect. Its className is not a string, which causes `includes` to fail.
|
||||
// Let's assume that SVG elements with a class name are not part of the portal and should not be filtered.
|
||||
return false;
|
||||
}
|
||||
return el.className.includes('chakra-portal') || el.id.includes('react-select');
|
||||
};
|
||||
|
||||
/**
|
||||
* Example, used in components like Dialogs and Popovers, so they can close
|
||||
@@ -119,11 +128,7 @@ export function useFilterableOutsideClick(props: UseOutsideClickProps) {
|
||||
}, [handler, ref, savedHandler, state, enabled, filter]);
|
||||
}
|
||||
|
||||
function isValidEvent(
|
||||
event: Event,
|
||||
ref: React.RefObject<HTMLElement | null>,
|
||||
filter?: (el: HTMLElement) => boolean
|
||||
): boolean {
|
||||
function isValidEvent(event: Event, ref: React.RefObject<HTMLElement | null>, filter?: FilterFunction): boolean {
|
||||
const target = (event.composedPath?.()[0] ?? event.target) as HTMLElement;
|
||||
|
||||
if (target) {
|
||||
@@ -137,6 +142,7 @@ function isValidEvent(
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is the main logic change from the original hook.
|
||||
if (filter) {
|
||||
// Check if the click is inside an element matching the filter.
|
||||
// This is used for portal-awareness or other general exclusion cases.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
|
||||
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
opacity: 0.3,
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
},
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.2s',
|
||||
};
|
||||
|
||||
export const RefImageList = memo((props: FlexProps) => {
|
||||
const ids = useAppSelector(selectRefImageEntityIds);
|
||||
|
||||
if (ids.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={2} {...props}>
|
||||
{ids.map((id) => (
|
||||
<RefImageIdContext.Provider key={id} value={id}>
|
||||
<RefImage />
|
||||
</RefImageIdContext.Provider>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RefImageList.displayName = 'RefImageList';
|
||||
@@ -1,62 +1,77 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import type {
|
||||
SystemStyleObject} from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Divider,
|
||||
Flex,
|
||||
IconButton,
|
||||
Image,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
Portal,
|
||||
Portal
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
|
||||
import type { UseDisclosure } from 'common/hooks/useBoolean';
|
||||
import { useDisclosure } from 'common/hooks/useBoolean';
|
||||
import { useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
|
||||
import { IPAdapterSettings } from 'features/controlLayers/components/RefImage/IPAdapterSettings';
|
||||
import { DEFAULT_FILTER, useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
|
||||
import { RefImageHeader } from 'features/controlLayers/components/RefImage/RefImageHeader';
|
||||
import { RefImageSettings } from 'features/controlLayers/components/RefImage/RefImageSettings';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
opacity: 0.5,
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
},
|
||||
"&[data-is-open='true']": {
|
||||
opacity: 1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.2s',
|
||||
};
|
||||
// There is some awkwardness here with closing the popover when clicking outside of it, related to Chakra's
|
||||
// handling of refs, portals, outside clicks, and a race condition with framer-motion animations that can leave
|
||||
// the popover closed when its internal state is still open.
|
||||
//
|
||||
// We have to manually manage the popover open state to work around the race condition, and then have to do special
|
||||
// handling to close the popover when clicking outside of it.
|
||||
|
||||
// We have to reach outside react to identify the popover trigger element instead of using refs, thanks to how Chakra
|
||||
// handles refs for PopoverAnchor internally. Maybe there is some way to merge them but I couldn't figure it out.
|
||||
const getRefImagePopoverTriggerId = (id: string) => `ref-image-popover-trigger-${id}`;
|
||||
|
||||
export const RefImage = memo(() => {
|
||||
const id = useRefImageIdContext();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const disclosure = useDisclosure(false);
|
||||
const selectEntity = useMemo(
|
||||
() => createSelector(selectRefImagesSlice, (refImages) => selectRefImageEntityOrThrow(refImages, id, 'RefImage')),
|
||||
// This filter prevents the popover from closing when clicking on a sibling portal element, like the dropdown menu
|
||||
// inside the ref image settings popover. It also prevents the popover from closing when clicking on the popover's
|
||||
// own trigger element.
|
||||
const filter = useCallback(
|
||||
(el: HTMLElement | SVGElement) => {
|
||||
return DEFAULT_FILTER(el) || el.id === getRefImagePopoverTriggerId(id);
|
||||
},
|
||||
[id]
|
||||
);
|
||||
const entity = useAppSelector(selectEntity);
|
||||
useFilterableOutsideClick({ ref, handler: disclosure.close });
|
||||
useFilterableOutsideClick({ ref, handler: disclosure.close, filter });
|
||||
|
||||
return (
|
||||
<Popover isLazy lazyBehavior="unmount" isOpen={disclosure.isOpen} closeOnBlur={false}>
|
||||
<PopoverAnchor>
|
||||
<Flex role="button" w={16} h={16} sx={sx} onClick={disclosure.open} data-is-open={disclosure.isOpen}>
|
||||
<Thumbnail image={entity.config.image} />
|
||||
</Flex>
|
||||
</PopoverAnchor>
|
||||
<Popover
|
||||
// The popover contains a react-select component, which uses a portal to render its options. This portal
|
||||
// is itself not lazy. As a result, if we do not unmount the popover when it is closed, the react-select
|
||||
// component still exists but is invisible, and intercepts clicks!
|
||||
isLazy
|
||||
lazyBehavior="unmount"
|
||||
isOpen={disclosure.isOpen}
|
||||
closeOnBlur={false}
|
||||
modifiers={POPPER_MODIFIERS}
|
||||
>
|
||||
<Thumbnail disclosure={disclosure} />
|
||||
<Portal>
|
||||
<PopoverContent ref={ref}>
|
||||
<PopoverContent ref={ref} w={400}>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<IPAdapterSettings />
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<RefImageHeader />
|
||||
<Divider />
|
||||
<RefImageSettings />
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
@@ -65,8 +80,58 @@ export const RefImage = memo(() => {
|
||||
});
|
||||
RefImage.displayName = 'RefImage';
|
||||
|
||||
const Thumbnail = memo(({ image }: { image: ImageWithDims | null }) => {
|
||||
const { data: imageDTO } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
return <Image src={imageDTO?.thumbnail_url} objectFit="contain" maxW="full" maxH="full" />;
|
||||
const imageSx: SystemStyleObject = {
|
||||
opacity: 0.5,
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
},
|
||||
"&[data-is-open='true']": {
|
||||
opacity: 1,
|
||||
},
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.2s',
|
||||
};
|
||||
|
||||
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
|
||||
const id = useRefImageIdContext();
|
||||
const entity = useRefImageEntity(id);
|
||||
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
|
||||
|
||||
if (!imageDTO || !entity.config.image) {
|
||||
return (
|
||||
<PopoverAnchor>
|
||||
<IconButton
|
||||
id={getRefImagePopoverTriggerId(id)}
|
||||
aria-label="Open Reference Image Settings"
|
||||
h="full"
|
||||
variant="ghost"
|
||||
aspectRatio="1/1"
|
||||
borderWidth="2px !important"
|
||||
borderStyle="dashed !important"
|
||||
borderColor="errorAlpha.500"
|
||||
borderRadius="base"
|
||||
icon={<PiImageBold />}
|
||||
colorScheme="error"
|
||||
onClick={disclosure.toggle}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PopoverAnchor>
|
||||
<Image
|
||||
id={getRefImagePopoverTriggerId(id)}
|
||||
role="button"
|
||||
src={imageDTO.thumbnail_url}
|
||||
objectFit="contain"
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
onClick={disclosure.toggle}
|
||||
// sx={imageSx}
|
||||
// data-is-open={disclosure.isOpen}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
);
|
||||
});
|
||||
Thumbnail.displayName = 'Thumbnail';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { refImageDeleted } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiTrashBold } from 'react-icons/pi';
|
||||
|
||||
export const RefImageHeader = memo(() => {
|
||||
const id = useRefImageIdContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const entity = useRefImageEntity(id);
|
||||
const deleteRefImage = useCallback(() => {
|
||||
dispatch(refImageDeleted({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="space-between" alignItems="center" w="full">
|
||||
{entity.config.image !== null && (
|
||||
<Text fontWeight="semibold" color="base.300">
|
||||
Reference Image
|
||||
</Text>
|
||||
)}
|
||||
{entity.config.image === null && (
|
||||
<Text fontWeight="semibold" color="base.300">
|
||||
Reference Image - No Image Selected
|
||||
</Text>
|
||||
)}
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiTrashBold />}
|
||||
onClick={deleteRefImage}
|
||||
aria-label="Delete reference image"
|
||||
colorScheme="error"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
RefImageHeader.displayName = 'RefImageHeader';
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { FlexProps } from '@invoke-ai/ui-library';
|
||||
import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
|
||||
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { useAddGlobalReferenceImage } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { memo } from 'react';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const RefImageList = memo((props: FlexProps) => {
|
||||
const ids = useAppSelector(selectRefImageEntityIds);
|
||||
const addRefImage = useAddGlobalReferenceImage();
|
||||
return (
|
||||
<Flex gap={2} h={16} {...props}>
|
||||
{ids.map((id) => (
|
||||
<RefImageIdContext.Provider key={id} value={id}>
|
||||
<RefImage />
|
||||
</RefImageIdContext.Provider>
|
||||
))}
|
||||
<Spacer />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
h="full"
|
||||
borderWidth="2px !important"
|
||||
borderStyle="dashed !important"
|
||||
borderRadius="base"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addRefImage}
|
||||
isDisabled={ids.length >= 5} // Limit to 5 reference images
|
||||
>
|
||||
Ref Image
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RefImageList.displayName = 'RefImageList';
|
||||
|
||||
const AddRefImageIconButton = memo(() => {
|
||||
const addRefImage = useAddGlobalReferenceImage();
|
||||
return (
|
||||
<IconButton
|
||||
aria-label="Add reference image"
|
||||
h="full"
|
||||
variant="ghost"
|
||||
aspectRatio="1/1"
|
||||
borderWidth={2}
|
||||
borderStyle="dashed"
|
||||
borderRadius="base"
|
||||
onClick={addRefImage}
|
||||
icon={<PiPlusBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
AddRefImageIconButton.displayName = 'AddRefImageIconButton';
|
||||
|
||||
const AddRefImageButton = memo((props) => {
|
||||
const addRefImage = useAddGlobalReferenceImage();
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
h="full"
|
||||
borderWidth={2}
|
||||
borderStyle="dashed"
|
||||
borderRadius="base"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addRefImage}
|
||||
>
|
||||
Ref Image
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
AddRefImageButton.displayName = 'AddRefImageButton';
|
||||
@@ -2,8 +2,6 @@ import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
@@ -17,7 +15,6 @@ export const RefImageNoImageState = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const id = useRefImageIdContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
setGlobalReferenceImage({ imageDTO, id, dispatch });
|
||||
@@ -28,7 +25,6 @@ export const RefImageNoImageState = memo(() => {
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ id }),
|
||||
@@ -37,17 +33,10 @@ export const RefImageNoImageState = memo(() => {
|
||||
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
|
||||
),
|
||||
GalleryButton: (
|
||||
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
PullBboxButton: (
|
||||
<Button onClick={pullBboxIntoIPAdapter} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
UploadButton: <Button size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />,
|
||||
GalleryButton: <Button onClick={onClickGalleryButton} size="sm" variant="link" color="base.300" />,
|
||||
}),
|
||||
[isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
|
||||
[onClickGalleryButton, uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -60,7 +49,6 @@ export const RefImageNoImageState = memo(() => {
|
||||
dndTarget={setGlobalReferenceImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label={t('controlLayers.useImage')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { setGlobalReferenceImage } from 'features/imageActions/actions';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const RefImageNoImageStateWithCanvasOptions = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const id = useRefImageIdContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
setGlobalReferenceImage({ imageDTO, id, dispatch });
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||
const onClickGalleryButton = useCallback(() => {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ id }),
|
||||
[id]
|
||||
);
|
||||
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
|
||||
),
|
||||
GalleryButton: (
|
||||
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
PullBboxButton: (
|
||||
<Button onClick={pullBboxIntoIPAdapter} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
|
||||
),
|
||||
}),
|
||||
[isBusy, onClickGalleryButton, pullBboxIntoIPAdapter, uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
|
||||
<Text textAlign="center" color="base.300">
|
||||
<Trans i18nKey="controlLayers.referenceImageEmptyStateWithCanvasOptions" components={components} />
|
||||
</Text>
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
<DndDropTarget
|
||||
dndTarget={setGlobalReferenceImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label={t('controlLayers.useImage')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RefImageNoImageStateWithCanvasOptions.displayName = 'RefImageNoImageStateWithCanvasOptions';
|
||||
@@ -4,10 +4,16 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
||||
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
|
||||
import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
|
||||
import { PullBboxIntoRefImageIconButton } from 'features/controlLayers/components/common/PullBboxIntoRefImageIconButton';
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
|
||||
import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
|
||||
import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
|
||||
import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions';
|
||||
import {
|
||||
CanvasManagerProviderGate,
|
||||
useCanvasManagerSafe,
|
||||
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
@@ -22,17 +28,16 @@ import {
|
||||
selectRefImageEntityOrThrow,
|
||||
selectRefImagesSlice,
|
||||
} from 'features/controlLayers/store/refImagesSlice';
|
||||
import {
|
||||
type CLIPVisionModelV2,
|
||||
type FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||
type IPMethodV2,
|
||||
isFLUXReduxConfig,
|
||||
isIPAdapterConfig,
|
||||
import type {
|
||||
CLIPVisionModelV2,
|
||||
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||
IPMethodV2,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
|
||||
import { RefImageImage } from './RefImageImage';
|
||||
@@ -43,12 +48,12 @@ const buildSelectConfig = (id: string) =>
|
||||
(refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').config
|
||||
);
|
||||
|
||||
const IPAdapterSettingsContent = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const RefImageSettingsContent = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const id = useRefImageIdContext();
|
||||
const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
|
||||
const config = useAppSelector(selectConfig);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
const onChangeBeginEndStepPct = useCallback(
|
||||
(beginEndStepPct: [number, number]) => {
|
||||
@@ -103,8 +108,6 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
() => setGlobalReferenceImageDndTarget.getData({ id }, config.image?.image_name),
|
||||
[id, config.image?.image_name]
|
||||
);
|
||||
// const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||
// const isBusy = useCanvasIsBusy();
|
||||
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
|
||||
@@ -115,14 +118,11 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
{isIPAdapterConfig(config) && (
|
||||
<IPAdapterCLIPVisionModel model={config.clipVisionModel} onChange={onChangeCLIPVisionModel} />
|
||||
)}
|
||||
{/* <IconButton
|
||||
onClick={pullBboxIntoIPAdapter}
|
||||
isDisabled={isBusy}
|
||||
variant="ghost"
|
||||
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
tooltip={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
/> */}
|
||||
{tab === 'canvas' && (
|
||||
<CanvasManagerProviderGate>
|
||||
<PullBboxIntoRefImageIconButton />
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex gap={2} w="full">
|
||||
{isIPAdapterConfig(config) && (
|
||||
@@ -153,7 +153,7 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
|
||||
RefImageSettingsContent.displayName = 'RefImageSettingsContent';
|
||||
|
||||
const buildSelectIPAdapterHasImage = (id: string) =>
|
||||
createSelector(selectRefImagesSlice, (refImages) => {
|
||||
@@ -161,17 +161,26 @@ const buildSelectIPAdapterHasImage = (id: string) =>
|
||||
return !!referenceImage && referenceImage.config.image !== null;
|
||||
});
|
||||
|
||||
export const IPAdapterSettings = memo(() => {
|
||||
export const RefImageSettings = memo(() => {
|
||||
const id = useRefImageIdContext();
|
||||
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(id), [id]);
|
||||
const hasImage = useAppSelector(selectIPAdapterHasImage);
|
||||
|
||||
if (!hasImage && canvasManager && tab === 'canvas') {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<RefImageNoImageStateWithCanvasOptions />
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasImage) {
|
||||
return <RefImageNoImageState />;
|
||||
}
|
||||
|
||||
return <IPAdapterSettingsContent />;
|
||||
return <RefImageSettingsContent />;
|
||||
});
|
||||
|
||||
IPAdapterSettings.displayName = 'IPAdapterSettings';
|
||||
RefImageSettings.displayName = 'RefImageSettings';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useRefImageEntity = (id: string) => {
|
||||
const selectEntity = useMemo(
|
||||
() =>
|
||||
createSelector(selectRefImagesSlice, (refImages) =>
|
||||
selectRefImageEntityOrThrow(refImages, id, `useRefImageState(${id})`)
|
||||
),
|
||||
[id]
|
||||
);
|
||||
const entity = useAppSelector(selectEntity);
|
||||
return entity;
|
||||
};
|
||||
@@ -83,7 +83,7 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={2} p={4}>
|
||||
<Text textAlign="center" color="base.300">
|
||||
<Trans i18nKey="controlLayers.referenceImageEmptyState" components={components} />
|
||||
<Trans i18nKey="controlLayers.referenceImageEmptyStateWithCanvasTab" components={components} />
|
||||
</Text>
|
||||
</Flex>
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||
|
||||
export const PullBboxIntoRefImageIconButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const id = useRefImageIdContext();
|
||||
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={pullBboxIntoIPAdapter}
|
||||
isDisabled={isBusy}
|
||||
variant="ghost"
|
||||
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
tooltip={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
PullBboxIntoRefImageIconButton.displayName = 'PullBboxIntoRefImageIconButton';
|
||||
@@ -192,7 +192,7 @@ export const useAddGlobalReferenceImage = () => {
|
||||
const func = useCallback(() => {
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
const overrides = { config };
|
||||
dispatch(refImageAdded({ isSelected: true, overrides }));
|
||||
dispatch(refImageAdded({ overrides }));
|
||||
}, [dispatch, getState]);
|
||||
|
||||
return func;
|
||||
|
||||
@@ -40,7 +40,7 @@ export const refImagesSlice = createSlice({
|
||||
|
||||
state.entities.push(entityState);
|
||||
},
|
||||
prepare: (payload?: { overrides?: PartialDeep<RefImageState>; isSelected?: boolean }) => ({
|
||||
prepare: (payload?: { overrides?: PartialDeep<RefImageState> }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('reference_image') },
|
||||
}),
|
||||
},
|
||||
@@ -200,6 +200,7 @@ export const refImagesSlice = createSlice({
|
||||
|
||||
export const {
|
||||
refImageAdded,
|
||||
refImageDeleted,
|
||||
refImageImageChanged,
|
||||
refImageIPAdapterMethodChanged,
|
||||
refImageModelChanged,
|
||||
|
||||
@@ -248,7 +248,7 @@ export const newCanvasFromImage = async (arg: {
|
||||
const config = deepClone(getDefaultRefImageConfig(getState));
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
|
||||
dispatch(refImageAdded({ overrides: { config }, isSelected: true }));
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
|
||||
import { RefImageList } from 'features/controlLayers/components/RefImage/IPAdapterList';
|
||||
import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
|
||||
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
|
||||
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||
@@ -108,7 +107,6 @@ export const ParamPositivePrompt = memo(() => {
|
||||
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
|
||||
/>
|
||||
)}
|
||||
<RefImageList position="absolute" bottom={2} left={2} />
|
||||
</Box>
|
||||
</PromptPopover>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { RefImageList } from 'features/controlLayers/components/RefImage/RefImageList';
|
||||
import { createParamsSelector, selectIsChatGTP4o, selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
||||
@@ -21,6 +22,7 @@ export const Prompts = memo(() => {
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<ParamPositivePrompt />
|
||||
{withStylePrompts && <ParamSDXLPositiveStylePrompt />}
|
||||
<RefImageList />
|
||||
{!isFLUX && !isChatGPT4o && <ParamNegativePrompt />}
|
||||
{withStylePrompts && <ParamSDXLNegativeStylePrompt />}
|
||||
</Flex>
|
||||
|
||||
@@ -5,6 +5,9 @@ import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* TabMountGate is a component that conditionally renders its children based on whether the specified tab is enabled.
|
||||
*/
|
||||
export const TabMountGate = memo(({ tab, children }: PropsWithChildren<{ tab: TabName }>) => {
|
||||
const selectIsTabEnabled = useMemo(
|
||||
() => createSelector(selectConfigSlice, (config) => !config.disabledTabs.includes(tab)),
|
||||
|
||||
Reference in New Issue
Block a user