feat(ui): progress images in gallery and viewer

This commit is contained in:
psychedelicious 2023-05-10 13:58:34 +10:00
parent e94d0b2d40
commit fdc2232ea0
17 changed files with 309 additions and 96 deletions

View File

@ -531,7 +531,7 @@
"useCanvasBeta": "Use Canvas Beta Layout",
"enableImageDebugging": "Enable Image Debugging",
"useSlidersForAll": "Use Sliders For All Options",
"autoShowProgress": "Auto Show Progress Images",
"showProgressInViewer": "Show Progress Images in Viewer",
"resetWebUI": "Reset Web UI",
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",

View File

@ -27,7 +27,7 @@ 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 ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
const DEFAULT_CONFIG = {};
@ -124,7 +124,6 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
<Portal>
<FloatingGalleryButton />
</Portal>
<ProgressImagePreview />
</Grid>
);
};

View File

@ -10,6 +10,7 @@ export const actionSanitizer = <A extends AnyAction>(action: A): A => {
// Sanitize nodes as needed
forEach(action.payload.nodes, (node, key) => {
// Don't log the whole freaking dataURL
if (node.type === 'dataURL_image') {
const { dataURL, ...rest } = node;
sanitizedNodes[key] = { ...rest, dataURL: '<dataURL>' };

View File

@ -27,7 +27,7 @@ const IAIPopover = (props: IAIPopoverProps) => {
return (
<Popover isLazy={isLazy} {...rest}>
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
<PopoverContent>
<PopoverContent shadow="dark-lg">
{hasArrow && <PopoverArrow />}
{children}
</PopoverContent>

View File

@ -5,7 +5,13 @@ import {
ButtonGroup,
Flex,
FlexProps,
IconButton,
Link,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
useDisclosure,
useToast,
} from '@chakra-ui/react';
@ -46,6 +52,7 @@ import {
FaShare,
FaShareAlt,
FaTrash,
FaWrench,
} from 'react-icons/fa';
import {
gallerySelector,
@ -62,6 +69,7 @@ import { requestedImageDeletion } from '../store/actions';
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
import { allParametersSet } from 'features/parameters/store/generationSlice';
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
const currentImageButtonsSelector = createSelector(
[
@ -451,7 +459,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</Link>
</Flex>
</IAIPopover>
<IAIIconButton
{/* <IAIIconButton
icon={shouldHidePreview ? <FaEyeSlash /> : <FaEye />}
tooltip={
!shouldHidePreview
@ -465,7 +473,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}
isChecked={shouldHidePreview}
onClick={handlePreviewVisibility}
/>
/> */}
{isLightboxEnabled && (
<IAIIconButton
icon={<FaExpand />}
@ -592,23 +600,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIIconButton
onClick={handleInitiateDelete}
icon={<FaTrash />}
tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!image || !isConnected}
colorScheme="error"
/>
<DeleteImageButton image={image} />
</ButtonGroup>
</Flex>
{image && (
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
)}
</>
);
};

View File

@ -1,4 +1,4 @@
import { Box, Flex, Image } from '@chakra-ui/react';
import { Box, Flex, Image, Skeleton, useBoolean } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl';
@ -10,16 +10,25 @@ import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons';
import CurrentImageHidden from './CurrentImageHidden';
import { DragEvent, memo, useCallback } from 'react';
import { systemSelector } from 'features/system/store/systemSelectors';
import CurrentImageFallback from './CurrentImageFallback';
export const imagesSelector = createSelector(
[uiSelector, gallerySelector],
(ui, gallery) => {
const { shouldShowImageDetails, shouldHidePreview } = ui;
[uiSelector, gallerySelector, systemSelector],
(ui, gallery, system) => {
const {
shouldShowImageDetails,
shouldHidePreview,
shouldShowProgressInViewer,
} = ui;
const { selectedImage } = gallery;
const { progressImage } = system;
return {
shouldShowImageDetails,
shouldHidePreview,
image: selectedImage,
progressImage,
shouldShowProgressInViewer,
};
},
{
@ -30,10 +39,17 @@ export const imagesSelector = createSelector(
);
const CurrentImagePreview = () => {
const { shouldShowImageDetails, image, shouldHidePreview } =
useAppSelector(imagesSelector);
const {
shouldShowImageDetails,
image,
shouldHidePreview,
progressImage,
shouldShowProgressInViewer,
} = useAppSelector(imagesSelector);
const { getUrl } = useGetUrl();
const [isLoaded, { on, off }] = useBoolean();
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
if (!image) {
@ -56,13 +72,11 @@ const CurrentImagePreview = () => {
height: '100%',
}}
>
{image && (
{progressImage && shouldShowProgressInViewer ? (
<Image
onDragStart={handleDragStart}
src={shouldHidePreview ? undefined : getUrl(image.url)}
width={image.metadata.width || 'auto'}
height={image.metadata.height || 'auto'}
fallback={shouldHidePreview ? <CurrentImageHidden /> : undefined}
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
sx={{
objectFit: 'contain',
maxWidth: '100%',
@ -72,6 +86,31 @@ const CurrentImagePreview = () => {
borderRadius: 'base',
}}
/>
) : (
image && (
<Image
onDragStart={handleDragStart}
fallbackStrategy="beforeLoadOrError"
src={shouldHidePreview ? undefined : getUrl(image.url)}
width={image.metadata.width || 'auto'}
height={image.metadata.height || 'auto'}
fallback={
shouldHidePreview ? (
<CurrentImageHidden />
) : (
<CurrentImageFallback />
)
}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
borderRadius: 'base',
}}
/>
)
)}
{shouldShowImageDetails && image && 'metadata' in image && (
<Box

View File

@ -0,0 +1,62 @@
import { Box, Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { systemSelector } from 'features/system/store/systemSelectors';
import { memo } from 'react';
import { gallerySelector } from '../store/gallerySelectors';
const selector = createSelector(
[systemSelector, gallerySelector],
(system, gallery) => {
const { shouldUseSingleGalleryColumn, galleryImageObjectFit } = gallery;
const { progressImage } = system;
return {
progressImage,
shouldUseSingleGalleryColumn,
galleryImageObjectFit,
};
},
defaultSelectorOptions
);
const GalleryProgressImage = () => {
const { progressImage, shouldUseSingleGalleryColumn, galleryImageObjectFit } =
useAppSelector(selector);
if (!progressImage) {
return null;
}
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
aspectRatio: '1/1',
}}
>
<Image
draggable={false}
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
sx={{
objectFit: shouldUseSingleGalleryColumn
? 'contain'
: galleryImageObjectFit,
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
borderRadius: 'base',
}}
/>
</Flex>
);
};
export default memo(GalleryProgressImage);

View File

@ -0,0 +1,92 @@
import { createSelector } from '@reduxjs/toolkit';
import { useDisclosure } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { systemSelector } from 'features/system/store/systemSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { memo, useCallback } from 'react';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import DeleteImageModal from '../DeleteImageModal';
import { requestedImageDeletion } from 'features/gallery/store/actions';
import { Image } from 'app/types/invokeai';
const selector = createSelector(
[systemSelector],
(system) => {
const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
isProcessing,
isConnected,
};
},
defaultSelectorOptions
);
type DeleteImageButtonProps = {
image: Image | undefined;
};
const DeleteImageButton = (props: DeleteImageButtonProps) => {
const { image } = props;
const dispatch = useAppDispatch();
const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } =
useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { t } = useTranslation();
const handleDelete = useCallback(() => {
if (canDeleteImage && image) {
dispatch(requestedImageDeletion(image));
}
}, [image, canDeleteImage, dispatch]);
const handleInitiateDelete = useCallback(() => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
}, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
useHotkeys('delete', handleInitiateDelete, [
image,
shouldConfirmOnDelete,
isConnected,
isProcessing,
]);
return (
<>
<IAIIconButton
onClick={handleInitiateDelete}
icon={<FaTrash />}
tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!image || !isConnected}
colorScheme="error"
/>
{image && (
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
)}
</>
);
};
export default memo(DeleteImageButton);

View File

@ -5,6 +5,7 @@ import {
FlexProps,
Grid,
Icon,
Image,
Text,
forwardRef,
} from '@chakra-ui/react';
@ -14,7 +15,10 @@ import IAICheckbox from 'common/components/IAICheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider';
import { imageGallerySelector } from 'features/gallery/store/gallerySelectors';
import {
gallerySelector,
imageGallerySelector,
} from 'features/gallery/store/gallerySelectors';
import {
setCurrentCategory,
setGalleryImageMinimumWidth,
@ -50,30 +54,48 @@ import { uploadsAdapter } from '../store/uploadsSlice';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview';
import ProgressImage from 'features/parameters/components/ProgressImage';
import { systemSelector } from 'features/system/store/systemSelectors';
import { Image as ImageType } from 'app/types/invokeai';
import { ProgressImage as ProgressImageType } from 'services/events/types';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import GalleryProgressImage from './GalleryProgressImage';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
const gallerySelector = createSelector(
[
(state: RootState) => state.uploads,
(state: RootState) => state.results,
(state: RootState) => state.gallery,
],
(uploads, results, gallery) => {
const selector = createSelector(
[(state: RootState) => state],
(state) => {
const { results, uploads, system, gallery } = state;
const { currentCategory } = gallery;
return currentCategory === 'results'
? {
images: resultsAdapter.getSelectors().selectAll(results),
isLoading: results.isLoading,
areMoreImagesAvailable: results.page < results.pages - 1,
}
: {
images: uploadsAdapter.getSelectors().selectAll(uploads),
isLoading: uploads.isLoading,
areMoreImagesAvailable: uploads.page < uploads.pages - 1,
};
}
const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
if (system.progressImage) {
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
}
if (currentCategory === 'results') {
return {
images: tempImages.concat(
resultsAdapter.getSelectors().selectAll(results)
),
isLoading: results.isLoading,
areMoreImagesAvailable: results.page < results.pages - 1,
};
}
return {
images: tempImages.concat(
uploadsAdapter.getSelectors().selectAll(uploads)
),
isLoading: uploads.isLoading,
areMoreImagesAvailable: uploads.page < uploads.pages - 1,
};
},
defaultSelectorOptions
);
const ImageGalleryContent = () => {
@ -108,7 +130,7 @@ const ImageGalleryContent = () => {
} = useAppSelector(imageGallerySelector);
const { images, areMoreImagesAvailable, isLoading } =
useAppSelector(gallerySelector);
useAppSelector(selector);
const handleClickLoadMore = () => {
if (currentCategory === 'results') {
@ -186,8 +208,6 @@ const ImageGalleryContent = () => {
h: 'full',
w: 'full',
borderRadius: 'base',
// bg: 'base.850',
// p: 2,
}}
>
<Flex
@ -311,16 +331,24 @@ const ImageGalleryContent = () => {
endReached={handleEndReached}
scrollerRef={(ref) => setScrollerRef(ref)}
itemContent={(index, image) => {
const { name } = image;
const isSelected = selectedImage?.name === name;
const isSelected =
image === PROGRESS_IMAGE_PLACEHOLDER
? false
: selectedImage?.name === image?.name;
return (
<Flex sx={{ pb: 2 }}>
<HoverableImage
key={`${name}-${image.thumbnail}`}
image={image}
isSelected={isSelected}
/>
{image === PROGRESS_IMAGE_PLACEHOLDER ? (
<GalleryProgressImage
key={PROGRESS_IMAGE_PLACEHOLDER}
/>
) : (
<HoverableImage
key={`${image.name}-${image.thumbnail}`}
image={image}
isSelected={isSelected}
/>
)}
</Flex>
);
}}
@ -336,12 +364,16 @@ const ImageGalleryContent = () => {
}}
scrollerRef={setScroller}
itemContent={(index, image) => {
const { name } = image;
const isSelected = selectedImage?.name === name;
const isSelected =
image === PROGRESS_IMAGE_PLACEHOLDER
? false
: selectedImage?.name === image?.name;
return (
return image === PROGRESS_IMAGE_PLACEHOLDER ? (
<GalleryProgressImage key={PROGRESS_IMAGE_PLACEHOLDER} />
) : (
<HoverableImage
key={`${name}-${image.thumbnail}`}
key={`${image.name}-${image.thumbnail}`}
image={image}
isSelected={isSelected}
/>

View File

@ -28,13 +28,14 @@ export const buildImg2ImgNode = (
img2imgStrength: strength,
shouldFitToWidthHeight: fit,
shouldRandomizeSeed,
initialImage,
} = generation;
const initialImage = initialImageSelector(state);
// const initialImage = initialImageSelector(state);
if (!initialImage) {
// TODO: handle this
// throw 'no initial image';
throw 'no initial image';
}
const imageToImageNode: ImageToImageInvocation = {
@ -47,12 +48,10 @@ export const buildImg2ImgNode = (
cfg_scale: cfgScale,
scheduler: sampler as ImageToImageInvocation['scheduler'],
model,
image: initialImage
? {
image_name: initialImage.name,
image_type: initialImage.type,
}
: undefined,
image: {
image_name: initialImage.name,
image_type: initialImage.type,
},
strength,
fit,
};

View File

@ -28,7 +28,7 @@ import {
} from 'features/system/store/systemSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import {
setShouldAutoShowProgressImages,
setShouldShowProgressInViewer,
setShouldUseCanvasBetaLayout,
setShouldUseSliders,
} from 'features/ui/store/uiSlice';
@ -54,7 +54,7 @@ const selector = createSelector(
const {
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
shouldShowProgressInViewer,
} = ui;
return {
@ -63,7 +63,7 @@ const selector = createSelector(
enableImageDebugging,
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
shouldShowProgressInViewer,
consoleLogLevel,
shouldLogToConsole,
};
@ -114,7 +114,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
enableImageDebugging,
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
shouldShowProgressInViewer,
consoleLogLevel,
shouldLogToConsole,
} = useAppSelector(selector);
@ -197,10 +197,10 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
}
/>
<IAISwitch
label={t('settings.autoShowProgress')}
isChecked={shouldAutoShowProgressImages}
label={t('settings.showProgressInViewer')}
isChecked={shouldShowProgressInViewer}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoShowProgressImages(e.target.checked))
dispatch(setShouldShowProgressInViewer(e.target.checked))
}
/>
</Flex>

View File

@ -9,6 +9,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useRef } from 'react';
import { FaCircle } from 'react-icons/fa';
import { useHoverDirty } from 'react-use';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
const statusIndicatorSelector = createSelector(
systemSelector,
@ -31,9 +32,7 @@ const statusIndicatorSelector = createSelector(
currentStatusHasSteps,
};
},
{
memoizeOptions: { resultEqualityCheck: isEqual },
}
defaultSelectorOptions
);
const StatusIndicator = () => {

View File

@ -354,6 +354,7 @@ export const systemSlice = createSlice({
state.currentStep = 0;
state.totalSteps = 0;
state.statusTranslationKey = 'common.statusProcessingComplete';
state.progressImage = null;
if (state.canceledSession === data.graph_execution_state_id) {
state.isProcessing = false;
@ -373,6 +374,7 @@ export const systemSlice = createSlice({
state.currentStep = 0;
state.totalSteps = 0;
state.statusTranslationKey = 'common.statusError';
state.progressImage = null;
state.toastQueue.push(
makeToast({ title: t('toast.serverError'), status: 'error' })

View File

@ -23,7 +23,7 @@ export const initialUIState: UIState = {
canvasTabAccordionState: [],
floatingProgressImageRect: { x: 0, y: 0, width: 0, height: 0 },
shouldShowProgressImages: false,
shouldAutoShowProgressImages: false,
shouldShowProgressInViewer: false,
shouldShowImageParameters: false,
};
@ -135,11 +135,8 @@ export const uiSlice = createSlice({
setShouldShowProgressImages: (state, action: PayloadAction<boolean>) => {
state.shouldShowProgressImages = action.payload;
},
setShouldAutoShowProgressImages: (
state,
action: PayloadAction<boolean>
) => {
state.shouldAutoShowProgressImages = action.payload;
setShouldShowProgressInViewer: (state, action: PayloadAction<boolean>) => {
state.shouldShowProgressInViewer = action.payload;
},
shouldShowImageParametersChanged: (
state,
@ -173,7 +170,7 @@ export const {
floatingProgressImageMoved,
floatingProgressImageResized,
setShouldShowProgressImages,
setShouldAutoShowProgressImages,
setShouldShowProgressInViewer,
shouldShowImageParametersChanged,
} = uiSlice.actions;

View File

@ -31,6 +31,6 @@ export interface UIState {
canvasTabAccordionState: number[];
floatingProgressImageRect: Rect;
shouldShowProgressImages: boolean;
shouldAutoShowProgressImages: boolean;
shouldShowProgressInViewer: boolean;
shouldShowImageParameters: boolean;
}

View File

@ -20,9 +20,6 @@ const invokeAIContent = defineStyle((_props) => {
minW: 'unset',
width: 'unset',
p: 4,
borderWidth: '2px',
borderStyle: 'solid',
borderColor: 'base.600',
bg: 'base.800',
};
});