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/styled-system": "^2.9.0",
|
||||||
"@chakra-ui/theme-tools": "^2.0.16",
|
"@chakra-ui/theme-tools": "^2.0.16",
|
||||||
"@dagrejs/graphlib": "^2.1.12",
|
"@dagrejs/graphlib": "^2.1.12",
|
||||||
|
"@dnd-kit/core": "^6.0.8",
|
||||||
|
"@dnd-kit/modifiers": "^6.0.1",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource/inter": "^4.5.15",
|
||||||
|
@ -645,5 +645,9 @@
|
|||||||
"betaDarkenOutside": "Darken Outside",
|
"betaDarkenOutside": "Darken Outside",
|
||||||
"betaLimitToBox": "Limit To Box",
|
"betaLimitToBox": "Limit To Box",
|
||||||
"betaPreserveMasked": "Preserve Masked"
|
"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 { configChanged } from 'features/system/store/configSlice';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useLogger } from 'app/logging/useLogger';
|
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 = {};
|
const DEFAULT_CONFIG = {};
|
||||||
|
|
||||||
@ -40,6 +50,9 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
|
|||||||
const log = useLogger();
|
const log = useLogger();
|
||||||
|
|
||||||
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
||||||
|
const shouldShowProgressImage = useAppSelector(
|
||||||
|
(state) => state.ui.shouldShowProgressImage
|
||||||
|
);
|
||||||
|
|
||||||
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
|
||||||
|
|
||||||
@ -63,64 +76,90 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
|
|||||||
setLoadingOverridden(true);
|
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 (
|
return (
|
||||||
<Grid w="100vw" h="100vh" position="relative">
|
<DndContext
|
||||||
{isLightboxEnabled && <Lightbox />}
|
onDragEnd={handleDragEnd}
|
||||||
<ImageUploader>
|
sensors={sensors}
|
||||||
<ProgressBar />
|
modifiers={[restrictToWindowEdges]}
|
||||||
<Grid
|
>
|
||||||
gap={4}
|
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||||
p={4}
|
{isLightboxEnabled && <Lightbox />}
|
||||||
gridAutoRows="min-content auto"
|
<ImageUploader>
|
||||||
w={APP_WIDTH}
|
<ProgressBar />
|
||||||
h={APP_HEIGHT}
|
<Grid
|
||||||
>
|
|
||||||
{children || <SiteHeader />}
|
|
||||||
<Flex
|
|
||||||
gap={4}
|
gap={4}
|
||||||
w={{ base: '100vw', xl: 'full' }}
|
p={4}
|
||||||
h="full"
|
gridAutoRows="min-content auto"
|
||||||
flexDir={{ base: 'column', xl: 'row' }}
|
w={APP_WIDTH}
|
||||||
|
h={APP_HEIGHT}
|
||||||
>
|
>
|
||||||
<InvokeTabs />
|
{children || <SiteHeader />}
|
||||||
<ImageGalleryPanel />
|
<Flex
|
||||||
</Flex>
|
gap={4}
|
||||||
</Grid>
|
w={{ base: '100vw', xl: 'full' }}
|
||||||
</ImageUploader>
|
h="full"
|
||||||
|
flexDir={{ base: 'column', xl: 'row' }}
|
||||||
|
>
|
||||||
|
<InvokeTabs />
|
||||||
|
<ImageGalleryPanel />
|
||||||
|
</Flex>
|
||||||
|
</Grid>
|
||||||
|
</ImageUploader>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{!isApplicationReady && !loadingOverridden && (
|
{!isApplicationReady && !loadingOverridden && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="loading"
|
key="loading"
|
||||||
initial={{ opacity: 1 }}
|
initial={{ opacity: 1 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
style={{ zIndex: 3 }}
|
style={{ zIndex: 3 }}
|
||||||
>
|
>
|
||||||
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
||||||
<Loading />
|
<Loading />
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
onClick={handleOverrideClicked}
|
onClick={handleOverrideClicked}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={0}
|
top={0}
|
||||||
right={0}
|
right={0}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
w="2rem"
|
w="2rem"
|
||||||
h="2rem"
|
h="2rem"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
<FloatingParametersPanelButtons />
|
<FloatingParametersPanelButtons />
|
||||||
</Portal>
|
</Portal>
|
||||||
<Portal>
|
<Portal>
|
||||||
<FloatingGalleryButton />
|
<FloatingGalleryButton />
|
||||||
</Portal>
|
</Portal>
|
||||||
</Grid>
|
<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 { Provider } from 'react-redux';
|
||||||
import { PersistGate } from 'redux-persist/integration/react';
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
import { store } from 'app/store/store';
|
import { store } from 'app/store/store';
|
||||||
|
@ -233,7 +233,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
|
|||||||
hidden={hideTooltip}
|
hidden={hideTooltip}
|
||||||
{...sliderTooltipProps}
|
{...sliderTooltipProps}
|
||||||
>
|
>
|
||||||
<SliderThumb {...sliderThumbProps} />
|
<SliderThumb {...sliderThumbProps} zIndex={0} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Slider>
|
</Slider>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
||||||
import * as InvokeAI from 'app/types/invokeai';
|
import * as InvokeAI from 'app/types/invokeai';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { addImage } from 'features/gallery/store/gallerySlice';
|
// import { addImage } from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
addToast,
|
addToast,
|
||||||
setCurrentStatus,
|
setCurrentStatus,
|
||||||
|
@ -17,31 +17,11 @@ export const imagesSelector = createSelector(
|
|||||||
[uiSelector, selectedImageSelector, systemSelector],
|
[uiSelector, selectedImageSelector, systemSelector],
|
||||||
(ui, selectedImage, system) => {
|
(ui, selectedImage, system) => {
|
||||||
const { shouldShowImageDetails, shouldHidePreview } = ui;
|
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 {
|
return {
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
shouldHidePreview,
|
shouldHidePreview,
|
||||||
imageToDisplay,
|
image: selectedImage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,7 +32,7 @@ export const imagesSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const CurrentImagePreview = () => {
|
const CurrentImagePreview = () => {
|
||||||
const { shouldShowImageDetails, imageToDisplay, shouldHidePreview } =
|
const { shouldShowImageDetails, image, shouldHidePreview } =
|
||||||
useAppSelector(imagesSelector);
|
useAppSelector(imagesSelector);
|
||||||
const { getUrl } = useGetUrl();
|
const { getUrl } = useGetUrl();
|
||||||
|
|
||||||
@ -66,54 +46,37 @@ const CurrentImagePreview = () => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imageToDisplay && (
|
{image && (
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={shouldHidePreview ? undefined : getUrl(image.url)}
|
||||||
shouldHidePreview
|
width={image.metadata.width}
|
||||||
? undefined
|
height={image.metadata.height}
|
||||||
: imageToDisplay.isProgressImage
|
fallback={shouldHidePreview ? <CurrentImageHidden /> : undefined}
|
||||||
? imageToDisplay.url
|
|
||||||
: getUrl(imageToDisplay.url)
|
|
||||||
}
|
|
||||||
width={imageToDisplay.width}
|
|
||||||
height={imageToDisplay.height}
|
|
||||||
fallback={
|
|
||||||
shouldHidePreview ? (
|
|
||||||
<CurrentImageHidden />
|
|
||||||
) : !imageToDisplay.isProgressImage ? (
|
|
||||||
<CurrentImageFallback />
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
sx={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
imageRendering: imageToDisplay.isProgressImage
|
|
||||||
? 'pixelated'
|
|
||||||
: 'initial',
|
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
||||||
{shouldShowImageDetails &&
|
{shouldShowImageDetails && image && 'metadata' in image && (
|
||||||
imageToDisplay &&
|
<Box
|
||||||
'metadata' in imageToDisplay.image && (
|
sx={{
|
||||||
<Box
|
position: 'absolute',
|
||||||
sx={{
|
top: '0',
|
||||||
position: 'absolute',
|
width: '100%',
|
||||||
top: '0',
|
height: '100%',
|
||||||
width: '100%',
|
borderRadius: 'base',
|
||||||
height: '100%',
|
overflow: 'scroll',
|
||||||
borderRadius: 'base',
|
}}
|
||||||
overflow: 'scroll',
|
>
|
||||||
}}
|
<ImageMetadataViewer image={image} />
|
||||||
>
|
</Box>
|
||||||
<ImageMetadataViewer image={imageToDisplay.image} />
|
)}
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -125,7 +125,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
}, [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]);
|
}, [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]);
|
||||||
|
|
||||||
const handleSelectImage = useCallback(() => {
|
const handleSelectImage = useCallback(() => {
|
||||||
dispatch(imageSelected(image.name));
|
dispatch(imageSelected(image));
|
||||||
}, [image, dispatch]);
|
}, [image, dispatch]);
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
|
@ -49,7 +49,7 @@ const gallerySelector = createSelector(
|
|||||||
(uploads, results, gallery) => {
|
(uploads, results, gallery) => {
|
||||||
const { currentCategory } = gallery;
|
const { currentCategory } = gallery;
|
||||||
|
|
||||||
return currentCategory === 'result'
|
return currentCategory === 'results'
|
||||||
? {
|
? {
|
||||||
images: resultsAdapter.getSelectors().selectAll(results),
|
images: resultsAdapter.getSelectors().selectAll(results),
|
||||||
isLoading: results.isLoading,
|
isLoading: results.isLoading,
|
||||||
@ -72,7 +72,6 @@ const ImageGalleryContent = () => {
|
|||||||
const {
|
const {
|
||||||
// images,
|
// images,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
currentImageUuid,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryGridTemplateColumns,
|
galleryGridTemplateColumns,
|
||||||
@ -80,6 +79,7 @@ const ImageGalleryContent = () => {
|
|||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
// areMoreImagesAvailable,
|
// areMoreImagesAvailable,
|
||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
|
selectedImage,
|
||||||
} = useAppSelector(imageGallerySelector);
|
} = useAppSelector(imageGallerySelector);
|
||||||
|
|
||||||
const { images, areMoreImagesAvailable, isLoading } =
|
const { images, areMoreImagesAvailable, isLoading } =
|
||||||
@ -89,11 +89,11 @@ const ImageGalleryContent = () => {
|
|||||||
// dispatch(requestImages(currentCategory));
|
// dispatch(requestImages(currentCategory));
|
||||||
// };
|
// };
|
||||||
const handleClickLoadMore = () => {
|
const handleClickLoadMore = () => {
|
||||||
if (currentCategory === 'result') {
|
if (currentCategory === 'results') {
|
||||||
dispatch(receivedResultImagesPage());
|
dispatch(receivedResultImagesPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCategory === 'user') {
|
if (currentCategory === 'uploads') {
|
||||||
dispatch(receivedUploadImagesPage());
|
dispatch(receivedUploadImagesPage());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -147,34 +147,34 @@ const ImageGalleryContent = () => {
|
|||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('gallery.showGenerations')}
|
aria-label={t('gallery.showGenerations')}
|
||||||
tooltip={t('gallery.showGenerations')}
|
tooltip={t('gallery.showGenerations')}
|
||||||
isChecked={currentCategory === 'result'}
|
isChecked={currentCategory === 'results'}
|
||||||
role="radio"
|
role="radio"
|
||||||
icon={<FaImage />}
|
icon={<FaImage />}
|
||||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('gallery.showUploads')}
|
aria-label={t('gallery.showUploads')}
|
||||||
tooltip={t('gallery.showUploads')}
|
tooltip={t('gallery.showUploads')}
|
||||||
role="radio"
|
role="radio"
|
||||||
isChecked={currentCategory === 'user'}
|
isChecked={currentCategory === 'uploads'}
|
||||||
icon={<FaUser />}
|
icon={<FaUser />}
|
||||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
size="sm"
|
size="sm"
|
||||||
isChecked={currentCategory === 'result'}
|
isChecked={currentCategory === 'results'}
|
||||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
{t('gallery.generations')}
|
{t('gallery.generations')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
size="sm"
|
size="sm"
|
||||||
isChecked={currentCategory === 'user'}
|
isChecked={currentCategory === 'uploads'}
|
||||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
{t('gallery.uploads')}
|
{t('gallery.uploads')}
|
||||||
@ -251,7 +251,7 @@ const ImageGalleryContent = () => {
|
|||||||
>
|
>
|
||||||
{images.map((image) => {
|
{images.map((image) => {
|
||||||
const { name } = image;
|
const { name } = image;
|
||||||
const isSelected = currentImageUuid === name;
|
const isSelected = selectedImage?.name === name;
|
||||||
return (
|
return (
|
||||||
<HoverableImage
|
<HoverableImage
|
||||||
key={`${name}-${image.thumbnail}`}
|
key={`${name}-${image.thumbnail}`}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import {
|
||||||
selectNextImage,
|
// selectNextImage,
|
||||||
selectPrevImage,
|
// selectPrevImage,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
@ -6,11 +6,13 @@ import { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||||
import { gallerySelector } from '../store/gallerySelectors';
|
import { gallerySelector } from '../store/gallerySelectors';
|
||||||
import {
|
import { RootState } from 'app/store/store';
|
||||||
GalleryCategory,
|
import { selectResultsEntities } from '../store/resultsSlice';
|
||||||
selectNextImage,
|
// import {
|
||||||
selectPrevImage,
|
// GalleryCategory,
|
||||||
} from '../store/gallerySlice';
|
// selectNextImage,
|
||||||
|
// selectPrevImage,
|
||||||
|
// } from '../store/gallerySlice';
|
||||||
|
|
||||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -23,19 +25,22 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const nextPrevImageButtonsSelector = createSelector(
|
export const nextPrevImageButtonsSelector = createSelector(
|
||||||
gallerySelector,
|
[(state: RootState) => state, gallerySelector],
|
||||||
(gallery) => {
|
(state, gallery) => {
|
||||||
const { currentImage } = gallery;
|
const { selectedImage, currentCategory } = gallery;
|
||||||
|
|
||||||
const tempImages =
|
if (!selectedImage) {
|
||||||
gallery.categories[
|
return {
|
||||||
currentImage ? (currentImage.category as GalleryCategory) : 'result'
|
isOnFirstImage: true,
|
||||||
].images;
|
isOnLastImage: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const currentImageIndex = tempImages.findIndex(
|
const currentImageIndex = state[currentCategory].ids.findIndex(
|
||||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
(i) => i === selectedImage.name
|
||||||
);
|
);
|
||||||
const imagesLength = tempImages.length;
|
|
||||||
|
const imagesLength = state[currentCategory].ids.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
@ -81,7 +86,6 @@ const NextPrevImageButtons = () => {
|
|||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
zIndex: 1,
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
@ -22,17 +22,22 @@ import {
|
|||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
export const gallerySelector = (state: RootState) => state.gallery;
|
||||||
|
|
||||||
export const imageGallerySelector = createSelector(
|
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 {
|
const {
|
||||||
categories,
|
|
||||||
currentCategory,
|
currentCategory,
|
||||||
currentImageUuid,
|
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
galleryWidth,
|
galleryWidth,
|
||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
|
selectedImage,
|
||||||
} = gallery;
|
} = gallery;
|
||||||
|
|
||||||
const { shouldPinGallery } = ui;
|
const { shouldPinGallery } = ui;
|
||||||
@ -40,7 +45,6 @@ export const imageGallerySelector = createSelector(
|
|||||||
const { isLightboxOpen } = lightbox;
|
const { isLightboxOpen } = lightbox;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentImageUuid,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
@ -49,9 +53,7 @@ export const imageGallerySelector = createSelector(
|
|||||||
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
images: categories[currentCategory].images,
|
images: state[currentCategory].entities,
|
||||||
areMoreImagesAvailable:
|
|
||||||
categories[currentCategory].areMoreImagesAvailable,
|
|
||||||
galleryWidth,
|
galleryWidth,
|
||||||
shouldEnableResize:
|
shouldEnableResize:
|
||||||
isLightboxOpen ||
|
isLightboxOpen ||
|
||||||
@ -59,6 +61,7 @@ export const imageGallerySelector = createSelector(
|
|||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
|
selectedImage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -69,16 +72,16 @@ export const imageGallerySelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const selectedImageSelector = createSelector(
|
export const selectedImageSelector = createSelector(
|
||||||
[gallerySelector, selectResultsEntities, selectUploadsEntities],
|
[(state: RootState) => state, gallerySelector],
|
||||||
(gallery, allResults, allUploads) => {
|
(state, gallery) => {
|
||||||
const selectedImageName = gallery.selectedImageName;
|
const selectedImage = gallery.selectedImage;
|
||||||
|
|
||||||
if (selectedImageName in allResults) {
|
if (selectedImage?.type === 'results') {
|
||||||
return allResults[selectedImageName];
|
return selectResultsById(state, selectedImage.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedImageName in allUploads) {
|
if (selectedImage?.type === 'uploads') {
|
||||||
return allUploads[selectedImageName];
|
return selectUploadsById(state, selectedImage.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,260 +1,80 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import * as InvokeAI from 'app/types/invokeai';
|
|
||||||
import { invocationComplete } from 'services/events/actions';
|
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 { isImageOutput } from 'services/types/guards';
|
||||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||||
import { imageUploaded } from 'services/thunks/image';
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
|
import { SelectedImage } from 'features/parameters/store/generationSlice';
|
||||||
export type GalleryCategory = 'user' | 'result';
|
|
||||||
|
|
||||||
export type AddImagesPayload = {
|
|
||||||
images: Array<InvokeAI._Image>;
|
|
||||||
areMoreImagesAvailable: boolean;
|
|
||||||
category: GalleryCategory;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
|
|
||||||
export type Gallery = {
|
|
||||||
images: InvokeAI._Image[];
|
|
||||||
latest_mtime?: number;
|
|
||||||
earliest_mtime?: number;
|
|
||||||
areMoreImagesAvailable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GalleryState {
|
export interface GalleryState {
|
||||||
/**
|
/**
|
||||||
* The selected image's unique name
|
* The selected image
|
||||||
* Use `selectedImageSelector` to access the image
|
|
||||||
*/
|
*/
|
||||||
selectedImageName: string;
|
selectedImage?: SelectedImage;
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
};
|
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||||
shouldAutoSwitchToNewImages: boolean;
|
shouldAutoSwitchToNewImages: boolean;
|
||||||
categories: {
|
|
||||||
user: Gallery;
|
|
||||||
result: Gallery;
|
|
||||||
};
|
|
||||||
currentCategory: GalleryCategory;
|
|
||||||
galleryWidth: number;
|
galleryWidth: number;
|
||||||
shouldUseSingleGalleryColumn: boolean;
|
shouldUseSingleGalleryColumn: boolean;
|
||||||
|
currentCategory: 'results' | 'uploads';
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GalleryState = {
|
const initialState: GalleryState = {
|
||||||
selectedImageName: '',
|
selectedImage: undefined,
|
||||||
currentImageUuid: '',
|
|
||||||
galleryImageMinimumWidth: 64,
|
galleryImageMinimumWidth: 64,
|
||||||
galleryImageObjectFit: 'cover',
|
galleryImageObjectFit: 'cover',
|
||||||
shouldAutoSwitchToNewImages: true,
|
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,
|
galleryWidth: 300,
|
||||||
shouldUseSingleGalleryColumn: false,
|
shouldUseSingleGalleryColumn: false,
|
||||||
|
currentCategory: 'results',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
imageSelected: (state, action: PayloadAction<string>) => {
|
imageSelected: (
|
||||||
state.selectedImageName = action.payload;
|
|
||||||
},
|
|
||||||
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
|
|
||||||
state.currentImage = action.payload;
|
|
||||||
state.currentImageUuid = action.payload.uuid;
|
|
||||||
},
|
|
||||||
removeImage: (
|
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<InvokeAI.ImageDeletedResponse>
|
action: PayloadAction<SelectedImage | undefined>
|
||||||
) => {
|
) => {
|
||||||
const { uuid, category } = action.payload;
|
state.selectedImage = 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;
|
|
||||||
},
|
},
|
||||||
addImage: (
|
// selectNextImage: (state) => {
|
||||||
state,
|
// const { currentImage } = state;
|
||||||
action: PayloadAction<{
|
// if (!currentImage) return;
|
||||||
image: InvokeAI._Image;
|
// const tempImages =
|
||||||
category: GalleryCategory;
|
// state.categories[currentImage.category as GalleryCategory].images;
|
||||||
}>
|
|
||||||
) => {
|
|
||||||
const { image: newImage, category } = action.payload;
|
|
||||||
const { uuid, url, mtime } = newImage;
|
|
||||||
|
|
||||||
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 (currentImage) {
|
||||||
if (tempCategory.images.find((i) => i.url === url && i.mtime === mtime)) {
|
// const currentImageIndex = tempImages.findIndex(
|
||||||
return;
|
// (i) => i.uuid === currentImage.uuid
|
||||||
}
|
// );
|
||||||
|
// if (currentImageIndex > 0) {
|
||||||
tempCategory.images.unshift(newImage);
|
// const newCurrentImage = tempImages[currentImageIndex - 1];
|
||||||
if (state.shouldAutoSwitchToNewImages) {
|
// state.currentImage = newCurrentImage;
|
||||||
state.currentImageUuid = uuid;
|
// state.currentImageUuid = newCurrentImage.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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||||
state.galleryImageMinimumWidth = action.payload;
|
state.galleryImageMinimumWidth = action.payload;
|
||||||
},
|
},
|
||||||
@ -267,7 +87,10 @@ export const gallerySlice = createSlice({
|
|||||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitchToNewImages = action.payload;
|
state.shouldAutoSwitchToNewImages = action.payload;
|
||||||
},
|
},
|
||||||
setCurrentCategory: (state, action: PayloadAction<GalleryCategory>) => {
|
setCurrentCategory: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<'results' | 'uploads'>
|
||||||
|
) => {
|
||||||
state.currentCategory = action.payload;
|
state.currentCategory = action.payload;
|
||||||
},
|
},
|
||||||
setGalleryWidth: (state, action: PayloadAction<number>) => {
|
setGalleryWidth: (state, action: PayloadAction<number>) => {
|
||||||
@ -286,9 +109,11 @@ export const gallerySlice = createSlice({
|
|||||||
*/
|
*/
|
||||||
builder.addCase(invocationComplete, (state, action) => {
|
builder.addCase(invocationComplete, (state, action) => {
|
||||||
const { data } = action.payload;
|
const { data } = action.payload;
|
||||||
if (isImageOutput(data.result)) {
|
if (isImageOutput(data.result) && state.shouldAutoSwitchToNewImages) {
|
||||||
state.selectedImageName = data.result.image.image_name;
|
state.selectedImage = {
|
||||||
state.intermediateImage = undefined;
|
name: data.result.image.image_name,
|
||||||
|
type: 'results',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -299,27 +124,19 @@ export const gallerySlice = createSlice({
|
|||||||
const { response } = action.payload;
|
const { response } = action.payload;
|
||||||
|
|
||||||
const uploadedImage = deserializeImageResponse(response);
|
const uploadedImage = deserializeImageResponse(response);
|
||||||
state.selectedImageName = uploadedImage.name;
|
state.selectedImage = { name: uploadedImage.name, type: 'uploads' };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
imageSelected,
|
imageSelected,
|
||||||
addImage,
|
|
||||||
clearIntermediateImage,
|
|
||||||
removeImage,
|
|
||||||
setCurrentImage,
|
|
||||||
addGalleryImages,
|
|
||||||
setIntermediateImage,
|
|
||||||
selectNextImage,
|
|
||||||
selectPrevImage,
|
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
setGalleryImageObjectFit,
|
setGalleryImageObjectFit,
|
||||||
setShouldAutoSwitchToNewImages,
|
setShouldAutoSwitchToNewImages,
|
||||||
setCurrentCategory,
|
|
||||||
setGalleryWidth,
|
setGalleryWidth,
|
||||||
setShouldUseSingleGalleryColumn,
|
setShouldUseSingleGalleryColumn,
|
||||||
|
setCurrentCategory,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
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 { clamp } from 'lodash-es';
|
||||||
import { ImageField, ImageType } from 'services/api';
|
import { ImageField, ImageType } from 'services/api';
|
||||||
|
|
||||||
export type InitialImage = {
|
export type SelectedImage = {
|
||||||
name: string;
|
name: string;
|
||||||
type: ImageType;
|
type: ImageType;
|
||||||
};
|
};
|
||||||
@ -17,7 +17,7 @@ export interface GenerationState {
|
|||||||
height: number;
|
height: number;
|
||||||
img2imgStrength: number;
|
img2imgStrength: number;
|
||||||
infillMethod: string;
|
infillMethod: string;
|
||||||
initialImage?: InitialImage; // can be an Image or url
|
initialImage?: SelectedImage; // can be an Image or url
|
||||||
iterations: number;
|
iterations: number;
|
||||||
maskPath: string;
|
maskPath: string;
|
||||||
perlin: number;
|
perlin: number;
|
||||||
@ -351,7 +351,7 @@ export const generationSlice = createSlice({
|
|||||||
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
|
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
|
||||||
state.verticalSymmetrySteps = action.payload;
|
state.verticalSymmetrySteps = action.payload;
|
||||||
},
|
},
|
||||||
initialImageSelected: (state, action: PayloadAction<InitialImage>) => {
|
initialImageSelected: (state, action: PayloadAction<SelectedImage>) => {
|
||||||
state.initialImage = action.payload;
|
state.initialImage = action.payload;
|
||||||
state.isImageToImageEnabled = true;
|
state.isImageToImageEnabled = true;
|
||||||
},
|
},
|
||||||
|
@ -307,7 +307,7 @@ export const systemSlice = createSlice({
|
|||||||
state.totalSteps = 0;
|
state.totalSteps = 0;
|
||||||
// state.currentIteration = 0;
|
// state.currentIteration = 0;
|
||||||
// state.totalIterations = 0;
|
// state.totalIterations = 0;
|
||||||
state.statusTranslationKey = 'common.statusPreparing';
|
state.statusTranslationKey = 'common.statusGenerating';
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,7 +347,6 @@ export const systemSlice = createSlice({
|
|||||||
state.currentStatusHasSteps = false;
|
state.currentStatusHasSteps = false;
|
||||||
state.currentStep = 0;
|
state.currentStep = 0;
|
||||||
state.totalSteps = 0;
|
state.totalSteps = 0;
|
||||||
state.progressImage = null;
|
|
||||||
state.statusTranslationKey = 'common.statusProcessingComplete';
|
state.statusTranslationKey = 'common.statusProcessingComplete';
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -364,7 +363,6 @@ export const systemSlice = createSlice({
|
|||||||
state.currentStatusHasSteps = false;
|
state.currentStatusHasSteps = false;
|
||||||
state.currentStep = 0;
|
state.currentStep = 0;
|
||||||
state.totalSteps = 0;
|
state.totalSteps = 0;
|
||||||
state.progressImage = null;
|
|
||||||
state.statusTranslationKey = 'common.statusError';
|
state.statusTranslationKey = 'common.statusError';
|
||||||
|
|
||||||
state.toastQueue.push(
|
state.toastQueue.push(
|
||||||
@ -391,7 +389,6 @@ export const systemSlice = createSlice({
|
|||||||
state.isCancelScheduled = false;
|
state.isCancelScheduled = false;
|
||||||
state.currentStep = 0;
|
state.currentStep = 0;
|
||||||
state.totalSteps = 0;
|
state.totalSteps = 0;
|
||||||
state.progressImage = null;
|
|
||||||
state.statusTranslationKey = 'common.statusConnected';
|
state.statusTranslationKey = 'common.statusConnected';
|
||||||
|
|
||||||
state.toastQueue.push(
|
state.toastQueue.push(
|
||||||
@ -410,7 +407,6 @@ export const systemSlice = createSlice({
|
|||||||
state.isCancelScheduled = false;
|
state.isCancelScheduled = false;
|
||||||
state.currentStep = 0;
|
state.currentStep = 0;
|
||||||
state.totalSteps = 0;
|
state.totalSteps = 0;
|
||||||
state.progressImage = null;
|
|
||||||
state.statusTranslationKey = 'common.statusConnected';
|
state.statusTranslationKey = 'common.statusConnected';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||||
|
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
|
||||||
|
|
||||||
const GenerateContent = () => {
|
const GenerateContent = () => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
|
@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import { setActiveTabReducer } from './extraReducers';
|
import { setActiveTabReducer } from './extraReducers';
|
||||||
import { InvokeTabName, tabMap } from './tabMap';
|
import { InvokeTabName, tabMap } from './tabMap';
|
||||||
import { AddNewModelType, UIState } from './uiTypes';
|
import { AddNewModelType, UIState } from './uiTypes';
|
||||||
|
import { Coordinates } from '@dnd-kit/core/dist/types';
|
||||||
|
|
||||||
const initialUIState: UIState = {
|
const initialUIState: UIState = {
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
@ -21,6 +22,8 @@ const initialUIState: UIState = {
|
|||||||
openLinearAccordionItems: [],
|
openLinearAccordionItems: [],
|
||||||
openGenerateAccordionItems: [],
|
openGenerateAccordionItems: [],
|
||||||
openUnifiedCanvasAccordionItems: [],
|
openUnifiedCanvasAccordionItems: [],
|
||||||
|
floatingProgressImageCoordinates: { x: 0, y: 0 },
|
||||||
|
shouldShowProgressImage: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: UIState = initialUIState;
|
const initialState: UIState = initialUIState;
|
||||||
@ -105,6 +108,15 @@ export const uiSlice = createSlice({
|
|||||||
state.openUnifiedCanvasAccordionItems = action.payload;
|
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,
|
toggleParametersPanel,
|
||||||
toggleGalleryPanel,
|
toggleGalleryPanel,
|
||||||
openAccordionItemsChanged,
|
openAccordionItemsChanged,
|
||||||
|
floatingProgressImageMoved,
|
||||||
|
shouldShowProgressImageChanged,
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
|
|
||||||
export default uiSlice.reducer;
|
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;
|
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
|
||||||
|
|
||||||
@ -19,4 +19,6 @@ export interface UIState {
|
|||||||
openLinearAccordionItems: number[];
|
openLinearAccordionItems: number[];
|
||||||
openGenerateAccordionItems: number[];
|
openGenerateAccordionItems: number[];
|
||||||
openUnifiedCanvasAccordionItems: number[];
|
openUnifiedCanvasAccordionItems: number[];
|
||||||
|
floatingProgressImageCoordinates: Coordinates;
|
||||||
|
shouldShowProgressImage: boolean;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp, isString } from 'lodash-es';
|
||||||
import { ImagesService } from 'services/api';
|
import { ImagesService } from 'services/api';
|
||||||
import { getHeaders } from 'services/util/getHeaders';
|
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.
|
// 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
|
// Unfortunately, we have to do this here, because the resultsSlice and uploadsSlice cannot change
|
||||||
// the selected image.
|
// the selected image.
|
||||||
const selectedImageName = getState().gallery.selectedImageName;
|
const selectedImageName = getState().gallery.selectedImage?.name;
|
||||||
|
|
||||||
if (selectedImageName === imageName) {
|
if (selectedImageName === imageName) {
|
||||||
const allIds = getState()[imageType].ids;
|
const allIds = getState()[imageType].ids;
|
||||||
@ -104,9 +104,13 @@ export const imageDeleted = createAppAsyncThunk(
|
|||||||
|
|
||||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||||
|
|
||||||
dispatch(
|
if (newSelectedImageId) {
|
||||||
imageSelected(newSelectedImageId ? newSelectedImageId.toString() : '')
|
dispatch(
|
||||||
);
|
imageSelected({ name: newSelectedImageId as string, type: imageType })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(imageSelected());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await ImagesService.deleteImage(arg);
|
const response = await ImagesService.deleteImage(arg);
|
||||||
|
@ -937,6 +937,37 @@
|
|||||||
gonzales-pe "^4.3.0"
|
gonzales-pe "^4.3.0"
|
||||||
node-source-walk "^5.0.1"
|
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":
|
"@emotion/babel-plugin@^11.10.8":
|
||||||
version "11.10.8"
|
version "11.10.8"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.8.tgz#bae325c902937665d00684038fd5294223ef9e1d"
|
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.8.tgz#bae325c902937665d00684038fd5294223ef9e1d"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user