mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add fill mode for slider comparison
This commit is contained in:
parent
e4ce188500
commit
3501636018
@ -384,7 +384,9 @@
|
|||||||
"selectAnImageToCompare": "Select an Image to Compare",
|
"selectAnImageToCompare": "Select an Image to Compare",
|
||||||
"slider": "Slider",
|
"slider": "Slider",
|
||||||
"sideBySide": "Side-by-Side",
|
"sideBySide": "Side-by-Side",
|
||||||
"swapImages": "Swap"
|
"swapImages": "Swap Images",
|
||||||
|
"compareOptions": "Comparison Options",
|
||||||
|
"sliderFitLabel": "Stretch second image to fit"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
|
import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
|
||||||
import { useMeasure } from '@reactuses/core';
|
import { useMeasure } from '@reactuses/core';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||||
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
|
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -29,6 +30,7 @@ type Props = {
|
|||||||
|
|
||||||
export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) => {
|
export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const sliderFit = useAppSelector((s) => s.gallery.sliderFit);
|
||||||
// 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);
|
||||||
// How wide the first image is
|
// How wide the first image is
|
||||||
@ -165,11 +167,11 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage }: Props) =
|
|||||||
<Image
|
<Image
|
||||||
src={secondImage.image_url}
|
src={secondImage.image_url}
|
||||||
fallbackSrc={secondImage.thumbnail_url}
|
fallbackSrc={secondImage.thumbnail_url}
|
||||||
w={secondImage.width}
|
w={sliderFit === 'fill' ? fittedSize.width : secondImage.width}
|
||||||
h={secondImage.height}
|
h={sliderFit === 'fill' ? fittedSize.height : secondImage.height}
|
||||||
maxW={fittedSize.width}
|
maxW={fittedSize.width}
|
||||||
maxH={fittedSize.height}
|
maxH={fittedSize.height}
|
||||||
objectFit="contain"
|
objectFit={sliderFit}
|
||||||
objectPosition="top left"
|
objectPosition="top left"
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Switch,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { comparedImagesSwapped, comparisonModeChanged } from 'features/gallery/store/gallerySlice';
|
import { comparedImagesSwapped, comparisonModeChanged, sliderFitChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiGearBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ImageComparisonToolbarButtons = memo(() => {
|
export const ImageComparisonToolbarButtons = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||||
|
const sliderFit = useAppSelector((s) => s.gallery.sliderFit);
|
||||||
const setComparisonModeSlider = useCallback(() => {
|
const setComparisonModeSlider = useCallback(() => {
|
||||||
dispatch(comparisonModeChanged('slider'));
|
dispatch(comparisonModeChanged('slider'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -17,6 +32,12 @@ export const ImageComparisonToolbarButtons = memo(() => {
|
|||||||
const swapImages = useCallback(() => {
|
const swapImages = useCallback(() => {
|
||||||
dispatch(comparedImagesSwapped());
|
dispatch(comparedImagesSwapped());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
const onSliderFitChanged = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(sliderFitChanged(e.target.checked ? 'fill' : 'contain'));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -31,6 +52,30 @@ export const ImageComparisonToolbarButtons = memo(() => {
|
|||||||
{t('gallery.sideBySide')}
|
{t('gallery.sideBySide')}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('gallery.compareOptions')}
|
||||||
|
tooltip={t('gallery.compareOptions')}
|
||||||
|
icon={<PiGearBold />}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex direction="column" gap={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{t('gallery.sliderFitLabel')}</FormLabel>
|
||||||
|
<Switch
|
||||||
|
isChecked={sliderFit === 'fill'}
|
||||||
|
isDisabled={comparisonMode !== 'slider'}
|
||||||
|
onChange={onSliderFitChanged}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<Button onClick={swapImages}>{t('gallery.swapImages')}</Button>
|
<Button onClick={swapImages}>{t('gallery.swapImages')}</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -24,6 +24,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
viewerMode: 'view',
|
viewerMode: 'view',
|
||||||
imageToCompare: null,
|
imageToCompare: null,
|
||||||
comparisonMode: 'slider',
|
comparisonMode: 'slider',
|
||||||
|
sliderFit: 'fill',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -97,6 +98,9 @@ export const gallerySlice = createSlice({
|
|||||||
state.imageToCompare = oldSelection[0] ?? null;
|
state.imageToCompare = oldSelection[0] ?? null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sliderFitChanged: (state, action: PayloadAction<'contain' | 'fill'>) => {
|
||||||
|
state.sliderFit = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
@ -138,6 +142,7 @@ export const {
|
|||||||
imageToCompareChanged,
|
imageToCompareChanged,
|
||||||
comparisonModeChanged,
|
comparisonModeChanged,
|
||||||
comparedImagesSwapped,
|
comparedImagesSwapped,
|
||||||
|
sliderFitChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
|
@ -24,5 +24,6 @@ export type GalleryState = {
|
|||||||
alwaysShowImageSizeBadge: boolean;
|
alwaysShowImageSizeBadge: boolean;
|
||||||
imageToCompare: ImageDTO | null;
|
imageToCompare: ImageDTO | null;
|
||||||
comparisonMode: ComparisonMode;
|
comparisonMode: ComparisonMode;
|
||||||
|
sliderFit: 'contain' | 'fill';
|
||||||
viewerMode: ViewerMode;
|
viewerMode: ViewerMode;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user