mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add comparison modes, side-by-side view
This commit is contained in:
@ -148,6 +148,8 @@
|
|||||||
"viewingDesc": "Review images in a large gallery view",
|
"viewingDesc": "Review images in a large gallery view",
|
||||||
"editing": "Editing",
|
"editing": "Editing",
|
||||||
"editingDesc": "Edit on the Control Layers canvas",
|
"editingDesc": "Edit on the Control Layers canvas",
|
||||||
|
"comparing": "Comparing",
|
||||||
|
"comparingDesc": "Comparing two images",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
@ -377,7 +379,8 @@
|
|||||||
"problemDeletingImages": "Problem Deleting Images",
|
"problemDeletingImages": "Problem Deleting Images",
|
||||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
||||||
"firstImage": "First Image",
|
"firstImage": "First Image",
|
||||||
"secondImage": "Second Image"
|
"secondImage": "Second Image",
|
||||||
|
"selectForCompare": "Select for Compare"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { enqueueRequested } from 'app/store/actions';
|
import { enqueueRequested } from 'app/store/actions';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { viewerModeChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
|
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
|
||||||
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
|
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
|
||||||
@ -34,7 +34,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
try {
|
try {
|
||||||
await req.unwrap();
|
await req.unwrap();
|
||||||
if (shouldShowProgressInViewer) {
|
if (shouldShowProgressInViewer) {
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(viewerModeChanged('view'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
req.reset();
|
req.reset();
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
rgLayerIPAdapterImageChanged,
|
rgLayerIPAdapterImageChanged,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@ -181,40 +181,31 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
* Image selection dropped on node image collection field
|
|
||||||
*/
|
|
||||||
// if (
|
|
||||||
// overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
|
||||||
// activeData.payloadType === 'IMAGE_DTO' &&
|
|
||||||
// activeData.payload.imageDTO
|
|
||||||
// ) {
|
|
||||||
// const { fieldName, nodeId } = overData.context;
|
|
||||||
// dispatch(
|
|
||||||
// fieldValueChanged({
|
|
||||||
// nodeId,
|
|
||||||
// fieldName,
|
|
||||||
// value: [activeData.payload.imageDTO],
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image dropped on user board
|
* Image dropped on user board
|
||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'ADD_TO_BOARD' &&
|
overData.actionType === 'SELECT_FOR_COMPARE' &&
|
||||||
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
|
activeData.payload.imageDTO
|
||||||
|
) {
|
||||||
|
const { imageDTO } = activeData.payload;
|
||||||
|
dispatch(imageToCompareChanged(imageDTO));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image dropped on 'none' board
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
overData.actionType === 'REMOVE_FROM_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
const { imageDTO } = activeData.payload;
|
const { imageDTO } = activeData.payload;
|
||||||
const { boardId } = overData.context;
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
board_id: boardId,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
galleryViewChanged,
|
galleryViewChanged,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
isImageViewerOpenChanged,
|
viewerModeChanged,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
@ -108,7 +108,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(imageSelected(imageDTO));
|
dispatch(imageSelected(imageDTO));
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(viewerModeChanged('view'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,10 @@ export type RemoveFromBoardDropData = BaseDropData & {
|
|||||||
actionType: 'REMOVE_FROM_BOARD';
|
actionType: 'REMOVE_FROM_BOARD';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SelectForCompareDropData = BaseDropData & {
|
||||||
|
actionType: 'SELECT_FOR_COMPARE';
|
||||||
|
};
|
||||||
|
|
||||||
export type TypesafeDroppableData =
|
export type TypesafeDroppableData =
|
||||||
| CurrentImageDropData
|
| CurrentImageDropData
|
||||||
| ControlAdapterDropData
|
| ControlAdapterDropData
|
||||||
@ -89,7 +93,8 @@ export type TypesafeDroppableData =
|
|||||||
| CALayerImageDropData
|
| CALayerImageDropData
|
||||||
| IPALayerImageDropData
|
| IPALayerImageDropData
|
||||||
| RGLayerIPAdapterImageDropData
|
| RGLayerIPAdapterImageDropData
|
||||||
| IILayerImageDropData;
|
| IILayerImageDropData
|
||||||
|
| SelectForCompareDropData;
|
||||||
|
|
||||||
type BaseDragData = {
|
type BaseDragData = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -29,6 +29,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
|
|||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'SET_NODES_IMAGE':
|
case 'SET_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
|
case 'SELECT_FOR_COMPARE':
|
||||||
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'ADD_TO_BOARD': {
|
case 'ADD_TO_BOARD': {
|
||||||
// If the board is the same, don't allow the drop
|
// If the board is the same, don't allow the drop
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
|||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||||
|
import { imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
PiDownloadSimpleBold,
|
PiDownloadSimpleBold,
|
||||||
PiFlowArrowBold,
|
PiFlowArrowBold,
|
||||||
PiFoldersBold,
|
PiFoldersBold,
|
||||||
|
PiImagesBold,
|
||||||
PiPlantBold,
|
PiPlantBold,
|
||||||
PiQuotesBold,
|
PiQuotesBold,
|
||||||
PiShareFatBold,
|
PiShareFatBold,
|
||||||
@ -117,6 +119,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
downloadImage(imageDTO.image_url, imageDTO.image_name);
|
downloadImage(imageDTO.image_url, imageDTO.image_name);
|
||||||
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
||||||
|
|
||||||
|
const handleSelectImageForCompare = useCallback(() => {
|
||||||
|
dispatch(imageToCompareChanged(imageDTO));
|
||||||
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
|
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
|
||||||
@ -130,6 +136,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleDownloadImage}>
|
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleDownloadImage}>
|
||||||
{t('parameters.downloadImage')}
|
{t('parameters.downloadImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem icon={<PiImagesBold />} onClickCapture={handleSelectImageForCompare}>
|
||||||
|
{t('gallery.selectForCompare')}
|
||||||
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
||||||
|
@ -11,7 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
|
|||||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { viewerModeChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -104,7 +104,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDoubleClick = useCallback(() => {
|
const onDoubleClick = useCallback(() => {
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(viewerModeChanged('view'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
|
@ -3,8 +3,9 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
import type { SelectForCompareDropData, TypesafeDraggableData } from 'features/dnd/types';
|
||||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||||
@ -22,21 +23,12 @@ const selectLastSelectedImageName = createSelector(
|
|||||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const droppableData: SelectForCompareDropData = {
|
||||||
isDragDisabled?: boolean;
|
id: 'current-image',
|
||||||
isDropDisabled?: boolean;
|
actionType: 'SELECT_FOR_COMPARE',
|
||||||
withNextPrevButtons?: boolean;
|
|
||||||
withMetadata?: boolean;
|
|
||||||
alwaysShowProgress?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CurrentImagePreview = ({
|
const CurrentImagePreview = () => {
|
||||||
isDragDisabled = false,
|
|
||||||
isDropDisabled = false,
|
|
||||||
withNextPrevButtons = true,
|
|
||||||
withMetadata = true,
|
|
||||||
alwaysShowProgress = false,
|
|
||||||
}: Props) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||||
@ -55,14 +47,6 @@ const CurrentImagePreview = ({
|
|||||||
}
|
}
|
||||||
}, [imageDTO]);
|
}, [imageDTO]);
|
||||||
|
|
||||||
const droppableData = useMemo<TypesafeDroppableData | undefined>(
|
|
||||||
() => ({
|
|
||||||
id: 'current-image',
|
|
||||||
actionType: 'SET_CURRENT_IMAGE',
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show and hide the next/prev buttons on mouse move
|
// Show and hide the next/prev buttons on mouse move
|
||||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
|
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
|
||||||
const timeoutId = useRef(0);
|
const timeoutId = useRef(0);
|
||||||
@ -86,15 +70,13 @@ const CurrentImagePreview = ({
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
{hasDenoiseProgress && (shouldShowProgressInViewer || alwaysShowProgress) ? (
|
{hasDenoiseProgress && shouldShowProgressInViewer ? (
|
||||||
<ProgressImage />
|
<ProgressImage />
|
||||||
) : (
|
) : (
|
||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
droppableData={droppableData}
|
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
isDragDisabled={isDragDisabled}
|
isDropDisabled={true}
|
||||||
isDropDisabled={isDropDisabled}
|
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
fitContainer
|
fitContainer
|
||||||
useThumbailFallback
|
useThumbailFallback
|
||||||
@ -103,13 +85,14 @@ const CurrentImagePreview = ({
|
|||||||
dataTestId="image-preview"
|
dataTestId="image-preview"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shouldShowImageDetails && imageDTO && withMetadata && (
|
<IAIDroppable data={droppableData} dropLabel="Select for Compare" />
|
||||||
|
{shouldShowImageDetails && imageDTO && (
|
||||||
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||||
<ImageMetadataViewer image={imageDTO} />
|
<ImageMetadataViewer image={imageDTO} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && (
|
{shouldShowNextPrevButtons && imageDTO && (
|
||||||
<Box
|
<Box
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
key="nextPrevButtons"
|
key="nextPrevButtons"
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import type { UseMeasureRect } from '@reactuses/core';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
|
import type { SelectForCompareDropData } from 'features/dnd/types';
|
||||||
|
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
|
||||||
|
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
containerSize: UseMeasureRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageComparison = memo(({ containerSize }: Props) => {
|
||||||
|
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||||
|
const { firstImage, secondImage } = useAppSelector((s) => {
|
||||||
|
const firstImage = s.gallery.selection.slice(-1)[0] ?? null;
|
||||||
|
const secondImage = s.gallery.imageToCompare;
|
||||||
|
return { firstImage, secondImage };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!firstImage || !secondImage) {
|
||||||
|
return <ImageComparisonWrapper>No images to compare</ImageComparisonWrapper>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparisonMode === 'slider') {
|
||||||
|
return (
|
||||||
|
<ImageComparisonWrapper>
|
||||||
|
<ImageComparisonSlider containerSize={containerSize} firstImage={firstImage} secondImage={secondImage} />
|
||||||
|
</ImageComparisonWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparisonMode === 'side-by-side') {
|
||||||
|
return (
|
||||||
|
<ImageComparisonWrapper>
|
||||||
|
<ImageComparisonSideBySide firstImage={firstImage} secondImage={secondImage} />
|
||||||
|
</ImageComparisonWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageComparison.displayName = 'ImageComparison';
|
||||||
|
|
||||||
|
const droppableData: SelectForCompareDropData = {
|
||||||
|
id: 'image-comparison',
|
||||||
|
actionType: 'SELECT_FOR_COMPARE',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageComparisonWrapper = memo((props: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.children}
|
||||||
|
<IAIDroppable data={droppableData} dropLabel="Select for Compare" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageComparisonWrapper.displayName = 'ImageComparisonWrapper';
|
@ -0,0 +1,72 @@
|
|||||||
|
import { Flex, Image } from '@invoke-ai/ui-library';
|
||||||
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||||
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/**
|
||||||
|
* The first image to compare
|
||||||
|
*/
|
||||||
|
firstImage: ImageDTO;
|
||||||
|
/**
|
||||||
|
* The second image to compare
|
||||||
|
*/
|
||||||
|
secondImage: ImageDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const onDoubleClickHandle = useCallback(() => {
|
||||||
|
if (!panelGroupRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panelGroupRef.current.setLayout([50, 50]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
|
||||||
|
<Flex w="full" h="full" maxW="full" maxH="full" position="absolute" alignItems="center" justifyContent="center">
|
||||||
|
<PanelGroup ref={panelGroupRef} direction="horizontal" id="image-comparison-side-by-side">
|
||||||
|
<Panel minSize={20}>
|
||||||
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
|
<Image
|
||||||
|
src={firstImage.image_url}
|
||||||
|
fallbackSrc={firstImage.thumbnail_url}
|
||||||
|
objectFit="contain"
|
||||||
|
w={firstImage.width}
|
||||||
|
h={firstImage.height}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Panel>
|
||||||
|
<ResizeHandle
|
||||||
|
id="image-comparison-side-by-side-handle"
|
||||||
|
onDoubleClick={onDoubleClickHandle}
|
||||||
|
orientation="vertical"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Panel minSize={20}>
|
||||||
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
|
<Image
|
||||||
|
src={secondImage.image_url}
|
||||||
|
fallbackSrc={secondImage.thumbnail_url}
|
||||||
|
objectFit="contain"
|
||||||
|
w={secondImage.width}
|
||||||
|
h={secondImage.height}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Panel>
|
||||||
|
</PanelGroup>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageComparisonSideBySide.displayName = 'ImageComparisonSideBySide';
|
@ -31,7 +31,7 @@ type Props = {
|
|||||||
containerSize: UseMeasureRect;
|
containerSize: UseMeasureRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageSliderComparison = memo(({ firstImage, secondImage, containerSize }: Props) => {
|
export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerSize }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
|
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
|
||||||
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
|
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
|
||||||
@ -260,4 +260,4 @@ export const ImageSliderComparison = memo(({ firstImage, secondImage, containerS
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ImageSliderComparison.displayName = 'ImageSliderComparison';
|
ImageComparisonSlider.displayName = 'ImageComparisonSlider';
|
@ -0,0 +1,39 @@
|
|||||||
|
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { comparisonModeChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const ImageComparisonToolbarButtons = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||||
|
const setComparisonModeSlider = useCallback(() => {
|
||||||
|
dispatch(comparisonModeChanged('slider'));
|
||||||
|
}, [dispatch]);
|
||||||
|
const setComparisonModeSideBySide = useCallback(() => {
|
||||||
|
dispatch(comparisonModeChanged('side-by-side'));
|
||||||
|
}, [dispatch]);
|
||||||
|
const setComparisonModeOverlay = useCallback(() => {
|
||||||
|
dispatch(comparisonModeChanged('overlay'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonGroup variant="outline">
|
||||||
|
<Button onClick={setComparisonModeSlider} colorScheme={comparisonMode === 'slider' ? 'invokeBlue' : 'base'}>
|
||||||
|
Slider
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={setComparisonModeSideBySide}
|
||||||
|
colorScheme={comparisonMode === 'side-by-side' ? 'invokeBlue' : 'base'}
|
||||||
|
>
|
||||||
|
Side-by-Side
|
||||||
|
</Button>
|
||||||
|
<Button onClick={setComparisonModeOverlay} colorScheme={comparisonMode === 'overlay' ? 'invokeBlue' : 'base'}>
|
||||||
|
Overlay
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageComparisonToolbarButtons.displayName = 'ImageComparisonToolbarButtons';
|
@ -2,7 +2,8 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
|||||||
import { useMeasure } from '@reactuses/core';
|
import { useMeasure } from '@reactuses/core';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison';
|
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
||||||
|
import { ImageComparisonToolbarButtons } from 'features/gallery/components/ImageViewer/ImageComparisonToolbarButtons';
|
||||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
@ -17,7 +18,7 @@ import { ViewerToggleMenu } from './ViewerToggleMenu';
|
|||||||
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
|
|
||||||
export const ImageViewer = memo(() => {
|
export const ImageViewer = memo(() => {
|
||||||
const { isOpen, onToggle, onClose } = useImageViewer();
|
const { viewerMode, onToggle, openEditor } = useImageViewer();
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -26,16 +27,11 @@ export const ImageViewer = memo(() => {
|
|||||||
if (!isViewerEnabled) {
|
if (!isViewerEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return isOpen;
|
return viewerMode === 'view' || viewerMode === 'compare';
|
||||||
}, [isOpen, isViewerEnabled]);
|
}, [viewerMode, isViewerEnabled]);
|
||||||
|
|
||||||
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
||||||
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
useHotkeys('esc', openEditor, { enabled: isViewerEnabled }, [isViewerEnabled, openEditor]);
|
||||||
|
|
||||||
const { firstImage, secondImage } = useAppSelector((s) => {
|
|
||||||
const images = s.gallery.selection.slice(-2);
|
|
||||||
return { firstImage: images[0] ?? null, secondImage: images[0] ? images[1] ?? null : null };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shouldShowViewer) {
|
if (!shouldShowViewer) {
|
||||||
return null;
|
return null;
|
||||||
@ -65,7 +61,8 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
<CurrentImageButtons />
|
{viewerMode === 'view' && <CurrentImageButtons />}
|
||||||
|
{viewerMode === 'compare' && <ImageComparisonToolbarButtons />}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
@ -74,10 +71,8 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box ref={containerRef} w="full" h="full">
|
<Box ref={containerRef} w="full" h="full">
|
||||||
{firstImage && !secondImage && <CurrentImagePreview />}
|
{viewerMode === 'view' && <CurrentImagePreview />}
|
||||||
{firstImage && secondImage && (
|
{viewerMode === 'compare' && <ImageComparison containerSize={containerSize} />}
|
||||||
<ImageSliderComparison containerSize={containerSize} firstImage={firstImage} secondImage={secondImage} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -9,22 +9,45 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Text,
|
Text,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiImagesBold, PiPencilBold } from 'react-icons/pi';
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
import { useImageViewer } from './useImageViewer';
|
||||||
|
|
||||||
export const ViewerToggleMenu = () => {
|
export const ViewerToggleMenu = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onClose, onOpen } = useImageViewer();
|
const { viewerMode, openEditor, openViewer, openCompare } = useImageViewer();
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
if (viewerMode === 'view') {
|
||||||
|
return <Icon as={PiEyeBold} />;
|
||||||
|
}
|
||||||
|
if (viewerMode === 'edit') {
|
||||||
|
return <Icon as={PiPencilBold} />;
|
||||||
|
}
|
||||||
|
if (viewerMode === 'compare') {
|
||||||
|
return <Icon as={PiImagesBold} />;
|
||||||
|
}
|
||||||
|
}, [viewerMode]);
|
||||||
|
const label = useMemo(() => {
|
||||||
|
if (viewerMode === 'view') {
|
||||||
|
return t('common.viewing');
|
||||||
|
}
|
||||||
|
if (viewerMode === 'edit') {
|
||||||
|
return t('common.editing');
|
||||||
|
}
|
||||||
|
if (viewerMode === 'compare') {
|
||||||
|
return t('common.comparing');
|
||||||
|
}
|
||||||
|
}, [t, viewerMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button variant="outline" data-testid="toggle-viewer-menu-button">
|
<Button variant="outline" data-testid="toggle-viewer-menu-button">
|
||||||
<Flex gap={3} w="full" alignItems="center">
|
<Flex gap={3} w="full" alignItems="center">
|
||||||
{isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
|
{icon}
|
||||||
<Text fontSize="md">{isOpen ? t('common.viewing') : t('common.editing')}</Text>
|
<Text fontSize="md">{label}</Text>
|
||||||
<Icon as={PiCaretDownBold} />
|
<Icon as={PiCaretDownBold} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
@ -33,9 +56,9 @@ export const ViewerToggleMenu = () => {
|
|||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex flexDir="column">
|
<Flex flexDir="column">
|
||||||
<Button onClick={onOpen} variant="ghost" h="auto" w="auto" p={2}>
|
<Button onClick={openViewer} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
<Flex gap={2} w="full">
|
<Flex gap={2} w="full">
|
||||||
<Icon as={PiCheckBold} visibility={isOpen ? 'visible' : 'hidden'} />
|
<Icon as={PiCheckBold} visibility={viewerMode === 'view' ? 'visible' : 'hidden'} />
|
||||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
<Text fontWeight="semibold" color="base.100">
|
<Text fontWeight="semibold" color="base.100">
|
||||||
{t('common.viewing')}
|
{t('common.viewing')}
|
||||||
@ -46,9 +69,9 @@ export const ViewerToggleMenu = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} variant="ghost" h="auto" w="auto" p={2}>
|
<Button onClick={openEditor} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
<Flex gap={2} w="full">
|
<Flex gap={2} w="full">
|
||||||
<Icon as={PiCheckBold} visibility={isOpen ? 'hidden' : 'visible'} />
|
<Icon as={PiCheckBold} visibility={viewerMode === 'edit' ? 'visible' : 'hidden'} />
|
||||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
<Text fontWeight="semibold" color="base.100">
|
<Text fontWeight="semibold" color="base.100">
|
||||||
{t('common.editing')}
|
{t('common.editing')}
|
||||||
@ -59,6 +82,19 @@ export const ViewerToggleMenu = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={openCompare} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
<Icon as={PiCheckBold} visibility={viewerMode === 'compare' ? 'visible' : 'hidden'} />
|
||||||
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
|
<Text fontWeight="semibold" color="base.100">
|
||||||
|
{t('common.comparing')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="normal" color="base.300">
|
||||||
|
{t('common.comparingDesc')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { viewerModeChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const useImageViewer = () => {
|
export const useImageViewer = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
const viewerMode = useAppSelector((s) => s.gallery.viewerMode);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const openEditor = useCallback(() => {
|
||||||
dispatch(isImageViewerOpenChanged(false));
|
dispatch(viewerModeChanged('edit'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const onOpen = useCallback(() => {
|
const openViewer = useCallback(() => {
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(viewerModeChanged('view'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const onToggle = useCallback(() => {
|
const onToggle = useCallback(() => {
|
||||||
dispatch(isImageViewerOpenChanged(!isOpen));
|
dispatch(viewerModeChanged(viewerMode === 'view' ? 'edit' : 'view'));
|
||||||
}, [dispatch, isOpen]);
|
}, [dispatch, viewerMode]);
|
||||||
|
|
||||||
return { isOpen, onOpen, onClose, onToggle };
|
const openCompare = useCallback(() => {
|
||||||
|
dispatch(viewerModeChanged('compare'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return { viewerMode, openEditor, openViewer, openCompare, onToggle };
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { boardsApi } from 'services/api/endpoints/boards';
|
|||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
import type { BoardId, GalleryState, GalleryView } from './types';
|
import type { BoardId, ComparisonMode, GalleryState, GalleryView, ViewerMode } from './types';
|
||||||
import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types';
|
import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types';
|
||||||
|
|
||||||
const initialGalleryState: GalleryState = {
|
const initialGalleryState: GalleryState = {
|
||||||
@ -21,7 +21,9 @@ const initialGalleryState: GalleryState = {
|
|||||||
boardSearchText: '',
|
boardSearchText: '',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isImageViewerOpen: true,
|
viewerMode: 'view',
|
||||||
|
imageToCompare: null,
|
||||||
|
comparisonMode: 'slider',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -34,6 +36,15 @@ export const gallerySlice = createSlice({
|
|||||||
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||||
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
||||||
},
|
},
|
||||||
|
imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||||
|
state.imageToCompare = action.payload;
|
||||||
|
if (action.payload) {
|
||||||
|
state.viewerMode = 'compare';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
|
||||||
|
state.comparisonMode = action.payload;
|
||||||
|
},
|
||||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitch = action.payload;
|
state.shouldAutoSwitch = action.payload;
|
||||||
},
|
},
|
||||||
@ -76,8 +87,8 @@ export const gallerySlice = createSlice({
|
|||||||
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.alwaysShowImageSizeBadge = action.payload;
|
state.alwaysShowImageSizeBadge = action.payload;
|
||||||
},
|
},
|
||||||
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
viewerModeChanged: (state, action: PayloadAction<ViewerMode>) => {
|
||||||
state.isImageViewerOpen = action.payload;
|
state.viewerMode = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
@ -116,7 +127,9 @@ export const {
|
|||||||
boardSearchTextChanged,
|
boardSearchTextChanged,
|
||||||
moreImagesLoaded,
|
moreImagesLoaded,
|
||||||
alwaysShowImageSizeBadgeChanged,
|
alwaysShowImageSizeBadgeChanged,
|
||||||
isImageViewerOpenChanged,
|
viewerModeChanged,
|
||||||
|
imageToCompareChanged,
|
||||||
|
comparisonModeChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
@ -138,5 +151,5 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
|||||||
name: gallerySlice.name,
|
name: gallerySlice.name,
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
migrate: migrateGalleryState,
|
migrate: migrateGalleryState,
|
||||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'isImageViewerOpen'],
|
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'viewerMode', 'imageToCompare'],
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,8 @@ export const IMAGE_LIMIT = 20;
|
|||||||
|
|
||||||
export type GalleryView = 'images' | 'assets';
|
export type GalleryView = 'images' | 'assets';
|
||||||
export type BoardId = 'none' | (string & Record<never, never>);
|
export type BoardId = 'none' | (string & Record<never, never>);
|
||||||
|
export type ComparisonMode = 'slider' | 'side-by-side' | 'overlay';
|
||||||
|
export type ViewerMode = 'edit' | 'view' | 'compare';
|
||||||
|
|
||||||
export type GalleryState = {
|
export type GalleryState = {
|
||||||
selection: ImageDTO[];
|
selection: ImageDTO[];
|
||||||
@ -20,5 +22,7 @@ export type GalleryState = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
alwaysShowImageSizeBadge: boolean;
|
alwaysShowImageSizeBadge: boolean;
|
||||||
isImageViewerOpen: boolean;
|
imageToCompare: ImageDTO | null;
|
||||||
|
comparisonMode: ComparisonMode;
|
||||||
|
viewerMode: ViewerMode;
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/u
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { viewerModeChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
||||||
@ -51,7 +51,7 @@ const ParametersPanelTextToImage = () => {
|
|||||||
const onChangeTabs = useCallback(
|
const onChangeTabs = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
if (i === 1) {
|
if (i === 1) {
|
||||||
dispatch(isImageViewerOpenChanged(false));
|
dispatch(viewerModeChanged('edit'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
Reference in New Issue
Block a user