feat(ui): wip canvas migration, createListenerMiddleware

This commit is contained in:
psychedelicious 2023-05-03 19:27:29 +10:00
parent a75148cb16
commit 6ab5d28cf3
29 changed files with 352 additions and 175 deletions

View File

@ -118,6 +118,7 @@
"@types/node": "^18.16.2", "@types/node": "^18.16.2",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^18.2.1",
"@types/react-redux": "^7.1.25",
"@types/react-transition-group": "^4.4.5", "@types/react-transition-group": "^4.4.5",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/eslint-plugin": "^5.59.1",

View File

@ -550,7 +550,7 @@
"imageCopied": "Image Copied", "imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied", "imageLinkCopied": "Image Link Copied",
"imageNotLoaded": "No Image Loaded", "imageNotLoaded": "No Image Loaded",
"imageNotLoadedDesc": "No image found to send to image to image module", "imageNotLoadedDesc": "Could not find image",
"imageSavedToGallery": "Image Saved to Gallery", "imageSavedToGallery": "Image Saved to Gallery",
"canvasMerged": "Canvas Merged", "canvasMerged": "Canvas Merged",
"sentToImageToImage": "Sent To Image To Image", "sentToImageToImage": "Sent To Image To Image",

View File

@ -0,0 +1,59 @@
import {
createListenerMiddleware,
addListener,
ListenerEffect,
AnyAction,
} from '@reduxjs/toolkit';
import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit';
import type { RootState, AppDispatch } from '../../store';
import { initialImageSelected } from 'features/parameters/store/actions';
import { initialImageListener } from './listeners/initialImageListener';
import {
imageResultReceivedListener,
imageResultReceivedPrediate,
} from './listeners/invocationCompleteListener';
import { imageUploaded } from 'services/thunks/image';
import { imageUploadedListener } from './listeners/imageUploadedListener';
export const listenerMiddleware = createListenerMiddleware();
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
export const startAppListening =
listenerMiddleware.startListening as AppStartListening;
export const addAppListener = addListener as TypedAddListener<
RootState,
AppDispatch
>;
export type AppListenerEffect = ListenerEffect<
AnyAction,
RootState,
AppDispatch
>;
/**
* Initial image selected
*/
startAppListening({
actionCreator: initialImageSelected,
effect: initialImageListener,
});
/**
* Image Result received
*/
startAppListening({
predicate: imageResultReceivedPrediate,
effect: imageResultReceivedListener,
});
/**
* Image Uploaded
*/
startAppListening({
actionCreator: imageUploaded.fulfilled,
effect: imageUploadedListener,
});

View File

@ -0,0 +1,19 @@
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { AppListenerEffect } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
export const imageUploadedListener: AppListenerEffect = (
action,
{ dispatch, getState }
) => {
const { response } = action.payload;
const state = getState();
const image = deserializeImageResponse(response);
dispatch(uploadAdded(image));
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
};

View File

@ -0,0 +1,53 @@
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { Image, isInvokeAIImage } from 'app/types/invokeai';
import { selectResultsById } from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { makeToast } from 'features/system/hooks/useToastWatcher';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { AnyAction, ListenerEffect } from '@reduxjs/toolkit';
import { AppDispatch, RootState } from 'app/store/store';
export const initialImageListener: ListenerEffect<
AnyAction,
RootState,
AppDispatch
> = (action, { getState, dispatch }) => {
if (!action.payload) {
dispatch(
addToast(
makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
)
);
return;
}
if (isInvokeAIImage(action.payload)) {
dispatch(initialImageChanged(action.payload));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
return;
}
const { name, type } = action.payload;
let image: Image | undefined;
const state = getState();
if (type === 'results') {
image = selectResultsById(state, name);
} else if (type === 'uploads') {
image = selectUploadsById(state, name);
}
if (!image) {
dispatch(
addToast(
makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
)
);
return;
}
dispatch(initialImageChanged(image));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
};

View File

@ -0,0 +1,75 @@
import { AnyListenerPredicate } from '@reduxjs/toolkit';
import { invocationComplete } from 'services/events/actions';
import { isImageOutput } from 'services/types/guards';
import { RootState } from 'app/store/store';
import {
buildImageUrls,
extractTimestampFromImageName,
} from 'services/util/deserializeImageField';
import { Image } from 'app/types/invokeai';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import { AppListenerEffect } from '..';
export const imageResultReceivedPrediate: AnyListenerPredicate<RootState> = (
action,
_currentState,
_originalState
) => {
if (
invocationComplete.match(action) &&
isImageOutput(action.payload.data.result)
) {
return true;
}
return false;
};
export const imageResultReceivedListener: AppListenerEffect = (
action,
{ getState, dispatch }
) => {
const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) {
const name = result.image.image_name;
const type = result.image.image_type;
const state = getState();
// if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name);
const image: Image = {
name,
type,
url,
thumbnail,
metadata: {
created: timestamp,
width: result.width,
height: result.height,
invokeai: {
session_id: graph_execution_state_id,
...(node ? { node } : {}),
},
},
};
dispatch(resultAdded(image));
if (state.config.shouldFetchImages) {
dispatch(imageReceived({ imageName: name, imageType: type }));
dispatch(
thumbnailReceived({
thumbnailName: name,
thumbnailType: type,
})
);
}
}
};

View File

@ -1,4 +1,9 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit'; import {
AnyAction,
ThunkDispatch,
combineReducers,
configureStore,
} from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist'; import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
@ -30,6 +35,7 @@ import { systemDenylist } from 'features/system/store/systemPersistDenylist';
import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; import { uiDenylist } from 'features/ui/store/uiPersistDenylist';
import { resultsDenylist } from 'features/gallery/store/resultsPersistDenylist'; import { resultsDenylist } from 'features/gallery/store/resultsPersistDenylist';
import { uploadsDenylist } from 'features/gallery/store/uploadsPersistDenylist'; import { uploadsDenylist } from 'features/gallery/store/uploadsPersistDenylist';
import { listenerMiddleware } from './middleware/listenerMiddleware';
/** /**
* redux-persist provides an easy and reliable way to persist state across reloads. * redux-persist provides an easy and reliable way to persist state across reloads.
@ -101,7 +107,9 @@ export const store = configureStore({
getDefaultMiddleware({ getDefaultMiddleware({
immutableCheck: false, immutableCheck: false,
serializableCheck: false, serializableCheck: false,
}).concat(dynamicMiddlewares), })
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: { devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [ actionsDenylist: [
@ -120,4 +128,5 @@ export const store = configureStore({
export type AppGetState = typeof store.getState; export type AppGetState = typeof store.getState;
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;
export type AppThunkDispatch = ThunkDispatch<RootState, any, AnyAction>;
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;

View File

@ -1,6 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from 'app/store/store'; import { AppDispatch, AppThunkDispatch, RootState } from 'app/store/store';
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch = () => useDispatch<AppThunkDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -13,6 +13,7 @@
*/ */
import { GalleryCategory } from 'features/gallery/store/gallerySlice'; import { GalleryCategory } from 'features/gallery/store/gallerySlice';
import { SelectedImage } from 'features/parameters/store/actions';
import { FacetoolType } from 'features/parameters/store/postprocessingSlice'; import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types'; import { IRect } from 'konva/lib/types';
@ -126,6 +127,14 @@ export type Image = {
metadata: ImageResponseMetadata; metadata: ImageResponseMetadata;
}; };
export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => {
if ('url' in obj && 'thumbnail' in obj) {
return true;
}
return false;
};
/** /**
* Types related to the system status. * Types related to the system status.
*/ */

View File

@ -29,7 +29,6 @@ import {
isCanvasBaseImage, isCanvasBaseImage,
isCanvasMaskLine, isCanvasMaskLine,
} from './canvasTypes'; } from './canvasTypes';
import { invocationComplete } from 'services/events/actions';
export const initialLayerState: CanvasLayerState = { export const initialLayerState: CanvasLayerState = {
objects: [], objects: [],
@ -816,11 +815,6 @@ export const canvasSlice = createSlice({
state.isTransformingBoundingBox = false; state.isTransformingBoundingBox = false;
}, },
}, },
extraReducers(builder) {
builder.addCase(invocationComplete, (state, action) => {
//
});
},
}); });
export const { export const {

View File

@ -22,7 +22,7 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings';
import { import {
initialImageSelected, initialImageChanged,
setAllParameters, setAllParameters,
// setInitialImage, // setInitialImage,
setSeed, setSeed,
@ -68,6 +68,7 @@ import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { imageDeleted } from 'services/thunks/image'; import { imageDeleted } from 'services/thunks/image';
import { useParameters } from 'features/parameters/hooks/useParameters'; import { useParameters } from 'features/parameters/hooks/useParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -264,8 +265,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys('p', handleUsePrompt, [image]); useHotkeys('p', handleUsePrompt, [image]);
const handleSendToImageToImage = useCallback(() => { const handleSendToImageToImage = useCallback(() => {
sendToImageToImage(image); dispatch(initialImageSelected(image));
}, [image, sendToImageToImage]); }, [dispatch, image]);
useHotkeys('shift+i', handleSendToImageToImage, [image]); useHotkeys('shift+i', handleSendToImageToImage, [image]);

View File

@ -11,7 +11,7 @@ import CurrentImageFallback from './CurrentImageFallback';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
import CurrentImageHidden from './CurrentImageHidden'; import CurrentImageHidden from './CurrentImageHidden';
import { memo } from 'react'; import { DragEvent, memo, useCallback } from 'react';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[uiSelector, selectedImageSelector, systemSelector], [uiSelector, selectedImageSelector, systemSelector],
@ -36,6 +36,18 @@ const CurrentImagePreview = () => {
useAppSelector(imagesSelector); useAppSelector(imagesSelector);
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
if (!image) {
return;
}
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';
},
[image]
);
return ( return (
<Flex <Flex
sx={{ sx={{
@ -48,6 +60,7 @@ const CurrentImagePreview = () => {
> >
{image && ( {image && (
<Image <Image
onDragStart={handleDragStart}
src={shouldHidePreview ? undefined : getUrl(image.url)} src={shouldHidePreview ? undefined : getUrl(image.url)}
width={image.metadata.width} width={image.metadata.width}
height={image.metadata.height} height={image.metadata.height}

View File

@ -134,7 +134,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
console.log('dragging');
e.dataTransfer.setData('invokeai/imageName', image.name); e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';

View File

@ -4,7 +4,7 @@ import { invocationComplete } from 'services/events/actions';
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'; import { Image } from 'app/types/invokeai';
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
@ -12,7 +12,7 @@ export interface GalleryState {
/** /**
* The selected image * The selected image
*/ */
selectedImage?: SelectedImage; selectedImage?: Image;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType; galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean; shouldAutoSwitchToNewImages: boolean;
@ -22,7 +22,6 @@ export interface GalleryState {
} }
const initialState: GalleryState = { const initialState: GalleryState = {
selectedImage: undefined,
galleryImageMinimumWidth: 64, galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover', galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true, shouldAutoSwitchToNewImages: true,
@ -35,10 +34,7 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState, initialState,
reducers: { reducers: {
imageSelected: ( imageSelected: (state, action: PayloadAction<Image | undefined>) => {
state,
action: PayloadAction<SelectedImage | undefined>
) => {
state.selectedImage = action.payload; state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch? // TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false; // state.shouldAutoSwitchToNewImages = false;
@ -84,16 +80,6 @@ export const gallerySlice = createSlice({
}; };
} }
}); });
/**
* Upload Image - FULFILLED
*/
builder.addCase(imageUploaded.fulfilled, (state, action) => {
const { response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
state.selectedImage = { name: uploadedImage.name, type: 'uploads' };
});
}, },
}); });

View File

@ -73,43 +73,43 @@ const resultsSlice = createSlice({
state.isLoading = false; state.isLoading = false;
}); });
/** // /**
* Invocation Complete // * Invocation Complete
*/ // */
builder.addCase(invocationComplete, (state, action) => { // builder.addCase(invocationComplete, (state, action) => {
const { data, shouldFetchImages } = action.payload; // const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data; // const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) { // if (isImageOutput(result)) {
const name = result.image.image_name; // const name = result.image.image_name;
const type = result.image.image_type; // const type = result.image.image_type;
// if we need to refetch, set URLs to placeholder for now // // if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = shouldFetchImages // const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' } // ? { url: '', thumbnail: '' }
: buildImageUrls(type, name); // : buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name); // const timestamp = extractTimestampFromImageName(name);
const image: Image = { // const image: Image = {
name, // name,
type, // type,
url, // url,
thumbnail, // thumbnail,
metadata: { // metadata: {
created: timestamp, // created: timestamp,
width: result.width, // width: result.width,
height: result.height, // height: result.height,
invokeai: { // invokeai: {
session_id: graph_execution_state_id, // session_id: graph_execution_state_id,
...(node ? { node } : {}), // ...(node ? { node } : {}),
}, // },
}, // },
}; // };
resultsAdapter.setOne(state, image); // resultsAdapter.setOne(state, image);
} // }
}); // });
/** /**
* Image Received - FULFILLED * Image Received - FULFILLED

View File

@ -35,7 +35,7 @@ const uploadsSlice = createSlice({
name: 'uploads', name: 'uploads',
initialState: initialUploadsState, initialState: initialUploadsState,
reducers: { reducers: {
uploadAdded: uploadsAdapter.addOne, uploadAdded: uploadsAdapter.upsertOne,
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
/** /**
@ -61,17 +61,6 @@ const uploadsSlice = createSlice({
state.isLoading = false; state.isLoading = false;
}); });
/**
* Upload Image - FULFILLED
*/
builder.addCase(imageUploaded.fulfilled, (state, action) => {
const { location, response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
uploadsAdapter.setOne(state, uploadedImage);
});
/** /**
* Delete Image - FULFILLED * Delete Image - FULFILLED
*/ */

View File

@ -5,9 +5,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName';
import { import generationSlice, {
clearInitialImage, clearInitialImage,
initialImageSelected, initialImageChanged,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -15,23 +15,26 @@ import { DragEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ImageType } from 'services/api'; import { ImageType } from 'services/api';
import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; import ImageToImageOverlay from 'common/components/ImageToImageOverlay';
import { initialImageSelector } from 'features/parameters/store/generationSelectors'; import {
generationSelector,
initialImageSelector,
} from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions';
const selector = createSelector( const selector = createSelector(
[initialImageSelector], [generationSelector],
(initialImage) => { (generation) => {
const { initialImage, isImageToImageEnabled } = generation;
return { return {
initialImage, initialImage,
isImageToImageEnabled,
}; };
}, },
{ memoizeOptions: { resultEqualityCheck: isEqual } } { memoizeOptions: { resultEqualityCheck: isEqual } }
); );
const InitialImagePreview = () => { const InitialImagePreview = () => {
const isImageToImageEnabled = useAppSelector( const { initialImage, isImageToImageEnabled } = useAppSelector(selector);
(state: RootState) => state.generation.isImageToImageEnabled
);
const { initialImage } = useAppSelector(selector);
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -55,22 +58,13 @@ const InitialImagePreview = () => {
const handleDrop = useCallback( const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
setIsLoaded(false); setIsLoaded(false);
const name = e.dataTransfer.getData('invokeai/imageName'); const name = e.dataTransfer.getData('invokeai/imageName');
const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; const type = e.dataTransfer.getData('invokeai/imageType') as ImageType;
if (!name || !type) {
return;
}
const image = getImageByNameAndType(name, type);
if (!image) {
return;
}
dispatch(initialImageSelected({ name, type })); dispatch(initialImageSelected({ name, type }));
}, },
[getImageByNameAndType, dispatch] [dispatch]
); );
return ( return (

View File

@ -4,9 +4,11 @@ import { isFinite, isString } from 'lodash-es';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useSetBothPrompts from './usePrompt'; import useSetBothPrompts from './usePrompt';
import { initialImageSelected, setSeed } from '../store/generationSlice'; import { initialImageChanged, setSeed } from '../store/generationSlice';
import { isImage, isImageField } from 'services/types/guards'; import { isImage, isImageField } from 'services/types/guards';
import { NUMPY_RAND_MAX } from 'app/constants'; import { NUMPY_RAND_MAX } from 'app/constants';
import { initialImageSelected } from '../store/actions';
import { Image } from 'app/types/invokeai';
export const useParameters = () => { export const useParameters = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -86,7 +88,7 @@ export const useParameters = () => {
} }
dispatch( dispatch(
initialImageSelected({ name: image.image_name, type: image.image_type }) initialImageChanged({ name: image.image_name, type: image.image_type })
); );
toast({ toast({
title: t('toast.initialImageSet'), title: t('toast.initialImageSet'),
@ -102,27 +104,10 @@ export const useParameters = () => {
* Sets image as initial image with toast * Sets image as initial image with toast
*/ */
const sendToImageToImage = useCallback( const sendToImageToImage = useCallback(
(image: unknown) => { (image: Image) => {
if (!isImage(image)) {
toast({
title: t('toast.imageNotLoaded'),
description: t('toast.imageNotLoadedDesc'),
status: 'warning',
duration: 2500,
isClosable: true,
});
return;
}
dispatch(initialImageSelected({ name: image.name, type: image.type })); dispatch(initialImageSelected({ name: image.name, type: image.type }));
toast({
title: t('toast.sentToImageToImage'),
status: 'info',
duration: 2500,
isClosable: true,
});
}, },
[t, toast, dispatch] [dispatch]
); );
return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage }; return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage };

View File

@ -0,0 +1,12 @@
import { createAction } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai';
import { ImageType } from 'services/api';
export type SelectedImage = {
name: string;
type: ImageType;
};
export const initialImageSelected = createAction<
Image | SelectedImage | undefined
>('generation/initialImageSelected');

View File

@ -7,17 +7,12 @@ 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 SelectedImage = {
name: string;
type: ImageType;
};
export interface GenerationState { export interface GenerationState {
cfgScale: number; cfgScale: number;
height: number; height: number;
img2imgStrength: number; img2imgStrength: number;
infillMethod: string; infillMethod: string;
initialImage?: SelectedImage; // can be an Image or url initialImage?: InvokeAI.Image; // can be an Image or url
iterations: number; iterations: number;
maskPath: string; maskPath: string;
perlin: number; perlin: number;
@ -351,7 +346,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<SelectedImage>) => { initialImageChanged: (state, action: PayloadAction<InvokeAI.Image>) => {
state.initialImage = action.payload; state.initialImage = action.payload;
state.isImageToImageEnabled = true; state.isImageToImageEnabled = true;
}, },
@ -399,7 +394,7 @@ export const {
setShouldUseSymmetry, setShouldUseSymmetry,
setHorizontalSymmetrySteps, setHorizontalSymmetrySteps,
setVerticalSymmetrySteps, setVerticalSymmetrySteps,
initialImageSelected, initialImageChanged,
isImageToImageEnabledChanged, isImageToImageEnabledChanged,
} = generationSlice.actions; } = generationSlice.actions;

View File

@ -15,7 +15,7 @@ import {
} from 'services/events/actions'; } from 'services/events/actions';
import { ProgressImage } from 'services/events/types'; import { ProgressImage } from 'services/events/types';
import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { makeToast } from '../hooks/useToastWatcher'; import { makeToast } from '../hooks/useToastWatcher';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model'; import { receivedModels } from 'services/thunks/model';
@ -434,13 +434,6 @@ export const systemSlice = createSlice({
state.statusTranslationKey = 'common.statusConnected'; state.statusTranslationKey = 'common.statusConnected';
}); });
/**
* Initial Image Selected
*/
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(t('toast.sentToImageToImage')));
});
/** /**
* Received available models from the backend * Received available models from the backend
*/ */

View File

@ -1,7 +1,7 @@
import { Box, BoxProps, Grid, GridItem } from '@chakra-ui/react'; import { Box, BoxProps, Grid, GridItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { import {
activeTabNameSelector, activeTabNameSelector,
uiSelector, uiSelector,

View File

@ -3,7 +3,7 @@
/* eslint-disable */ /* eslint-disable */
/** /**
* Outputs an image from a base 64 data URL. * Outputs an image from a data URL.
*/ */
export type DataURLToImageInvocation = { export type DataURLToImageInvocation = {
/** /**

View File

@ -2,7 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export const $DataURLToImageInvocation = { export const $DataURLToImageInvocation = {
description: `Outputs an image from a base 64 data URL.`, description: `Outputs an image from a data URL.`,
properties: { properties: {
id: { id: {
type: 'string', type: 'string',

View File

@ -1,4 +1,4 @@
import { createAction } from '@reduxjs/toolkit'; import { AnyAction, createAction } from '@reduxjs/toolkit';
import { import {
GeneratorProgressEvent, GeneratorProgressEvent,
GraphExecutionStateCompleteEvent, GraphExecutionStateCompleteEvent,

View File

@ -5,20 +5,14 @@ import {
ClientToServerEvents, ClientToServerEvents,
ServerToClientEvents, ServerToClientEvents,
} from 'services/events/types'; } from 'services/events/types';
import { import { socketSubscribed, socketUnsubscribed } from './actions';
invocationComplete, import { AppThunkDispatch, RootState } from 'app/store/store';
socketSubscribed,
socketUnsubscribed,
} from './actions';
import { AppDispatch, RootState } from 'app/store/store';
import { getTimestamp } from 'common/util/getTimestamp'; import { getTimestamp } from 'common/util/getTimestamp';
import { import {
sessionInvoked, sessionInvoked,
isFulfilledSessionCreatedAction, isFulfilledSessionCreatedAction,
} from 'services/thunks/session'; } from 'services/thunks/session';
import { OpenAPI } from 'services/api'; import { OpenAPI } from 'services/api';
import { isImageOutput } from 'services/types/guards';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import { setEventListeners } from 'services/events/util/setEventListeners'; import { setEventListeners } from 'services/events/util/setEventListeners';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
@ -56,13 +50,15 @@ export const socketMiddleware = () => {
); );
const middleware: Middleware = const middleware: Middleware =
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => { (storeApi: MiddlewareAPI<AppThunkDispatch, RootState>) =>
const { dispatch, getState } = store; (next) =>
(action) => {
const { dispatch, getState } = storeApi;
// Set listeners for `connect` and `disconnect` events once // Set listeners for `connect` and `disconnect` events once
// Must happen in middleware to get access to `dispatch` // Must happen in middleware to get access to `dispatch`
if (!areListenersSet) { if (!areListenersSet) {
setEventListeners({ store, socket, log: socketioLog }); setEventListeners({ storeApi, socket, log: socketioLog });
areListenersSet = true; areListenersSet = true;
@ -107,26 +103,6 @@ export const socketMiddleware = () => {
dispatch(sessionInvoked({ sessionId })); dispatch(sessionInvoked({ sessionId }));
} }
if (invocationComplete.match(action)) {
const { config } = getState();
if (config.shouldFetchImages) {
const { result } = action.payload.data;
if (isImageOutput(result)) {
const imageName = result.image.image_name;
const imageType = result.image.image_type;
dispatch(imageReceived({ imageName, imageType }));
dispatch(
thumbnailReceived({
thumbnailName: imageName,
thumbnailType: imageType,
})
);
}
}
}
next(action); next(action);
}; };

View File

@ -27,13 +27,13 @@ import { addToast } from '../../../features/system/store/systemSlice';
type SetEventListenersArg = { type SetEventListenersArg = {
socket: Socket<ServerToClientEvents, ClientToServerEvents>; socket: Socket<ServerToClientEvents, ClientToServerEvents>;
store: MiddlewareAPI<AppDispatch, RootState>; storeApi: MiddlewareAPI<AppDispatch, RootState>;
log: Logger<JsonObject>; log: Logger<JsonObject>;
}; };
export const setEventListeners = (arg: SetEventListenersArg) => { export const setEventListeners = (arg: SetEventListenersArg) => {
const { socket, store, log } = arg; const { socket, storeApi, log } = arg;
const { dispatch, getState } = store; const { dispatch, getState } = storeApi;
/** /**
* Connect * Connect

View File

@ -20,7 +20,12 @@
"*": ["./src/*"] "*": ["./src/*"]
} }
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "*.d.ts"], "include": [
"src/**/*.ts",
"src/**/*.tsx",
"*.d.ts",
"src/app/store/middleware/listenerMiddleware"
],
"exclude": ["src/services/fixtures/*", "node_modules", "dist"], "exclude": ["src/services/fixtures/*", "node_modules", "dist"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -1836,7 +1836,7 @@
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
"@types/hoist-non-react-statics@^3.3.1": "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -1907,6 +1907,16 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-redux@^7.1.25":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-transition-group@^4.4.5": "@types/react-transition-group@^4.4.5":
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
@ -5687,7 +5697,7 @@ redux-thunk@^2.4.2:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
redux@^4.2.1: redux@^4.0.0, redux@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==