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:
psychedelicious 2024-06-01 11:43:49 +10:00
parent 0f0a6852f1
commit ff2b2fad83
13 changed files with 58 additions and 115 deletions

View File

@ -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",

View File

@ -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;
}

View File

@ -18,7 +18,7 @@ type BaseDropData = {
id: string;
};
type CurrentImageDropData = BaseDropData & {
export type CurrentImageDropData = BaseDropData & {
actionType: 'SET_CURRENT_IMAGE';
};

View File

@ -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

View File

@ -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} />

View File

@ -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';

View File

@ -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';

View File

@ -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}

View File

@ -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 />}

View File

@ -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';

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
};