mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revise drop zones
The main viewer area has two drop zones: - Select for Viewer - Select for Compare These do what you'd imagine they would do.
This commit is contained in:
parent
0f0a6852f1
commit
ff2b2fad83
@ -380,6 +380,7 @@
|
||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
||||
"viewerImage": "Viewer Image",
|
||||
"compareImage": "Compare Image",
|
||||
"selectForViewer": "Select for Viewer",
|
||||
"selectForCompare": "Select for Compare",
|
||||
"selectAnImageToCompare": "Select an Image to Compare",
|
||||
"slider": "Slider",
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageSelected, imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@ -54,6 +54,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(imageSelected(activeData.payload.imageDTO));
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -195,6 +196,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
) {
|
||||
const { imageDTO } = activeData.payload;
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ type BaseDropData = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type CurrentImageDropData = BaseDropData & {
|
||||
export type CurrentImageDropData = BaseDropData & {
|
||||
actionType: 'SET_CURRENT_IMAGE';
|
||||
};
|
||||
|
||||
|
@ -30,11 +30,7 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?
|
||||
case 'SET_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SELECT_FOR_COMPARE':
|
||||
return (
|
||||
payloadType === 'IMAGE_DTO' &&
|
||||
activeData.id !== 'image-compare-first-image' &&
|
||||
activeData.id !== 'image-compare-second-image'
|
||||
);
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'ADD_TO_BOARD': {
|
||||
// If the board is the same, don't allow the drop
|
||||
|
||||
|
@ -6,7 +6,6 @@ import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
@ -75,12 +74,10 @@ const CurrentImagePreview = () => {
|
||||
isUploadDisabled={true}
|
||||
fitContainer
|
||||
useThumbailFallback
|
||||
dropLabel={t('gallery.setCurrentImage')}
|
||||
noContentFallback={<IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />}
|
||||
dataTestId="image-preview"
|
||||
/>
|
||||
)}
|
||||
<ImageComparisonDroppable />
|
||||
{shouldShowImageDetails && imageDTO && (
|
||||
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||
<ImageMetadataViewer image={imageDTO} />
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { SelectForCompareDropData } from 'features/dnd/types';
|
||||
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
|
||||
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
|
||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
|
||||
const firstImage = gallerySlice.selection.slice(-1)[0] ?? null;
|
||||
@ -24,56 +20,17 @@ export const ImageComparison = memo(() => {
|
||||
const { firstImage, secondImage } = useAppSelector(selector);
|
||||
|
||||
if (!firstImage || !secondImage) {
|
||||
return (
|
||||
<ImageComparisonWrapper firstImage={firstImage} secondImage={secondImage}>
|
||||
<IAINoContentFallback label={t('gallery.selectAnImageToCompare')} icon={PiImagesBold} />
|
||||
</ImageComparisonWrapper>
|
||||
);
|
||||
// Should rarely/never happen - we don't render this component unless we have images to compare
|
||||
return <IAINoContentFallback label={t('gallery.selectAnImageToCompare')} icon={PiImagesBold} />;
|
||||
}
|
||||
|
||||
if (comparisonMode === 'slider') {
|
||||
return (
|
||||
<ImageComparisonWrapper firstImage={firstImage} secondImage={secondImage}>
|
||||
<ImageComparisonSlider firstImage={firstImage} secondImage={secondImage} />
|
||||
</ImageComparisonWrapper>
|
||||
);
|
||||
return <ImageComparisonSlider firstImage={firstImage} secondImage={secondImage} />;
|
||||
}
|
||||
|
||||
if (comparisonMode === 'side-by-side') {
|
||||
return (
|
||||
<ImageComparisonWrapper firstImage={firstImage} secondImage={secondImage}>
|
||||
<ImageComparisonSideBySide firstImage={firstImage} secondImage={secondImage} />
|
||||
</ImageComparisonWrapper>
|
||||
);
|
||||
return <ImageComparisonSideBySide firstImage={firstImage} secondImage={secondImage} />;
|
||||
}
|
||||
});
|
||||
|
||||
ImageComparison.displayName = 'ImageComparison';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
firstImage: ImageDTO | null;
|
||||
secondImage: ImageDTO | null;
|
||||
}>;
|
||||
|
||||
const ImageComparisonWrapper = memo((props: Props) => {
|
||||
const droppableData = useMemo<SelectForCompareDropData>(
|
||||
() => ({
|
||||
id: 'image-comparison',
|
||||
actionType: 'SELECT_FOR_COMPARE',
|
||||
context: {
|
||||
firstImageName: props.firstImage?.image_name,
|
||||
secondImageName: props.secondImage?.image_name,
|
||||
},
|
||||
}),
|
||||
[props.firstImage?.image_name, props.secondImage?.image_name]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<IAIDroppable data={droppableData} dropLabel="Select for Compare" />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ImageComparisonWrapper.displayName = 'ImageComparisonWrapper';
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { SelectForCompareDropData } from 'features/dnd/types';
|
||||
import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types';
|
||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -12,10 +13,15 @@ const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
|
||||
return { firstImage, secondImage };
|
||||
});
|
||||
|
||||
const setCurrentImageDropData: CurrentImageDropData = {
|
||||
id: 'current-image',
|
||||
actionType: 'SET_CURRENT_IMAGE',
|
||||
};
|
||||
|
||||
export const ImageComparisonDroppable = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { firstImage, secondImage } = useAppSelector(selector);
|
||||
const droppableData = useMemo<SelectForCompareDropData>(
|
||||
const selectForCompareDropData = useMemo<SelectForCompareDropData>(
|
||||
() => ({
|
||||
id: 'image-comparison',
|
||||
actionType: 'SELECT_FOR_COMPARE',
|
||||
@ -27,7 +33,16 @@ export const ImageComparisonDroppable = memo(() => {
|
||||
[firstImage?.image_name, secondImage?.image_name]
|
||||
);
|
||||
|
||||
return <IAIDroppable data={droppableData} dropLabel={t('gallery.selectForCompare')} />;
|
||||
return (
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||
<Flex position="relative" flex={1}>
|
||||
<IAIDroppable data={setCurrentImageDropData} dropLabel={t('gallery.selectForViewer')} />
|
||||
</Flex>
|
||||
<Flex position="relative" flex={1}>
|
||||
<IAIDroppable data={selectForCompareDropData} dropLabel={t('gallery.selectForCompare')} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
ImageComparisonDroppable.displayName = 'ImageComparisonDroppable';
|
||||
|
@ -132,7 +132,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
|
||||
justifyContent="center"
|
||||
>
|
||||
<Flex
|
||||
id="image-comparison-container"
|
||||
id="image-comparison-wrapper"
|
||||
w="full"
|
||||
h="full"
|
||||
maxW="full"
|
||||
@ -144,7 +144,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
|
||||
<Box
|
||||
ref={imageContainerRef}
|
||||
position="relative"
|
||||
id="image-comparison-second-image-container"
|
||||
id="image-comparison-image-container"
|
||||
w={fittedSize.width}
|
||||
h={fittedSize.height}
|
||||
maxW="full"
|
||||
@ -163,9 +163,10 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
|
||||
backgroundImage={STAGE_BG_DATAURL}
|
||||
backgroundRepeat="repeat"
|
||||
opacity={0.2}
|
||||
zIndex={-1}
|
||||
/>
|
||||
<Image
|
||||
position="relative"
|
||||
id="image-comparison-second-image"
|
||||
src={secondImage.image_url}
|
||||
fallbackSrc={secondImage.thumbnail_url}
|
||||
w={sliderFit === 'fill' ? fittedSize.width : (fittedSize.width * secondImage.width) / firstImage.width}
|
||||
@ -197,6 +198,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image
|
||||
id="image-comparison-first-image"
|
||||
src={firstImage.image_url}
|
||||
fallbackSrc={firstImage.thumbnail_url}
|
||||
w={fittedSize.width}
|
||||
|
@ -15,14 +15,18 @@ const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows
|
||||
export const ImageViewer = memo(() => {
|
||||
const { isOpen, onToggle, onClose } = useImageViewer();
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const workflowsMode = useAppSelector((s) => s.workflow.mode);
|
||||
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
||||
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
||||
const shouldShowViewer = useMemo(() => {
|
||||
if (activeTabName === 'workflows' && workflowsMode === 'view') {
|
||||
return true;
|
||||
}
|
||||
if (!isViewerEnabled) {
|
||||
return false;
|
||||
}
|
||||
return isOpen;
|
||||
}, [isOpen, isViewerEnabled]);
|
||||
}, [isOpen, isViewerEnabled, workflowsMode, activeTabName]);
|
||||
|
||||
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
||||
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
||||
@ -45,7 +49,6 @@ export const ImageViewer = memo(() => {
|
||||
rowGap={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||
>
|
||||
{isComparing && <CompareToolbar />}
|
||||
{!isComparing && <ViewerToolbar />}
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
|
||||
export const ImageViewerWorkflows = memo(() => {
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
p={2}
|
||||
rowGap={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||
>
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineEnd="auto">
|
||||
<ToggleProgressButton />
|
||||
<ToggleMetadataViewerButton />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex flex={1} gap={2} justifyContent="center">
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<CurrentImagePreview />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
ImageViewerWorkflows.displayName = 'ImageViewerWorkflows';
|
@ -1,12 +1,23 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { memo } from 'react';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const workflowsMode = useAppSelector((s) => s.workflow.mode);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const shouldShowToggleMenu = useMemo(() => {
|
||||
if (activeTabName !== 'workflows') {
|
||||
return true;
|
||||
}
|
||||
return workflowsMode === 'edit';
|
||||
}, [workflowsMode, activeTabName]);
|
||||
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
@ -20,7 +31,7 @@ export const ViewerToolbar = memo(() => {
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto">
|
||||
<ViewerToggleMenu />
|
||||
{shouldShowToggleMenu && <ViewerToggleMenu />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ImageViewerWorkflows } from 'features/gallery/components/ImageViewer/ImageViewerWorkflows';
|
||||
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||
import { memo } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
@ -10,7 +11,8 @@ const NodesTab = () => {
|
||||
if (mode === 'view') {
|
||||
return (
|
||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<ImageViewerWorkflows />
|
||||
<ImageViewer />
|
||||
<ImageComparisonDroppable />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
@ -8,6 +9,7 @@ const TextToImageTab = () => {
|
||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<ControlLayersEditor />
|
||||
<ImageViewer />
|
||||
<ImageComparisonDroppable />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user