From 4665f0df403008a54bc759350ca18df3110b5ceb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:10:58 +1000 Subject: [PATCH] refactor(ui): use image names for selection instead of dtos Update the frontend to incorporate the previous changes to how image selection and general image identification is handled in the frontend. --- .../src/app/components/GlobalImageHotkeys.tsx | 4 +- .../addArchivedOrDeletedBoardListener.ts | 2 +- .../listeners/boardIdSelected.ts | 4 +- .../ensureImageIsSelectedListener.ts | 2 +- .../listeners/galleryImageClicked.ts | 26 +- .../listeners/galleryOffsetChanged.ts | 16 +- .../listeners/imageAddedToBoard.ts | 8 +- .../listeners/imagesStarred.ts | 35 +- .../listeners/imagesUnstarred.ts | 35 +- invokeai/frontend/web/src/app/store/store.ts | 3 +- .../web/src/common/hooks/useSelectorAsAtom.ts | 28 + .../components/ChangeBoardModal.tsx | 6 +- .../changeBoardModal/store/initialState.ts | 2 +- .../features/changeBoardModal/store/slice.ts | 7 +- .../features/changeBoardModal/store/types.ts | 4 +- .../components/DeleteImageModal.tsx | 2 +- .../features/deleteImageModal/store/state.ts | 57 +- .../dnd/DndDragPreviewMultipleImage.tsx | 11 +- invokeai/frontend/web/src/features/dnd/dnd.ts | 26 +- .../components/Boards/DeleteBoardModal.tsx | 4 +- .../ImageMenuItemChangeBoard.tsx | 2 +- .../ImageContextMenu/ImageMenuItemDelete.tsx | 2 +- .../ImageMenuItemOpenInViewer.tsx | 2 +- .../ImageMenuItemSelectForCompare.tsx | 4 +- .../ImageMenuItemStarUnstar.tsx | 4 +- .../MultipleSelectionMenuItems.tsx | 32 +- .../components/ImageGrid/GalleryImage.tsx | 17 +- .../GalleryImageDeleteIconButton.tsx | 2 +- .../GalleryImageOpenInViewerIconButton.tsx | 2 +- .../ImageGrid/GalleryImageStarIconButton.tsx | 4 +- .../ImageGrid/GallerySelectionCountTag.tsx | 2 +- .../ImageViewer/CurrentImageButtons.tsx | 36 +- .../ImageViewer/CurrentImagePreview.tsx | 17 +- .../components/ImageViewer/ImageViewer.tsx | 12 +- .../ImageViewer/ImageViewerPanel.tsx | 89 ++- .../ToggleMetadataViewerButton.tsx | 12 +- .../components/ImageViewer/ViewerToolbar.tsx | 12 +- .../gallery/hooks/useGalleryNavigation.ts | 6 +- .../features/gallery/hooks/useImageActions.ts | 2 +- .../gallery/store/gallerySelectors.ts | 4 - .../features/gallery/store/gallerySlice.ts | 22 +- .../web/src/features/gallery/store/types.ts | 6 +- .../web/src/features/imageActions/actions.ts | 18 +- .../nodes/CurrentImage/CurrentImageNode.tsx | 4 +- .../PostProcessing/PostProcessingPopover.tsx | 11 +- .../ui/layouts/canvas-tab-auto-layout.tsx | 10 - .../ui/layouts/generate-tab-auto-layout.tsx | 10 - .../ui/layouts/upscaling-tab-auto-layout.tsx | 10 - .../ui/layouts/workflows-tab-auto-layout.tsx | 10 - .../web/src/services/api/endpoints/images.ts | 518 +++++++----------- .../services/events/onInvocationComplete.tsx | 2 +- 51 files changed, 551 insertions(+), 615 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts diff --git a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx index c4826a9441..3752ef402f 100644 --- a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx @@ -7,11 +7,13 @@ import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { memo } from 'react'; +import { useImageDTO } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; export const GlobalImageHotkeys = memo(() => { useAssertSingleton('GlobalImageHotkeys'); - const imageDTO = useAppSelector(selectLastSelectedImage); + const imageName = useAppSelector(selectLastSelectedImage); + const imageDTO = useImageDTO(imageName); if (!imageDTO) { return null; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts index a3831664c4..b031f1fcd6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts @@ -25,7 +25,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis matcher: matchAnyBoardDeleted, effect: (action, { dispatch, getState }) => { const state = getState(); - const deletedBoardId = action.meta.arg.originalArgs; + const deletedBoardId = action.meta.arg.originalArgs.board_id; const { autoAddBoardId, selectedBoardId } = state.gallery; // If the deleted board was currently selected, we should reset the selected board to uncategorized diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts index 4ec0075dac..899e88a85c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -30,9 +30,9 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening) const selectedImage = boardImagesData.items.find( (item) => item.image_name === action.payload.selectedImageName ); - dispatch(imageSelected(selectedImage || null)); + dispatch(imageSelected(selectedImage?.image_name ?? null)); } else if (boardImagesData) { - dispatch(imageSelected(boardImagesData.items[0] || null)); + dispatch(imageSelected(boardImagesData.items[0]?.image_name ?? null)); } else { // board has no images - deselect dispatch(imageSelected(null)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts index f07fe68c1b..2e120d192c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener.ts @@ -9,7 +9,7 @@ export const addEnsureImageIsSelectedListener = (startAppListening: AppStartList effect: (action, { dispatch, getState }) => { const selection = getState().gallery.selection; if (selection.length === 0) { - dispatch(imageSelected(action.payload.items[0] ?? null)); + dispatch(imageSelected(action.payload.items[0]?.image_name ?? null)); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts index 5271d655d9..4356b77e69 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts @@ -2,11 +2,11 @@ import { createAction } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; +import { uniq } from 'lodash-es'; import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; export const galleryImageClicked = createAction<{ - imageDTO: ImageDTO; + imageName: string; shiftKey: boolean; ctrlKey: boolean; metaKey: boolean; @@ -28,7 +28,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen startAppListening({ actionCreator: galleryImageClicked, effect: (action, { dispatch, getState }) => { - const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload; + const { imageName, shiftKey, ctrlKey, metaKey, altKey } = action.payload; const state = getState(); const queryArgs = selectListImagesQueryArgs(state); const queryResult = imagesApi.endpoints.listImages.select(queryArgs)(state); @@ -42,31 +42,31 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen const selection = state.gallery.selection; if (altKey) { - if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) { + if (state.gallery.imageToCompare === imageName) { dispatch(imageToCompareChanged(null)); } else { - dispatch(imageToCompareChanged(imageDTO)); + dispatch(imageToCompareChanged(imageName)); } } else if (shiftKey) { - const rangeEndImageName = imageDTO.image_name; - const lastSelectedImage = selection[selection.length - 1]?.image_name; + const rangeEndImageName = imageName; + const lastSelectedImage = selection.at(-1); const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage); const currentClickedIndex = imageDTOs.findIndex((n) => n.image_name === rangeEndImageName); if (lastClickedIndex > -1 && currentClickedIndex > -1) { // We have a valid range! const start = Math.min(lastClickedIndex, currentClickedIndex); const end = Math.max(lastClickedIndex, currentClickedIndex); - const imagesToSelect = imageDTOs.slice(start, end + 1); - dispatch(selectionChanged(selection.concat(imagesToSelect))); + const imagesToSelect = imageDTOs.slice(start, end + 1).map(({ image_name }) => image_name); + dispatch(selectionChanged(uniq(selection.concat(imagesToSelect)))); } } else if (ctrlKey || metaKey) { - if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) { - dispatch(selectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name))); + if (selection.some((n) => n === imageName) && selection.length > 1) { + dispatch(selectionChanged(uniq(selection.filter((n) => n !== imageName)))); } else { - dispatch(selectionChanged(selection.concat(imageDTO))); + dispatch(selectionChanged(uniq(selection.concat(imageName)))); } } else { - dispatch(selectionChanged([imageDTO])); + dispatch(selectionChanged([imageName])); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged.ts index 51095700e3..359fa647d9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged.ts @@ -84,14 +84,14 @@ export const addGalleryOffsetChangedListener = (startAppListening: AppStartListe if (offset < prevOffset) { // We've gone backwards const lastImage = imageDTOs[imageDTOs.length - 1]; - if (!selection.some((selectedImage) => selectedImage.image_name === lastImage?.image_name)) { - dispatch(selectionChanged(lastImage ? [lastImage] : [])); + if (!selection.some((selectedImage) => selectedImage === lastImage?.image_name)) { + dispatch(selectionChanged(lastImage ? [lastImage.image_name] : [])); } } else { // We've gone forwards const firstImage = imageDTOs[0]; - if (!selection.some((selectedImage) => selectedImage.image_name === firstImage?.image_name)) { - dispatch(selectionChanged(firstImage ? [firstImage] : [])); + if (!selection.some((selectedImage) => selectedImage === firstImage?.image_name)) { + dispatch(selectionChanged(firstImage ? [firstImage.image_name] : [])); } } return; @@ -102,14 +102,14 @@ export const addGalleryOffsetChangedListener = (startAppListening: AppStartListe if (offset < prevOffset) { // We've gone backwards const lastImage = imageDTOs[imageDTOs.length - 1]; - if (lastImage && imageToCompare?.image_name !== lastImage.image_name) { - dispatch(imageToCompareChanged(lastImage)); + if (lastImage && imageToCompare !== lastImage.image_name) { + dispatch(imageToCompareChanged(lastImage.image_name)); } } else { // We've gone forwards const firstImage = imageDTOs[0]; - if (firstImage && imageToCompare?.image_name !== firstImage.image_name) { - dispatch(imageToCompareChanged(firstImage)); + if (firstImage && imageToCompare !== firstImage.image_name) { + dispatch(imageToCompareChanged(firstImage.image_name)); } } return; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts index 38e2127c0d..c01e562e4d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts @@ -8,16 +8,16 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar startAppListening({ matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled, effect: (action) => { - const { board_id, imageDTO } = action.meta.arg.originalArgs; - log.debug({ board_id, imageDTO }, 'Image added to board'); + const { board_id, image_name } = action.meta.arg.originalArgs; + log.debug({ board_id, image_name }, 'Image added to board'); }, }); startAppListening({ matcher: imagesApi.endpoints.addImageToBoard.matchRejected, effect: (action) => { - const { board_id, imageDTO } = action.meta.arg.originalArgs; - log.debug({ board_id, imageDTO }, 'Problem adding image to board'); + const { board_id, image_name } = action.meta.arg.originalArgs; + log.debug({ board_id, image_name }, 'Problem adding image to board'); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts index 0337b995f5..f4d014b055 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts @@ -1,30 +1,25 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; export const addImagesStarredListener = (startAppListening: AppStartListening) => { startAppListening({ matcher: imagesApi.endpoints.starImages.matchFulfilled, effect: (action, { dispatch, getState }) => { - const { updated_image_names: starredImages } = action.payload; - - const state = getState(); - - const { selection } = state.gallery; - const updatedSelection: ImageDTO[] = []; - - selection.forEach((selectedImageDTO) => { - if (starredImages.includes(selectedImageDTO.image_name)) { - updatedSelection.push({ - ...selectedImageDTO, - starred: true, - }); - } else { - updatedSelection.push(selectedImageDTO); - } - }); - dispatch(selectionChanged(updatedSelection)); + // const { updated_image_names: starredImages } = action.payload; + // const state = getState(); + // const { selection } = state.gallery; + // const updatedSelection: ImageDTO[] = []; + // selection.forEach((selectedImageDTO) => { + // if (starredImages.includes(selectedImageDTO.image_name)) { + // updatedSelection.push({ + // ...selectedImageDTO, + // starred: true, + // }); + // } else { + // updatedSelection.push(selectedImageDTO); + // } + // }); + // dispatch(selectionChanged(updatedSelection)); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts index ad6c26fd0c..00aaf28e91 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts @@ -1,30 +1,25 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; export const addImagesUnstarredListener = (startAppListening: AppStartListening) => { startAppListening({ matcher: imagesApi.endpoints.unstarImages.matchFulfilled, effect: (action, { dispatch, getState }) => { - const { updated_image_names: unstarredImages } = action.payload; - - const state = getState(); - - const { selection } = state.gallery; - const updatedSelection: ImageDTO[] = []; - - selection.forEach((selectedImageDTO) => { - if (unstarredImages.includes(selectedImageDTO.image_name)) { - updatedSelection.push({ - ...selectedImageDTO, - starred: false, - }); - } else { - updatedSelection.push(selectedImageDTO); - } - }); - dispatch(selectionChanged(updatedSelection)); + // const { updated_image_names: unstarredImages } = action.payload; + // const state = getState(); + // const { selection } = state.gallery; + // const updatedSelection: ImageDTO[] = []; + // selection.forEach((selectedImageDTO) => { + // if (unstarredImages.includes(selectedImageDTO.image_name)) { + // updatedSelection.push({ + // ...selectedImageDTO, + // starred: false, + // }); + // } else { + // updatedSelection.push(selectedImageDTO); + // } + // }); + // dispatch(selectionChanged(updatedSelection)); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index ec757494f5..9397144751 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -39,6 +39,7 @@ import { authToastMiddleware } from 'services/api/authToastMiddleware'; import type { JsonObject } from 'type-fest'; import { STORAGE_PREFIX } from './constants'; +import { getDebugLoggerMiddleware } from './middleware/debugLoggerMiddleware'; import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; @@ -176,7 +177,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) => .concat(api.middleware) .concat(dynamicMiddlewares) .concat(authToastMiddleware) - // .concat(getDebugLoggerMiddleware()) + .concat(getDebugLoggerMiddleware()) .prepend(listenerMiddleware.middleware), enhancers: (getDefaultEnhancers) => { const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer()); diff --git a/invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts b/invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts new file mode 100644 index 0000000000..f333ce86d8 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts @@ -0,0 +1,28 @@ +import type { Selector } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import { useAppStore } from 'app/store/storeHooks'; +import type { Atom, WritableAtom } from 'nanostores'; +import { atom } from 'nanostores'; +import { useEffect, useState } from 'react'; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export const useSelectorAsAtom = >(selector: T): Atom> => { + const store = useAppStore(); + const $atom = useState>>(() => atom>(selector(store.getState())))[0]; + + useEffect(() => { + const unsubscribe = store.subscribe(() => { + const prev = $atom.get(); + const next = selector(store.getState()); + if (prev !== next) { + $atom.set(next); + } + }); + + return () => { + unsubscribe(); + }; + }, [$atom, selector, store]); + + return $atom; +}; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 61993b8baa..839809914b 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -16,7 +16,7 @@ import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 's const selectImagesToChange = createMemoizedSelector( selectChangeBoardModalSlice, - (changeBoardModal) => changeBoardModal.imagesToChange + (changeBoardModal) => changeBoardModal.image_names ); const selectIsModalOpen = createSelector( @@ -57,10 +57,10 @@ const ChangeBoardModal = () => { } if (selectedBoard === 'none') { - removeImagesFromBoard({ imageDTOs: imagesToChange }); + removeImagesFromBoard({ image_names: imagesToChange }); } else { addImagesToBoard({ - imageDTOs: imagesToChange, + image_names: imagesToChange, board_id: selectedBoard, }); } diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts index e129c6f76f..0d7b2c3ec0 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts @@ -2,5 +2,5 @@ import type { ChangeBoardModalState } from './types'; export const initialState: ChangeBoardModalState = { isModalOpen: false, - imagesToChange: [], + image_names: [], }; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts index 4b374b066a..c7c690d38c 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts @@ -1,7 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { ImageDTO } from 'services/api/types'; import { initialState } from './initialState'; @@ -12,11 +11,11 @@ export const changeBoardModalSlice = createSlice({ isModalOpenChanged: (state, action: PayloadAction) => { state.isModalOpen = action.payload; }, - imagesToChangeSelected: (state, action: PayloadAction) => { - state.imagesToChange = action.payload; + imagesToChangeSelected: (state, action: PayloadAction) => { + state.image_names = action.payload; }, changeBoardReset: (state) => { - state.imagesToChange = []; + state.image_names = []; state.isModalOpen = false; }, }, diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts index f1a825480e..c46a7aa7fa 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts @@ -1,6 +1,4 @@ -import type { ImageDTO } from 'services/api/types'; - export type ChangeBoardModalState = { isModalOpen: boolean; - imagesToChange: ImageDTO[]; + image_names: string[]; }; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 1e1213b1f2..f2aae77f0f 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -22,7 +22,7 @@ export const DeleteImageModal = memo(() => { return ( ({ - imageDTOs: [], + image_names: [], usagePerImage: [], usageSummary: { isControlLayerImage: false, @@ -54,21 +53,21 @@ const getInitialState = (): DeleteImagesModalState => ({ const $deleteModalState = atom(getInitialState()); -const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise => { +const deleteImagesWithDialog = async (image_names: string[]): Promise => { const { getState, dispatch } = getStore(); - const imageUsage = getImageUsageFromImageDTOs(imageDTOs, getState()); + const imageUsage = getImageUsageFromImageNames(image_names, getState()); const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState()); if (!shouldConfirmOnDelete && !isAnyImageInUse(imageUsage)) { // If we don't need to confirm and the images are not in use, delete them directly - await handleDeletions(imageDTOs, dispatch, getState); + await handleDeletions(image_names, dispatch, getState); } return new Promise((resolve, reject) => { $deleteModalState.set({ usagePerImage: imageUsage, usageSummary: getImageUsageSummary(imageUsage), - imageDTOs, + image_names, isOpen: true, resolve, reject, @@ -76,12 +75,12 @@ const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise => { }); }; -const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, getState: AppGetState) => { +const handleDeletions = async (image_names: string[], dispatch: AppDispatch, getState: AppGetState) => { try { const state = getState(); - await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap(); + await dispatch(imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })).unwrap(); - if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) { + if (intersection(state.gallery.selection, image_names).length > 0) { // Some selected images were deleted, need to select the next image const queryArgs = selectListImagesQueryArgs(state); const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state); @@ -93,11 +92,11 @@ const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, get } // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - for (const imageDTO of imageDTOs) { - deleteNodesImages(state, dispatch, imageDTO); - deleteControlLayerImages(state, dispatch, imageDTO); - deleteReferenceImages(state, dispatch, imageDTO); - deleteRasterLayerImages(state, dispatch, imageDTO); + for (const image_name of image_names) { + deleteNodesImages(state, dispatch, image_name); + deleteControlLayerImages(state, dispatch, image_name); + deleteReferenceImages(state, dispatch, image_name); + deleteRasterLayerImages(state, dispatch, image_name); } } catch { // no-op @@ -106,7 +105,7 @@ const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, get const confirmDeletion = async (dispatch: AppDispatch, getState: AppGetState) => { const state = $deleteModalState.get(); - await handleDeletions(state.imageDTOs, dispatch, getState); + await handleDeletions(state.image_names, dispatch, getState); state.resolve?.(); closeSilently(); }; @@ -142,8 +141,8 @@ export const useDeleteImageModalApi = () => { return api; }; -const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): ImageUsage[] => { - if (imageDTOs.length === 0) { +const getImageUsageFromImageNames = (image_names: string[], state: RootState): ImageUsage[] => { + if (image_names.length === 0) { return []; } @@ -152,7 +151,7 @@ const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): Im const upscale = selectUpscaleSlice(state); const refImages = selectRefImagesSlice(state); - return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, refImages, image_name)); + return image_names.map((image_name) => getImageUsage(nodes, canvas, upscale, refImages, image_name)); }; const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({ @@ -178,7 +177,7 @@ const isAnyImageInUse = (imageUsage: ImageUsage[]): boolean => ); // Some utils to delete images from different parts of the app -const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { +const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { const actions: Param0[] = []; state.nodes.present.nodes.forEach((node) => { if (!isInvocationNode(node)) { @@ -186,7 +185,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im } forEach(node.data.inputs, (input) => { - if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) { + if (isImageFieldInputInstance(input) && input.value?.image_name === image_name) { actions.push( fieldImageValueChanged({ nodeId: node.data.id, @@ -201,7 +200,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im fieldImageCollectionValueChanged({ nodeId: node.data.id, fieldName: input.name, - value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name), + value: input.value?.filter((value) => value?.image_name !== image_name), }) ); } @@ -211,11 +210,11 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im actions.forEach(dispatch); }; -const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { +const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { - if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) { + if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { shouldDelete = true; break; } @@ -226,19 +225,19 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image }); }; -const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { +const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { selectReferenceImageEntities(state).forEach((entity) => { - if (entity.config.image?.image_name === imageDTO.image_name) { + if (entity.config.image?.image_name === image_name) { dispatch(refImageImageChanged({ id: entity.id, imageDTO: null })); } }); }; -const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { +const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { - if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) { + if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { shouldDelete = true; break; } diff --git a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx index 7232d0fc2a..2633d9e508 100644 --- a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx +++ b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx @@ -6,10 +6,9 @@ import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } f import { memo } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; -import type { ImageDTO } from 'services/api/types'; import type { Param0 } from 'tsafe'; -const DndDragPreviewMultipleImage = memo(({ imageDTOs }: { imageDTOs: ImageDTO[] }) => { +const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string[] }) => { const { t } = useTranslation(); return ( - {imageDTOs.length} + {image_names.length} {t('parameters.images')} ); @@ -32,11 +31,11 @@ DndDragPreviewMultipleImage.displayName = 'DndDragPreviewMultipleImage'; export type DndDragPreviewMultipleImageState = { type: 'multiple-image'; container: HTMLElement; - imageDTOs: ImageDTO[]; + image_names: string[]; }; export const createMultipleImageDragPreview = (arg: DndDragPreviewMultipleImageState) => - createPortal(, arg.container); + createPortal(, arg.container); type SetMultipleDragPreviewArg = { multipleImageDndData: MultipleImageDndSourceData; @@ -52,7 +51,7 @@ export const setMultipleImageDragPreview = ({ const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs; setCustomNativeDragPreview({ render({ container }) { - setDragPreviewState({ type: 'multiple-image', container, imageDTOs: multipleImageDndData.payload.imageDTOs }); + setDragPreviewState({ type: 'multiple-image', container, image_names: multipleImageDndData.payload.image_names }); return () => setDragPreviewState(null); }, nativeSetDragImage, diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts index df768f4ec6..5247b82b11 100644 --- a/invokeai/frontend/web/src/features/dnd/dnd.ts +++ b/invokeai/frontend/web/src/features/dnd/dnd.ts @@ -87,7 +87,7 @@ const _multipleImage = buildTypeAndKey('multiple-image'); export type MultipleImageDndSourceData = DndData< typeof _multipleImage.type, typeof _multipleImage.key, - { imageDTOs: ImageDTO[]; boardId: BoardId } + { image_names: string[]; board_id: BoardId } >; export const multipleImageDndSource: DndSource = { ..._multipleImage, @@ -305,7 +305,7 @@ export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget< if (singleImageDndSource.typeGuard(sourceData)) { newValue.push({ image_name: sourceData.payload.imageDTO.image_name }); } else { - newValue.push(...sourceData.payload.imageDTOs.map(({ image_name }) => ({ image_name }))); + newValue.push(...sourceData.payload.image_names.map((image_name) => ({ image_name }))); } dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: newValue })); @@ -330,17 +330,17 @@ export const setComparisonImageDndTarget: DndTarget { const { imageDTO } = sourceData.payload; - setComparisonImage({ imageDTO, dispatch }); + setComparisonImage({ image_name: imageDTO.image_name, dispatch }); }, }; //#endregion @@ -450,7 +450,7 @@ export const addImageToBoardDndTarget: DndTarget< return currentBoard !== destinationBoard; } if (multipleImageDndSource.typeGuard(sourceData)) { - const currentBoard = sourceData.payload.boardId; + const currentBoard = sourceData.payload.board_id; const destinationBoard = targetData.payload.boardId; return currentBoard !== destinationBoard; } @@ -460,13 +460,13 @@ export const addImageToBoardDndTarget: DndTarget< if (singleImageDndSource.typeGuard(sourceData)) { const { imageDTO } = sourceData.payload; const { boardId } = targetData.payload; - addImagesToBoard({ imageDTOs: [imageDTO], boardId, dispatch }); + addImagesToBoard({ image_names: [imageDTO.image_name], boardId, dispatch }); } if (multipleImageDndSource.typeGuard(sourceData)) { - const { imageDTOs } = sourceData.payload; + const { image_names } = sourceData.payload; const { boardId } = targetData.payload; - addImagesToBoard({ imageDTOs, boardId, dispatch }); + addImagesToBoard({ image_names, boardId, dispatch }); } }, }; @@ -494,7 +494,7 @@ export const removeImageFromBoardDndTarget: DndTarget< } if (multipleImageDndSource.typeGuard(sourceData)) { - const currentBoard = sourceData.payload.boardId; + const currentBoard = sourceData.payload.board_id; return currentBoard !== 'none'; } @@ -503,12 +503,12 @@ export const removeImageFromBoardDndTarget: DndTarget< handler: ({ sourceData, dispatch }) => { if (singleImageDndSource.typeGuard(sourceData)) { const { imageDTO } = sourceData.payload; - removeImagesFromBoard({ imageDTOs: [imageDTO], dispatch }); + removeImagesFromBoard({ image_names: [imageDTO.image_name], dispatch }); } if (multipleImageDndSource.typeGuard(sourceData)) { - const { imageDTOs } = sourceData.payload; - removeImagesFromBoard({ imageDTOs, dispatch }); + const { image_names } = sourceData.payload; + removeImagesFromBoard({ image_names, dispatch }); } }, }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index fa016a6b46..b1a9b6129e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -91,7 +91,7 @@ const DeleteBoardModal = () => { if (!boardToDelete || boardToDelete === 'none') { return; } - deleteBoardOnly(boardToDelete.board_id); + deleteBoardOnly({ board_id: boardToDelete.board_id }); $boardToDelete.set(null); }, [boardToDelete, deleteBoardOnly]); @@ -99,7 +99,7 @@ const DeleteBoardModal = () => { if (!boardToDelete || boardToDelete === 'none') { return; } - deleteBoardAndImages(boardToDelete.board_id); + deleteBoardAndImages({ board_id: boardToDelete.board_id }); $boardToDelete.set(null); }, [boardToDelete, deleteBoardAndImages]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard.tsx index 200b08b4c2..331ccb5538 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard.tsx @@ -12,7 +12,7 @@ export const ImageMenuItemChangeBoard = memo(() => { const imageDTO = useImageDTOContext(); const onClick = useCallback(() => { - dispatch(imagesToChangeSelected([imageDTO])); + dispatch(imagesToChangeSelected([imageDTO.image_name])); dispatch(isModalOpenChanged(true)); }, [dispatch, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx index fcddd75483..2708381b19 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemDelete.tsx @@ -12,7 +12,7 @@ export const ImageMenuItemDelete = memo(() => { const onClick = useCallback(async () => { try { - await deleteImageModal.delete([imageDTO]); + await deleteImageModal.delete([imageDTO.image_name]); } catch { // noop; } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx index f6753316b6..cae757d3fd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx @@ -12,7 +12,7 @@ export const ImageMenuItemOpenInViewer = memo(() => { const imageDTO = useImageDTOContext(); const onClick = useCallback(() => { dispatch(imageToCompareChanged(null)); - dispatch(imageSelected(imageDTO)); + dispatch(imageSelected(imageDTO.image_name)); // TODO: figure out how to select the closest image viewer... }, [dispatch, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare.tsx index a95bb0e765..129671819f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare.tsx @@ -12,13 +12,13 @@ export const ImageMenuItemSelectForCompare = memo(() => { const dispatch = useAppDispatch(); const imageDTO = useImageDTOContext(); const selectMaySelectForCompare = useMemo( - () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name), + () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare !== imageDTO.image_name), [imageDTO.image_name] ); const maySelectForCompare = useAppSelector(selectMaySelectForCompare); const onClick = useCallback(() => { - dispatch(imageToCompareChanged(imageDTO)); + dispatch(imageToCompareChanged(imageDTO.image_name)); }, [dispatch, imageDTO]); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar.tsx index a82e8ed2a8..fd89a328d4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar.tsx @@ -16,13 +16,13 @@ export const ImageMenuItemStarUnstar = memo(() => { const starImage = useCallback(() => { if (imageDTO) { - starImages({ imageDTOs: [imageDTO] }); + starImages({ image_names: [imageDTO.image_name] }); } }, [starImages, imageDTO]); const unstarImage = useCallback(() => { if (imageDTO) { - unstarImages({ imageDTOs: [imageDTO] }); + unstarImages({ image_names: [imageDTO.image_name] }); } }, [unstarImages, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx index 8e83889542..c4c232dc13 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi'; import { @@ -37,37 +37,25 @@ const MultipleSelectionMenuItems = () => { }, [deleteImageModal, selection]); const handleStarSelection = useCallback(() => { - starImages({ imageDTOs: selection }); + starImages({ image_names: selection }); }, [starImages, selection]); const handleUnstarSelection = useCallback(() => { - unstarImages({ imageDTOs: selection }); + unstarImages({ image_names: selection }); }, [unstarImages, selection]); const handleBulkDownload = useCallback(() => { - bulkDownload({ image_names: selection.map((img) => img.image_name) }); + bulkDownload({ image_names: selection }); }, [selection, bulkDownload]); - const areAllStarred = useMemo(() => { - return selection.every((img) => img.starred); - }, [selection]); - - const areAllUnstarred = useMemo(() => { - return selection.every((img) => !img.starred); - }, [selection]); - return ( <> - {areAllStarred && ( - } onClickCapture={handleUnstarSelection}> - {customStarUi ? customStarUi.off.text : `Unstar All`} - - )} - {(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && ( - } onClickCapture={handleStarSelection}> - {customStarUi ? customStarUi.on.text : `Star All`} - - )} + } onClickCapture={handleUnstarSelection}> + {customStarUi ? customStarUi.off.text : `Unstar All`} + + } onClickCapture={handleStarSelection}> + {customStarUi ? customStarUi.on.text : `Star All`} + {isBulkDownloadEnabled && ( } onClickCapture={handleBulkDownload}> {t('gallery.downloadSelection')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index e54f12d82c..2067d75f49 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -93,7 +93,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { const ref = useRef(null); const dndId = useId(); const selectIsSelectedForCompare = useMemo( - () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name), + () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare === imageDTO.image_name), [imageDTO.image_name] ); const isSelectedForCompare = useAppSelector(selectIsSelectedForCompare); @@ -101,7 +101,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { () => createSelector(selectGallerySlice, (gallery) => { for (const selectedImage of gallery.selection) { - if (selectedImage.image_name === imageDTO.image_name) { + if (selectedImage === imageDTO.image_name) { return true; } } @@ -126,11 +126,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { // multi-image drag. if ( gallery.selection.length > 1 && - gallery.selection.find(({ image_name }) => image_name === imageDTO.image_name) !== undefined + gallery.selection.find((image_name) => image_name === imageDTO.image_name) !== undefined ) { return multipleImageDndSource.getData({ - imageDTOs: gallery.selection, - boardId: gallery.selectedBoardId, + image_names: gallery.selection, + board_id: gallery.selectedBoardId, }); } @@ -167,7 +167,10 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { onDragStart: ({ source }) => { // When we start dragging multiple images, set the dragging state to true if the dragged image is part of the // selection. This is called for all drag events. - if (multipleImageDndSource.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) { + if ( + multipleImageDndSource.typeGuard(source.data) && + source.data.payload.image_names.includes(imageDTO.image_name) + ) { setIsDragging(true); } }, @@ -193,7 +196,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { (e) => { store.dispatch( galleryImageClicked({ - imageDTO, + imageName: imageDTO.image_name, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx index 574890f0ed..137bb3e6fd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageDeleteIconButton.tsx @@ -22,7 +22,7 @@ export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => { if (!imageDTO) { return; } - deleteImageModal.delete([imageDTO]); + deleteImageModal.delete([imageDTO.image_name]); }, [deleteImageModal, imageDTO] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx index bd742430ea..1de72b4026 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx @@ -19,7 +19,7 @@ export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) => const onClick = useCallback(() => { dispatch(imageToCompareChanged(null)); - dispatch(imageSelected(imageDTO)); + dispatch(imageSelected(imageDTO.image_name)); focusPanel(VIEWER_PANEL_ID); }, [dispatch, focusPanel, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageStarIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageStarIconButton.tsx index 60eb497106..0306c2095d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageStarIconButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageStarIconButton.tsx @@ -17,9 +17,9 @@ export const GalleryImageStarIconButton = memo(({ imageDTO }: Props) => { const toggleStarredState = useCallback(() => { if (imageDTO.starred) { - unstarImages({ imageDTOs: [imageDTO] }); + unstarImages({ image_names: [imageDTO.image_name] }); } else { - starImages({ imageDTOs: [imageDTO] }); + starImages({ image_names: [imageDTO.image_name] }); } }, [starImages, unstarImages, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx index 1ee42cf5cb..e76936c090 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx @@ -19,7 +19,7 @@ export const GallerySelectionCountTag = memo(() => { const isGalleryFocused = useIsRegionFocused('gallery'); const onSelectPage = useCallback(() => { - dispatch(selectionChanged([...selection, ...imageDTOs])); + dispatch(selectionChanged([...selection, ...imageDTOs.map(({ image_name }) => image_name)])); }, [dispatch, selection, imageDTOs]); useRegisteredHotkeys({ diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 9332a2bfaf..ee2fe1e884 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,6 +1,5 @@ import { Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; @@ -10,6 +9,7 @@ import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors import { $hasTemplates } from 'features/nodes/store/nodesSlice'; import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -21,14 +21,22 @@ import { PiQuotesBold, PiRulerBold, } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { useImageDTO } from 'services/api/endpoints/images'; + +import { useImageViewerContext } from './ImageViewerPanel'; export const CurrentImageButtons = memo(() => { - const lastSelectedImage = useAppSelector(selectLastSelectedImage); - const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); const { t } = useTranslation(); + const ctx = useImageViewerContext(); + const hasProgressImage = useStore(ctx.$hasProgressImage); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + + const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer; + + const imageName = useAppSelector(selectLastSelectedImage); + const imageDTO = useImageDTO(imageName); const hasTemplates = useStore($hasTemplates); - const imageActions = useImageActions(imageDTO ?? null); + const imageActions = useImageActions(imageDTO); const isStaging = useAppSelector(selectIsStaging); const isUpscalingEnabled = useFeatureStatus('upscaling'); @@ -39,7 +47,7 @@ export const CurrentImageButtons = memo(() => { as={IconButton} aria-label={t('parameters.imageActions')} tooltip={t('parameters.imageActions')} - isDisabled={!imageDTO} + isDisabled={isDisabledOverride || !imageDTO} variant="link" alignSelf="stretch" icon={} @@ -53,7 +61,7 @@ export const CurrentImageButtons = memo(() => { icon={} tooltip={`${t('nodes.loadWorkflow')} (W)`} aria-label={`${t('nodes.loadWorkflow')} (W)`} - isDisabled={!imageDTO || !imageActions.hasWorkflow || !hasTemplates} + isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasWorkflow || !hasTemplates} variant="link" alignSelf="stretch" onClick={imageActions.loadWorkflow} @@ -62,7 +70,7 @@ export const CurrentImageButtons = memo(() => { icon={} tooltip={`${t('parameters.remixImage')} (R)`} aria-label={`${t('parameters.remixImage')} (R)`} - isDisabled={!imageDTO || !imageActions.hasMetadata} + isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata} variant="link" alignSelf="stretch" onClick={imageActions.remix} @@ -71,7 +79,7 @@ export const CurrentImageButtons = memo(() => { icon={} tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!imageDTO || !imageActions.hasPrompts} + isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasPrompts} variant="link" alignSelf="stretch" onClick={imageActions.recallPrompts} @@ -80,7 +88,7 @@ export const CurrentImageButtons = memo(() => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!imageDTO || !imageActions.hasSeed} + isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasSeed} variant="link" alignSelf="stretch" onClick={imageActions.recallSeed} @@ -92,23 +100,23 @@ export const CurrentImageButtons = memo(() => { variant="link" alignSelf="stretch" onClick={imageActions.recallSize} - isDisabled={!imageDTO || isStaging} + isDisabled={isDisabledOverride || !imageDTO || isStaging} /> } tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} - isDisabled={!imageDTO || !imageActions.hasMetadata} + isDisabled={isDisabledOverride || !imageDTO || !imageActions.hasMetadata} variant="link" alignSelf="stretch" onClick={imageActions.recallAll} /> - {isUpscalingEnabled && } + {isUpscalingEnabled && } - + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 7d0ef2e1d4..652ebd9381 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -6,7 +6,7 @@ import { DndImage } from 'features/dnd/DndImage'; import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common'; -import { selectShouldShowImageDetails } from 'features/ui/store/uiSelectors'; +import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import type { AnimationProps } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; @@ -20,6 +20,8 @@ import { ProgressIndicator } from './ProgressIndicator2'; export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => { const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + const socket = useStore($socket); const [progressEvent, setProgressEvent] = useState(null); const [progressImage, setProgressImage] = useState(null); @@ -65,6 +67,8 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) } }, [imageDTO, progressEvent]); + const withProgress = shouldShowProgressInViewer && progressEvent && progressImage; + return ( - {imageDTO ? ( + {imageDTO && ( - ) : ( - )} - {progressEvent && progressImage && ( + {!imageDTO && } + {withProgress && ( @@ -90,9 +93,9 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) )} - {imageDTO && } + {imageDTO && !withProgress && } - {shouldShowImageDetails && imageDTO && ( + {shouldShowImageDetails && imageDTO && !withProgress && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 6ca2e1b8c5..2ff613ef40 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -1,11 +1,10 @@ -import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common'; import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview'; import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison'; -import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors'; +import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { memo } from 'react'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { useImageDTO } from 'services/api/endpoints/images'; // type Props = { // closeButton?: ReactNode; @@ -28,9 +27,10 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images'; // }; export const ImageViewer = memo(() => { - const lastSelectedImageName = useAppSelector(selectLastSelectedImageName); - const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken); - const comparisonImageDTO = useAppSelector(selectImageToCompare); + const lastSelectedImageName = useAppSelector(selectLastSelectedImage); + const lastSelectedImageDTO = useImageDTO(lastSelectedImageName); + const comparisonImageName = useAppSelector(selectImageToCompare); + const comparisonImageDTO = useImageDTO(comparisonImageName); if (lastSelectedImageDTO && comparisonImageDTO) { return ; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx index 765a6e0909..bc7cda66b2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx @@ -1,13 +1,86 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar'; -import { memo } from 'react'; +import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common'; +import { type Atom, atom, computed } from 'nanostores'; +import type { PropsWithChildren } from 'react'; +import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import type { ImageDTO, S } from 'services/api/types'; +import { $socket } from 'services/events/stores'; +import { assert } from 'tsafe'; -export const ImageViewerPanel = memo(() => ( - - - - - -)); +export const ImageViewerPanel = memo(() => { + return ( + + + + + + + + ); +}); ImageViewerPanel.displayName = 'ImageViewerPanel'; + +type ImageViewerContextValue = { + $progressEvent: Atom; + $progressImage: Atom; + $hasProgressImage: Atom; + onLoadImage: (imageDTO: ImageDTO) => void; +}; + +const ImageViewerContext = createContext(null); + +const ImageViewerContextProvider = memo((props: PropsWithChildren) => { + const socket = useStore($socket); + const $progressEvent = useState(() => atom(null))[0]; + const $progressImage = useState(() => atom(null))[0]; + const $hasProgressImage = useState(() => computed($progressImage, (progressImage) => progressImage !== null))[0]; + + useEffect(() => { + if (!socket) { + return; + } + + const onInvocationProgress = (data: S['InvocationProgressEvent']) => { + $progressEvent.set(data); + if (data.image) { + $progressImage.set(data.image); + } + }; + + socket.on('invocation_progress', onInvocationProgress); + + return () => { + socket.off('invocation_progress', onInvocationProgress); + }; + }, [$progressEvent, $progressImage, socket]); + + const onLoadImage = useCallback( + (imageDTO: ImageDTO) => { + const progressEvent = $progressEvent.get(); + if (!progressEvent || !imageDTO) { + return; + } + if (progressEvent.session_id === imageDTO.session_id) { + $progressImage.set(null); + } + }, + [$progressEvent, $progressImage] + ); + + const value = useMemo( + () => ({ $progressEvent, $progressImage, $hasProgressImage, onLoadImage }), + [$hasProgressImage, $progressEvent, $progressImage, onLoadImage] + ); + + return {props.children}; +}); +ImageViewerContextProvider.displayName = 'ImageViewerContextProvider'; + +export const useImageViewerContext = () => { + const value = useContext(ImageViewerContext); + assert(value !== null, 'useImageViewerContext must be used within a ImageViewerContextProvider'); + return value; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx index e40cb510c6..1824ca1353 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -1,15 +1,24 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { selectShouldShowImageDetails } from 'features/ui/store/uiSelectors'; +import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiInfoBold } from 'react-icons/pi'; +import { useImageViewerContext } from './ImageViewerPanel'; + export const ToggleMetadataViewerButton = memo(() => { const dispatch = useAppDispatch(); + const ctx = useImageViewerContext(); + const hasProgressImage = useStore(ctx.$hasProgressImage); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + + const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer; + const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails); const imageDTO = useAppSelector(selectLastSelectedImage); const { t } = useTranslation(); @@ -35,6 +44,7 @@ export const ToggleMetadataViewerButton = memo(() => { alignSelf="stretch" colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'} data-testid="toggle-show-metadata-button" + isDisabled={isDisabledOverride} /> ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx index 1c5a67bae8..12167f5f61 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -1,16 +1,18 @@ -import { ButtonGroup, Flex } from '@invoke-ai/ui-library'; +import { Flex, Spacer } from '@invoke-ai/ui-library'; import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; import { memo } from 'react'; import { CurrentImageButtons } from './CurrentImageButtons'; +import { ToggleProgressButton } from './ToggleProgressButton'; export const ViewerToolbar = memo(() => { return ( - - - - + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts index 26cf31cc89..a25e96f0cc 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts @@ -177,7 +177,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { if (imageDTOs.length === 0 || !lastSelectedImage) { return 0; } - return imageDTOs.findIndex((i) => i.image_name === lastSelectedImage.image_name); + return imageDTOs.findIndex((i) => i.image_name === lastSelectedImage); }, [imageDTOs, lastSelectedImage]); const handleNavigation = useCallback( @@ -187,9 +187,9 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { return; } if (alt) { - dispatch(imageToCompareChanged(image)); + dispatch(imageToCompareChanged(image.image_name)); } else { - dispatch(imageSelected(image)); + dispatch(imageSelected(image.image_name)); } scrollToImage(image.image_name, index); }, diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index 90cc41e689..1582f0d24b 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -199,7 +199,7 @@ export const useImageActions = (imageDTO: ImageDTO | null) => { if (!imageDTO) { return; } - deleteImageModal.delete([imageDTO]); + deleteImageModal.delete([imageDTO.image_name]); }, [deleteImageModal, imageDTO]); return { diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 6a5ff11351..a3f10d47e9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -8,10 +8,6 @@ import type { ListBoardsArgs, ListImagesArgs } from 'services/api/types'; export const selectFirstSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(0)); export const selectLastSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(-1)); -export const selectLastSelectedImageName = createSelector( - selectGallerySlice, - (gallery) => gallery.selection.at(-1)?.image_name -); export const selectGalleryLimit = createSelector(selectGallerySlice, (gallery) => gallery.limit); export const selectListImagesQueryArgs = createMemoizedSelector( diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 94afbe1f90..c21ae398cb 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,8 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { isEqual, uniqBy } from 'lodash-es'; -import type { BoardRecordOrderBy, ImageDTO } from 'services/api/types'; +import { isEqual, uniq } from 'lodash-es'; +import type { BoardRecordOrderBy } from 'services/api/types'; import type { BoardId, ComparisonMode, GalleryState, GalleryView, OrderDir } from './types'; @@ -33,14 +33,14 @@ export const gallerySlice = createSlice({ name: 'gallery', initialState: initialGalleryState, reducers: { - imageSelected: (state, action: PayloadAction) => { + imageSelected: (state, action: PayloadAction) => { // Let's be efficient here and not update the selection unless it has actually changed. This helps to prevent // unnecessary re-renders of the gallery. - const selectedImage = action.payload; + const selectedImageName = action.payload; // If we got `null`, clear the selection - if (!selectedImage) { + if (!selectedImageName) { // But only if we have images selected if (state.selection.length > 0) { state.selection = []; @@ -50,24 +50,24 @@ export const gallerySlice = createSlice({ // If we have multiple images selected, clear the selection and select the new image if (state.selection.length !== 1) { - state.selection = [selectedImage]; + state.selection = [selectedImageName]; return; } // If the selected image is different from the current selection, clear the selection and select the new image - if (!isEqual(state.selection[0], selectedImage)) { - state.selection = [selectedImage]; + if (!isEqual(state.selection[0], selectedImageName)) { + state.selection = [selectedImageName]; return; } // Else we have the same image selected, do nothing }, - selectionChanged: (state, action: PayloadAction) => { + selectionChanged: (state, action: PayloadAction) => { // Let's be efficient here and not update the selection unless it has actually changed. This helps to prevent // unnecessary re-renders of the gallery. // Remove duplicates from the selection - const newSelection = uniqBy(action.payload, (i) => i.image_name); + const newSelection = uniq(action.payload); // If the new selection has a different length, update the selection if (newSelection.length !== state.selection.length) { @@ -83,7 +83,7 @@ export const gallerySlice = createSlice({ // Else we have the same selection, do nothing }, - imageToCompareChanged: (state, action: PayloadAction) => { + imageToCompareChanged: (state, action: PayloadAction) => { state.imageToCompare = action.payload; }, comparisonModeChanged: (state, action: PayloadAction) => { diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index 2da48031b4..ad901e6d78 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -1,4 +1,4 @@ -import type { BoardRecordOrderBy, ImageCategory, ImageDTO } from 'services/api/types'; +import type { BoardRecordOrderBy, ImageCategory } from 'services/api/types'; export const IMAGE_CATEGORIES: ImageCategory[] = ['general']; export const ASSETS_CATEGORIES: ImageCategory[] = ['control', 'mask', 'user', 'other']; @@ -10,7 +10,7 @@ export type ComparisonFit = 'contain' | 'fill'; export type OrderDir = 'ASC' | 'DESC'; export type GalleryState = { - selection: ImageDTO[]; + selection: string[]; shouldAutoSwitch: boolean; autoAssignBoardOnClick: boolean; autoAddBoardId: BoardId; @@ -24,7 +24,7 @@ export type GalleryState = { orderDir: OrderDir; searchTerm: string; alwaysShowImageSizeBadge: boolean; - imageToCompare: ImageDTO | null; + imageToCompare: string | null; comparisonMode: ComparisonMode; comparisonFit: ComparisonFit; shouldShowArchivedBoards: boolean; diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 2d91dbc177..296c9a639d 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -71,9 +71,9 @@ export const setNodeImageFieldImage = (arg: { dispatch(fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO })); }; -export const setComparisonImage = (arg: { imageDTO: ImageDTO; dispatch: AppDispatch }) => { - const { imageDTO, dispatch } = arg; - dispatch(imageToCompareChanged(imageDTO)); +export const setComparisonImage = (arg: { image_name: string; dispatch: AppDispatch }) => { + const { image_name, dispatch } = arg; + dispatch(imageToCompareChanged(image_name)); }; export const createNewCanvasEntityFromImage = (arg: { @@ -292,14 +292,14 @@ export const replaceCanvasEntityObjectsWithImage = (arg: { ); }; -export const addImagesToBoard = (arg: { imageDTOs: ImageDTO[]; boardId: BoardId; dispatch: AppDispatch }) => { - const { imageDTOs, boardId, dispatch } = arg; - dispatch(imagesApi.endpoints.addImagesToBoard.initiate({ imageDTOs, board_id: boardId }, { track: false })); +export const addImagesToBoard = (arg: { image_names: string[]; boardId: BoardId; dispatch: AppDispatch }) => { + const { image_names, boardId, dispatch } = arg; + dispatch(imagesApi.endpoints.addImagesToBoard.initiate({ image_names, board_id: boardId }, { track: false })); dispatch(selectionChanged([])); }; -export const removeImagesFromBoard = (arg: { imageDTOs: ImageDTO[]; dispatch: AppDispatch }) => { - const { imageDTOs, dispatch } = arg; - dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ imageDTOs }, { track: false })); +export const removeImagesFromBoard = (arg: { image_names: string[]; dispatch: AppDispatch }) => { + const { image_names, dispatch } = arg; + dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ image_names }, { track: false })); dispatch(selectionChanged([])); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx index d89e452dfd..1a1001ab9f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx @@ -13,11 +13,13 @@ import { motion } from 'framer-motion'; import type { CSSProperties, PropsWithChildren } from 'react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useImageDTO } from 'services/api/endpoints/images'; import { $lastProgressEvent } from 'services/events/stores'; const CurrentImageNode = (props: NodeProps) => { - const imageDTO = useAppSelector(selectLastSelectedImage); + const image_name = useAppSelector(selectLastSelectedImage); const lastProgressEvent = useStore($lastProgressEvent); + const imageDTO = useImageDTO(image_name); if (lastProgressEvent?.image) { return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx index 2d8503775d..15c5e19e7f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx @@ -21,10 +21,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { PiFrameCornersBold } from 'react-icons/pi'; import type { ImageDTO } from 'services/api/types'; -type Props = { imageDTO?: ImageDTO }; +type Props = { imageDTO: ImageDTO | null; isDisabled: boolean }; export const PostProcessingPopover = memo((props: Props) => { - const { imageDTO } = props; + const { imageDTO, isDisabled } = props; const dispatch = useAppDispatch(); const postProcessingModel = useAppSelector(selectPostProcessingModel); const inProgress = useIsQueueMutationInProgress(); @@ -49,6 +49,7 @@ export const PostProcessingPopover = memo((props: Props) => { aria-label={t('parameters.postProcessing')} variant="link" alignSelf="stretch" + isDisabled={isDisabled} /> @@ -56,7 +57,11 @@ export const PostProcessingPopover = memo((props: Props) => { {!postProcessingModel && } - diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx index c82a7832f5..7e6004be1d 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx @@ -75,16 +75,6 @@ const initializeCenterPanelLayout = (api: DockviewApi) => { referencePanel: LAUNCHPAD_PANEL_ID, }, }); - api.addPanel({ - id: PROGRESS_PANEL_ID, - component: PROGRESS_PANEL_ID, - title: 'Generation Progress', - tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, - position: { - direction: 'within', - referencePanel: LAUNCHPAD_PANEL_ID, - }, - }); api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx index b96059aa32..9041c9f444 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx @@ -60,16 +60,6 @@ const initializeCenterPanelLayout = (api: DockviewApi) => { referencePanel: LAUNCHPAD_PANEL_ID, }, }); - api.addPanel({ - id: PROGRESS_PANEL_ID, - component: PROGRESS_PANEL_ID, - title: 'Generation Progress', - tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, - position: { - direction: 'within', - referencePanel: LAUNCHPAD_PANEL_ID, - }, - }); api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx index 22f2b26da2..ec2e378a87 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx @@ -60,16 +60,6 @@ const initializeCenterLayout = (api: DockviewApi) => { referencePanel: LAUNCHPAD_PANEL_ID, }, }); - api.addPanel({ - id: PROGRESS_PANEL_ID, - component: PROGRESS_PANEL_ID, - title: 'Generation Progress', - tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, - position: { - direction: 'within', - referencePanel: LAUNCHPAD_PANEL_ID, - }, - }); api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx index 07974fa3fb..1c11a50ebb 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx @@ -73,16 +73,6 @@ const initializeCenterPanelLayout = (api: DockviewApi) => { referencePanel: LAUNCHPAD_PANEL_ID, }, }); - api.addPanel({ - id: PROGRESS_PANEL_ID, - component: PROGRESS_PANEL_ID, - title: 'Generation Progress', - tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, - position: { - direction: 'within', - referencePanel: LAUNCHPAD_PANEL_ID, - }, - }); api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); }; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 30aad43449..0938d4a2a9 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -1,11 +1,9 @@ +import { skipToken } from '@reduxjs/toolkit/query'; import { $authToken } from 'app/store/nanostores/authToken'; import { getStore } from 'app/store/nanostores/store'; -import type { BoardId } from 'features/gallery/store/types'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import { uniqBy } from 'lodash-es'; import type { components, paths } from 'services/api/schema'; import type { - DeleteBoardResult, GraphAndWorkflowResponse, ImageDTO, ImageUploadEntryRequest, @@ -15,7 +13,6 @@ import type { UploadImageArg, } from 'services/api/types'; import { getCategories, getListImagesUrl } from 'services/api/util'; -import stableHash from 'stable-hash'; import type { Param0 } from 'tsafe'; import type { JsonObject } from 'type-fest'; @@ -94,141 +91,77 @@ export const imagesApi = api.injectEndpoints({ query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}/workflow`) }), providesTags: (result, error, image_name) => [{ type: 'ImageWorkflow', id: image_name }], }), - deleteImage: build.mutation({ + deleteImage: build.mutation< + paths['/api/v1/images/i/{image_name}']['delete']['responses']['200']['content']['application/json'], + paths['/api/v1/images/i/{image_name}']['delete']['parameters']['path'] + >({ query: ({ image_name }) => ({ url: buildImagesUrl(`i/${image_name}`), method: 'DELETE', }), - invalidatesTags: (result, error, imageDTO) => { - const categories = getCategories(imageDTO); - const boardId = imageDTO.board_id ?? 'none'; - - return [ - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories, - }), - }, - { - type: 'Board', - id: boardId, - }, - { - type: 'BoardImagesTotal', - id: boardId, - }, - ]; - }, - }), - - deleteImages: build.mutation({ - query: ({ imageDTOs }) => { - const image_names = imageDTOs.map((imageDTO) => imageDTO.image_name); - return { - url: buildImagesUrl('delete'), - method: 'POST', - body: { - image_names, - }, - }; - }, - invalidatesTags: (result, error, { imageDTOs }) => { - const tags: ApiTagDescription[] = []; - for (const imageDTO of imageDTOs) { - const categories = getCategories(imageDTO); - const boardId = imageDTO.board_id ?? 'none'; - - tags.push( - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories, - }), - }, - { - type: 'Board', - id: boardId, - }, - { - type: 'BoardImagesTotal', - id: boardId, - } - ); + invalidatesTags: (result) => { + if (!result) { + return []; } - - const dedupedTags = uniqBy(tags, stableHash); - return dedupedTags; + // We ignore the deleted images when getting tags to invalidate. If we did not, we will invalidate the queries + // that fetch image DTOs, metadata, and workflows. But we have just deleted those images! Invalidating the tags + // will force those queries to re-fetch, and the requests will of course 404. + return getTagsToInvalidateForBoardAffectingMutation(result.affected_boards); }, }), - deleteUncategorizedImages: build.mutation({ + deleteImages: build.mutation< + paths['/api/v1/images/delete']['post']['responses']['200']['content']['application/json'], + paths['/api/v1/images/delete']['post']['requestBody']['content']['application/json'] + >({ + query: (body) => ({ + url: buildImagesUrl('delete'), + method: 'POST', + body, + }), + invalidatesTags: (result) => { + if (!result) { + return []; + } + // We ignore the deleted images when getting tags to invalidate. If we did not, we will invalidate the queries + // that fetch image DTOs, metadata, and workflows. But we have just deleted those images! Invalidating the tags + // will force those queries to re-fetch, and the requests will of course 404. + return getTagsToInvalidateForBoardAffectingMutation(result.affected_boards); + }, + }), + deleteUncategorizedImages: build.mutation< + paths['/api/v1/images/uncategorized']['delete']['responses']['200']['content']['application/json'], + void + >({ query: () => ({ url: buildImagesUrl('uncategorized'), method: 'DELETE' }), invalidatesTags: (result) => { - if (result && result.deleted_images.length > 0) { - const boardId = 'none'; - - const tags: ApiTagDescription[] = [ - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories: IMAGE_CATEGORIES, - }), - }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories: ASSETS_CATEGORIES, - }), - }, - { - type: 'Board', - id: boardId, - }, - { - type: 'BoardImagesTotal', - id: boardId, - }, - ]; - - return tags; + if (!result) { + return []; } - return []; + // We ignore the deleted images when getting tags to invalidate. If we did not, we will invalidate the queries + // that fetch image DTOs, metadata, and workflows. But we have just deleted those images! Invalidating the tags + // will force those queries to re-fetch, and the requests will of course 404. + return getTagsToInvalidateForBoardAffectingMutation(result.affected_boards); }, }), /** * Change an image's `is_intermediate` property. */ - changeImageIsIntermediate: build.mutation({ - query: ({ imageDTO, is_intermediate }) => ({ - url: buildImagesUrl(`i/${imageDTO.image_name}`), + changeImageIsIntermediate: build.mutation< + paths['/api/v1/images/i/{image_name}']['patch']['responses']['200']['content']['application/json'], + { image_name: string; is_intermediate: boolean } + >({ + query: ({ image_name, is_intermediate }) => ({ + url: buildImagesUrl(`i/${image_name}`), method: 'PATCH', body: { is_intermediate }, }), - invalidatesTags: (result, error, { imageDTO }) => { - const categories = getCategories(imageDTO); - const boardId = imageDTO.board_id ?? 'none'; - + invalidatesTags: (result) => { + if (!result) { + return []; + } return [ - { type: 'Image', id: imageDTO.image_name }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories, - }), - }, - { - type: 'Board', - id: boardId, - }, - { - type: 'BoardImagesTotal', - id: boardId, - }, + ...getTagsToInvalidateForImageMutation([result.image_name]), + ...getTagsToInvalidateForBoardAffectingMutation([result.board_id ?? 'none']), ]; }, }), @@ -236,38 +169,22 @@ export const imagesApi = api.injectEndpoints({ * Star a list of images. */ starImages: build.mutation< - paths['/api/v1/images/unstar']['post']['responses']['200']['content']['application/json'], - { imageDTOs: ImageDTO[] } + paths['/api/v1/images/star']['post']['responses']['200']['content']['application/json'], + paths['/api/v1/images/star']['post']['requestBody']['content']['application/json'] >({ - query: ({ imageDTOs: images }) => ({ + query: (body) => ({ url: buildImagesUrl('star'), method: 'POST', - body: { image_names: images.map((img) => img.image_name) }, + body, }), - invalidatesTags: (result, error, { imageDTOs }) => { - // assume all images are on the same board/category - if (imageDTOs[0]) { - const categories = getCategories(imageDTOs[0]); - const boardId = imageDTOs[0].board_id ?? 'none'; - const tags: ApiTagDescription[] = [ - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories, - }), - }, - { - type: 'Board', - id: boardId, - }, - ]; - for (const imageDTO of imageDTOs) { - tags.push({ type: 'Image', id: imageDTO.image_name }); - } - return tags; + invalidatesTags: (result) => { + if (!result) { + return []; } - return []; + return [ + ...getTagsToInvalidateForImageMutation(result.starred_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), + ]; }, }), /** @@ -275,40 +192,27 @@ export const imagesApi = api.injectEndpoints({ */ unstarImages: build.mutation< paths['/api/v1/images/unstar']['post']['responses']['200']['content']['application/json'], - { imageDTOs: ImageDTO[] } + paths['/api/v1/images/unstar']['post']['requestBody']['content']['application/json'] >({ - query: ({ imageDTOs: images }) => ({ + query: (body) => ({ url: buildImagesUrl('unstar'), method: 'POST', - body: { image_names: images.map((img) => img.image_name) }, + body, }), - invalidatesTags: (result, error, { imageDTOs }) => { - // assume all images are on the same board/category - if (imageDTOs[0]) { - const categories = getCategories(imageDTOs[0]); - const boardId = imageDTOs[0].board_id ?? 'none'; - const tags: ApiTagDescription[] = [ - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: boardId, - categories, - }), - }, - { - type: 'Board', - id: boardId, - }, - ]; - for (const imageDTO of imageDTOs) { - tags.push({ type: 'Image', id: imageDTO.image_name }); - } - return tags; + invalidatesTags: (result) => { + if (!result) { + return []; } - return []; + return [ + ...getTagsToInvalidateForImageMutation(result.unstarred_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), + ]; }, }), - uploadImage: build.mutation({ + uploadImage: build.mutation< + paths['/api/v1/images/upload']['post']['responses']['201']['content']['application/json'], + UploadImageArg + >({ query: ({ file, image_category, is_intermediate, session_id, board_id, crop_visible, metadata, resize_to }) => { const formData = new FormData(); formData.append('file', file); @@ -366,8 +270,11 @@ export const imagesApi = api.injectEndpoints({ body: { width, height, board_id }, }), }), - deleteBoard: build.mutation({ - query: (board_id) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }), + deleteBoard: build.mutation< + paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'], + paths['/api/v1/boards/{board_id}']['delete']['parameters']['path'] + >({ + query: ({ board_id }) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }), invalidatesTags: () => [ { type: 'Board', id: LIST_TAG }, // invalidate the 'No Board' cache @@ -388,192 +295,95 @@ export const imagesApi = api.injectEndpoints({ ], }), - deleteBoardAndImages: build.mutation({ - query: (board_id) => ({ + deleteBoardAndImages: build.mutation< + paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'], + paths['/api/v1/boards/{board_id}']['delete']['parameters']['path'] + >({ + query: ({ board_id }) => ({ url: buildBoardsUrl(board_id), method: 'DELETE', params: { include_images: true }, }), invalidatesTags: () => [{ type: 'Board', id: LIST_TAG }], }), - addImageToBoard: build.mutation({ - query: ({ board_id, imageDTO }) => { - const { image_name } = imageDTO; + addImageToBoard: build.mutation< + paths['/api/v1/board_images/']['post']['responses']['201']['content']['application/json'], + paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'] + >({ + query: (body) => { return { url: buildBoardImagesUrl(), method: 'POST', - body: { board_id, image_name }, + body, }; }, - invalidatesTags: (result, error, { board_id, imageDTO }) => { + invalidatesTags: (result) => { + if (!result) { + return []; + } return [ - { type: 'Image', id: imageDTO.image_name }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id, - categories: getCategories(imageDTO), - }), - }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTO.board_id ?? 'none', - categories: getCategories(imageDTO), - }), - }, - { type: 'Board', id: board_id }, - { type: 'Board', id: imageDTO.board_id ?? 'none' }, - { - type: 'BoardImagesTotal', - id: imageDTO.board_id ?? 'none', - }, - { - type: 'BoardImagesTotal', - id: board_id, - }, + ...getTagsToInvalidateForImageMutation(result.added_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), ]; }, }), - removeImageFromBoard: build.mutation({ - query: ({ imageDTO }) => { - const { image_name } = imageDTO; + removeImageFromBoard: build.mutation< + paths['/api/v1/board_images/']['delete']['responses']['201']['content']['application/json'], + paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'] + >({ + query: (body) => { return { url: buildBoardImagesUrl(), method: 'DELETE', - body: { image_name }, + body, }; }, - invalidatesTags: (result, error, { imageDTO }) => { + invalidatesTags: (result) => { + if (!result) { + return []; + } return [ - { type: 'Image', id: imageDTO.image_name }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTO.board_id, - categories: getCategories(imageDTO), - }), - }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: 'none', - categories: getCategories(imageDTO), - }), - }, - { type: 'Board', id: imageDTO.board_id ?? 'none' }, - { type: 'Board', id: 'none' }, - { - type: 'BoardImagesTotal', - id: imageDTO.board_id ?? 'none', - }, - { type: 'BoardImagesTotal', id: 'none' }, + ...getTagsToInvalidateForImageMutation(result.removed_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), ]; }, }), addImagesToBoard: build.mutation< - components['schemas']['AddImagesToBoardResult'], - { - board_id: string; - imageDTOs: ImageDTO[]; - } + paths['/api/v1/board_images/batch']['post']['responses']['201']['content']['application/json'], + paths['/api/v1/board_images/batch']['post']['requestBody']['content']['application/json'] >({ - query: ({ board_id, imageDTOs }) => ({ + query: (body) => ({ url: buildBoardImagesUrl('batch'), method: 'POST', - body: { - image_names: imageDTOs.map((i) => i.image_name), - board_id, - }, + body, }), - invalidatesTags: (result, error, { board_id, imageDTOs }) => { - const tags: ApiTagDescription[] = []; - if (imageDTOs[0]) { - tags.push({ - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTOs[0].board_id ?? 'none', - categories: getCategories(imageDTOs[0]), - }), - }); - tags.push({ - type: 'ImageList', - id: getListImagesUrl({ - board_id: board_id, - categories: getCategories(imageDTOs[0]), - }), - }); - tags.push({ type: 'Board', id: imageDTOs[0].board_id ?? 'none' }); - tags.push({ - type: 'BoardImagesTotal', - id: imageDTOs[0].board_id ?? 'none', - }); + invalidatesTags: (result) => { + if (!result) { + return []; } - for (const imageDTO of imageDTOs) { - tags.push({ type: 'Image', id: imageDTO.image_name }); - } - tags.push({ type: 'Board', id: board_id }); - tags.push({ - type: 'BoardImagesTotal', - id: board_id ?? 'none', - }); - return tags; + return [ + ...getTagsToInvalidateForImageMutation(result.added_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), + ]; }, }), removeImagesFromBoard: build.mutation< - components['schemas']['RemoveImagesFromBoardResult'], - { - imageDTOs: ImageDTO[]; - } + paths['/api/v1/board_images/batch/delete']['post']['responses']['201']['content']['application/json'], + paths['/api/v1/board_images/batch/delete']['post']['requestBody']['content']['application/json'] >({ - query: ({ imageDTOs }) => ({ + query: (body) => ({ url: buildBoardImagesUrl('batch/delete'), method: 'POST', - body: { - image_names: imageDTOs.map((i) => i.image_name), - }, + body, }), - invalidatesTags: (result, error, { imageDTOs }) => { - const touchedBoardIds: string[] = []; - const tags: ApiTagDescription[] = []; - - if (imageDTOs[0]) { - tags.push({ - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTOs[0].board_id, - categories: getCategories(imageDTOs[0]), - }), - }); - tags.push({ - type: 'ImageList', - id: getListImagesUrl({ - board_id: 'none', - categories: getCategories(imageDTOs[0]), - }), - }); - tags.push({ - type: 'BoardImagesTotal', - id: 'none', - }); + invalidatesTags: (result) => { + if (!result) { + return []; } - - result?.removed_image_names.forEach((image_name) => { - const board_id = imageDTOs.find((i) => i.image_name === image_name)?.board_id; - - if (!board_id || touchedBoardIds.includes(board_id)) { - tags.push({ type: 'Board', id: 'none' }); - return; - } - tags.push({ type: 'Image', id: image_name }); - tags.push({ type: 'Board', id: board_id }); - tags.push({ - type: 'BoardImagesTotal', - id: board_id ?? 'none', - }); - }); - - return tags; + return [ + ...getTagsToInvalidateForImageMutation(result.removed_images), + ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards), + ]; }, }), bulkDownloadImages: build.mutation< @@ -711,3 +521,63 @@ export const imageDTOToFile = async (imageDTO: ImageDTO): Promise => { const file = new File([blob], `copy_of_${imageDTO.image_name}`, { type: 'image/png' }); return file; }; + +export const useImageDTO = (imageName: string | null | undefined) => { + const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken); + return imageDTO ?? null; +}; + +export const getTagsToInvalidateForImageMutation = (image_names: string[]): ApiTagDescription[] => { + const tags: ApiTagDescription[] = []; + + for (const image_name of image_names) { + tags.push({ + type: 'Image', + id: image_name, + }); + tags.push({ + type: 'ImageMetadata', + id: image_name, + }); + tags.push({ + type: 'ImageWorkflow', + id: image_name, + }); + } + + return tags; +}; + +export const getTagsToInvalidateForBoardAffectingMutation = (affected_boards: string[]): ApiTagDescription[] => { + const tags: ApiTagDescription[] = []; + + for (const board_id of affected_boards) { + tags.push({ + type: 'ImageList', + id: getListImagesUrl({ + board_id, + categories: IMAGE_CATEGORIES, + }), + }); + + tags.push({ + type: 'ImageList', + id: getListImagesUrl({ + board_id, + categories: ASSETS_CATEGORIES, + }), + }); + + tags.push({ + type: 'Board', + id: board_id, + }); + + tags.push({ + type: 'BoardImagesTotal', + id: board_id, + }); + } + + return tags; +}; diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx index c40d3fb5a0..da6adb2fb6 100644 --- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx +++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx @@ -117,7 +117,7 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi ); } else { // Else just select the image, no need to switch boards - dispatch(imageSelected(lastImageDTO)); + dispatch(imageSelected(lastImageDTO.image_name)); if (galleryView !== 'images') { // We also need to update the gallery view to images. This also updates the offset.