From 270657a62c9337cad011d4973adaee126dfee7af Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Sun, 30 Apr 2023 22:01:34 +1000
Subject: [PATCH] feat(ui): gallery & progress image refactor
---
invokeai/frontend/web/package.json | 2 +
invokeai/frontend/web/public/locales/en.json | 4 +
.../frontend/web/src/app/components/App.tsx | 145 +++++----
.../web/src/app/components/InvokeAIUI.tsx | 9 +-
.../web/src/common/components/IAISlider.tsx | 2 +-
.../store/thunks/mergeAndUploadCanvas.ts | 2 +-
.../components/CurrentImagePreview.tsx | 79 ++---
.../gallery/components/HoverableImage.tsx | 2 +-
.../components/ImageGalleryContent.tsx | 26 +-
.../gallery/components/ImageGalleryPanel.tsx | 4 +-
.../components/NextPrevImageButtons.tsx | 36 ++-
.../gallery/store/gallerySelectors.ts | 33 +-
.../features/gallery/store/gallerySlice.ts | 287 ++++--------------
.../components/ProgressImagePreview.tsx | 168 ++++++++++
.../parameters/store/generationSlice.ts | 6 +-
.../src/features/system/store/systemSlice.ts | 6 +-
.../tabs/Generate/GenerateContent.tsx | 2 +
.../web/src/features/ui/store/uiSlice.ts | 14 +
.../web/src/features/ui/store/uiTypes.ts | 4 +-
.../frontend/web/src/services/thunks/image.ts | 14 +-
invokeai/frontend/web/yarn.lock | 31 ++
21 files changed, 466 insertions(+), 410 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 326213c4f2..0d41e06897 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -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",
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index d3ccbcb395..e4e49138dc 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -645,5 +645,9 @@
"betaDarkenOutside": "Darken Outside",
"betaLimitToBox": "Limit To Box",
"betaPreserveMasked": "Preserve Masked"
+ },
+ "ui": {
+ "showProgressImages": "Show Progress Images",
+ "hideProgressImages": "Hide Progress Images"
}
}
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 37d0c7ba72..dff59efdb1 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -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 (
-
- {isLightboxEnabled && }
-
-
-
- {children || }
-
+
+ {isLightboxEnabled && }
+
+
+
-
-
-
-
-
+ {children || }
+
+
+
+
+
+
-
- {!isApplicationReady && !loadingOverridden && (
-
-
-
-
-
-
- )}
-
+
+ {!isApplicationReady && !loadingOverridden && (
+
+
+
+
+
+
+ )}
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
index 97a8be6fc1..ca48770119 100644
--- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
+++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
@@ -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';
diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx
index 44f039c433..48080e8970 100644
--- a/invokeai/frontend/web/src/common/components/IAISlider.tsx
+++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx
@@ -233,7 +233,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
hidden={hideTooltip}
{...sliderTooltipProps}
>
-
+
diff --git a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts
index d9feba63d3..e14871e11b 100644
--- a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts
+++ b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts
@@ -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,
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index 8855bf11d3..cb666dd128 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -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 && (
- ) : !imageToDisplay.isProgressImage ? (
-
- ) : undefined
- }
+ src={shouldHidePreview ? undefined : getUrl(image.url)}
+ width={image.metadata.width}
+ height={image.metadata.height}
+ fallback={shouldHidePreview ? : undefined}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
- imageRendering: imageToDisplay.isProgressImage
- ? 'pixelated'
- : 'initial',
borderRadius: 'base',
}}
/>
)}
{!shouldShowImageDetails && }
- {shouldShowImageDetails &&
- imageToDisplay &&
- 'metadata' in imageToDisplay.image && (
-
-
-
- )}
+ {shouldShowImageDetails && image && 'metadata' in image && (
+
+
+
+ )}
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index bd9ff92082..d0ff9aee40 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -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(
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index e08e934e75..6524452e90 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -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 = () => {
}
- onClick={() => dispatch(setCurrentCategory('result'))}
+ onClick={() => dispatch(setCurrentCategory('results'))}
/>
}
- onClick={() => dispatch(setCurrentCategory('user'))}
+ onClick={() => dispatch(setCurrentCategory('uploads'))}
/>
>
) : (
<>
dispatch(setCurrentCategory('result'))}
+ isChecked={currentCategory === 'results'}
+ onClick={() => dispatch(setCurrentCategory('results'))}
flexGrow={1}
>
{t('gallery.generations')}
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 (
{
- 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 = () => {
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);
}
}
);
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index 4d752a151b..4a7aa8a7e7 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -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;
- 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) => {
- state.selectedImageName = action.payload;
- },
- setCurrentImage: (state, action: PayloadAction) => {
- state.currentImage = action.payload;
- state.currentImageUuid = action.payload.uuid;
- },
- removeImage: (
+ imageSelected: (
state,
- action: PayloadAction
+ action: PayloadAction
) => {
- 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) => {
- 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) => {
state.galleryImageMinimumWidth = action.payload;
},
@@ -267,7 +87,10 @@ export const gallerySlice = createSlice({
setShouldAutoSwitchToNewImages: (state, action: PayloadAction) => {
state.shouldAutoSwitchToNewImages = action.payload;
},
- setCurrentCategory: (state, action: PayloadAction) => {
+ setCurrentCategory: (
+ state,
+ action: PayloadAction<'results' | 'uploads'>
+ ) => {
state.currentCategory = action.payload;
},
setGalleryWidth: (state, action: PayloadAction) => {
@@ -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;
diff --git a/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx
new file mode 100644
index 0000000000..63771c3ecd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx
@@ -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 ? (
+
+
+
+
+
+
+ {progressImage ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ }
+ sx={{
+ position: 'absolute',
+ top: 2,
+ insetInlineEnd: 2,
+ }}
+ variant="ghost"
+ />
+
+
+
+
+
+ ) : (
+ }
+ />
+ );
+};
+
+export default memo(ProgressImagePreview);
diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
index 5dbe6b2c28..9d9d689cb0 100644
--- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
+++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
@@ -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) => {
state.verticalSymmetrySteps = action.payload;
},
- initialImageSelected: (state, action: PayloadAction) => {
+ initialImageSelected: (state, action: PayloadAction) => {
state.initialImage = action.payload;
state.isImageToImageEnabled = true;
},
diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
index cfaffdb65c..e7f58d2f4b 100644
--- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
@@ -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';
});
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx
index 53fdcb4a49..de7b738956 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx
@@ -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 (
) => {
+ const { x, y } = state.floatingProgressImageCoordinates;
+ const { x: _x, y: _y } = action.payload;
+
+ state.floatingProgressImageCoordinates = { x: x + _x, y: y + _y };
+ },
+ shouldShowProgressImageChanged: (state, action: PayloadAction) => {
+ state.shouldShowProgressImage = action.payload;
+ },
},
});
@@ -128,6 +140,8 @@ export const {
toggleParametersPanel,
toggleGalleryPanel,
openAccordionItemsChanged,
+ floatingProgressImageMoved,
+ shouldShowProgressImageChanged,
} = uiSlice.actions;
export default uiSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index 66e85ce71b..309b110d4d 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -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;
}
diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts
index c4da9d9f16..a0d8f504b7 100644
--- a/invokeai/frontend/web/src/services/thunks/image.ts
+++ b/invokeai/frontend/web/src/services/thunks/image.ts
@@ -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);
diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock
index 88fec64725..f7f9af51ef 100644
--- a/invokeai/frontend/web/yarn.lock
+++ b/invokeai/frontend/web/yarn.lock
@@ -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"