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 { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import type { RootState, AppDispatch } from '../../store';
|
import type { RootState, AppDispatch } from '../../store';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||||
import { initialImageListener } from './listeners/initialImageListener';
|
import { addImageResultReceivedListener } from './listeners/invocationComplete';
|
||||||
|
import { addImageUploadedListener } from './listeners/imageUploaded';
|
||||||
|
import { addRequestedImageDeletionListener } from './listeners/imageDeleted';
|
||||||
import {
|
import {
|
||||||
imageResultReceivedListener,
|
canvasGraphBuilt,
|
||||||
imageResultReceivedPrediate,
|
sessionCreated,
|
||||||
} from './listeners/invocationCompleteListener';
|
sessionInvoked,
|
||||||
import { imageUploaded } from 'services/thunks/image';
|
} from 'services/thunks/session';
|
||||||
import { imageUploadedListener } from './listeners/imageUploadedListener';
|
import { tabMap } from 'features/ui/store/tabMap';
|
||||||
|
import {
|
||||||
|
canvasSessionIdChanged,
|
||||||
|
stagingAreaInitialized,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -34,26 +40,30 @@ export type AppListenerEffect = ListenerEffect<
|
|||||||
AppDispatch
|
AppDispatch
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
addImageUploadedListener();
|
||||||
* Initial image selected
|
addInitialImageSelectedListener();
|
||||||
*/
|
addImageResultReceivedListener();
|
||||||
startAppListening({
|
addRequestedImageDeletionListener();
|
||||||
actionCreator: initialImageSelected,
|
|
||||||
effect: initialImageListener,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image Result received
|
|
||||||
*/
|
|
||||||
startAppListening({
|
startAppListening({
|
||||||
predicate: imageResultReceivedPrediate,
|
actionCreator: canvasGraphBuilt.fulfilled,
|
||||||
effect: imageResultReceivedListener,
|
effect: async (action, { dispatch, getState, condition, fork, take }) => {
|
||||||
});
|
const [{ meta }] = await take(sessionInvoked.fulfilled.match);
|
||||||
|
const { sessionId } = meta.arg;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
/**
|
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||||
* Image Uploaded
|
dispatch(
|
||||||
*/
|
stagingAreaInitialized({
|
||||||
startAppListening({
|
sessionId,
|
||||||
actionCreator: imageUploaded.fulfilled,
|
boundingBox: {
|
||||||
effect: imageUploadedListener,
|
...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],
|
[canvasSelector],
|
||||||
(canvas) => {
|
(canvas) => {
|
||||||
const {
|
const {
|
||||||
layerState: {
|
layerState,
|
||||||
stagingArea: { images, selectedImageIndex },
|
|
||||||
},
|
|
||||||
shouldShowStagingImage,
|
shouldShowStagingImage,
|
||||||
shouldShowStagingOutline,
|
shouldShowStagingOutline,
|
||||||
boundingBoxCoordinates: { x, y },
|
boundingBoxCoordinates: { x, y },
|
||||||
boundingBoxDimensions: { width, height },
|
boundingBoxDimensions: { width, height },
|
||||||
} = canvas;
|
} = canvas;
|
||||||
|
|
||||||
|
const { selectedImageIndex, images } = layerState.stagingArea;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentStagingAreaImage:
|
currentStagingAreaImage:
|
||||||
images.length > 0 ? images[selectedImageIndex] : undefined,
|
images.length > 0 && selectedImageIndex !== undefined
|
||||||
|
? images[selectedImageIndex]
|
||||||
|
: undefined,
|
||||||
isOnFirstImage: selectedImageIndex === 0,
|
isOnFirstImage: selectedImageIndex === 0,
|
||||||
isOnLastImage: selectedImageIndex === images.length - 1,
|
isOnLastImage: selectedImageIndex === images.length - 1,
|
||||||
shouldShowStagingImage,
|
shouldShowStagingImage,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
CanvasLayer,
|
CanvasLayer,
|
||||||
CanvasLayerState,
|
CanvasLayerState,
|
||||||
CanvasMaskLine,
|
CanvasMaskLine,
|
||||||
|
CanvasSession,
|
||||||
CanvasState,
|
CanvasState,
|
||||||
CanvasTool,
|
CanvasTool,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@ -29,6 +30,7 @@ import {
|
|||||||
isCanvasBaseImage,
|
isCanvasBaseImage,
|
||||||
isCanvasMaskLine,
|
isCanvasMaskLine,
|
||||||
} from './canvasTypes';
|
} from './canvasTypes';
|
||||||
|
import { stringToArray } from 'konva/lib/shapes/Text';
|
||||||
|
|
||||||
export const initialLayerState: CanvasLayerState = {
|
export const initialLayerState: CanvasLayerState = {
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -285,16 +287,28 @@ export const canvasSlice = createSlice({
|
|||||||
setIsMoveStageKeyHeld: (state, action: PayloadAction<boolean>) => {
|
setIsMoveStageKeyHeld: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isMoveStageKeyHeld = action.payload;
|
state.isMoveStageKeyHeld = action.payload;
|
||||||
},
|
},
|
||||||
addImageToStagingArea: (
|
canvasSessionIdChanged: (state, action: PayloadAction<string>) => {
|
||||||
|
state.layerState.stagingArea.sessionId = action.payload;
|
||||||
|
},
|
||||||
|
stagingAreaInitialized: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{ sessionId: string; boundingBox: IRect }>
|
||||||
boundingBox: IRect;
|
|
||||||
image: InvokeAI.Image;
|
|
||||||
}>
|
|
||||||
) => {
|
) => {
|
||||||
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));
|
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||||
|
|
||||||
@ -305,7 +319,7 @@ export const canvasSlice = createSlice({
|
|||||||
state.layerState.stagingArea.images.push({
|
state.layerState.stagingArea.images.push({
|
||||||
kind: 'image',
|
kind: 'image',
|
||||||
layer: 'base',
|
layer: 'base',
|
||||||
...boundingBox,
|
...state.layerState.stagingArea.boundingBox,
|
||||||
image,
|
image,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -321,9 +335,7 @@ export const canvasSlice = createSlice({
|
|||||||
state.pastLayerStates.shift();
|
state.pastLayerStates.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.layerState.stagingArea = {
|
state.layerState.stagingArea = { ...initialLayerState.stagingArea };
|
||||||
...initialLayerState.stagingArea,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.shouldShowStagingOutline = true;
|
state.shouldShowStagingOutline = true;
|
||||||
@ -661,6 +673,10 @@ export const canvasSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
nextStagingAreaImage: (state) => {
|
nextStagingAreaImage: (state) => {
|
||||||
|
if (!state.layerState.stagingArea.images.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
||||||
const length = state.layerState.stagingArea.images.length;
|
const length = state.layerState.stagingArea.images.length;
|
||||||
|
|
||||||
@ -670,6 +686,10 @@ export const canvasSlice = createSlice({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
prevStagingAreaImage: (state) => {
|
prevStagingAreaImage: (state) => {
|
||||||
|
if (!state.layerState.stagingArea.images.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
const currentIndex = state.layerState.stagingArea.selectedImageIndex;
|
||||||
|
|
||||||
state.layerState.stagingArea.selectedImageIndex = Math.max(
|
state.layerState.stagingArea.selectedImageIndex = Math.max(
|
||||||
@ -678,6 +698,10 @@ export const canvasSlice = createSlice({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
commitStagingAreaImage: (state) => {
|
commitStagingAreaImage: (state) => {
|
||||||
|
if (!state.layerState.stagingArea.images.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { images, selectedImageIndex } = state.layerState.stagingArea;
|
const { images, selectedImageIndex } = state.layerState.stagingArea;
|
||||||
|
|
||||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||||
@ -883,6 +907,8 @@ export const {
|
|||||||
undo,
|
undo,
|
||||||
setScaledBoundingBoxDimensions,
|
setScaledBoundingBoxDimensions,
|
||||||
setShouldRestrictStrokesToBox,
|
setShouldRestrictStrokesToBox,
|
||||||
|
stagingAreaInitialized,
|
||||||
|
canvasSessionIdChanged,
|
||||||
} = canvasSlice.actions;
|
} = canvasSlice.actions;
|
||||||
|
|
||||||
export default canvasSlice.reducer;
|
export default canvasSlice.reducer;
|
||||||
|
@ -90,9 +90,16 @@ export type CanvasLayerState = {
|
|||||||
stagingArea: {
|
stagingArea: {
|
||||||
images: CanvasImage[];
|
images: CanvasImage[];
|
||||||
selectedImageIndex: number;
|
selectedImageIndex: number;
|
||||||
|
sessionId?: string;
|
||||||
|
boundingBox?: IRect;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CanvasSession = {
|
||||||
|
sessionId: string;
|
||||||
|
boundingBox: IRect;
|
||||||
|
};
|
||||||
|
|
||||||
// type guards
|
// type guards
|
||||||
export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine =>
|
export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine =>
|
||||||
obj.kind === 'line' && obj.layer === 'mask';
|
obj.kind === 'line' && obj.layer === 'mask';
|
||||||
@ -162,5 +169,4 @@ export interface CanvasState {
|
|||||||
stageDimensions: Dimensions;
|
stageDimensions: Dimensions;
|
||||||
stageScale: number;
|
stageScale: number;
|
||||||
tool: CanvasTool;
|
tool: CanvasTool;
|
||||||
pendingBoundingBox?: IRect;
|
|
||||||
}
|
}
|
||||||
|
@ -66,9 +66,9 @@ import { useCallback } from 'react';
|
|||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
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 { useParameters } from 'features/parameters/hooks/useParameters';
|
import { useParameters } from 'features/parameters/hooks/useParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
|
import { requestedImageDeletion } from '../store/actions';
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -376,7 +376,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (canDeleteImage && image) {
|
if (canDeleteImage && image) {
|
||||||
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
|
dispatch(requestedImageDeletion(image));
|
||||||
}
|
}
|
||||||
}, [image, canDeleteImage, dispatch]);
|
}, [image, canDeleteImage, dispatch]);
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import { useGetUrl } from 'common/util/getUrl';
|
import { useGetUrl } from 'common/util/getUrl';
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||||
import { imageDeleted } from 'services/thunks/image';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||||
@ -36,6 +35,8 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useParameters } from 'features/parameters/hooks/useParameters';
|
import { useParameters } from 'features/parameters/hooks/useParameters';
|
||||||
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
|
import { requestedImageDeletion } from '../store/actions';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||||
@ -115,7 +116,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
// Immediately deletes an image
|
// Immediately deletes an image
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (canDeleteImage && image) {
|
if (canDeleteImage && image) {
|
||||||
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
|
dispatch(requestedImageDeletion(image));
|
||||||
}
|
}
|
||||||
}, [dispatch, image, canDeleteImage]);
|
}, [dispatch, image, canDeleteImage]);
|
||||||
|
|
||||||
@ -151,8 +152,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
}, [image, recallSeed]);
|
}, [image, recallSeed]);
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
sendToImageToImage(image);
|
dispatch(initialImageSelected(image));
|
||||||
}, [image, sendToImageToImage]);
|
}, [dispatch, image]);
|
||||||
|
|
||||||
const handleRecallInitialImage = useCallback(() => {
|
const handleRecallInitialImage = useCallback(() => {
|
||||||
recallInitialImage(image.metadata.invokeai?.node?.image);
|
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 type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } 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';
|
import { Image } from 'app/types/invokeai';
|
||||||
|
|
||||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
@ -67,20 +63,6 @@ export const gallerySlice = createSlice({
|
|||||||
state.shouldUseSingleGalleryColumn = action.payload;
|
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 {
|
export const {
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||||
import { Image } from 'app/types/invokeai';
|
import { Image } from 'app/types/invokeai';
|
||||||
import { invocationComplete } from 'services/events/actions';
|
|
||||||
|
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import {
|
import {
|
||||||
receivedResultImagesPage,
|
receivedResultImagesPage,
|
||||||
IMAGES_PER_PAGE,
|
IMAGES_PER_PAGE,
|
||||||
} from 'services/thunks/gallery';
|
} from 'services/thunks/gallery';
|
||||||
import { isImageOutput } from 'services/types/guards';
|
|
||||||
import {
|
|
||||||
buildImageUrls,
|
|
||||||
extractTimestampFromImageName,
|
|
||||||
} from 'services/util/deserializeImageField';
|
|
||||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||||
import {
|
import {
|
||||||
imageDeleted,
|
imageDeleted,
|
||||||
@ -73,44 +67,6 @@ const resultsSlice = createSlice({
|
|||||||
state.isLoading = false;
|
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
|
* 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;
|
const { imageType, imageName } = action.meta.arg;
|
||||||
|
|
||||||
if (imageType === 'results') {
|
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;
|
const { imageType, imageName } = action.meta.arg;
|
||||||
|
|
||||||
if (imageType === 'uploads') {
|
if (imageType === 'uploads') {
|
||||||
|
@ -88,7 +88,7 @@ export const useParameters = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
initialImageChanged({ name: image.image_name, type: image.image_type })
|
initialImageSelected({ name: image.image_name, type: image.image_type })
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: t('toast.initialImageSet'),
|
title: t('toast.initialImageSet'),
|
||||||
|
@ -11,6 +11,7 @@ import { getTimestamp } from 'common/util/getTimestamp';
|
|||||||
import {
|
import {
|
||||||
sessionInvoked,
|
sessionInvoked,
|
||||||
isFulfilledSessionCreatedAction,
|
isFulfilledSessionCreatedAction,
|
||||||
|
sessionCreated,
|
||||||
} from 'services/thunks/session';
|
} from 'services/thunks/session';
|
||||||
import { OpenAPI } from 'services/api';
|
import { OpenAPI } from 'services/api';
|
||||||
import { setEventListeners } from 'services/events/util/setEventListeners';
|
import { setEventListeners } from 'services/events/util/setEventListeners';
|
||||||
@ -65,7 +66,7 @@ export const socketMiddleware = () => {
|
|||||||
socket.connect();
|
socket.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFulfilledSessionCreatedAction(action)) {
|
if (sessionCreated.fulfilled.match(action)) {
|
||||||
const sessionId = action.payload.id;
|
const sessionId = action.payload.id;
|
||||||
const sessionLog = socketioLog.child({ sessionId });
|
const sessionLog = socketioLog.child({ sessionId });
|
||||||
const oldSessionId = getState().system.sessionId;
|
const oldSessionId = getState().system.sessionId;
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { clamp, isString } from 'lodash-es';
|
|
||||||
import { ImagesService } from 'services/api';
|
import { ImagesService } from 'services/api';
|
||||||
import { getHeaders } from 'services/util/getHeaders';
|
import { getHeaders } from 'services/util/getHeaders';
|
||||||
|
|
||||||
@ -15,7 +12,7 @@ type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0];
|
|||||||
*/
|
*/
|
||||||
export const imageReceived = createAppAsyncThunk(
|
export const imageReceived = createAppAsyncThunk(
|
||||||
'api/imageReceived',
|
'api/imageReceived',
|
||||||
async (arg: ImageReceivedArg, _thunkApi) => {
|
async (arg: ImageReceivedArg) => {
|
||||||
const response = await ImagesService.getImage(arg);
|
const response = await ImagesService.getImage(arg);
|
||||||
|
|
||||||
imagesLog.info({ arg, response }, 'Received image');
|
imagesLog.info({ arg, response }, 'Received image');
|
||||||
@ -33,7 +30,7 @@ type ThumbnailReceivedArg = Parameters<
|
|||||||
*/
|
*/
|
||||||
export const thumbnailReceived = createAppAsyncThunk(
|
export const thumbnailReceived = createAppAsyncThunk(
|
||||||
'api/thumbnailReceived',
|
'api/thumbnailReceived',
|
||||||
async (arg: ThumbnailReceivedArg, _thunkApi) => {
|
async (arg: ThumbnailReceivedArg) => {
|
||||||
const response = await ImagesService.getThumbnail(arg);
|
const response = await ImagesService.getThumbnail(arg);
|
||||||
|
|
||||||
imagesLog.info({ arg, response }, 'Received thumbnail');
|
imagesLog.info({ arg, response }, 'Received thumbnail');
|
||||||
@ -49,7 +46,7 @@ type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
|
|||||||
*/
|
*/
|
||||||
export const imageUploaded = createAppAsyncThunk(
|
export const imageUploaded = createAppAsyncThunk(
|
||||||
'api/imageUploaded',
|
'api/imageUploaded',
|
||||||
async (arg: ImageUploadedArg, _thunkApi) => {
|
async (arg: ImageUploadedArg) => {
|
||||||
const response = await ImagesService.uploadImage(arg);
|
const response = await ImagesService.uploadImage(arg);
|
||||||
const { location } = getHeaders(response);
|
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];
|
type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,45 +66,7 @@ type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0];
|
|||||||
*/
|
*/
|
||||||
export const imageDeleted = createAppAsyncThunk(
|
export const imageDeleted = createAppAsyncThunk(
|
||||||
'api/imageDeleted',
|
'api/imageDeleted',
|
||||||
async (arg: ImageDeletedArg, { getState, dispatch }) => {
|
async (arg: ImageDeletedArg) => {
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await ImagesService.deleteImage(arg);
|
const response = await ImagesService.deleteImage(arg);
|
||||||
|
|
||||||
imagesLog.info(
|
imagesLog.info(
|
||||||
|
Loading…
Reference in New Issue
Block a user