mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip canvas migration 4
This commit is contained in:
parent
6ab5d28cf3
commit
ed1f096a6f
@ -7,14 +7,20 @@ import {
|
||||
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 { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||
import { addImageResultReceivedListener } from './listeners/invocationComplete';
|
||||
import { addImageUploadedListener } from './listeners/imageUploaded';
|
||||
import { addRequestedImageDeletionListener } from './listeners/imageDeleted';
|
||||
import {
|
||||
imageResultReceivedListener,
|
||||
imageResultReceivedPrediate,
|
||||
} from './listeners/invocationCompleteListener';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { imageUploadedListener } from './listeners/imageUploadedListener';
|
||||
canvasGraphBuilt,
|
||||
sessionCreated,
|
||||
sessionInvoked,
|
||||
} from 'services/thunks/session';
|
||||
import { tabMap } from 'features/ui/store/tabMap';
|
||||
import {
|
||||
canvasSessionIdChanged,
|
||||
stagingAreaInitialized,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -34,26 +40,30 @@ export type AppListenerEffect = ListenerEffect<
|
||||
AppDispatch
|
||||
>;
|
||||
|
||||
/**
|
||||
* Initial image selected
|
||||
*/
|
||||
startAppListening({
|
||||
actionCreator: initialImageSelected,
|
||||
effect: initialImageListener,
|
||||
});
|
||||
addImageUploadedListener();
|
||||
addInitialImageSelectedListener();
|
||||
addImageResultReceivedListener();
|
||||
addRequestedImageDeletionListener();
|
||||
|
||||
/**
|
||||
* Image Result received
|
||||
*/
|
||||
startAppListening({
|
||||
predicate: imageResultReceivedPrediate,
|
||||
effect: imageResultReceivedListener,
|
||||
});
|
||||
actionCreator: canvasGraphBuilt.fulfilled,
|
||||
effect: async (action, { dispatch, getState, condition, fork, take }) => {
|
||||
const [{ meta }] = await take(sessionInvoked.fulfilled.match);
|
||||
const { sessionId } = meta.arg;
|
||||
const state = getState();
|
||||
|
||||
/**
|
||||
* Image Uploaded
|
||||
*/
|
||||
startAppListening({
|
||||
actionCreator: imageUploaded.fulfilled,
|
||||
effect: imageUploadedListener,
|
||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||
dispatch(
|
||||
stagingAreaInitialized({
|
||||
sessionId,
|
||||
boundingBox: {
|
||||
...state.canvas.boundingBoxCoordinates,
|
||||
...state.canvas.boundingBoxDimensions,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(canvasSessionIdChanged(sessionId));
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { requestedImageDeletion } from 'features/gallery/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { imageDeleted } from 'services/thunks/image';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||
|
||||
export const addRequestedImageDeletionListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: requestedImageDeletion,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const image = action.payload;
|
||||
if (!image) {
|
||||
moduleLog.warn('No image provided');
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, type } = image;
|
||||
|
||||
if (type !== 'uploads' && type !== 'results') {
|
||||
moduleLog.warn({ data: image }, `Invalid image type ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedImageName = getState().gallery.selectedImage?.name;
|
||||
|
||||
if (selectedImageName === name) {
|
||||
const allIds = getState()[type].ids;
|
||||
const allEntities = getState()[type].entities;
|
||||
|
||||
const deletedImageIndex = allIds.findIndex(
|
||||
(result) => result.toString() === name
|
||||
);
|
||||
|
||||
const filteredIds = allIds.filter((id) => id.toString() !== name);
|
||||
|
||||
const newSelectedImageIndex = clamp(
|
||||
deletedImageIndex,
|
||||
0,
|
||||
filteredIds.length - 1
|
||||
);
|
||||
|
||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||
|
||||
const newSelectedImage = allEntities[newSelectedImageId];
|
||||
|
||||
if (newSelectedImageId) {
|
||||
dispatch(imageSelected(newSelectedImage));
|
||||
} else {
|
||||
dispatch(imageSelected());
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(imageDeleted({ imageName: name, imageType: type }));
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||
import { startAppListening } from '..';
|
||||
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
|
||||
export const addImageUploadedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageUploaded.fulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { response } = action.payload;
|
||||
const state = getState();
|
||||
const image = deserializeImageResponse(response);
|
||||
|
||||
dispatch(uploadAdded(image));
|
||||
|
||||
if (state.gallery.shouldAutoSwitchToNewImages) {
|
||||
dispatch(imageSelected(image));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
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));
|
||||
}
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
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'))));
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
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 { startAppListening } from '..';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
|
||||
export const addInitialImageSelectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: initialImageSelected,
|
||||
effect: (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'))));
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,88 @@
|
||||
import { invocationComplete } from 'services/events/actions';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
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 { startAppListening } from '..';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
|
||||
const nodeDenylist = ['dataURL_image'];
|
||||
|
||||
export const addImageResultReceivedListener = () => {
|
||||
startAppListening({
|
||||
predicate: (action) => {
|
||||
if (
|
||||
invocationComplete.match(action) &&
|
||||
isImageOutput(action.payload.data.result)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
if (!invocationComplete.match(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, shouldFetchImages } = action.payload;
|
||||
const { result, node, graph_execution_state_id } = data;
|
||||
|
||||
if (isImageOutput(result) && !nodeDenylist.includes(node.type)) {
|
||||
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.gallery.shouldAutoSwitchToNewImages) {
|
||||
dispatch(imageSelected(image));
|
||||
}
|
||||
|
||||
if (state.config.shouldFetchImages) {
|
||||
dispatch(imageReceived({ imageName: name, imageType: type }));
|
||||
dispatch(
|
||||
thumbnailReceived({
|
||||
thumbnailName: name,
|
||||
thumbnailType: type,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
graph_execution_state_id ===
|
||||
state.canvas.layerState.stagingArea.sessionId
|
||||
) {
|
||||
dispatch(addImageToStagingArea(image));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -1,75 +0,0 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -12,18 +12,20 @@ const selector = createSelector(
|
||||
[canvasSelector],
|
||||
(canvas) => {
|
||||
const {
|
||||
layerState: {
|
||||
stagingArea: { images, selectedImageIndex },
|
||||
},
|
||||
layerState,
|
||||
shouldShowStagingImage,
|
||||
shouldShowStagingOutline,
|
||||
boundingBoxCoordinates: { x, y },
|
||||
boundingBoxDimensions: { width, height },
|
||||
} = canvas;
|
||||
|
||||
const { selectedImageIndex, images } = layerState.stagingArea;
|
||||
|
||||
return {
|
||||
currentStagingAreaImage:
|
||||
images.length > 0 ? images[selectedImageIndex] : undefined,
|
||||
images.length > 0 && selectedImageIndex !== undefined
|
||||
? images[selectedImageIndex]
|
||||
: undefined,
|
||||
isOnFirstImage: selectedImageIndex === 0,
|
||||
isOnLastImage: selectedImageIndex === images.length - 1,
|
||||
shouldShowStagingImage,
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
CanvasLayer,
|
||||
CanvasLayerState,
|
||||
CanvasMaskLine,
|
||||
CanvasSession,
|
||||
CanvasState,
|
||||
CanvasTool,
|
||||
Dimensions,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
isCanvasBaseImage,
|
||||
isCanvasMaskLine,
|
||||
} from './canvasTypes';
|
||||
import { stringToArray } from 'konva/lib/shapes/Text';
|
||||
|
||||
export const initialLayerState: CanvasLayerState = {
|
||||
objects: [],
|
||||
@ -285,16 +287,28 @@ export const canvasSlice = createSlice({
|
||||
setIsMoveStageKeyHeld: (state, action: PayloadAction<boolean>) => {
|
||||
state.isMoveStageKeyHeld = action.payload;
|
||||
},
|
||||
addImageToStagingArea: (
|
||||
canvasSessionIdChanged: (state, action: PayloadAction<string>) => {
|
||||
state.layerState.stagingArea.sessionId = action.payload;
|
||||
},
|
||||
stagingAreaInitialized: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
boundingBox: IRect;
|
||||
image: InvokeAI.Image;
|
||||
}>
|
||||
action: PayloadAction<{ sessionId: string; boundingBox: IRect }>
|
||||
) => {
|
||||
const { boundingBox, image } = action.payload;
|
||||
const { sessionId, boundingBox } = action.payload;
|
||||
|
||||
if (!boundingBox || !image) return;
|
||||
state.layerState.stagingArea = {
|
||||
boundingBox,
|
||||
sessionId,
|
||||
images: [],
|
||||
selectedImageIndex: -1,
|
||||
};
|
||||
},
|
||||
addImageToStagingArea: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
const image = action.payload;
|
||||
|
||||
if (!image || !state.layerState.stagingArea.boundingBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
|
||||
@ -305,7 +319,7 @@ export const canvasSlice = createSlice({
|
||||
state.layerState.stagingArea.images.push({
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
...boundingBox,
|
||||
...state.layerState.stagingArea.boundingBox,
|
||||
image,
|
||||
});
|
||||
|
||||
@ -321,9 +335,7 @@ export const canvasSlice = createSlice({
|
||||
state.pastLayerStates.shift();
|
||||
}
|
||||
|
||||
state.layerState.stagingArea = {
|
||||
...initialLayerState.stagingArea,
|
||||
};
|
||||
state.layerState.stagingArea = { ...initialLayerState.stagingArea };
|
||||
|
||||
state.futureLayerStates = [];
|
||||
state.shouldShowStagingOutline = true;
|
||||
@ -661,6 +673,10 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
},
|
||||
nextStagingAreaImage: (state) => {
|
||||
if (!state.layerState.stagingArea.images.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
||||
const length = state.layerState.stagingArea.images.length;
|
||||
|
||||
@ -670,6 +686,10 @@ export const canvasSlice = createSlice({
|
||||
);
|
||||
},
|
||||
prevStagingAreaImage: (state) => {
|
||||
if (!state.layerState.stagingArea.images.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
||||
|
||||
state.layerState.stagingArea.selectedImageIndex = Math.max(
|
||||
@ -678,6 +698,10 @@ export const canvasSlice = createSlice({
|
||||
);
|
||||
},
|
||||
commitStagingAreaImage: (state) => {
|
||||
if (!state.layerState.stagingArea.images.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { images, selectedImageIndex } = state.layerState.stagingArea;
|
||||
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
@ -883,6 +907,8 @@ export const {
|
||||
undo,
|
||||
setScaledBoundingBoxDimensions,
|
||||
setShouldRestrictStrokesToBox,
|
||||
stagingAreaInitialized,
|
||||
canvasSessionIdChanged,
|
||||
} = canvasSlice.actions;
|
||||
|
||||
export default canvasSlice.reducer;
|
||||
|
@ -90,9 +90,16 @@ export type CanvasLayerState = {
|
||||
stagingArea: {
|
||||
images: CanvasImage[];
|
||||
selectedImageIndex: number;
|
||||
sessionId?: string;
|
||||
boundingBox?: IRect;
|
||||
};
|
||||
};
|
||||
|
||||
export type CanvasSession = {
|
||||
sessionId: string;
|
||||
boundingBox: IRect;
|
||||
};
|
||||
|
||||
// type guards
|
||||
export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine =>
|
||||
obj.kind === 'line' && obj.layer === 'mask';
|
||||
@ -162,5 +169,4 @@ export interface CanvasState {
|
||||
stageDimensions: Dimensions;
|
||||
stageScale: number;
|
||||
tool: CanvasTool;
|
||||
pendingBoundingBox?: IRect;
|
||||
}
|
||||
|
@ -66,9 +66,9 @@ import { useCallback } from 'react';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { imageDeleted } from 'services/thunks/image';
|
||||
import { useParameters } from 'features/parameters/hooks/useParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { requestedImageDeletion } from '../store/actions';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[
|
||||
@ -376,7 +376,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (canDeleteImage && image) {
|
||||
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
|
||||
dispatch(requestedImageDeletion(image));
|
||||
}
|
||||
}, [image, canDeleteImage, dispatch]);
|
||||
|
||||
|
@ -28,7 +28,6 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { imageDeleted } from 'services/thunks/image';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
@ -36,6 +35,8 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useParameters } from 'features/parameters/hooks/useParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { requestedImageDeletion } from '../store/actions';
|
||||
|
||||
export const selector = createSelector(
|
||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||
@ -115,7 +116,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
// Immediately deletes an image
|
||||
const handleDelete = useCallback(() => {
|
||||
if (canDeleteImage && image) {
|
||||
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
|
||||
dispatch(requestedImageDeletion(image));
|
||||
}
|
||||
}, [dispatch, image, canDeleteImage]);
|
||||
|
||||
@ -151,8 +152,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
}, [image, recallSeed]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
sendToImageToImage(image);
|
||||
}, [image, sendToImageToImage]);
|
||||
dispatch(initialImageSelected(image));
|
||||
}, [dispatch, image]);
|
||||
|
||||
const handleRecallInitialImage = useCallback(() => {
|
||||
recallInitialImage(image.metadata.invokeai?.node?.image);
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { Image } from 'app/types/invokeai';
|
||||
import { SelectedImage } from 'features/parameters/store/actions';
|
||||
|
||||
export const requestedImageDeletion = createAction<
|
||||
Image | SelectedImage | undefined
|
||||
>('gallery/requestedImageDeletion');
|
@ -1,9 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { invocationComplete } from 'services/events/actions';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { Image } from 'app/types/invokeai';
|
||||
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
@ -67,20 +63,6 @@ export const gallerySlice = createSlice({
|
||||
state.shouldUseSingleGalleryColumn = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
/**
|
||||
* Invocation Complete
|
||||
*/
|
||||
builder.addCase(invocationComplete, (state, action) => {
|
||||
const { data } = action.payload;
|
||||
if (isImageOutput(data.result) && state.shouldAutoSwitchToNewImages) {
|
||||
state.selectedImage = {
|
||||
name: data.result.image.image_name,
|
||||
type: 'results',
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
@ -1,17 +1,11 @@
|
||||
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||
import { Image } from 'app/types/invokeai';
|
||||
import { invocationComplete } from 'services/events/actions';
|
||||
|
||||
import { RootState } from 'app/store/store';
|
||||
import {
|
||||
receivedResultImagesPage,
|
||||
IMAGES_PER_PAGE,
|
||||
} from 'services/thunks/gallery';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import {
|
||||
buildImageUrls,
|
||||
extractTimestampFromImageName,
|
||||
} from 'services/util/deserializeImageField';
|
||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||
import {
|
||||
imageDeleted,
|
||||
@ -73,44 +67,6 @@ const resultsSlice = createSlice({
|
||||
state.isLoading = false;
|
||||
});
|
||||
|
||||
// /**
|
||||
// * Invocation Complete
|
||||
// */
|
||||
// builder.addCase(invocationComplete, (state, action) => {
|
||||
// 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;
|
||||
|
||||
// // 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 } : {}),
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
// resultsAdapter.setOne(state, image);
|
||||
// }
|
||||
// });
|
||||
|
||||
/**
|
||||
* Image Received - FULFILLED
|
||||
*/
|
||||
@ -142,9 +98,10 @@ const resultsSlice = createSlice({
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Image - FULFILLED
|
||||
* Delete Image - PENDING
|
||||
* Pre-emptively remove the image from the gallery
|
||||
*/
|
||||
builder.addCase(imageDeleted.fulfilled, (state, action) => {
|
||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
||||
const { imageType, imageName } = action.meta.arg;
|
||||
|
||||
if (imageType === 'results') {
|
||||
|
@ -62,9 +62,10 @@ const uploadsSlice = createSlice({
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Image - FULFILLED
|
||||
* Delete Image - pending
|
||||
* Pre-emptively remove the image from the gallery
|
||||
*/
|
||||
builder.addCase(imageDeleted.fulfilled, (state, action) => {
|
||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
||||
const { imageType, imageName } = action.meta.arg;
|
||||
|
||||
if (imageType === 'uploads') {
|
||||
|
@ -88,7 +88,7 @@ export const useParameters = () => {
|
||||
}
|
||||
|
||||
dispatch(
|
||||
initialImageChanged({ name: image.image_name, type: image.image_type })
|
||||
initialImageSelected({ name: image.image_name, type: image.image_type })
|
||||
);
|
||||
toast({
|
||||
title: t('toast.initialImageSet'),
|
||||
|
@ -11,6 +11,7 @@ import { getTimestamp } from 'common/util/getTimestamp';
|
||||
import {
|
||||
sessionInvoked,
|
||||
isFulfilledSessionCreatedAction,
|
||||
sessionCreated,
|
||||
} from 'services/thunks/session';
|
||||
import { OpenAPI } from 'services/api';
|
||||
import { setEventListeners } from 'services/events/util/setEventListeners';
|
||||
@ -65,7 +66,7 @@ export const socketMiddleware = () => {
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
if (isFulfilledSessionCreatedAction(action)) {
|
||||
if (sessionCreated.fulfilled.match(action)) {
|
||||
const sessionId = action.payload.id;
|
||||
const sessionLog = socketioLog.child({ sessionId });
|
||||
const oldSessionId = getState().system.sessionId;
|
||||
|
@ -1,8 +1,5 @@
|
||||
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, isString } from 'lodash-es';
|
||||
import { ImagesService } from 'services/api';
|
||||
import { getHeaders } from 'services/util/getHeaders';
|
||||
|
||||
@ -15,7 +12,7 @@ type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0];
|
||||
*/
|
||||
export const imageReceived = createAppAsyncThunk(
|
||||
'api/imageReceived',
|
||||
async (arg: ImageReceivedArg, _thunkApi) => {
|
||||
async (arg: ImageReceivedArg) => {
|
||||
const response = await ImagesService.getImage(arg);
|
||||
|
||||
imagesLog.info({ arg, response }, 'Received image');
|
||||
@ -33,7 +30,7 @@ type ThumbnailReceivedArg = Parameters<
|
||||
*/
|
||||
export const thumbnailReceived = createAppAsyncThunk(
|
||||
'api/thumbnailReceived',
|
||||
async (arg: ThumbnailReceivedArg, _thunkApi) => {
|
||||
async (arg: ThumbnailReceivedArg) => {
|
||||
const response = await ImagesService.getThumbnail(arg);
|
||||
|
||||
imagesLog.info({ arg, response }, 'Received thumbnail');
|
||||
@ -49,7 +46,7 @@ type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
|
||||
*/
|
||||
export const imageUploaded = createAppAsyncThunk(
|
||||
'api/imageUploaded',
|
||||
async (arg: ImageUploadedArg, _thunkApi) => {
|
||||
async (arg: ImageUploadedArg) => {
|
||||
const response = await ImagesService.uploadImage(arg);
|
||||
const { location } = getHeaders(response);
|
||||
|
||||
@ -62,11 +59,6 @@ export const imageUploaded = createAppAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk
|
||||
*/
|
||||
export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded);
|
||||
|
||||
type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0];
|
||||
|
||||
/**
|
||||
@ -74,45 +66,7 @@ type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0];
|
||||
*/
|
||||
export const imageDeleted = createAppAsyncThunk(
|
||||
'api/imageDeleted',
|
||||
async (arg: ImageDeletedArg, { getState, dispatch }) => {
|
||||
const { imageType, imageName } = arg;
|
||||
|
||||
if (imageType !== 'uploads' && imageType !== 'results') {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: move this logic to another thunk?
|
||||
// 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.selectedImage?.name;
|
||||
|
||||
if (selectedImageName === imageName) {
|
||||
const allIds = getState()[imageType].ids;
|
||||
|
||||
const deletedImageIndex = allIds.findIndex(
|
||||
(result) => result.toString() === imageName
|
||||
);
|
||||
|
||||
const filteredIds = allIds.filter((id) => id.toString() !== imageName);
|
||||
|
||||
const newSelectedImageIndex = clamp(
|
||||
deletedImageIndex,
|
||||
0,
|
||||
filteredIds.length - 1
|
||||
);
|
||||
|
||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||
|
||||
if (newSelectedImageId) {
|
||||
dispatch(
|
||||
imageSelected({ name: newSelectedImageId as string, type: imageType })
|
||||
);
|
||||
} else {
|
||||
dispatch(imageSelected());
|
||||
}
|
||||
}
|
||||
|
||||
async (arg: ImageDeletedArg) => {
|
||||
const response = await ImagesService.deleteImage(arg);
|
||||
|
||||
imagesLog.info(
|
||||
|
Loading…
Reference in New Issue
Block a user