mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): gallery & progress image refactor
This commit is contained in:
parent
3601b9c860
commit
270657a62c
@ -61,6 +61,8 @@
|
||||
"@chakra-ui/styled-system": "^2.9.0",
|
||||
"@chakra-ui/theme-tools": "^2.0.16",
|
||||
"@dagrejs/graphlib": "^2.1.12",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
|
@ -645,5 +645,9 @@
|
||||
"betaDarkenOutside": "Darken Outside",
|
||||
"betaLimitToBox": "Limit To Box",
|
||||
"betaPreserveMasked": "Preserve Masked"
|
||||
},
|
||||
"ui": {
|
||||
"showProgressImages": "Show Progress Images",
|
||||
"hideProgressImages": "Hide Progress Images"
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,16 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { floatingProgressImageMoved } from 'features/ui/store/uiSlice';
|
||||
import { restrictToWindowEdges } from '@dnd-kit/modifiers';
|
||||
|
||||
const DEFAULT_CONFIG = {};
|
||||
|
||||
@ -40,6 +50,9 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
|
||||
const log = useLogger();
|
||||
|
||||
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
||||
const shouldShowProgressImage = useAppSelector(
|
||||
(state) => state.ui.shouldShowProgressImage
|
||||
);
|
||||
|
||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
||||
|
||||
@ -63,64 +76,90 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
|
||||
setLoadingOverridden(true);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
dispatch(
|
||||
floatingProgressImageMoved({ x: event.delta.x, y: event.delta.y })
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const pointer = useSensor(PointerSensor, {
|
||||
// Delay the drag events (allow clicks on elements)
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const sensors = useSensors(pointer);
|
||||
|
||||
return (
|
||||
<Grid w="100vw" h="100vh" position="relative">
|
||||
{isLightboxEnabled && <Lightbox />}
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
{children || <SiteHeader />}
|
||||
<Flex
|
||||
<DndContext
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToWindowEdges]}
|
||||
>
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{isLightboxEnabled && <Lightbox />}
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
w={{ base: '100vw', xl: 'full' }}
|
||||
h="full"
|
||||
flexDir={{ base: 'column', xl: 'row' }}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
<InvokeTabs />
|
||||
<ImageGalleryPanel />
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ImageUploader>
|
||||
{children || <SiteHeader />}
|
||||
<Flex
|
||||
gap={4}
|
||||
w={{ base: '100vw', xl: 'full' }}
|
||||
h="full"
|
||||
flexDir={{ base: 'column', xl: 'row' }}
|
||||
>
|
||||
<InvokeTabs />
|
||||
<ImageGalleryPanel />
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ImageUploader>
|
||||
|
||||
<AnimatePresence>
|
||||
{!isApplicationReady && !loadingOverridden && (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ zIndex: 3 }}
|
||||
>
|
||||
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
||||
<Loading />
|
||||
</Box>
|
||||
<Box
|
||||
onClick={handleOverrideClicked}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
cursor="pointer"
|
||||
w="2rem"
|
||||
h="2rem"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{!isApplicationReady && !loadingOverridden && (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ zIndex: 3 }}
|
||||
>
|
||||
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
||||
<Loading />
|
||||
</Box>
|
||||
<Box
|
||||
onClick={handleOverrideClicked}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
cursor="pointer"
|
||||
w="2rem"
|
||||
h="2rem"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
<ProgressImagePreview />
|
||||
</Grid>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,11 @@
|
||||
import React, { lazy, memo, PropsWithChildren, useEffect } from 'react';
|
||||
import React, {
|
||||
lazy,
|
||||
memo,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { store } from 'app/store/store';
|
||||
|
@ -233,7 +233,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
|
||||
hidden={hideTooltip}
|
||||
{...sliderTooltipProps}
|
||||
>
|
||||
<SliderThumb {...sliderThumbProps} />
|
||||
<SliderThumb {...sliderThumbProps} zIndex={0} />
|
||||
</Tooltip>
|
||||
</Slider>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
||||
import * as InvokeAI from 'app/types/invokeai';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { addImage } from 'features/gallery/store/gallerySlice';
|
||||
// import { addImage } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
addToast,
|
||||
setCurrentStatus,
|
||||
|
@ -17,31 +17,11 @@ export const imagesSelector = createSelector(
|
||||
[uiSelector, selectedImageSelector, systemSelector],
|
||||
(ui, selectedImage, system) => {
|
||||
const { shouldShowImageDetails, shouldHidePreview } = ui;
|
||||
const { progressImage } = system;
|
||||
|
||||
// TODO: Clean this up, this is really gross
|
||||
const imageToDisplay = progressImage
|
||||
? {
|
||||
url: progressImage.dataURL,
|
||||
width: progressImage.width,
|
||||
height: progressImage.height,
|
||||
isProgressImage: true,
|
||||
image: progressImage,
|
||||
}
|
||||
: selectedImage
|
||||
? {
|
||||
url: selectedImage.url,
|
||||
width: selectedImage.metadata.width,
|
||||
height: selectedImage.metadata.height,
|
||||
isProgressImage: false,
|
||||
image: selectedImage,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
shouldShowImageDetails,
|
||||
shouldHidePreview,
|
||||
imageToDisplay,
|
||||
image: selectedImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -52,7 +32,7 @@ export const imagesSelector = createSelector(
|
||||
);
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const { shouldShowImageDetails, imageToDisplay, shouldHidePreview } =
|
||||
const { shouldShowImageDetails, image, shouldHidePreview } =
|
||||
useAppSelector(imagesSelector);
|
||||
const { getUrl } = useGetUrl();
|
||||
|
||||
@ -66,54 +46,37 @@ const CurrentImagePreview = () => {
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{imageToDisplay && (
|
||||
{image && (
|
||||
<Image
|
||||
src={
|
||||
shouldHidePreview
|
||||
? undefined
|
||||
: imageToDisplay.isProgressImage
|
||||
? imageToDisplay.url
|
||||
: getUrl(imageToDisplay.url)
|
||||
}
|
||||
width={imageToDisplay.width}
|
||||
height={imageToDisplay.height}
|
||||
fallback={
|
||||
shouldHidePreview ? (
|
||||
<CurrentImageHidden />
|
||||
) : !imageToDisplay.isProgressImage ? (
|
||||
<CurrentImageFallback />
|
||||
) : undefined
|
||||
}
|
||||
src={shouldHidePreview ? undefined : getUrl(image.url)}
|
||||
width={image.metadata.width}
|
||||
height={image.metadata.height}
|
||||
fallback={shouldHidePreview ? <CurrentImageHidden /> : undefined}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: 'auto',
|
||||
position: 'absolute',
|
||||
imageRendering: imageToDisplay.isProgressImage
|
||||
? 'pixelated'
|
||||
: 'initial',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
||||
{shouldShowImageDetails &&
|
||||
imageToDisplay &&
|
||||
'metadata' in imageToDisplay.image && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'base',
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay.image} />
|
||||
</Box>
|
||||
)}
|
||||
{shouldShowImageDetails && image && 'metadata' in image && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'base',
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
>
|
||||
<ImageMetadataViewer image={image} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -125,7 +125,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
}, [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]);
|
||||
|
||||
const handleSelectImage = useCallback(() => {
|
||||
dispatch(imageSelected(image.name));
|
||||
dispatch(imageSelected(image));
|
||||
}, [image, dispatch]);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
|
@ -49,7 +49,7 @@ const gallerySelector = createSelector(
|
||||
(uploads, results, gallery) => {
|
||||
const { currentCategory } = gallery;
|
||||
|
||||
return currentCategory === 'result'
|
||||
return currentCategory === 'results'
|
||||
? {
|
||||
images: resultsAdapter.getSelectors().selectAll(results),
|
||||
isLoading: results.isLoading,
|
||||
@ -72,7 +72,6 @@ const ImageGalleryContent = () => {
|
||||
const {
|
||||
// images,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryGridTemplateColumns,
|
||||
@ -80,6 +79,7 @@ const ImageGalleryContent = () => {
|
||||
shouldAutoSwitchToNewImages,
|
||||
// areMoreImagesAvailable,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
} = useAppSelector(imageGallerySelector);
|
||||
|
||||
const { images, areMoreImagesAvailable, isLoading } =
|
||||
@ -89,11 +89,11 @@ const ImageGalleryContent = () => {
|
||||
// dispatch(requestImages(currentCategory));
|
||||
// };
|
||||
const handleClickLoadMore = () => {
|
||||
if (currentCategory === 'result') {
|
||||
if (currentCategory === 'results') {
|
||||
dispatch(receivedResultImagesPage());
|
||||
}
|
||||
|
||||
if (currentCategory === 'user') {
|
||||
if (currentCategory === 'uploads') {
|
||||
dispatch(receivedUploadImagesPage());
|
||||
}
|
||||
};
|
||||
@ -147,34 +147,34 @@ const ImageGalleryContent = () => {
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showGenerations')}
|
||||
tooltip={t('gallery.showGenerations')}
|
||||
isChecked={currentCategory === 'result'}
|
||||
isChecked={currentCategory === 'results'}
|
||||
role="radio"
|
||||
icon={<FaImage />}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showUploads')}
|
||||
tooltip={t('gallery.showUploads')}
|
||||
role="radio"
|
||||
isChecked={currentCategory === 'user'}
|
||||
isChecked={currentCategory === 'uploads'}
|
||||
icon={<FaUser />}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'result'}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
isChecked={currentCategory === 'results'}
|
||||
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.generations')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'user'}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
isChecked={currentCategory === 'uploads'}
|
||||
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.uploads')}
|
||||
@ -251,7 +251,7 @@ const ImageGalleryContent = () => {
|
||||
>
|
||||
{images.map((image) => {
|
||||
const { name } = image;
|
||||
const isSelected = currentImageUuid === name;
|
||||
const isSelected = selectedImage?.name === name;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={`${name}-${image.thumbnail}`}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
// selectNextImage,
|
||||
// selectPrevImage,
|
||||
setGalleryImageMinimumWidth,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
|
@ -6,11 +6,13 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import {
|
||||
GalleryCategory,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
} from '../store/gallerySlice';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { selectResultsEntities } from '../store/resultsSlice';
|
||||
// import {
|
||||
// GalleryCategory,
|
||||
// selectNextImage,
|
||||
// selectPrevImage,
|
||||
// } from '../store/gallerySlice';
|
||||
|
||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
||||
height: '100%',
|
||||
@ -23,19 +25,22 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
|
||||
};
|
||||
|
||||
export const nextPrevImageButtonsSelector = createSelector(
|
||||
gallerySelector,
|
||||
(gallery) => {
|
||||
const { currentImage } = gallery;
|
||||
[(state: RootState) => state, gallerySelector],
|
||||
(state, gallery) => {
|
||||
const { selectedImage, currentCategory } = gallery;
|
||||
|
||||
const tempImages =
|
||||
gallery.categories[
|
||||
currentImage ? (currentImage.category as GalleryCategory) : 'result'
|
||||
].images;
|
||||
if (!selectedImage) {
|
||||
return {
|
||||
isOnFirstImage: true,
|
||||
isOnLastImage: true,
|
||||
};
|
||||
}
|
||||
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||
const currentImageIndex = state[currentCategory].ids.findIndex(
|
||||
(i) => i === selectedImage.name
|
||||
);
|
||||
const imagesLength = tempImages.length;
|
||||
|
||||
const imagesLength = state[currentCategory].ids.length;
|
||||
|
||||
return {
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
@ -81,7 +86,6 @@ const NextPrevImageButtons = () => {
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
zIndex: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
pointerEvents: 'none',
|
||||
|
@ -22,17 +22,22 @@ import {
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[gallerySelector, uiSelector, lightboxSelector, activeTabNameSelector],
|
||||
(gallery, ui, lightbox, activeTabName) => {
|
||||
[
|
||||
(state: RootState) => state,
|
||||
gallerySelector,
|
||||
uiSelector,
|
||||
lightboxSelector,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(state, gallery, ui, lightbox, activeTabName) => {
|
||||
const {
|
||||
categories,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
galleryWidth,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
} = gallery;
|
||||
|
||||
const { shouldPinGallery } = ui;
|
||||
@ -40,7 +45,6 @@ export const imageGallerySelector = createSelector(
|
||||
const { isLightboxOpen } = lightbox;
|
||||
|
||||
return {
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
@ -49,9 +53,7 @@ export const imageGallerySelector = createSelector(
|
||||
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||
shouldAutoSwitchToNewImages,
|
||||
currentCategory,
|
||||
images: categories[currentCategory].images,
|
||||
areMoreImagesAvailable:
|
||||
categories[currentCategory].areMoreImagesAvailable,
|
||||
images: state[currentCategory].entities,
|
||||
galleryWidth,
|
||||
shouldEnableResize:
|
||||
isLightboxOpen ||
|
||||
@ -59,6 +61,7 @@ export const imageGallerySelector = createSelector(
|
||||
? false
|
||||
: true,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -69,16 +72,16 @@ export const imageGallerySelector = createSelector(
|
||||
);
|
||||
|
||||
export const selectedImageSelector = createSelector(
|
||||
[gallerySelector, selectResultsEntities, selectUploadsEntities],
|
||||
(gallery, allResults, allUploads) => {
|
||||
const selectedImageName = gallery.selectedImageName;
|
||||
[(state: RootState) => state, gallerySelector],
|
||||
(state, gallery) => {
|
||||
const selectedImage = gallery.selectedImage;
|
||||
|
||||
if (selectedImageName in allResults) {
|
||||
return allResults[selectedImageName];
|
||||
if (selectedImage?.type === 'results') {
|
||||
return selectResultsById(state, selectedImage.name);
|
||||
}
|
||||
|
||||
if (selectedImageName in allUploads) {
|
||||
return allUploads[selectedImageName];
|
||||
if (selectedImage?.type === 'uploads') {
|
||||
return selectUploadsById(state, selectedImage.name);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -1,260 +1,80 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import * as InvokeAI from 'app/types/invokeai';
|
||||
import { invocationComplete } from 'services/events/actions';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
|
||||
export type GalleryCategory = 'user' | 'result';
|
||||
|
||||
export type AddImagesPayload = {
|
||||
images: Array<InvokeAI._Image>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
import { SelectedImage } from 'features/parameters/store/generationSlice';
|
||||
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
export type Gallery = {
|
||||
images: InvokeAI._Image[];
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
areMoreImagesAvailable: boolean;
|
||||
};
|
||||
|
||||
export interface GalleryState {
|
||||
/**
|
||||
* The selected image's unique name
|
||||
* Use `selectedImageSelector` to access the image
|
||||
* The selected image
|
||||
*/
|
||||
selectedImageName: string;
|
||||
/**
|
||||
* The currently selected image
|
||||
* @deprecated See `state.gallery.selectedImageName`
|
||||
*/
|
||||
currentImage?: InvokeAI._Image;
|
||||
/**
|
||||
* The currently selected image's uuid.
|
||||
* @deprecated See `state.gallery.selectedImageName`, use `selectedImageSelector` to access the image
|
||||
*/
|
||||
currentImageUuid: string;
|
||||
/**
|
||||
* The current progress image
|
||||
* @deprecated See `state.system.progressImage`
|
||||
*/
|
||||
intermediateImage?: InvokeAI._Image & {
|
||||
boundingBox?: IRect;
|
||||
generationMode?: InvokeTabName;
|
||||
};
|
||||
selectedImage?: SelectedImage;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
categories: {
|
||||
user: Gallery;
|
||||
result: Gallery;
|
||||
};
|
||||
currentCategory: GalleryCategory;
|
||||
galleryWidth: number;
|
||||
shouldUseSingleGalleryColumn: boolean;
|
||||
currentCategory: 'results' | 'uploads';
|
||||
}
|
||||
|
||||
const initialState: GalleryState = {
|
||||
selectedImageName: '',
|
||||
currentImageUuid: '',
|
||||
selectedImage: undefined,
|
||||
galleryImageMinimumWidth: 64,
|
||||
galleryImageObjectFit: 'cover',
|
||||
shouldAutoSwitchToNewImages: true,
|
||||
currentCategory: 'result',
|
||||
categories: {
|
||||
user: {
|
||||
images: [],
|
||||
latest_mtime: undefined,
|
||||
earliest_mtime: undefined,
|
||||
areMoreImagesAvailable: true,
|
||||
},
|
||||
result: {
|
||||
images: [],
|
||||
latest_mtime: undefined,
|
||||
earliest_mtime: undefined,
|
||||
areMoreImagesAvailable: true,
|
||||
},
|
||||
},
|
||||
galleryWidth: 300,
|
||||
shouldUseSingleGalleryColumn: false,
|
||||
currentCategory: 'results',
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState,
|
||||
reducers: {
|
||||
imageSelected: (state, action: PayloadAction<string>) => {
|
||||
state.selectedImageName = action.payload;
|
||||
},
|
||||
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
|
||||
state.currentImage = action.payload;
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
},
|
||||
removeImage: (
|
||||
imageSelected: (
|
||||
state,
|
||||
action: PayloadAction<InvokeAI.ImageDeletedResponse>
|
||||
action: PayloadAction<SelectedImage | undefined>
|
||||
) => {
|
||||
const { uuid, category } = action.payload;
|
||||
|
||||
const tempImages = state.categories[category as GalleryCategory].images;
|
||||
|
||||
const newImages = tempImages.filter((image) => image.uuid !== uuid);
|
||||
|
||||
if (uuid === state.currentImageUuid) {
|
||||
/**
|
||||
* We are deleting the currently selected image.
|
||||
*
|
||||
* We want the new currentl selected image to be under the cursor in the
|
||||
* gallery, so we need to do some fanagling. The currently selected image
|
||||
* is set by its UUID, not its index in the image list.
|
||||
*
|
||||
* Get the currently selected image's index.
|
||||
*/
|
||||
const imageToDeleteIndex = tempImages.findIndex(
|
||||
(image) => image.uuid === uuid
|
||||
);
|
||||
|
||||
/**
|
||||
* New current image needs to be in the same spot, but because the gallery
|
||||
* is sorted in reverse order, the new current image's index will actuall be
|
||||
* one less than the deleted image's index.
|
||||
*
|
||||
* Clamp the new index to ensure it is valid..
|
||||
*/
|
||||
const newCurrentImageIndex = clamp(
|
||||
imageToDeleteIndex,
|
||||
0,
|
||||
newImages.length - 1
|
||||
);
|
||||
|
||||
state.currentImage = newImages.length
|
||||
? newImages[newCurrentImageIndex]
|
||||
: undefined;
|
||||
|
||||
state.currentImageUuid = newImages.length
|
||||
? newImages[newCurrentImageIndex].uuid
|
||||
: '';
|
||||
}
|
||||
|
||||
state.categories[category as GalleryCategory].images = newImages;
|
||||
state.selectedImage = action.payload;
|
||||
},
|
||||
addImage: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
image: InvokeAI._Image;
|
||||
category: GalleryCategory;
|
||||
}>
|
||||
) => {
|
||||
const { image: newImage, category } = action.payload;
|
||||
const { uuid, url, mtime } = newImage;
|
||||
// selectNextImage: (state) => {
|
||||
// const { currentImage } = state;
|
||||
// if (!currentImage) return;
|
||||
// const tempImages =
|
||||
// state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
const tempCategory = state.categories[category as GalleryCategory];
|
||||
// if (currentImage) {
|
||||
// const currentImageIndex = tempImages.findIndex(
|
||||
// (i) => i.uuid === currentImage.uuid
|
||||
// );
|
||||
// if (currentImageIndex < tempImages.length - 1) {
|
||||
// const newCurrentImage = tempImages[currentImageIndex + 1];
|
||||
// state.currentImage = newCurrentImage;
|
||||
// state.currentImageUuid = newCurrentImage.uuid;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// selectPrevImage: (state) => {
|
||||
// const { currentImage } = state;
|
||||
// if (!currentImage) return;
|
||||
// const tempImages =
|
||||
// state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
// Do not add duplicate images
|
||||
if (tempCategory.images.find((i) => i.url === url && i.mtime === mtime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tempCategory.images.unshift(newImage);
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.currentImageUuid = uuid;
|
||||
state.currentImage = newImage;
|
||||
state.currentCategory = category;
|
||||
}
|
||||
state.intermediateImage = undefined;
|
||||
tempCategory.latest_mtime = mtime;
|
||||
},
|
||||
setIntermediateImage: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
InvokeAI._Image & {
|
||||
boundingBox?: IRect;
|
||||
generationMode?: InvokeTabName;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
state.intermediateImage = action.payload;
|
||||
},
|
||||
clearIntermediateImage: (state) => {
|
||||
state.intermediateImage = undefined;
|
||||
},
|
||||
selectNextImage: (state) => {
|
||||
const { currentImage } = state;
|
||||
if (!currentImage) return;
|
||||
const tempImages =
|
||||
state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (currentImageIndex < tempImages.length - 1) {
|
||||
const newCurrentImage = tempImages[currentImageIndex + 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectPrevImage: (state) => {
|
||||
const { currentImage } = state;
|
||||
if (!currentImage) return;
|
||||
const tempImages =
|
||||
state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (currentImageIndex > 0) {
|
||||
const newCurrentImage = tempImages[currentImageIndex - 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
addGalleryImages: (state, action: PayloadAction<AddImagesPayload>) => {
|
||||
const { images, areMoreImagesAvailable, category } = action.payload;
|
||||
const tempImages = state.categories[category].images;
|
||||
|
||||
// const prevImages = category === 'user' ? state.userImages : state.resultImages
|
||||
|
||||
if (images.length > 0) {
|
||||
// Filter images that already exist in the gallery
|
||||
const newImages = images.filter(
|
||||
(newImage) =>
|
||||
!tempImages.find(
|
||||
(i) => i.url === newImage.url && i.mtime === newImage.mtime
|
||||
)
|
||||
);
|
||||
state.categories[category].images = tempImages
|
||||
.concat(newImages)
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (!state.currentImage) {
|
||||
const newCurrentImage = images[0];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
|
||||
// keep track of the timestamps of latest and earliest images received
|
||||
state.categories[category].latest_mtime = images[0].mtime;
|
||||
state.categories[category].earliest_mtime =
|
||||
images[images.length - 1].mtime;
|
||||
}
|
||||
|
||||
if (areMoreImagesAvailable !== undefined) {
|
||||
state.categories[category].areMoreImagesAvailable =
|
||||
areMoreImagesAvailable;
|
||||
}
|
||||
},
|
||||
// if (currentImage) {
|
||||
// const currentImageIndex = tempImages.findIndex(
|
||||
// (i) => i.uuid === currentImage.uuid
|
||||
// );
|
||||
// if (currentImageIndex > 0) {
|
||||
// const newCurrentImage = tempImages[currentImageIndex - 1];
|
||||
// state.currentImage = newCurrentImage;
|
||||
// state.currentImageUuid = newCurrentImage.uuid;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
@ -267,7 +87,10 @@ export const gallerySlice = createSlice({
|
||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldAutoSwitchToNewImages = action.payload;
|
||||
},
|
||||
setCurrentCategory: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
setCurrentCategory: (
|
||||
state,
|
||||
action: PayloadAction<'results' | 'uploads'>
|
||||
) => {
|
||||
state.currentCategory = action.payload;
|
||||
},
|
||||
setGalleryWidth: (state, action: PayloadAction<number>) => {
|
||||
@ -286,9 +109,11 @@ export const gallerySlice = createSlice({
|
||||
*/
|
||||
builder.addCase(invocationComplete, (state, action) => {
|
||||
const { data } = action.payload;
|
||||
if (isImageOutput(data.result)) {
|
||||
state.selectedImageName = data.result.image.image_name;
|
||||
state.intermediateImage = undefined;
|
||||
if (isImageOutput(data.result) && state.shouldAutoSwitchToNewImages) {
|
||||
state.selectedImage = {
|
||||
name: data.result.image.image_name,
|
||||
type: 'results',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -299,27 +124,19 @@ export const gallerySlice = createSlice({
|
||||
const { response } = action.payload;
|
||||
|
||||
const uploadedImage = deserializeImageResponse(response);
|
||||
state.selectedImageName = uploadedImage.name;
|
||||
state.selectedImage = { name: uploadedImage.name, type: 'uploads' };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
imageSelected,
|
||||
addImage,
|
||||
clearIntermediateImage,
|
||||
removeImage,
|
||||
setCurrentImage,
|
||||
addGalleryImages,
|
||||
setIntermediateImage,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setCurrentCategory,
|
||||
setGalleryWidth,
|
||||
setShouldUseSingleGalleryColumn,
|
||||
setCurrentCategory,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
@ -0,0 +1,168 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
Image,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { memo, useCallback, useRef, MouseEvent } from 'react';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { FaEye, FaImage } from 'react-icons/fa';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { useBoolean, useHoverDirty } from 'react-use';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createSelector([systemSelector, uiSelector], (system, ui) => {
|
||||
const { progressImage } = system;
|
||||
const { floatingProgressImageCoordinates, shouldShowProgressImage } = ui;
|
||||
|
||||
return {
|
||||
progressImage,
|
||||
coords: floatingProgressImageCoordinates,
|
||||
shouldShowProgressImage,
|
||||
};
|
||||
});
|
||||
|
||||
const ProgressImagePreview = () => {
|
||||
const { progressImage, coords, shouldShowProgressImage } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const [shouldShowProgressImages, toggleShouldShowProgressImages] =
|
||||
useBoolean(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: 'progress-image',
|
||||
});
|
||||
|
||||
const transformStyles = transform
|
||||
? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
}
|
||||
: {};
|
||||
|
||||
return shouldShowProgressImages ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: `${coords.x}px`,
|
||||
top: `${coords.y}px`,
|
||||
}}
|
||||
>
|
||||
<Box ref={setNodeRef} sx={transformStyles}>
|
||||
<Box
|
||||
sx={{
|
||||
boxShadow: 'dark-lg',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.800',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Resizable
|
||||
defaultSize={{ width: 300, height: 300 }}
|
||||
minWidth={200}
|
||||
minHeight={200}
|
||||
boundsByDirection={true}
|
||||
enable={{ bottomRight: true }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
cursor: 'move',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'relative',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
{progressImage ? (
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'relative',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={progressImage.dataURL}
|
||||
width={progressImage.width}
|
||||
height={progressImage.height}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
objectFit: 'contain',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: 'auto',
|
||||
imageRendering: 'pixelated',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon color="base.400" boxSize={32} as={FaImage}></Icon>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<IAIIconButton
|
||||
onClick={toggleShouldShowProgressImages}
|
||||
aria-label={t('ui.hideProgressImages')}
|
||||
size="xs"
|
||||
icon={<CloseIcon />}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
insetInlineEnd: 2,
|
||||
}}
|
||||
variant="ghost"
|
||||
/>
|
||||
</Flex>
|
||||
</Resizable>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<IAIIconButton
|
||||
onClick={toggleShouldShowProgressImages}
|
||||
tooltip={t('ui.showProgressImages')}
|
||||
sx={{ position: 'absolute', bottom: 4, insetInlineStart: 4 }}
|
||||
aria-label={t('ui.showProgressImages')}
|
||||
icon={<FaEye />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ProgressImagePreview);
|
@ -7,7 +7,7 @@ import { seedWeightsToString } from 'common/util/seedWeightPairs';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { ImageField, ImageType } from 'services/api';
|
||||
|
||||
export type InitialImage = {
|
||||
export type SelectedImage = {
|
||||
name: string;
|
||||
type: ImageType;
|
||||
};
|
||||
@ -17,7 +17,7 @@ export interface GenerationState {
|
||||
height: number;
|
||||
img2imgStrength: number;
|
||||
infillMethod: string;
|
||||
initialImage?: InitialImage; // can be an Image or url
|
||||
initialImage?: SelectedImage; // can be an Image or url
|
||||
iterations: number;
|
||||
maskPath: string;
|
||||
perlin: number;
|
||||
@ -351,7 +351,7 @@ export const generationSlice = createSlice({
|
||||
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
|
||||
state.verticalSymmetrySteps = action.payload;
|
||||
},
|
||||
initialImageSelected: (state, action: PayloadAction<InitialImage>) => {
|
||||
initialImageSelected: (state, action: PayloadAction<SelectedImage>) => {
|
||||
state.initialImage = action.payload;
|
||||
state.isImageToImageEnabled = true;
|
||||
},
|
||||
|
@ -307,7 +307,7 @@ export const systemSlice = createSlice({
|
||||
state.totalSteps = 0;
|
||||
// state.currentIteration = 0;
|
||||
// state.totalIterations = 0;
|
||||
state.statusTranslationKey = 'common.statusPreparing';
|
||||
state.statusTranslationKey = 'common.statusGenerating';
|
||||
});
|
||||
|
||||
/**
|
||||
@ -347,7 +347,6 @@ export const systemSlice = createSlice({
|
||||
state.currentStatusHasSteps = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.progressImage = null;
|
||||
state.statusTranslationKey = 'common.statusProcessingComplete';
|
||||
});
|
||||
|
||||
@ -364,7 +363,6 @@ export const systemSlice = createSlice({
|
||||
state.currentStatusHasSteps = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.progressImage = null;
|
||||
state.statusTranslationKey = 'common.statusError';
|
||||
|
||||
state.toastQueue.push(
|
||||
@ -391,7 +389,6 @@ export const systemSlice = createSlice({
|
||||
state.isCancelScheduled = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.progressImage = null;
|
||||
state.statusTranslationKey = 'common.statusConnected';
|
||||
|
||||
state.toastQueue.push(
|
||||
@ -410,7 +407,6 @@ export const systemSlice = createSlice({
|
||||
state.isCancelScheduled = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.progressImage = null;
|
||||
state.statusTranslationKey = 'common.statusConnected';
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
|
||||
|
||||
const GenerateContent = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'base',
|
||||
|
@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import { setActiveTabReducer } from './extraReducers';
|
||||
import { InvokeTabName, tabMap } from './tabMap';
|
||||
import { AddNewModelType, UIState } from './uiTypes';
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types';
|
||||
|
||||
const initialUIState: UIState = {
|
||||
activeTab: 0,
|
||||
@ -21,6 +22,8 @@ const initialUIState: UIState = {
|
||||
openLinearAccordionItems: [],
|
||||
openGenerateAccordionItems: [],
|
||||
openUnifiedCanvasAccordionItems: [],
|
||||
floatingProgressImageCoordinates: { x: 0, y: 0 },
|
||||
shouldShowProgressImage: false,
|
||||
};
|
||||
|
||||
const initialState: UIState = initialUIState;
|
||||
@ -105,6 +108,15 @@ export const uiSlice = createSlice({
|
||||
state.openUnifiedCanvasAccordionItems = action.payload;
|
||||
}
|
||||
},
|
||||
floatingProgressImageMoved: (state, action: PayloadAction<Coordinates>) => {
|
||||
const { x, y } = state.floatingProgressImageCoordinates;
|
||||
const { x: _x, y: _y } = action.payload;
|
||||
|
||||
state.floatingProgressImageCoordinates = { x: x + _x, y: y + _y };
|
||||
},
|
||||
shouldShowProgressImageChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowProgressImage = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -128,6 +140,8 @@ export const {
|
||||
toggleParametersPanel,
|
||||
toggleGalleryPanel,
|
||||
openAccordionItemsChanged,
|
||||
floatingProgressImageMoved,
|
||||
shouldShowProgressImageChanged,
|
||||
} = uiSlice.actions;
|
||||
|
||||
export default uiSlice.reducer;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { InvokeTabName } from './tabMap';
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types';
|
||||
|
||||
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
|
||||
|
||||
@ -19,4 +19,6 @@ export interface UIState {
|
||||
openLinearAccordionItems: number[];
|
||||
openGenerateAccordionItems: number[];
|
||||
openUnifiedCanvasAccordionItems: number[];
|
||||
floatingProgressImageCoordinates: Coordinates;
|
||||
shouldShowProgressImage: boolean;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { clamp, isString } from 'lodash-es';
|
||||
import { ImagesService } from 'services/api';
|
||||
import { getHeaders } from 'services/util/getHeaders';
|
||||
|
||||
@ -85,7 +85,7 @@ export const imageDeleted = createAppAsyncThunk(
|
||||
// Determine which image should replace the deleted image, if the deleted image is the selected image.
|
||||
// Unfortunately, we have to do this here, because the resultsSlice and uploadsSlice cannot change
|
||||
// the selected image.
|
||||
const selectedImageName = getState().gallery.selectedImageName;
|
||||
const selectedImageName = getState().gallery.selectedImage?.name;
|
||||
|
||||
if (selectedImageName === imageName) {
|
||||
const allIds = getState()[imageType].ids;
|
||||
@ -104,9 +104,13 @@ export const imageDeleted = createAppAsyncThunk(
|
||||
|
||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||
|
||||
dispatch(
|
||||
imageSelected(newSelectedImageId ? newSelectedImageId.toString() : '')
|
||||
);
|
||||
if (newSelectedImageId) {
|
||||
dispatch(
|
||||
imageSelected({ name: newSelectedImageId as string, type: imageType })
|
||||
);
|
||||
} else {
|
||||
dispatch(imageSelected());
|
||||
}
|
||||
}
|
||||
|
||||
const response = await ImagesService.deleteImage(arg);
|
||||
|
@ -937,6 +937,37 @@
|
||||
gonzales-pe "^4.3.0"
|
||||
node-source-walk "^5.0.1"
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c"
|
||||
integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@^6.0.8":
|
||||
version "6.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005"
|
||||
integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^3.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/modifiers@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz#9e39b25fd6e323659604cc74488fe044d33188c8"
|
||||
integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^3.2.1":
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a"
|
||||
integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emotion/babel-plugin@^11.10.8":
|
||||
version "11.10.8"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.8.tgz#bae325c902937665d00684038fd5294223ef9e1d"
|
||||
|
Loading…
Reference in New Issue
Block a user