feat(ui): control image auto-process

This commit is contained in:
psychedelicious 2023-06-02 21:30:21 +10:00
parent fa290aff8d
commit 72b4371804
11 changed files with 395 additions and 45 deletions

View File

@ -71,7 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener'; import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetProcessorParamsChangedListener } from './listeners/controlNetProcessorParamsChanged'; import { addControlNetAutoProcessListener } from './listeners/controlNetProcessorParamsChanged';
export const listenerMiddleware = createListenerMiddleware(); export const listenerMiddleware = createListenerMiddleware();
@ -178,4 +178,4 @@ addImageCategoriesChangedListener();
// ControlNet // ControlNet
addControlNetImageProcessedListener(); addControlNetImageProcessedListener();
addControlNetProcessorParamsChangedListener(); addControlNetAutoProcessListener();

View File

@ -15,7 +15,10 @@ const moduleLog = log.child({ namespace: 'controlNet' });
export const addControlNetImageProcessedListener = () => { export const addControlNetImageProcessedListener = () => {
startAppListening({ startAppListening({
actionCreator: controlNetImageProcessed, actionCreator: controlNetImageProcessed,
effect: async (action, { dispatch, getState, take }) => { effect: async (
action,
{ dispatch, getState, take, unsubscribe, subscribe }
) => {
const { controlNetId } = action.payload; const { controlNetId } = action.payload;
const controlNet = getState().controlNet.controlNets[controlNetId]; const controlNet = getState().controlNet.controlNets[controlNetId];

View File

@ -2,6 +2,7 @@ import { startAppListening } from '..';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { controlNetImageProcessed } from 'features/controlNet/store/actions'; import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import { import {
controlNetImageChanged,
controlNetProcessorParamsChanged, controlNetProcessorParamsChanged,
controlNetProcessorTypeChanged, controlNetProcessorTypeChanged,
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
@ -13,10 +14,11 @@ const moduleLog = log.child({ namespace: 'controlNet' });
* *
* The network request is debounced by 1 second. * The network request is debounced by 1 second.
*/ */
export const addControlNetProcessorParamsChangedListener = () => { export const addControlNetAutoProcessListener = () => {
startAppListening({ startAppListening({
predicate: (action) => predicate: (action) =>
controlNetProcessorParamsChanged.match(action) || controlNetProcessorParamsChanged.match(action) ||
controlNetImageChanged.match(action) ||
controlNetProcessorTypeChanged.match(action), controlNetProcessorTypeChanged.match(action),
effect: async ( effect: async (
action, action,
@ -35,11 +37,19 @@ export const addControlNetProcessorParamsChangedListener = () => {
const { controlNetId } = action.payload; const { controlNetId } = action.payload;
if (!state.controlNet.controlNets[controlNetId].controlImage) {
moduleLog.trace(
{ data: { controlNetId } },
'No ControlNet image to auto-process'
);
return;
}
// Cancel any in-progress instances of this listener // Cancel any in-progress instances of this listener
cancelActiveListeners(); cancelActiveListeners();
// Delay before starting actual work // Delay before starting actual work
await delay(1000); await delay(300);
dispatch(controlNetImageProcessed({ controlNetId })); dispatch(controlNetImageProcessed({ controlNetId }));
}, },

View File

@ -1,18 +1,21 @@
import { memo, useCallback } from 'react'; import { memo, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { import {
ControlNet, ControlNet,
controlNetImageChanged, controlNetImageChanged,
controlNetProcessedImageChanged, controlNetProcessedImageChanged,
controlNetRemoved, controlNetRemoved,
controlNetSelector,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetModel from './parameters/ParamControlNetModel';
import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetWeight from './parameters/ParamControlNetWeight';
import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct'; import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct';
import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct'; import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct';
import { import {
Box,
Flex, Flex,
Spinner,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
@ -22,10 +25,15 @@ import {
import IAISelectableImage from './parameters/IAISelectableImage'; import IAISelectableImage from './parameters/IAISelectableImage';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { FaUndo } from 'react-icons/fa'; import { FaUndo } from 'react-icons/fa';
import { TbSquareToggle } from 'react-icons/tb';
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
import ControlNetProcessorComponent from './ControlNetProcessorComponent'; import ControlNetProcessorComponent from './ControlNetProcessorComponent';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import ControlNetPreprocessButton from './ControlNetPreprocessButton'; import ControlNetPreprocessButton from './ControlNetPreprocessButton';
import IAIIconButton from 'common/components/IAIIconButton';
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
import ControlNetImagePreview from './ControlNetImagePreview';
import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
type ControlNetProps = { type ControlNetProps = {
controlNet: ControlNet; controlNet: ControlNet;
@ -45,15 +53,6 @@ const ControlNet = (props: ControlNetProps) => {
processorNode, processorNode,
} = props.controlNet; } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isReady = useIsApplicationReady();
const handleControlImageChanged = useCallback(
(controlImage: ImageDTO) => {
dispatch(controlNetImageChanged({ controlNetId, controlImage }));
},
[controlNetId, dispatch]
);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
dispatch( dispatch(
controlNetProcessedImageChanged({ controlNetProcessedImageChanged({
@ -63,21 +62,16 @@ const ControlNet = (props: ControlNetProps) => {
); );
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
const handleControlImageReset = useCallback(() => {
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
}, [controlNetId, dispatch]);
const handleControlNetRemoved = useCallback(() => { const handleControlNetRemoved = useCallback(() => {
dispatch(controlNetRemoved(controlNetId)); dispatch(controlNetRemoved(controlNetId));
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
return ( return (
<Flex sx={{ flexDir: 'column', gap: 3 }}> <Flex sx={{ flexDir: 'column', gap: 3 }}>
<IAISelectableImage <ControlNetImagePreview
image={processedControlImage || controlImage} controlNetId={controlNetId}
onChange={handleControlImageChanged} controlImage={controlImage}
onReset={handleControlImageReset} processedControlImage={processedControlImage}
resetIconSize="sm"
/> />
<ParamControlNetModel controlNetId={controlNetId} model={model} /> <ParamControlNetModel controlNetId={controlNetId} model={model} />
<Tabs <Tabs
@ -105,12 +99,9 @@ const ControlNet = (props: ControlNetProps) => {
controlNetId={controlNetId} controlNetId={controlNetId}
weight={weight} weight={weight}
/> />
<ParamControlNetBeginStepPct <ParamControlNetBeginEnd
controlNetId={controlNetId} controlNetId={controlNetId}
beginStepPct={beginStepPct} beginStepPct={beginStepPct}
/>
<ParamControlNetEndStepPct
controlNetId={controlNetId}
endStepPct={endStepPct} endStepPct={endStepPct}
/> />
</TabPanel> </TabPanel>

View File

@ -0,0 +1,168 @@
import { memo, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api';
import {
controlNetImageChanged,
controlNetSelector,
} from '../store/controlNetSlice';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Box, Flex, Spinner } from '@chakra-ui/react';
import IAISelectableImage from './parameters/IAISelectableImage';
import { TbSquareToggle } from 'react-icons/tb';
import IAIIconButton from 'common/components/IAIIconButton';
import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { AnimatePresence, motion } from 'framer-motion';
const selector = createSelector(
controlNetSelector,
(controlNet) => {
const { isProcessingControlImage } = controlNet;
return { isProcessingControlImage };
},
defaultSelectorOptions
);
type Props = {
controlNetId: string;
controlImage: ImageDTO | null;
processedControlImage: ImageDTO | null;
};
const ControlNetImagePreview = (props: Props) => {
const { controlNetId, controlImage, processedControlImage } = props;
const dispatch = useAppDispatch();
const { isProcessingControlImage } = useAppSelector(selector);
const [shouldShowProcessedImage, setShouldShowProcessedImage] =
useState(true);
const handleControlImageChanged = useCallback(
(controlImage: ImageDTO) => {
dispatch(controlNetImageChanged({ controlNetId, controlImage }));
},
[controlNetId, dispatch]
);
const handleControlImageReset = useCallback(() => {
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
}, [controlNetId, dispatch]);
const shouldShowProcessedImageBackdrop =
Number(controlImage?.width) > Number(processedControlImage?.width) ||
Number(controlImage?.height) > Number(processedControlImage?.height);
return (
<Box sx={{ position: 'relative', aspectRatio: '1/1' }}>
<IAISelectableImage
image={controlImage}
onChange={handleControlImageChanged}
onReset={handleControlImageReset}
isDropDisabled={Boolean(processedControlImage)}
fallback={<ProcessedImageFallback />}
withResetIcon
resetIconSize="sm"
/>
<AnimatePresence>
{controlImage &&
processedControlImage &&
shouldShowProcessedImage &&
!isProcessingControlImage && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
>
<Box
sx={{
position: 'absolute',
w: 'full',
h: 'full',
top: 0,
insetInlineStart: 0,
}}
>
{shouldShowProcessedImageBackdrop && (
<Box
sx={{
w: 'full',
h: 'full',
bg: 'base.900',
opacity: 0.7,
}}
/>
)}
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
w: 'full',
h: 'full',
}}
>
<IAISelectableImage
image={processedControlImage}
onChange={handleControlImageChanged}
onReset={handleControlImageReset}
withResetIcon
resetIconSize="sm"
fallback={<ProcessedImageFallback />}
/>
</Box>
</Box>
</motion.div>
)}
</AnimatePresence>
{isProcessingControlImage && (
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
w: 'full',
h: 'full',
}}
>
<ProcessedImageFallback />
</Box>
)}
{processedControlImage && !isProcessingControlImage && (
<Box sx={{ position: 'absolute', bottom: 0, insetInlineEnd: 0, p: 2 }}>
<IAIIconButton
aria-label="Hide Preview"
icon={<TbSquareToggle />}
size="sm"
onMouseOver={() => setShouldShowProcessedImage(false)}
onMouseOut={() => setShouldShowProcessedImage(true)}
/>
</Box>
)}
</Box>
);
};
export default memo(ControlNetImagePreview);
const ProcessedImageFallback = () => (
<Flex
sx={{
bg: 'base.900',
opacity: 0.7,
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
}}
>
<Spinner size="xl" />
</Flex>
);

View File

@ -12,7 +12,7 @@ import IAIIconButton from 'common/components/IAIIconButton';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { SyntheticEvent } from 'react'; import { ReactElement, SyntheticEvent } from 'react';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { FaImage, FaTimes } from 'react-icons/fa'; import { FaImage, FaTimes } from 'react-icons/fa';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
@ -26,14 +26,29 @@ type IAISelectableImageProps = {
onReset?: () => void; onReset?: () => void;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void; onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
resetIconSize?: IconButtonProps['size']; resetIconSize?: IconButtonProps['size'];
withResetIcon?: boolean;
withMetadataOverlay?: boolean;
isDropDisabled?: boolean;
fallback?: ReactElement;
}; };
const IAISelectableImage = (props: IAISelectableImageProps) => { const IAISelectableImage = (props: IAISelectableImageProps) => {
const { image, onChange, onReset, onError, resetIconSize = 'md' } = props; const {
image,
onChange,
onReset,
onError,
resetIconSize = 'md',
withResetIcon = false,
withMetadataOverlay = false,
isDropDisabled = false,
fallback = <ImageFallback />,
} = props;
const droppableId = useRef(uuidv4()); const droppableId = useRef(uuidv4());
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const { isOver, setNodeRef, active } = useDroppable({ const { isOver, setNodeRef, active } = useDroppable({
id: droppableId.current, id: droppableId.current,
disabled: isDropDisabled,
data: { data: {
handleDrop: onChange, handleDrop: onChange,
}, },
@ -54,6 +69,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
<Flex <Flex
sx={{ sx={{
w: 'full', w: 'full',
h: 'full',
position: 'relative', position: 'relative',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -62,15 +78,15 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
<Image <Image
src={getUrl(image.image_url)} src={getUrl(image.image_url)}
fallbackStrategy="beforeLoadOrError" fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallback />} fallback={fallback}
onError={onError} onError={onError}
draggable={false} draggable={false}
sx={{ sx={{
borderRadius: 'base', borderRadius: 'base',
}} }}
/> />
<ImageMetadataOverlay image={image} /> {withMetadataOverlay && <ImageMetadataOverlay image={image} />}
{onReset && ( {onReset && withResetIcon && (
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',

View File

@ -0,0 +1,123 @@
import {
FormControl,
FormLabel,
HStack,
RangeSlider,
RangeSliderFilledTrack,
RangeSliderMark,
RangeSliderThumb,
RangeSliderTrack,
Tooltip,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import {
controlNetBeginStepPctChanged,
controlNetEndStepPctChanged,
} from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BiReset } from 'react-icons/bi';
type Props = {
controlNetId: string;
beginStepPct: number;
endStepPct: number;
};
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
const ParamControlNetBeginEnd = (props: Props) => {
const { controlNetId, beginStepPct, endStepPct } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleStepPctChanged = useCallback(
(v: number[]) => {
dispatch(
controlNetBeginStepPctChanged({ controlNetId, beginStepPct: v[0] })
);
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: v[1] }));
},
[controlNetId, dispatch]
);
const handleStepPctReset = useCallback(() => {
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 1 }));
}, [controlNetId, dispatch]);
return (
<FormControl>
<FormLabel>Begin & End Step %</FormLabel>
<HStack w="100%" gap={2} alignItems="center">
<RangeSlider
aria-label={['Begin Step %', 'End Step %']}
value={[beginStepPct, endStepPct]}
onChange={handleStepPctChanged}
min={0}
max={1}
step={0.01}
minStepsBetweenThumbs={5}
>
<RangeSliderTrack>
<RangeSliderFilledTrack />
</RangeSliderTrack>
<Tooltip label={formatPct(beginStepPct)} placement="top" hasArrow>
<RangeSliderThumb index={0} />
</Tooltip>
<Tooltip label={formatPct(endStepPct)} placement="top" hasArrow>
<RangeSliderThumb index={1} />
</Tooltip>
<RangeSliderMark
value={0}
sx={{
fontSize: 'xs',
fontWeight: '500',
color: 'base.200',
insetInlineStart: '0 !important',
insetInlineEnd: 'unset !important',
mt: 1.5,
}}
>
0%
</RangeSliderMark>
<RangeSliderMark
value={0.5}
sx={{
fontSize: 'xs',
fontWeight: '500',
color: 'base.200',
mt: 1.5,
}}
>
50%
</RangeSliderMark>
<RangeSliderMark
value={1}
sx={{
fontSize: 'xs',
fontWeight: '500',
color: 'base.200',
insetInlineStart: 'unset !important',
insetInlineEnd: '0 !important',
mt: 1.5,
}}
>
100%
</RangeSliderMark>
</RangeSlider>
<IAIIconButton
size="sm"
aria-label={t('accessibility.reset')}
tooltip={t('accessibility.reset')}
icon={<BiReset />}
onClick={handleStepPctReset}
/>
</HStack>
</FormControl>
);
};
export default memo(ParamControlNetBeginEnd);

View File

@ -1,6 +1,9 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { controlNetBeginStepPctChanged } from 'features/controlNet/store/controlNetSlice'; import {
controlNetBeginStepPctChanged,
controlNetEndStepPctChanged,
} from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
type ParamControlNetBeginStepPctProps = { type ParamControlNetBeginStepPctProps = {
@ -21,9 +24,20 @@ const ParamControlNetBeginStepPct = (
[controlNetId, dispatch] [controlNetId, dispatch]
); );
const handleBeginStepPctReset = () => { const handleBeginStepPctReset = useCallback(() => {
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 })); dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
}; }, [controlNetId, dispatch]);
const handleEndStepPctChanged = useCallback(
(endStepPct: number) => {
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct }));
},
[controlNetId, dispatch]
);
const handleEndStepPctReset = useCallback(() => {
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 }));
}, [controlNetId, dispatch]);
return ( return (
<IAISlider <IAISlider

View File

@ -8,6 +8,7 @@ import {
RequiredControlNetProcessorNode, RequiredControlNetProcessorNode,
} from './types'; } from './types';
import { CONTROLNET_PROCESSORS } from './constants'; import { CONTROLNET_PROCESSORS } from './constants';
import { controlNetImageProcessed } from './actions';
export const CONTROLNET_MODELS = [ export const CONTROLNET_MODELS = [
'lllyasviel/sd-controlnet-canny', 'lllyasviel/sd-controlnet-canny',
@ -52,12 +53,14 @@ export type ControlNetState = {
controlNets: Record<string, ControlNet>; controlNets: Record<string, ControlNet>;
isEnabled: boolean; isEnabled: boolean;
shouldAutoProcess: boolean; shouldAutoProcess: boolean;
isProcessingControlImage: boolean;
}; };
export const initialControlNetState: ControlNetState = { export const initialControlNetState: ControlNetState = {
controlNets: {}, controlNets: {},
isEnabled: false, isEnabled: false,
shouldAutoProcess: true, shouldAutoProcess: true,
isProcessingControlImage: false,
}; };
export const controlNetSlice = createSlice({ export const controlNetSlice = createSlice({
@ -107,6 +110,9 @@ export const controlNetSlice = createSlice({
const { controlNetId, controlImage } = action.payload; const { controlNetId, controlImage } = action.payload;
state.controlNets[controlNetId].controlImage = controlImage; state.controlNets[controlNetId].controlImage = controlImage;
state.controlNets[controlNetId].processedControlImage = null; state.controlNets[controlNetId].processedControlImage = null;
if (state.shouldAutoProcess && controlImage !== null) {
state.isProcessingControlImage = true;
}
}, },
isControlNetImageProcessedToggled: ( isControlNetImageProcessedToggled: (
state, state,
@ -128,6 +134,7 @@ export const controlNetSlice = createSlice({
const { controlNetId, processedControlImage } = action.payload; const { controlNetId, processedControlImage } = action.payload;
state.controlNets[controlNetId].processedControlImage = state.controlNets[controlNetId].processedControlImage =
processedControlImage; processedControlImage;
state.isProcessingControlImage = false;
}, },
controlNetModelChanged: ( controlNetModelChanged: (
state, state,
@ -190,6 +197,15 @@ export const controlNetSlice = createSlice({
state.shouldAutoProcess = !state.shouldAutoProcess; state.shouldAutoProcess = !state.shouldAutoProcess;
}, },
}, },
extraReducers: (builder) => {
builder.addCase(controlNetImageProcessed, (state, action) => {
if (
state.controlNets[action.payload.controlNetId].controlImage !== null
) {
state.isProcessingControlImage = true;
}
});
},
}); });
export const { export const {

View File

@ -8,7 +8,7 @@ import { isEqual } from 'lodash-es';
import { gallerySelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
import { DragEvent, memo, useCallback } from 'react'; import { DragEvent, memo, useCallback, useEffect, useState } from 'react';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import ImageFallbackSpinner from './ImageFallbackSpinner'; import ImageFallbackSpinner from './ImageFallbackSpinner';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
@ -55,6 +55,7 @@ const CurrentImagePreview = () => {
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const toaster = useAppToaster(); const toaster = useAppToaster();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isLoaded, setIsLoaded] = useState(false);
const { attributes, listeners, setNodeRef } = useDraggable({ const { attributes, listeners, setNodeRef } = useDraggable({
id: `currentImage_${image?.image_name}`, id: `currentImage_${image?.image_name}`,
@ -74,11 +75,15 @@ const CurrentImagePreview = () => {
} }
}, [dispatch, toaster, shouldFetchImages]); }, [dispatch, toaster, shouldFetchImages]);
useEffect(() => {
setIsLoaded(false);
}, [image]);
return ( return (
<Flex <Flex
sx={{ sx={{
width: '100%', width: 'full',
height: '100%', height: 'full',
position: 'relative', position: 'relative',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -91,8 +96,8 @@ const CurrentImagePreview = () => {
height={progressImage.height} height={progressImage.height}
sx={{ sx={{
objectFit: 'contain', objectFit: 'contain',
maxWidth: '100%', maxWidth: 'full',
maxHeight: '100%', maxHeight: 'full',
height: 'auto', height: 'auto',
position: 'absolute', position: 'absolute',
borderRadius: 'base', borderRadius: 'base',
@ -124,8 +129,11 @@ const CurrentImagePreview = () => {
touchAction: 'none', touchAction: 'none',
}} }}
onError={handleError} onError={handleError}
onLoad={() => {
setIsLoaded(true);
}}
/> />
<ImageMetadataOverlay image={image} /> {isLoaded && <ImageMetadataOverlay image={image} />}
</Flex> </Flex>
) )
)} )}

View File

@ -14,7 +14,8 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
justifyContent: 'center', justifyContent: 'center',
position: 'absolute', position: 'absolute',
color: 'base.400', color: 'base.400',
minH: 40, minH: 36,
minW: 36,
}} }}
> >
<Spinner size={size} {...rest} /> <Spinner size={size} {...rest} />