mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 03:45:33 -05:00
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
28
invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts
Normal file
28
invokeai/frontend/web/src/common/hooks/useSelectorAsAtom.ts
Normal file
@@ -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 = <T extends Selector<RootState, any>>(selector: T): Atom<ReturnType<T>> => {
|
||||
const store = useAppStore();
|
||||
const $atom = useState<WritableAtom<ReturnType<T>>>(() => atom<ReturnType<T>>(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;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import type { ChangeBoardModalState } from './types';
|
||||
|
||||
export const initialState: ChangeBoardModalState = {
|
||||
isModalOpen: false,
|
||||
imagesToChange: [],
|
||||
image_names: [],
|
||||
};
|
||||
|
||||
@@ -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<boolean>) => {
|
||||
state.isModalOpen = action.payload;
|
||||
},
|
||||
imagesToChangeSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||
state.imagesToChange = action.payload;
|
||||
imagesToChangeSelected: (state, action: PayloadAction<string[]>) => {
|
||||
state.image_names = action.payload;
|
||||
},
|
||||
changeBoardReset: (state) => {
|
||||
state.imagesToChange = [];
|
||||
state.image_names = [];
|
||||
state.isModalOpen = false;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export type ChangeBoardModalState = {
|
||||
isModalOpen: boolean;
|
||||
imagesToChange: ImageDTO[];
|
||||
image_names: string[];
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const DeleteImageModal = memo(() => {
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
title={`${t('gallery.deleteImage', { count: state.imageDTOs.length })}2`}
|
||||
title={`${t('gallery.deleteImage', { count: state.image_names.length })}2`}
|
||||
isOpen={state.isOpen}
|
||||
onClose={api.close}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
|
||||
@@ -19,17 +19,16 @@ import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from '
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { selectUpscaleSlice, type UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { forEach, intersectionBy, some } from 'lodash-es';
|
||||
import { forEach, intersection, some } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
// Implements an awaitable modal dialog for deleting images
|
||||
|
||||
type DeleteImagesModalState = {
|
||||
imageDTOs: ImageDTO[];
|
||||
image_names: string[];
|
||||
usagePerImage: ImageUsage[];
|
||||
usageSummary: ImageUsage;
|
||||
isOpen: boolean;
|
||||
@@ -38,7 +37,7 @@ type DeleteImagesModalState = {
|
||||
};
|
||||
|
||||
const getInitialState = (): DeleteImagesModalState => ({
|
||||
imageDTOs: [],
|
||||
image_names: [],
|
||||
usagePerImage: [],
|
||||
usageSummary: {
|
||||
isControlLayerImage: false,
|
||||
@@ -54,21 +53,21 @@ const getInitialState = (): DeleteImagesModalState => ({
|
||||
|
||||
const $deleteModalState = atom<DeleteImagesModalState>(getInitialState());
|
||||
|
||||
const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise<void> => {
|
||||
const deleteImagesWithDialog = async (image_names: string[]): Promise<void> => {
|
||||
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<void>((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<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
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<typeof dispatch>[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Flex
|
||||
@@ -21,7 +20,7 @@ const DndDragPreviewMultipleImage = memo(({ imageDTOs }: { imageDTOs: ImageDTO[]
|
||||
bg="base.900"
|
||||
borderRadius="base"
|
||||
>
|
||||
<Heading>{imageDTOs.length}</Heading>
|
||||
<Heading>{image_names.length}</Heading>
|
||||
<Heading size="sm">{t('parameters.images')}</Heading>
|
||||
</Flex>
|
||||
);
|
||||
@@ -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(<DndDragPreviewMultipleImage imageDTOs={arg.imageDTOs} />, arg.container);
|
||||
createPortal(<DndDragPreviewMultipleImage image_names={arg.image_names} />, 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,
|
||||
|
||||
@@ -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<MultipleImageDndSourceData> = {
|
||||
..._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<SetComparisonImageDndTargetD
|
||||
}
|
||||
const { firstImage, secondImage } = selectComparisonImages(getState());
|
||||
// Do not allow the same images to be selected for comparison
|
||||
if (sourceData.payload.imageDTO.image_name === firstImage?.image_name) {
|
||||
if (sourceData.payload.imageDTO.image_name === firstImage) {
|
||||
return false;
|
||||
}
|
||||
if (sourceData.payload.imageDTO.image_name === secondImage?.image_name) {
|
||||
if (sourceData.payload.imageDTO.image_name === secondImage) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: ({ sourceData, dispatch }) => {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleUnstarSelection}>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarFill />} onClickCapture={handleStarSelection}>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleUnstarSelection}>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
</MenuItem>
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarFill />} onClickCapture={handleStarSelection}>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</MenuItem>
|
||||
{isBulkDownloadEnabled && (
|
||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleBulkDownload}>
|
||||
{t('gallery.downloadSelection')}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
const ref = useRef<HTMLImageElement>(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,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([imageDTO]);
|
||||
deleteImageModal.delete([imageDTO.image_name]);
|
||||
},
|
||||
[deleteImageModal, imageDTO]
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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={<PiDotsThreeOutlineFill />}
|
||||
@@ -53,7 +61,7 @@ export const CurrentImageButtons = memo(() => {
|
||||
icon={<PiFlowArrowBold />}
|
||||
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={<PiArrowsCounterClockwiseBold />}
|
||||
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={<PiQuotesBold />}
|
||||
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={<PiPlantBold />}
|
||||
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}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PiAsteriskBold />}
|
||||
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 && <PostProcessingPopover imageDTO={imageDTO} />}
|
||||
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} isDisabled={isDisabledOverride} />}
|
||||
|
||||
<Divider orientation="vertical" h={8} mx={2} />
|
||||
|
||||
<DeleteImageButton onClick={imageActions.delete} isDisabled={!imageDTO} />
|
||||
<DeleteImageButton onClick={imageActions.delete} isDisabled={isDisabledOverride || !imageDTO} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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<S['InvocationProgressEvent'] | null>(null);
|
||||
const [progressImage, setProgressImage] = useState<ProgressImageType | null>(null);
|
||||
@@ -65,6 +67,8 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
|
||||
}
|
||||
}, [imageDTO, progressEvent]);
|
||||
|
||||
const withProgress = shouldShowProgressInViewer && progressEvent && progressImage;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseOver={onMouseOver}
|
||||
@@ -75,14 +79,13 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
>
|
||||
{imageDTO ? (
|
||||
{imageDTO && (
|
||||
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
|
||||
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} />
|
||||
</Flex>
|
||||
) : (
|
||||
<NoContentForViewer />
|
||||
)}
|
||||
{progressEvent && progressImage && (
|
||||
{!imageDTO && <NoContentForViewer />}
|
||||
{withProgress && (
|
||||
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center" bg="base.900">
|
||||
<ProgressImage progressImage={progressImage} />
|
||||
<ProgressIndicator progressEvent={progressEvent} position="absolute" top={6} right={6} size={8} />
|
||||
@@ -90,9 +93,9 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
|
||||
)}
|
||||
<Flex flexDir="column" gap={2} position="absolute" top={0} insetInlineStart={0} alignItems="flex-start">
|
||||
<CanvasAlertsInvocationProgress />
|
||||
{imageDTO && <ImageMetadataMini imageName={imageDTO.image_name} />}
|
||||
{imageDTO && !withProgress && <ImageMetadataMini imageName={imageDTO.image_name} />}
|
||||
</Flex>
|
||||
{shouldShowImageDetails && imageDTO && (
|
||||
{shouldShowImageDetails && imageDTO && !withProgress && (
|
||||
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||
<ImageMetadataViewer image={imageDTO} />
|
||||
</Box>
|
||||
|
||||
@@ -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 <ImageComparison firstImage={lastSelectedImageDTO} secondImage={comparisonImageDTO} />;
|
||||
|
||||
@@ -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(() => (
|
||||
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
|
||||
<ViewerToolbar />
|
||||
<Divider />
|
||||
<ImageViewer />
|
||||
</Flex>
|
||||
));
|
||||
export const ImageViewerPanel = memo(() => {
|
||||
return (
|
||||
<ImageViewerContextProvider>
|
||||
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
|
||||
<ViewerToolbar />
|
||||
<Divider />
|
||||
<ImageViewer />
|
||||
</Flex>
|
||||
</ImageViewerContextProvider>
|
||||
);
|
||||
});
|
||||
ImageViewerPanel.displayName = 'ImageViewerPanel';
|
||||
|
||||
type ImageViewerContextValue = {
|
||||
$progressEvent: Atom<S['InvocationProgressEvent'] | null>;
|
||||
$progressImage: Atom<ProgressImageType | null>;
|
||||
$hasProgressImage: Atom<boolean>;
|
||||
onLoadImage: (imageDTO: ImageDTO) => void;
|
||||
};
|
||||
|
||||
const ImageViewerContext = createContext<ImageViewerContextValue | null>(null);
|
||||
|
||||
const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
|
||||
const socket = useStore($socket);
|
||||
const $progressEvent = useState(() => atom<S['InvocationProgressEvent'] | null>(null))[0];
|
||||
const $progressImage = useState(() => atom<ProgressImageType | null>(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 <ImageViewerContext.Provider value={value}>{props.children}</ImageViewerContext.Provider>;
|
||||
});
|
||||
ImageViewerContextProvider.displayName = 'ImageViewerContextProvider';
|
||||
|
||||
export const useImageViewerContext = () => {
|
||||
const value = useContext(ImageViewerContext);
|
||||
assert(value !== null, 'useImageViewerContext must be used within a ImageViewerContextProvider');
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Flex w="full" justifyContent="center" h={8}>
|
||||
<ButtonGroup>
|
||||
<ToggleMetadataViewerButton />
|
||||
<CurrentImageButtons />
|
||||
</ButtonGroup>
|
||||
<ToggleProgressButton />
|
||||
<Spacer />
|
||||
<CurrentImageButtons />
|
||||
<Spacer />
|
||||
<ToggleMetadataViewerButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -199,7 +199,7 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([imageDTO]);
|
||||
deleteImageModal.delete([imageDTO.image_name]);
|
||||
}, [deleteImageModal, imageDTO]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<ImageDTO | null>) => {
|
||||
imageSelected: (state, action: PayloadAction<string | null>) => {
|
||||
// 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<ImageDTO[]>) => {
|
||||
selectionChanged: (state, action: PayloadAction<string[]>) => {
|
||||
// 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<ImageDTO | null>) => {
|
||||
imageToCompareChanged: (state, action: PayloadAction<string | null>) => {
|
||||
state.imageToCompare = action.payload;
|
||||
},
|
||||
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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([]));
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
@@ -56,7 +57,11 @@ export const PostProcessingPopover = memo((props: Props) => {
|
||||
<Flex flexDirection="column" gap={4}>
|
||||
<ParamPostProcessingModel />
|
||||
{!postProcessingModel && <MissingModelWarning />}
|
||||
<Button size="sm" isDisabled={!imageDTO || inProgress || !postProcessingModel} onClick={handleClickUpscale}>
|
||||
<Button
|
||||
size="sm"
|
||||
isDisabled={isDisabled || !imageDTO || inProgress || !postProcessingModel}
|
||||
onClick={handleClickUpscale}
|
||||
>
|
||||
{t('parameters.processImage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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<void, ImageDTO>({
|
||||
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<components['schemas']['DeleteImagesFromListResult'], { imageDTOs: ImageDTO[] }>({
|
||||
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<components['schemas']['DeleteImagesFromListResult'], void>({
|
||||
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<ImageDTO, { imageDTO: ImageDTO; is_intermediate: boolean }>({
|
||||
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<ImageDTO, UploadImageArg>({
|
||||
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<DeleteBoardResult, string>({
|
||||
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<DeleteBoardResult, string>({
|
||||
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<void, { board_id: BoardId; imageDTO: ImageDTO }>({
|
||||
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<void, { imageDTO: ImageDTO }>({
|
||||
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<File> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user