From 449bc4dbe5caea7dd03b77b81fb01066dd59bd1c Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Sun, 2 Jun 2024 10:02:33 +1000
Subject: [PATCH] feat(ui): abstract out and share logic between comparisons
---
invokeai/frontend/web/package.json | 1 -
invokeai/frontend/web/pnpm-lock.yaml | 20 --
.../listeners/workflowLoadRequested.ts | 2 +-
.../web/src/common/hooks/useBoolean.ts | 21 ++
.../ImageViewer/ImageComparison.tsx | 15 +-
.../ImageViewer/ImageComparisonHover.tsx | 179 ++++++++++--------
.../ImageViewer/ImageComparisonLabel.tsx | 33 ++++
.../ImageViewer/ImageComparisonSideBySide.tsx | 49 +----
.../ImageViewer/ImageComparisonSlider.tsx | 111 +++--------
.../components/ImageViewer/ImageViewer.tsx | 9 +-
.../gallery/components/ImageViewer/common.ts | 57 ++++++
.../{useImageViewer.tsx => useImageViewer.ts} | 2 -
.../web/src/features/gallery/store/types.ts | 3 +-
13 files changed, 260 insertions(+), 242 deletions(-)
create mode 100644 invokeai/frontend/web/src/common/hooks/useBoolean.ts
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
rename invokeai/frontend/web/src/features/gallery/components/ImageViewer/{useImageViewer.tsx => useImageViewer.ts} (90%)
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 0211994f22..f2210e4c68 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -61,7 +61,6 @@
"@fontsource-variable/inter": "^5.0.18",
"@invoke-ai/ui-library": "^0.0.25",
"@nanostores/react": "^0.7.2",
- "@reactuses/core": "^5.0.14",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
"chakra-react-select": "^4.7.6",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index f9a3da4e39..64189f0d82 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -35,9 +35,6 @@ dependencies:
'@nanostores/react':
specifier: ^0.7.2
version: 0.7.2(nanostores@0.10.3)(react@18.3.1)
- '@reactuses/core':
- specifier: ^5.0.14
- version: 5.0.14(react@18.3.1)
'@reduxjs/toolkit':
specifier: 2.2.3
version: 2.2.3(react-redux@9.1.2)(react@18.3.1)
@@ -3985,18 +3982,6 @@ packages:
- immer
dev: false
- /@reactuses/core@5.0.14(react@18.3.1):
- resolution: {integrity: sha512-lg640pRPOPT0HZ8XQAA1VRZ47fLIvSd2JrUTtKpzm4t3MtZvza+w2RHBGgPsdmtiLV3GsJJC9x5ge7XOQmiJ/Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- js-cookie: 3.0.5
- lodash-es: 4.17.21
- react: 18.3.1
- screenfull: 5.2.0
- use-sync-external-store: 1.2.2(react@18.3.1)
- dev: false
-
/@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1):
resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==}
peerDependencies:
@@ -9683,11 +9668,6 @@ packages:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
dev: false
- /js-cookie@3.0.5:
- resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
- engines: {node: '>=14'}
- dev: false
-
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
index 9ccd967464..2c0caa0ec9 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
@@ -65,7 +65,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
});
}
- $needsFit.set(true)
+ $needsFit.set(true);
} catch (e) {
if (e instanceof WorkflowVersionError) {
// The workflow version was not recognized in the valid list of versions
diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts
new file mode 100644
index 0000000000..123e48cd75
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts
@@ -0,0 +1,21 @@
+import { useCallback, useMemo, useState } from 'react';
+
+export const useBoolean = (initialValue: boolean) => {
+ const [isTrue, set] = useState(initialValue);
+ const setTrue = useCallback(() => set(true), []);
+ const setFalse = useCallback(() => set(false), []);
+ const toggle = useCallback(() => set((v) => !v), []);
+
+ const api = useMemo(
+ () => ({
+ isTrue,
+ set,
+ setTrue,
+ setFalse,
+ toggle,
+ }),
+ [isTrue, set, setTrue, setFalse, toggle]
+ );
+
+ return api;
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
index 73148851c3..ca740a5c16 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx
@@ -1,6 +1,7 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover';
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
@@ -15,7 +16,11 @@ const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
return { firstImage, secondImage };
});
-export const ImageComparison = memo(() => {
+type Props = {
+ containerDims: Dimensions;
+};
+
+export const ImageComparison = memo(({ containerDims }: Props) => {
const { t } = useTranslation();
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
const { firstImage, secondImage } = useAppSelector(selector);
@@ -26,15 +31,17 @@ export const ImageComparison = memo(() => {
}
if (comparisonMode === 'slider') {
- return ;
+ return ;
}
if (comparisonMode === 'side-by-side') {
- return ;
+ return (
+
+ );
}
if (comparisonMode === 'hover') {
- return ;
+ return ;
}
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
index d00723a36e..a02e94b547 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
@@ -1,101 +1,114 @@
-import { Flex, Image, Text } from '@invoke-ai/ui-library';
+import { Box, Flex, Image } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
+import { useBoolean } from 'common/hooks/useBoolean';
import { preventDefault } from 'common/util/stopPropagation';
-import { DROP_SHADOW } from 'features/gallery/components/ImageViewer/useImageViewer';
-import { memo, useCallback, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import type { ImageDTO } from 'services/api/types';
+import type { Dimensions } from 'features/canvas/store/canvasTypes';
+import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
+import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
+import { memo, useMemo, useRef } from 'react';
-type Props = {
- /**
- * The first image to compare
- */
- firstImage: ImageDTO;
- /**
- * The second image to compare
- */
- secondImage: ImageDTO;
-};
+import type { ComparisonProps } from './common';
+import { fitDimsToContainer, getSecondImageDims } from './common';
-export const ImageComparisonHover = memo(({ firstImage, secondImage }: Props) => {
- const { t } = useTranslation();
+export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
- const [isMouseOver, setIsMouseOver] = useState(false);
- const onMouseOver = useCallback(() => {
- setIsMouseOver(true);
- }, []);
- const onMouseOut = useCallback(() => {
- setIsMouseOver(false);
- }, []);
+ const imageContainerRef = useRef(null);
+ const mouseOver = useBoolean(false);
+ const fittedDims = useMemo(
+ () => fitDimsToContainer(containerDims, firstImage),
+ [containerDims, firstImage]
+ );
+ const compareImageDims = useMemo(
+ () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage),
+ [comparisonFit, fittedDims, firstImage, secondImage]
+ );
return (
-
-
+
-
- {t('gallery.viewerImage')}
-
-
-
+
+
- {t('gallery.compareImage')}
-
-
-
+
+
+
+
+
+
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx
new file mode 100644
index 0000000000..a5a40dfc9c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx
@@ -0,0 +1,33 @@
+import type { TextProps } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { DROP_SHADOW } from './common';
+
+type Props = TextProps & {
+ type: 'first' | 'second';
+};
+
+export const ImageComparisonLabel = memo(({ type, ...rest }: Props) => {
+ const { t } = useTranslation();
+ return (
+
+ {type === 'first' ? t('gallery.viewerImage') : t('gallery.compareImage')}
+
+ );
+});
+
+ImageComparisonLabel.displayName = 'ImageComparisonLabel';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
index 49f03fb9c8..8bac2bb45d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx
@@ -1,25 +1,12 @@
-import { Flex, Image, Text } from '@invoke-ai/ui-library';
-import { DROP_SHADOW } from 'features/gallery/components/ImageViewer/useImageViewer';
+import { Flex, Image } from '@invoke-ai/ui-library';
+import type { ComparisonProps } from 'features/gallery/components/ImageViewer/common';
+import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
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();
+export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: ComparisonProps) => {
const panelGroupRef = useRef(null);
const onDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) {
@@ -44,19 +31,9 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
src={firstImage.image_url}
fallbackSrc={firstImage.thumbnail_url}
objectFit="contain"
+ borderRadius="base"
/>
-
- {t('gallery.viewerImage')}
-
+
@@ -78,19 +55,9 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Prop
src={secondImage.image_url}
fallbackSrc={secondImage.thumbnail_url}
objectFit="contain"
+ borderRadius="base"
/>
-
- {t('gallery.compareImage')}
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
index bda4c12eeb..8972af7d4f 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
@@ -1,15 +1,14 @@
-import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
-import { useMeasure } from '@reactuses/core';
+import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { preventDefault } from 'common/util/stopPropagation';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
+import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useTranslation } from 'react-i18next';
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
-import type { ImageDTO } from 'services/api/types';
-import { DROP_SHADOW } from './useImageViewer';
+import type { ComparisonProps } from './common';
+import { DROP_SHADOW, fitDimsToContainer, getSecondImageDims } from './common';
const INITIAL_POS = '50%';
const HANDLE_WIDTH = 2;
@@ -19,59 +18,28 @@ const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}px`;
const HANDLE_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`;
const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
-type Props = {
- /**
- * The first image to compare
- */
- firstImage: ImageDTO;
- /**
- * The second image to compare
- */
- secondImage: ImageDTO;
-};
-
-export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) => {
- const { t } = useTranslation();
+export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
// 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);
// How wide the first image is
const [width, setWidth] = useState(INITIAL_POS);
const handleRef = useRef(null);
- // If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho
- const containerRef = useRef(null);
- const [containerSize] = useMeasure(containerRef);
+ // To manage aspect ratios, we need to know the size of the container
const imageContainerRef = useRef(null);
// To keep things smooth, we use RAF to update the handle position & gate it to 60fps
const rafRef = useRef(null);
const lastMoveTimeRef = useRef(0);
- const fittedSize = useMemo(() => {
- // Fit the first image to the container
- if (containerSize.width === 0 || containerSize.height === 0) {
- return { width: firstImage.width, height: firstImage.height };
- }
- const targetAspectRatio = containerSize.width / containerSize.height;
- const imageAspectRatio = firstImage.width / firstImage.height;
+ const fittedDims = useMemo(
+ () => fitDimsToContainer(containerDims, firstImage),
+ [containerDims, firstImage]
+ );
- let width: number;
- let height: number;
-
- if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) {
- return { width: firstImage.width, height: firstImage.height };
- }
-
- if (imageAspectRatio > targetAspectRatio) {
- // Image is wider than container's aspect ratio
- width = containerSize.width;
- height = width / imageAspectRatio;
- } else {
- // Image is taller than container's aspect ratio
- height = containerSize.height;
- width = height * imageAspectRatio;
- }
- return { width, height };
- }, [containerSize, firstImage.height, firstImage.width]);
+ const compareImageDims = useMemo(
+ () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage),
+ [comparisonFit, fittedDims, firstImage, secondImage]
+ );
const updateHandlePos = useCallback((clientX: number) => {
if (!handleRef.current || !imageContainerRef.current) {
@@ -122,16 +90,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
);
return (
-
+
-
- {t('gallery.compareImage')}
-
+
-
- {t('gallery.viewerImage')}
-
+
{
}
return isOpen;
}, [isOpen, isViewerEnabled, workflowsMode, activeTabName]);
+ const [containerRef, containerDims] = useMeasure();
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
@@ -52,9 +55,9 @@ export const ImageViewer = memo(() => {
>
{isComparing && }
{!isComparing && }
-
+
{!isComparing && }
- {isComparing && }
+ {isComparing && }
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
new file mode 100644
index 0000000000..8d7f02c0fc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts
@@ -0,0 +1,57 @@
+import type { Dimensions } from 'features/canvas/store/canvasTypes';
+import type { ComparisonFit } from 'features/gallery/store/types';
+import type { ImageDTO } from 'services/api/types';
+
+export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
+
+export type ComparisonProps = {
+ firstImage: ImageDTO;
+ secondImage: ImageDTO;
+ containerDims: Dimensions;
+};
+
+export const fitDimsToContainer = (containerDims: Dimensions, imageDims: Dimensions): Dimensions => {
+ // Fall back to the image's dimensions if the container has no dimensions
+ if (containerDims.width === 0 || containerDims.height === 0) {
+ return { width: imageDims.width, height: imageDims.height };
+ }
+
+ // Fall back to the image's dimensions if the image fits within the container
+ if (imageDims.width <= containerDims.width && imageDims.height <= containerDims.height) {
+ return { width: imageDims.width, height: imageDims.height };
+ }
+
+ const targetAspectRatio = containerDims.width / containerDims.height;
+ const imageAspectRatio = imageDims.width / imageDims.height;
+
+ let width: number;
+ let height: number;
+
+ if (imageAspectRatio > targetAspectRatio) {
+ // Image is wider than container's aspect ratio
+ width = containerDims.width;
+ height = width / imageAspectRatio;
+ } else {
+ // Image is taller than container's aspect ratio
+ height = containerDims.height;
+ width = height * imageAspectRatio;
+ }
+ return { width, height };
+};
+
+/**
+ * Gets the dimensions of the second image in a comparison based on the comparison fit mode.
+ */
+export const getSecondImageDims = (
+ comparisonFit: ComparisonFit,
+ fittedDims: Dimensions,
+ firstImageDims: Dimensions,
+ secondImageDims: Dimensions
+): Dimensions => {
+ const width =
+ comparisonFit === 'fill' ? fittedDims.width : (fittedDims.width * secondImageDims.width) / firstImageDims.width;
+ const height =
+ comparisonFit === 'fill' ? fittedDims.height : (fittedDims.height * secondImageDims.height) / firstImageDims.height;
+
+ return { width, height };
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
similarity index 90%
rename from invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx
rename to invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
index 4232499c00..978fbc0cef 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
@@ -29,5 +29,3 @@ export const useImageViewer = () => {
return { isOpen, onOpen, onClose, onToggle };
};
-
-export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts
index 0b2618be65..a88715b0bd 100644
--- a/invokeai/frontend/web/src/features/gallery/store/types.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/types.ts
@@ -8,6 +8,7 @@ export const IMAGE_LIMIT = 20;
export type GalleryView = 'images' | 'assets';
export type BoardId = 'none' | (string & Record);
export type ComparisonMode = 'slider' | 'side-by-side' | 'hover';
+export type ComparisonFit = 'contain' | 'fill';
export type GalleryState = {
selection: ImageDTO[];
@@ -23,6 +24,6 @@ export type GalleryState = {
alwaysShowImageSizeBadge: boolean;
imageToCompare: ImageDTO | null;
comparisonMode: ComparisonMode;
- comparisonFit: 'contain' | 'fill';
+ comparisonFit: ComparisonFit;
isImageViewerOpen: boolean;
};