diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js index 34db9d466b..c48e08d45e 100644 --- a/invokeai/frontend/web/.eslintrc.js +++ b/invokeai/frontend/web/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { ], 'prettier/prettier': ['error', { endOfLine: 'auto' }], '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-empty-interface': [ 'error', { diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a6a516c380..4329261049 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -119,7 +119,7 @@ "pinGallery": "Pin Gallery", "allImagesLoaded": "All Images Loaded", "loadMore": "Load More", - "noImagesInGallery": "No Images In Gallery", + "noImagesInGallery": "No Images to Display", "deleteImage": "Delete Image", "deleteImageBin": "Deleted images will be sent to your operating system's Bin.", "deleteImagePermanent": "Deleted images cannot be restored.", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 8628360160..a05266d5f2 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -6,9 +6,7 @@ import { PartialAppConfig } from 'app/types/invokeai'; import ImageUploader from 'common/components/ImageUploader'; import GalleryDrawer from 'features/gallery/components/GalleryPanel'; import DeleteImageModal from 'features/imageDeletion/components/DeleteImageModal'; -import Lightbox from 'features/lightbox/components/Lightbox'; import SiteHeader from 'features/system/components/SiteHeader'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { configChanged } from 'features/system/store/configSlice'; import { languageSelector } from 'features/system/store/systemSelectors'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; @@ -34,8 +32,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { const log = useLogger(); - const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; - const dispatch = useAppDispatch(); useEffect(() => { @@ -54,7 +50,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { return ( <> - {isLightboxEnabled && } { - const gallerySelectionCount = state.gallery.selection.length; - const batchSelectionCount = state.batch.selection.length; - - return { - gallerySelectionCount, - batchSelectionCount, - }; - }, - defaultSelectorOptions -); - const DragPreview = (props: OverlayDragImageProps) => { - const { gallerySelectionCount, batchSelectionCount } = - useAppSelector(selector); - if (!props.dragData) { return; } @@ -82,7 +61,7 @@ const DragPreview = (props: OverlayDragImageProps) => { ); } - if (props.dragData.payloadType === 'BATCH_SELECTION') { + if (props.dragData.payloadType === 'IMAGE_NAMES') { return ( { ...STYLES, }} > - {batchSelectionCount} - Images - - ); - } - - if (props.dragData.payloadType === 'GALLERY_SELECTION') { - return ( - - {gallerySelectionCount} + {props.dragData.payload.image_names.length} Images ); diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx index 1b8687bf8e..6ce9b06bd9 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -6,18 +6,18 @@ import { useSensor, useSensors, } from '@dnd-kit/core'; +import { snapCenterToCursor } from '@dnd-kit/modifiers'; +import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { AnimatePresence, motion } from 'framer-motion'; import { PropsWithChildren, memo, useCallback, useState } from 'react'; import DragPreview from './DragPreview'; -import { snapCenterToCursor } from '@dnd-kit/modifiers'; -import { AnimatePresence, motion } from 'framer-motion'; import { DndContext, DragEndEvent, DragStartEvent, TypesafeDraggableData, } from './typesafeDnd'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { imageDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped'; type ImageDndContextProps = PropsWithChildren; @@ -42,18 +42,18 @@ const ImageDndContext = (props: ImageDndContextProps) => { if (!activeData || !overData) { return; } - dispatch(imageDropped({ overData, activeData })); + dispatch(dndDropped({ overData, activeData })); setActiveDragData(null); }, [dispatch] ); const mouseSensor = useSensor(MouseSensor, { - activationConstraint: { delay: 150, tolerance: 5 }, + activationConstraint: { distance: 10 }, }); const touchSensor = useSensor(TouchSensor, { - activationConstraint: { delay: 150, tolerance: 5 }, + activationConstraint: { distance: 10 }, }); // TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx index 1478ace748..003142390f 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx @@ -77,18 +77,14 @@ export type ImageDraggableData = BaseDragData & { payload: { imageDTO: ImageDTO }; }; -export type GallerySelectionDraggableData = BaseDragData & { - payloadType: 'GALLERY_SELECTION'; -}; - -export type BatchSelectionDraggableData = BaseDragData & { - payloadType: 'BATCH_SELECTION'; +export type ImageNamesDraggableData = BaseDragData & { + payloadType: 'IMAGE_NAMES'; + payload: { image_names: string[] }; }; export type TypesafeDraggableData = | ImageDraggableData - | GallerySelectionDraggableData - | BatchSelectionDraggableData; + | ImageNamesDraggableData; interface UseDroppableTypesafeArguments extends Omit { @@ -159,13 +155,11 @@ export const isValidDrop = ( case 'SET_NODES_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_MULTI_NODES_IMAGE': - return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION'; + return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES'; case 'ADD_TO_BATCH': - return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION'; + return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES'; case 'MOVE_BOARD': - return ( - payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION' || 'BATCH_SELECTION' - ); + return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES'; default: return false; } diff --git a/invokeai/frontend/web/src/app/socketio/actions.ts b/invokeai/frontend/web/src/app/socketio/actions.ts deleted file mode 100644 index bb2a0dd0cb..0000000000 --- a/invokeai/frontend/web/src/app/socketio/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -// import { createAction } from '@reduxjs/toolkit'; -// import * as InvokeAI from 'app/types/invokeai'; -// import { GalleryCategory } from 'features/gallery/store/gallerySlice'; -// import { InvokeTabName } from 'features/ui/store/tabMap'; - -// /** -// * We can't use redux-toolkit's createSlice() to make these actions, -// * because they have no associated reducer. They only exist to dispatch -// * requests to the server via socketio. These actions will be handled -// * by the middleware. -// */ - -// export const generateImage = createAction( -// 'socketio/generateImage' -// ); -// export const runESRGAN = createAction('socketio/runESRGAN'); -// export const runFacetool = createAction( -// 'socketio/runFacetool' -// ); -// export const deleteImage = createAction( -// 'socketio/deleteImage' -// ); -// export const requestImages = createAction( -// 'socketio/requestImages' -// ); -// export const requestNewImages = createAction( -// 'socketio/requestNewImages' -// ); -// export const cancelProcessing = createAction( -// 'socketio/cancelProcessing' -// ); - -// export const requestSystemConfig = createAction( -// 'socketio/requestSystemConfig' -// ); - -// export const searchForModels = createAction('socketio/searchForModels'); - -// export const addNewModel = createAction< -// InvokeAI.InvokeModelConfigProps | InvokeAI.InvokeDiffusersModelConfigProps -// >('socketio/addNewModel'); - -// export const deleteModel = createAction('socketio/deleteModel'); - -// export const convertToDiffusers = -// createAction( -// 'socketio/convertToDiffusers' -// ); - -// export const mergeDiffusersModels = -// createAction( -// 'socketio/mergeDiffusersModels' -// ); - -// export const requestModelChange = createAction( -// 'socketio/requestModelChange' -// ); - -// export const saveStagingAreaImageToGallery = createAction( -// 'socketio/saveStagingAreaImageToGallery' -// ); - -// export const emptyTempFolder = createAction( -// 'socketio/requestEmptyTempFolder' -// ); - -export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts deleted file mode 100644 index 8ed46cbc82..0000000000 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/types/invokeai'; -import type { RootState } from 'app/store/store'; -import { - frontendToBackendParameters, - FrontendToBackendParametersConfig, -} from 'common/util/parameterTranslation'; -import dateFormat from 'dateformat'; -import { - GalleryCategory, - GalleryState, - removeImage, -} from 'features/gallery/store/gallerySlice'; -import { - generationRequested, - modelChangeRequested, - modelConvertRequested, - modelMergingRequested, - setIsProcessing, -} from 'features/system/store/systemSlice'; -import { InvokeTabName } from 'features/ui/store/tabMap'; -import { Socket } from 'socket.io-client'; - -/** - * Returns an object containing all functions which use `socketio.emit()`. - * i.e. those which make server requests. - */ -const makeSocketIOEmitters = ( - store: MiddlewareAPI, RootState>, - socketio: Socket -) => { - // We need to dispatch actions to redux and get pieces of state from the store. - const { dispatch, getState } = store; - - return { - emitGenerateImage: (generationMode: InvokeTabName) => { - dispatch(setIsProcessing(true)); - - const state: RootState = getState(); - - const { - generation: generationState, - postprocessing: postprocessingState, - system: systemState, - canvas: canvasState, - } = state; - - const frontendToBackendParametersConfig: FrontendToBackendParametersConfig = - { - generationMode, - generationState, - postprocessingState, - canvasState, - systemState, - }; - - dispatch(generationRequested()); - - const { generationParameters, esrganParameters, facetoolParameters } = - frontendToBackendParameters(frontendToBackendParametersConfig); - - socketio.emit( - 'generateImage', - generationParameters, - esrganParameters, - facetoolParameters - ); - - // we need to truncate the init_mask base64 else it takes up the whole log - // TODO: handle maintaining masks for reproducibility in future - if (generationParameters.init_mask) { - generationParameters.init_mask = generationParameters.init_mask - .substr(0, 64) - .concat('...'); - } - if (generationParameters.init_img) { - generationParameters.init_img = generationParameters.init_img - .substr(0, 64) - .concat('...'); - } - - dispatch( - addLogEntry({ - timestamp: dateFormat(new Date(), 'isoDateTime'), - message: `Image generation requested: ${JSON.stringify({ - ...generationParameters, - ...esrganParameters, - ...facetoolParameters, - })}`, - }) - ); - }, - emitRunESRGAN: (imageToProcess: InvokeAI._Image) => { - dispatch(setIsProcessing(true)); - - const { - postprocessing: { - upscalingLevel, - upscalingDenoising, - upscalingStrength, - }, - } = getState(); - - const esrganParameters = { - upscale: [upscalingLevel, upscalingDenoising, upscalingStrength], - }; - socketio.emit('runPostprocessing', imageToProcess, { - type: 'esrgan', - ...esrganParameters, - }); - dispatch( - addLogEntry({ - timestamp: dateFormat(new Date(), 'isoDateTime'), - message: `ESRGAN upscale requested: ${JSON.stringify({ - file: imageToProcess.url, - ...esrganParameters, - })}`, - }) - ); - }, - emitRunFacetool: (imageToProcess: InvokeAI._Image) => { - dispatch(setIsProcessing(true)); - - const { - postprocessing: { facetoolType, facetoolStrength, codeformerFidelity }, - } = getState(); - - const facetoolParameters: Record = { - facetool_strength: facetoolStrength, - }; - - if (facetoolType === 'codeformer') { - facetoolParameters.codeformer_fidelity = codeformerFidelity; - } - - socketio.emit('runPostprocessing', imageToProcess, { - type: facetoolType, - ...facetoolParameters, - }); - dispatch( - addLogEntry({ - timestamp: dateFormat(new Date(), 'isoDateTime'), - message: `Face restoration (${facetoolType}) requested: ${JSON.stringify( - { - file: imageToProcess.url, - ...facetoolParameters, - } - )}`, - }) - ); - }, - emitDeleteImage: (imageToDelete: InvokeAI._Image) => { - const { url, uuid, category, thumbnail } = imageToDelete; - dispatch(removeImage(imageToDelete)); - socketio.emit('deleteImage', url, thumbnail, uuid, category); - }, - emitRequestImages: (category: GalleryCategory) => { - const gallery: GalleryState = getState().gallery; - const { earliest_mtime } = gallery.categories[category]; - socketio.emit('requestImages', category, earliest_mtime); - }, - emitRequestNewImages: (category: GalleryCategory) => { - const gallery: GalleryState = getState().gallery; - const { latest_mtime } = gallery.categories[category]; - socketio.emit('requestLatestImages', category, latest_mtime); - }, - emitCancelProcessing: () => { - socketio.emit('cancel'); - }, - emitRequestSystemConfig: () => { - socketio.emit('requestSystemConfig'); - }, - emitSearchForModels: (modelFolder: string) => { - socketio.emit('searchForModels', modelFolder); - }, - emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => { - socketio.emit('addNewModel', modelConfig); - }, - emitDeleteModel: (modelName: string) => { - socketio.emit('deleteModel', modelName); - }, - emitConvertToDiffusers: ( - modelToConvert: InvokeAI.InvokeModelConversionProps - ) => { - dispatch(modelConvertRequested()); - socketio.emit('convertToDiffusers', modelToConvert); - }, - emitMergeDiffusersModels: ( - modelMergeInfo: InvokeAI.InvokeModelMergingProps - ) => { - dispatch(modelMergingRequested()); - socketio.emit('mergeDiffusersModels', modelMergeInfo); - }, - emitRequestModelChange: (modelName: string) => { - dispatch(modelChangeRequested()); - socketio.emit('requestModelChange', modelName); - }, - emitSaveStagingAreaImageToGallery: (url: string) => { - socketio.emit('requestSaveStagingAreaImageToGallery', url); - }, - emitRequestEmptyTempFolder: () => { - socketio.emit('requestEmptyTempFolder'); - }, - }; -}; - -export default makeSocketIOEmitters; - -export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/listeners.ts b/invokeai/frontend/web/src/app/socketio/listeners.ts deleted file mode 100644 index cb6db260fc..0000000000 --- a/invokeai/frontend/web/src/app/socketio/listeners.ts +++ /dev/null @@ -1,502 +0,0 @@ -// import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; -// import dateFormat from 'dateformat'; -// import i18n from 'i18n'; -// import { v4 as uuidv4 } from 'uuid'; - -// import * as InvokeAI from 'app/types/invokeai'; - -// import { -// addToast, -// errorOccurred, -// processingCanceled, -// setCurrentStatus, -// setFoundModels, -// setIsCancelable, -// setIsConnected, -// setIsProcessing, -// setModelList, -// setSearchFolder, -// setSystemConfig, -// setSystemStatus, -// } from 'features/system/store/systemSlice'; - -// import { -// addGalleryImages, -// addImage, -// clearIntermediateImage, -// GalleryState, -// removeImage, -// setIntermediateImage, -// } from 'features/gallery/store/gallerySlice'; - -// import type { RootState } from 'app/store/store'; -// import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; -// import { -// clearInitialImage, -// initialImageSelected, -// setInfillMethod, -// // setInitialImage, -// setMaskPath, -// } from 'features/parameters/store/generationSlice'; -// import { tabMap } from 'features/ui/store/tabMap'; -// import { -// requestImages, -// requestNewImages, -// requestSystemConfig, -// } from './actions'; - -// /** -// * Returns an object containing listener callbacks for socketio events. -// * TODO: This file is large, but simple. Should it be split up further? -// */ -// const makeSocketIOListeners = ( -// store: MiddlewareAPI, RootState> -// ) => { -// const { dispatch, getState } = store; - -// return { -// /** -// * Callback to run when we receive a 'connect' event. -// */ -// onConnect: () => { -// try { -// dispatch(setIsConnected(true)); -// dispatch(setCurrentStatus(i18n.t('common.statusConnected'))); -// dispatch(requestSystemConfig()); -// const gallery: GalleryState = getState().gallery; - -// if (gallery.categories.result.latest_mtime) { -// dispatch(requestNewImages('result')); -// } else { -// dispatch(requestImages('result')); -// } - -// if (gallery.categories.user.latest_mtime) { -// dispatch(requestNewImages('user')); -// } else { -// dispatch(requestImages('user')); -// } -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'disconnect' event. -// */ -// onDisconnect: () => { -// try { -// dispatch(setIsConnected(false)); -// dispatch(setCurrentStatus(i18n.t('common.statusDisconnected'))); - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Disconnected from server`, -// level: 'warning', -// }) -// ); -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'generationResult' event. -// */ -// onGenerationResult: (data: InvokeAI.ImageResultResponse) => { -// try { -// const state = getState(); -// const { activeTab } = state.ui; -// const { shouldLoopback } = state.postprocessing; -// const { boundingBox: _, generationMode, ...rest } = data; - -// const newImage = { -// uuid: uuidv4(), -// ...rest, -// }; - -// if (['txt2img', 'img2img'].includes(generationMode)) { -// dispatch( -// addImage({ -// category: 'result', -// image: { ...newImage, category: 'result' }, -// }) -// ); -// } - -// if (generationMode === 'unifiedCanvas' && data.boundingBox) { -// const { boundingBox } = data; -// dispatch( -// addImageToStagingArea({ -// image: { ...newImage, category: 'temp' }, -// boundingBox, -// }) -// ); - -// if (state.canvas.shouldAutoSave) { -// dispatch( -// addImage({ -// image: { ...newImage, category: 'result' }, -// category: 'result', -// }) -// ); -// } -// } - -// // TODO: fix -// // if (shouldLoopback) { -// // const activeTabName = tabMap[activeTab]; -// // switch (activeTabName) { -// // case 'img2img': { -// // dispatch(initialImageSelected(newImage.uuid)); -// // // dispatch(setInitialImage(newImage)); -// // break; -// // } -// // } -// // } - -// dispatch(clearIntermediateImage()); - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Image generated: ${data.url}`, -// }) -// ); -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'intermediateResult' event. -// */ -// onIntermediateResult: (data: InvokeAI.ImageResultResponse) => { -// try { -// dispatch( -// setIntermediateImage({ -// uuid: uuidv4(), -// ...data, -// category: 'result', -// }) -// ); -// if (!data.isBase64) { -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Intermediate image generated: ${data.url}`, -// }) -// ); -// } -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive an 'esrganResult' event. -// */ -// onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => { -// try { -// dispatch( -// addImage({ -// category: 'result', -// image: { -// uuid: uuidv4(), -// ...data, -// category: 'result', -// }, -// }) -// ); - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Postprocessed: ${data.url}`, -// }) -// ); -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'progressUpdate' event. -// * TODO: Add additional progress phases -// */ -// onProgressUpdate: (data: InvokeAI.SystemStatus) => { -// try { -// dispatch(setIsProcessing(true)); -// dispatch(setSystemStatus(data)); -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'progressUpdate' event. -// */ -// onError: (data: InvokeAI.ErrorResponse) => { -// const { message, additionalData } = data; - -// if (additionalData) { -// // TODO: handle more data than short message -// } - -// try { -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Server error: ${message}`, -// level: 'error', -// }) -// ); -// dispatch(errorOccurred()); -// dispatch(clearIntermediateImage()); -// } catch (e) { -// console.error(e); -// } -// }, -// /** -// * Callback to run when we receive a 'galleryImages' event. -// */ -// onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => { -// const { images, areMoreImagesAvailable, category } = data; - -// /** -// * the logic here ideally would be in the reducer but we have a side effect: -// * generating a uuid. so the logic needs to be here, outside redux. -// */ - -// // Generate a UUID for each image -// const preparedImages = images.map((image): InvokeAI._Image => { -// return { -// uuid: uuidv4(), -// ...image, -// }; -// }); - -// dispatch( -// addGalleryImages({ -// images: preparedImages, -// areMoreImagesAvailable, -// category, -// }) -// ); - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Loaded ${images.length} images`, -// }) -// ); -// }, -// /** -// * Callback to run when we receive a 'processingCanceled' event. -// */ -// onProcessingCanceled: () => { -// dispatch(processingCanceled()); - -// const { intermediateImage } = getState().gallery; - -// if (intermediateImage) { -// if (!intermediateImage.isBase64) { -// dispatch( -// addImage({ -// category: 'result', -// image: intermediateImage, -// }) -// ); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Intermediate image saved: ${intermediateImage.url}`, -// }) -// ); -// } -// dispatch(clearIntermediateImage()); -// } - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Processing canceled`, -// level: 'warning', -// }) -// ); -// }, -// /** -// * Callback to run when we receive a 'imageDeleted' event. -// */ -// onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => { -// const { url } = data; - -// // remove image from gallery -// dispatch(removeImage(data)); - -// // remove references to image in options -// const { -// generation: { initialImage, maskPath }, -// } = getState(); - -// if ( -// initialImage === url || -// (initialImage as InvokeAI._Image)?.url === url -// ) { -// dispatch(clearInitialImage()); -// } - -// if (maskPath === url) { -// dispatch(setMaskPath('')); -// } - -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Image deleted: ${url}`, -// }) -// ); -// }, -// onSystemConfig: (data: InvokeAI.SystemConfig) => { -// dispatch(setSystemConfig(data)); -// if (!data.infill_methods.includes('patchmatch')) { -// dispatch(setInfillMethod(data.infill_methods[0])); -// } -// }, -// onFoundModels: (data: InvokeAI.FoundModelResponse) => { -// const { search_folder, found_models } = data; -// dispatch(setSearchFolder(search_folder)); -// dispatch(setFoundModels(found_models)); -// }, -// onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => { -// const { new_model_name, model_list, update } = data; -// dispatch(setModelList(model_list)); -// dispatch(setIsProcessing(false)); -// dispatch(setCurrentStatus(i18n.t('modelManager.modelAdded'))); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Model Added: ${new_model_name}`, -// level: 'info', -// }) -// ); -// dispatch( -// addToast({ -// title: !update -// ? `${i18n.t('modelManager.modelAdded')}: ${new_model_name}` -// : `${i18n.t('modelManager.modelUpdated')}: ${new_model_name}`, -// status: 'success', -// duration: 2500, -// isClosable: true, -// }) -// ); -// }, -// onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => { -// const { deleted_model_name, model_list } = data; -// dispatch(setModelList(model_list)); -// dispatch(setIsProcessing(false)); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `${i18n.t( -// 'modelManager.modelAdded' -// )}: ${deleted_model_name}`, -// level: 'info', -// }) -// ); -// dispatch( -// addToast({ -// title: `${i18n.t( -// 'modelManager.modelEntryDeleted' -// )}: ${deleted_model_name}`, -// status: 'success', -// duration: 2500, -// isClosable: true, -// }) -// ); -// }, -// onModelConverted: (data: InvokeAI.ModelConvertedResponse) => { -// const { converted_model_name, model_list } = data; -// dispatch(setModelList(model_list)); -// dispatch(setCurrentStatus(i18n.t('common.statusModelConverted'))); -// dispatch(setIsProcessing(false)); -// dispatch(setIsCancelable(true)); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Model converted: ${converted_model_name}`, -// level: 'info', -// }) -// ); -// dispatch( -// addToast({ -// title: `${i18n.t( -// 'modelManager.modelConverted' -// )}: ${converted_model_name}`, -// status: 'success', -// duration: 2500, -// isClosable: true, -// }) -// ); -// }, -// onModelsMerged: (data: InvokeAI.ModelsMergedResponse) => { -// const { merged_models, merged_model_name, model_list } = data; -// dispatch(setModelList(model_list)); -// dispatch(setCurrentStatus(i18n.t('common.statusMergedModels'))); -// dispatch(setIsProcessing(false)); -// dispatch(setIsCancelable(true)); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Models merged: ${merged_models}`, -// level: 'info', -// }) -// ); -// dispatch( -// addToast({ -// title: `${i18n.t('modelManager.modelsMerged')}: ${merged_model_name}`, -// status: 'success', -// duration: 2500, -// isClosable: true, -// }) -// ); -// }, -// onModelChanged: (data: InvokeAI.ModelChangeResponse) => { -// const { model_name, model_list } = data; -// dispatch(setModelList(model_list)); -// dispatch(setCurrentStatus(i18n.t('common.statusModelChanged'))); -// dispatch(setIsProcessing(false)); -// dispatch(setIsCancelable(true)); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Model changed: ${model_name}`, -// level: 'info', -// }) -// ); -// }, -// onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => { -// const { model_name, model_list } = data; -// dispatch(setModelList(model_list)); -// dispatch(setIsProcessing(false)); -// dispatch(setIsCancelable(true)); -// dispatch(errorOccurred()); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Model change failed: ${model_name}`, -// level: 'error', -// }) -// ); -// }, -// onTempFolderEmptied: () => { -// dispatch( -// addToast({ -// title: i18n.t('toast.tempFoldersEmptied'), -// status: 'success', -// duration: 2500, -// isClosable: true, -// }) -// ); -// }, -// }; -// }; - -// export default makeSocketIOListeners; - -export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/middleware.ts b/invokeai/frontend/web/src/app/socketio/middleware.ts deleted file mode 100644 index 88013ea222..0000000000 --- a/invokeai/frontend/web/src/app/socketio/middleware.ts +++ /dev/null @@ -1,248 +0,0 @@ -// import { Middleware } from '@reduxjs/toolkit'; -// import { io } from 'socket.io-client'; - -// import makeSocketIOEmitters from './emitters'; -// import makeSocketIOListeners from './listeners'; - -// import * as InvokeAI from 'app/types/invokeai'; - -// /** -// * Creates a socketio middleware to handle communication with server. -// * -// * Special `socketio/actionName` actions are created in actions.ts and -// * exported for use by the application, which treats them like any old -// * action, using `dispatch` to dispatch them. -// * -// * These actions are intercepted here, where `socketio.emit()` calls are -// * made on their behalf - see `emitters.ts`. The emitter functions -// * are the outbound communication to the server. -// * -// * Listeners are also established here - see `listeners.ts`. The listener -// * functions receive communication from the server and usually dispatch -// * some new action to handle whatever data was sent from the server. -// */ -// export const socketioMiddleware = () => { -// const { origin } = new URL(window.location.href); - -// const socketio = io(origin, { -// timeout: 60000, -// path: `${window.location.pathname}socket.io`, -// }); - -// socketio.disconnect(); - -// let areListenersSet = false; - -// const middleware: Middleware = (store) => (next) => (action) => { -// const { -// onConnect, -// onDisconnect, -// onError, -// onPostprocessingResult, -// onGenerationResult, -// onIntermediateResult, -// onProgressUpdate, -// onGalleryImages, -// onProcessingCanceled, -// onImageDeleted, -// onSystemConfig, -// onModelChanged, -// onFoundModels, -// onNewModelAdded, -// onModelDeleted, -// onModelConverted, -// onModelsMerged, -// onModelChangeFailed, -// onTempFolderEmptied, -// } = makeSocketIOListeners(store); - -// const { -// emitGenerateImage, -// emitRunESRGAN, -// emitRunFacetool, -// emitDeleteImage, -// emitRequestImages, -// emitRequestNewImages, -// emitCancelProcessing, -// emitRequestSystemConfig, -// emitSearchForModels, -// emitAddNewModel, -// emitDeleteModel, -// emitConvertToDiffusers, -// emitMergeDiffusersModels, -// emitRequestModelChange, -// emitSaveStagingAreaImageToGallery, -// emitRequestEmptyTempFolder, -// } = makeSocketIOEmitters(store, socketio); - -// /** -// * If this is the first time the middleware has been called (e.g. during store setup), -// * initialize all our socket.io listeners. -// */ -// if (!areListenersSet) { -// socketio.on('connect', () => onConnect()); - -// socketio.on('disconnect', () => onDisconnect()); - -// socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data)); - -// socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) => -// onGenerationResult(data) -// ); - -// socketio.on( -// 'postprocessingResult', -// (data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data) -// ); - -// socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) => -// onIntermediateResult(data) -// ); - -// socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) => -// onProgressUpdate(data) -// ); - -// socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) => -// onGalleryImages(data) -// ); - -// socketio.on('processingCanceled', () => { -// onProcessingCanceled(); -// }); - -// socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => { -// onImageDeleted(data); -// }); - -// socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => { -// onSystemConfig(data); -// }); - -// socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => { -// onFoundModels(data); -// }); - -// socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => { -// onNewModelAdded(data); -// }); - -// socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => { -// onModelDeleted(data); -// }); - -// socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => { -// onModelConverted(data); -// }); - -// socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => { -// onModelsMerged(data); -// }); - -// socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => { -// onModelChanged(data); -// }); - -// socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => { -// onModelChangeFailed(data); -// }); - -// socketio.on('tempFolderEmptied', () => { -// onTempFolderEmptied(); -// }); - -// areListenersSet = true; -// } - -// /** -// * Handle redux actions caught by middleware. -// */ -// switch (action.type) { -// case 'socketio/generateImage': { -// emitGenerateImage(action.payload); -// break; -// } - -// case 'socketio/runESRGAN': { -// emitRunESRGAN(action.payload); -// break; -// } - -// case 'socketio/runFacetool': { -// emitRunFacetool(action.payload); -// break; -// } - -// case 'socketio/deleteImage': { -// emitDeleteImage(action.payload); -// break; -// } - -// case 'socketio/requestImages': { -// emitRequestImages(action.payload); -// break; -// } - -// case 'socketio/requestNewImages': { -// emitRequestNewImages(action.payload); -// break; -// } - -// case 'socketio/cancelProcessing': { -// emitCancelProcessing(); -// break; -// } - -// case 'socketio/requestSystemConfig': { -// emitRequestSystemConfig(); -// break; -// } - -// case 'socketio/searchForModels': { -// emitSearchForModels(action.payload); -// break; -// } - -// case 'socketio/addNewModel': { -// emitAddNewModel(action.payload); -// break; -// } - -// case 'socketio/deleteModel': { -// emitDeleteModel(action.payload); -// break; -// } - -// case 'socketio/convertToDiffusers': { -// emitConvertToDiffusers(action.payload); -// break; -// } - -// case 'socketio/mergeDiffusersModels': { -// emitMergeDiffusersModels(action.payload); -// break; -// } - -// case 'socketio/requestModelChange': { -// emitRequestModelChange(action.payload); -// break; -// } - -// case 'socketio/saveStagingAreaImageToGallery': { -// emitSaveStagingAreaImageToGallery(action.payload); -// break; -// } - -// case 'socketio/requestEmptyTempFolder': { -// emitRequestEmptyTempFolder(); -// break; -// } -// } - -// next(action); -// }; - -// return middleware; -// }; - -export default {}; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts index ac1b9c5205..3407b3f7de 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts @@ -1,7 +1,6 @@ import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist'; import { controlNetDenylist } from 'features/controlNet/store/controlNetDenylist'; import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist'; -import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist'; import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist'; import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; @@ -16,7 +15,6 @@ const serializationDenylist: { canvas: canvasPersistDenylist, gallery: galleryPersistDenylist, generation: generationPersistDenylist, - lightbox: lightboxPersistDenylist, nodes: nodesPersistDenylist, postprocessing: postprocessingPersistDenylist, system: systemPersistDenylist, diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts index 23e6448987..5d94abd738 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts @@ -1,7 +1,6 @@ import { initialCanvasState } from 'features/canvas/store/canvasSlice'; import { initialControlNetState } from 'features/controlNet/store/controlNetSlice'; import { initialGalleryState } from 'features/gallery/store/gallerySlice'; -import { initialLightboxState } from 'features/lightbox/store/lightboxSlice'; import { initialNodesState } from 'features/nodes/store/nodesSlice'; import { initialGenerationState } from 'features/parameters/store/generationSlice'; import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice'; @@ -18,7 +17,6 @@ const initialStates: { canvas: initialCanvasState, gallery: initialGalleryState, generation: initialGenerationState, - lightbox: initialLightboxState, nodes: initialNodesState, postprocessing: initialPostprocessingState, system: initialSystemState, diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts index 8a6e112d27..6d41d488c8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts @@ -1,4 +1,8 @@ +/** + * This is a list of actions that should be excluded in the Redux DevTools. + */ export const actionsDenylist = [ + // very spammy canvas actions 'canvas/setCursorPosition', 'canvas/setStageCoordinates', 'canvas/setStageScale', @@ -7,7 +11,11 @@ export const actionsDenylist = [ 'canvas/setBoundingBoxDimensions', 'canvas/setIsDrawing', 'canvas/addPointToCurrentLine', + // bazillions during generation 'socket/socketGeneratorProgress', 'socket/appSocketGeneratorProgress', + // every time user presses shift 'hotkeys/shiftKeyPressed', + // this happens after every state change + '@@REMEMBER_PERSISTED', ]; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 8c5873903c..edeb156439 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -58,7 +58,6 @@ import { addReceivedPageOfImagesFulfilledListener, addReceivedPageOfImagesRejectedListener, } from './listeners/receivedPageOfImages'; -import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch'; import { addSessionCanceledFulfilledListener, addSessionCanceledPendingListener, @@ -215,9 +214,6 @@ addBoardIdSelectedListener(); // Node schemas addReceivedOpenAPISchemaListener(); -// Batches -addSelectionAddedToBatchListener(); - // DND addImageDroppedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts index dc38ba911a..9f7085db6f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts @@ -1,5 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { + ASSETS_CATEGORIES, + IMAGE_CATEGORIES, INITIAL_IMAGE_LIMIT, isLoadingChanged, } from 'features/gallery/store/gallerySlice'; @@ -20,7 +22,7 @@ export const addAppStartedListener = () => { // fill up the gallery tab with images await dispatch( receivedPageOfImages({ - categories: ['general'], + categories: IMAGE_CATEGORIES, is_intermediate: false, offset: 0, limit: INITIAL_IMAGE_LIMIT, @@ -30,7 +32,7 @@ export const addAppStartedListener = () => { // fill up the assets tab with images await dispatch( receivedPageOfImages({ - categories: ['control', 'mask', 'user', 'other'], + categories: ASSETS_CATEGORIES, is_intermediate: false, offset: 0, limit: INITIAL_IMAGE_LIMIT, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts index 6ce6665cc5..9ce17e3099 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -1,15 +1,18 @@ import { log } from 'app/logging/useLogger'; -import { startAppListening } from '..'; +import { selectFilteredImages } from 'features/gallery/store/gallerySelectors'; import { + ASSETS_CATEGORIES, + IMAGE_CATEGORIES, + boardIdSelected, imageSelected, selectImagesAll, - boardIdSelected, } from 'features/gallery/store/gallerySlice'; +import { boardsApi } from 'services/api/endpoints/boards'; import { IMAGES_PER_PAGE, receivedPageOfImages, } from 'services/api/thunks/image'; -import { boardsApi } from 'services/api/endpoints/boards'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'boards' }); @@ -24,19 +27,24 @@ export const addBoardIdSelectedListener = () => { const state = getState(); const allImages = selectImagesAll(state); - if (!board_id) { - // a board was unselected - dispatch(imageSelected(allImages[0]?.image_name)); + if (board_id === 'all') { + // Selected all images + dispatch(imageSelected(allImages[0]?.image_name ?? null)); return; } - const { categories } = state.gallery; + if (board_id === 'batch') { + // Selected the batch + dispatch(imageSelected(state.gallery.batchImageNames[0] ?? null)); + return; + } - const filteredImages = allImages.filter((i) => { - const isInCategory = categories.includes(i.image_category); - const isInSelectedBoard = board_id ? i.board_id === board_id : true; - return isInCategory && isInSelectedBoard; - }); + const filteredImages = selectFilteredImages(state); + + const categories = + state.gallery.galleryView === 'images' + ? IMAGE_CATEGORIES + : ASSETS_CATEGORIES; // get the board from the cache const { data: boards } = @@ -45,7 +53,7 @@ export const addBoardIdSelectedListener = () => { if (!board) { // can't find the board in cache... - dispatch(imageSelected(allImages[0]?.image_name)); + dispatch(boardIdSelected('all')); return; } @@ -63,48 +71,3 @@ export const addBoardIdSelectedListener = () => { }, }); }; - -export const addBoardIdSelected_changeSelectedImage_listener = () => { - startAppListening({ - actionCreator: boardIdSelected, - effect: (action, { getState, dispatch }) => { - const board_id = action.payload; - - const state = getState(); - - // we need to check if we need to fetch more images - - if (!board_id) { - // a board was unselected - we don't need to do anything - return; - } - - const { categories } = state.gallery; - - const filteredImages = selectImagesAll(state).filter((i) => { - const isInCategory = categories.includes(i.image_category); - const isInSelectedBoard = board_id ? i.board_id === board_id : true; - return isInCategory && isInSelectedBoard; - }); - - // get the board from the cache - const { data: boards } = - boardsApi.endpoints.listAllBoards.select()(state); - const board = boards?.find((b) => b.board_id === board_id); - if (!board) { - // can't find the board in cache... - return; - } - - // if we haven't loaded one full page of images from this board, load more - if ( - filteredImages.length < board.image_count && - filteredImages.length < IMAGES_PER_PAGE - ) { - dispatch( - receivedPageOfImages({ categories, board_id, is_intermediate: false }) - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts index e4d8c74bf9..c92eeac0db 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts @@ -1,6 +1,5 @@ import { log } from 'app/logging/useLogger'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; -import { imageDTOReceived } from 'services/api/thunks/image'; import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'boards' }); @@ -15,12 +14,6 @@ export const addImageAddedToBoardFulfilledListener = () => { { data: { board_id, image_name } }, 'Image added to board' ); - - dispatch( - imageDTOReceived({ - image_name, - }) - ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index f083a716a4..c90c08d94a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -1,10 +1,10 @@ import { log } from 'app/logging/useLogger'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { selectNextImageToSelect } from 'features/gallery/store/gallerySelectors'; import { imageRemoved, imageSelected, - selectFilteredImages, } from 'features/gallery/store/gallerySlice'; import { imageDeletionConfirmed, @@ -12,7 +12,6 @@ import { } from 'features/imageDeletion/store/imageDeletionSlice'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { clamp } from 'lodash-es'; import { api } from 'services/api'; import { imageDeleted } from 'services/api/thunks/image'; import { startAppListening } from '..'; @@ -37,26 +36,10 @@ export const addRequestedImageDeletionListener = () => { state.gallery.selection[state.gallery.selection.length - 1]; if (lastSelectedImage === image_name) { - const filteredImages = selectFilteredImages(state); - - const ids = filteredImages.map((i) => i.image_name); - - const deletedImageIndex = ids.findIndex( - (result) => result.toString() === image_name - ); - - const filteredIds = ids.filter((id) => id.toString() !== image_name); - - const newSelectedImageIndex = clamp( - deletedImageIndex, - 0, - filteredIds.length - 1 - ); - - const newSelectedImageId = filteredIds[newSelectedImageIndex]; + const newSelectedImageId = selectNextImageToSelect(state, image_name); if (newSelectedImageId) { - dispatch(imageSelected(newSelectedImageId as string)); + dispatch(imageSelected(newSelectedImageId)); } else { dispatch(imageSelected(null)); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 24a5bffec7..51894d50de 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,13 +4,12 @@ import { TypesafeDroppableData, } from 'app/components/ImageDnd/typesafeDnd'; import { log } from 'app/logging/useLogger'; -import { - imageAddedToBatch, - imagesAddedToBatch, -} from 'features/batch/store/batchSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { + imageSelected, + imagesAddedToBatch, +} from 'features/gallery/store/gallerySlice'; import { fieldValueChanged, imageCollectionFieldValueChanged, @@ -21,57 +20,66 @@ import { startAppListening } from '../'; const moduleLog = log.child({ namespace: 'dnd' }); -export const imageDropped = createAction<{ +export const dndDropped = createAction<{ overData: TypesafeDroppableData; activeData: TypesafeDraggableData; -}>('dnd/imageDropped'); +}>('dnd/dndDropped'); export const addImageDroppedListener = () => { startAppListening({ - actionCreator: imageDropped, - effect: (action, { dispatch, getState }) => { + actionCreator: dndDropped, + effect: async (action, { dispatch, getState, take }) => { const { activeData, overData } = action.payload; - const { actionType } = overData; const state = getState(); + moduleLog.debug( + { data: { activeData, overData } }, + 'Image or selection dropped' + ); + // set current image if ( - actionType === 'SET_CURRENT_IMAGE' && + overData.actionType === 'SET_CURRENT_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { dispatch(imageSelected(activeData.payload.imageDTO.image_name)); + return; } // set initial image if ( - actionType === 'SET_INITIAL_IMAGE' && + overData.actionType === 'SET_INITIAL_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { dispatch(initialImageChanged(activeData.payload.imageDTO)); + return; } // add image to batch if ( - actionType === 'ADD_TO_BATCH' && + overData.actionType === 'ADD_TO_BATCH' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - dispatch(imageAddedToBatch(activeData.payload.imageDTO.image_name)); + dispatch(imagesAddedToBatch([activeData.payload.imageDTO.image_name])); + return; } // add multiple images to batch if ( - actionType === 'ADD_TO_BATCH' && - activeData.payloadType === 'GALLERY_SELECTION' + overData.actionType === 'ADD_TO_BATCH' && + activeData.payloadType === 'IMAGE_NAMES' ) { - dispatch(imagesAddedToBatch(state.gallery.selection)); + dispatch(imagesAddedToBatch(activeData.payload.image_names)); + + return; } // set control image if ( - actionType === 'SET_CONTROLNET_IMAGE' && + overData.actionType === 'SET_CONTROLNET_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { @@ -82,20 +90,22 @@ export const addImageDroppedListener = () => { controlNetId, }) ); + return; } // set canvas image if ( - actionType === 'SET_CANVAS_INITIAL_IMAGE' && + overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { dispatch(setInitialCanvasImage(activeData.payload.imageDTO)); + return; } // set nodes image if ( - actionType === 'SET_NODES_IMAGE' && + overData.actionType === 'SET_NODES_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { @@ -107,11 +117,12 @@ export const addImageDroppedListener = () => { value: activeData.payload.imageDTO, }) ); + return; } // set multiple nodes images (single image handler) if ( - actionType === 'SET_MULTI_NODES_IMAGE' && + overData.actionType === 'SET_MULTI_NODES_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { @@ -123,43 +134,30 @@ export const addImageDroppedListener = () => { value: [activeData.payload.imageDTO], }) ); + return; } // set multiple nodes images (multiple images handler) if ( - actionType === 'SET_MULTI_NODES_IMAGE' && - activeData.payloadType === 'GALLERY_SELECTION' + overData.actionType === 'SET_MULTI_NODES_IMAGE' && + activeData.payloadType === 'IMAGE_NAMES' ) { const { fieldName, nodeId } = overData.context; dispatch( imageCollectionFieldValueChanged({ nodeId, fieldName, - value: state.gallery.selection.map((image_name) => ({ + value: activeData.payload.image_names.map((image_name) => ({ image_name, })), }) ); + return; } - // remove image from board - // TODO: remove board_id from `removeImageFromBoard()` endpoint - // TODO: handle multiple images - // if ( - // actionType === 'MOVE_BOARD' && - // activeData.payloadType === 'IMAGE_DTO' && - // activeData.payload.imageDTO && - // overData.boardId !== null - // ) { - // const { image_name } = activeData.payload.imageDTO; - // dispatch( - // boardImagesApi.endpoints.removeImageFromBoard.initiate({ image_name }) - // ); - // } - // add image to board if ( - actionType === 'MOVE_BOARD' && + overData.actionType === 'MOVE_BOARD' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO && overData.context.boardId @@ -172,17 +170,89 @@ export const addImageDroppedListener = () => { board_id: boardId, }) ); + return; } - // add multiple images to board - // TODO: add endpoint - // if ( - // actionType === 'ADD_TO_BATCH' && - // activeData.payloadType === 'IMAGE_NAMES' && - // activeData.payload.imageDTONames - // ) { - // dispatch(boardImagesApi.endpoints.addImagesToBoard.intiate({})); - // } + // remove image from board + if ( + overData.actionType === 'MOVE_BOARD' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO && + overData.context.boardId === null + ) { + const { image_name, board_id } = activeData.payload.imageDTO; + if (board_id) { + dispatch( + boardImagesApi.endpoints.removeImageFromBoard.initiate({ + image_name, + board_id, + }) + ); + } + return; + } + + // add gallery selection to board + if ( + overData.actionType === 'MOVE_BOARD' && + activeData.payloadType === 'IMAGE_NAMES' && + overData.context.boardId + ) { + console.log('adding gallery selection to board'); + const board_id = overData.context.boardId; + dispatch( + boardImagesApi.endpoints.addManyBoardImages.initiate({ + board_id, + image_names: activeData.payload.image_names, + }) + ); + return; + } + + // remove gallery selection from board + if ( + overData.actionType === 'MOVE_BOARD' && + activeData.payloadType === 'IMAGE_NAMES' && + overData.context.boardId === null + ) { + console.log('removing gallery selection to board'); + dispatch( + boardImagesApi.endpoints.deleteManyBoardImages.initiate({ + image_names: activeData.payload.image_names, + }) + ); + return; + } + + // add batch selection to board + if ( + overData.actionType === 'MOVE_BOARD' && + activeData.payloadType === 'IMAGE_NAMES' && + overData.context.boardId + ) { + const board_id = overData.context.boardId; + dispatch( + boardImagesApi.endpoints.addManyBoardImages.initiate({ + board_id, + image_names: activeData.payload.image_names, + }) + ); + return; + } + + // remove batch selection from board + if ( + overData.actionType === 'MOVE_BOARD' && + activeData.payloadType === 'IMAGE_NAMES' && + overData.context.boardId === null + ) { + dispatch( + boardImagesApi.endpoints.deleteManyBoardImages.initiate({ + image_names: activeData.payload.image_names, + }) + ); + return; + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts index 4cf144211c..3c6731bb31 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts @@ -1,6 +1,5 @@ import { log } from 'app/logging/useLogger'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; -import { imageDTOReceived } from 'services/api/thunks/image'; import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'boards' }); @@ -15,12 +14,6 @@ export const addImageRemovedFromBoardFulfilledListener = () => { { data: { board_id, image_name } }, 'Image added to board' ); - - dispatch( - imageDTOReceived({ - image_name, - }) - ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 0cd852c3de..cca01354b5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,13 +1,15 @@ -import { startAppListening } from '..'; -import { imageUploaded } from 'services/api/thunks/image'; -import { addToast } from 'features/system/store/systemSlice'; import { log } from 'app/logging/useLogger'; -import { imageUpserted } from 'features/gallery/store/gallerySlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice'; -import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { + imageUpserted, + imagesAddedToBatch, +} from 'features/gallery/store/gallerySlice'; import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; -import { imageAddedToBatch } from 'features/batch/store/batchSlice'; +import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { addToast } from 'features/system/store/systemSlice'; +import { imageUploaded } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'image' }); @@ -73,7 +75,7 @@ export const addImageUploadedFulfilledListener = () => { } if (postUploadAction?.type === 'ADD_TO_BATCH') { - dispatch(imageAddedToBatch(image.image_name)); + dispatch(imagesAddedToBatch([image.image_name])); return; } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/selectionAddedToBatch.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/selectionAddedToBatch.ts deleted file mode 100644 index dae72d92e7..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/selectionAddedToBatch.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { startAppListening } from '..'; -import { log } from 'app/logging/useLogger'; -import { - imagesAddedToBatch, - selectionAddedToBatch, -} from 'features/batch/store/batchSlice'; - -const moduleLog = log.child({ namespace: 'batch' }); - -export const addSelectionAddedToBatchListener = () => { - startAppListening({ - actionCreator: selectionAddedToBatch, - effect: (action, { dispatch, getState }) => { - const { selection } = getState().gallery; - - dispatch(imagesAddedToBatch(selection)); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 80688a1585..da09b496d7 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -9,14 +9,12 @@ import { import dynamicMiddlewares from 'redux-dynamic-middlewares'; import { rememberEnhancer, rememberReducer } from 'redux-remember'; -import batchReducer from 'features/batch/store/batchSlice'; import canvasReducer from 'features/canvas/store/canvasSlice'; import controlNetReducer from 'features/controlNet/store/controlNetSlice'; import dynamicPromptsReducer from 'features/dynamicPrompts/store/slice'; import boardsReducer from 'features/gallery/store/boardSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; import imageDeletionReducer from 'features/imageDeletion/store/imageDeletionSlice'; -import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import loraReducer from 'features/lora/store/loraSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; import generationReducer from 'features/parameters/store/generationSlice'; @@ -40,7 +38,6 @@ const allReducers = { canvas: canvasReducer, gallery: galleryReducer, generation: generationReducer, - lightbox: lightboxReducer, nodes: nodesReducer, postprocessing: postprocessingReducer, system: systemReducer, @@ -50,7 +47,6 @@ const allReducers = { controlNet: controlNetReducer, boards: boardsReducer, dynamicPrompts: dynamicPromptsReducer, - batch: batchReducer, imageDeletion: imageDeletionReducer, lora: loraReducer, [api.reducerPath]: api.reducer, @@ -64,18 +60,13 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'canvas', 'gallery', 'generation', - 'lightbox', 'nodes', 'postprocessing', 'system', 'ui', 'controlNet', 'dynamicPrompts', - 'batch', 'lora', - // 'boards', - // 'hotkeys', - // 'config', ]; export const store = configureStore({ @@ -101,10 +92,26 @@ export const store = configureStore({ .concat(dynamicMiddlewares) .prepend(listenerMiddleware.middleware), devTools: { - actionsDenylist, actionSanitizer, stateSanitizer, trace: true, + predicate: (state, action) => { + // TODO: hook up to the log level param in system slice + // manually type state, cannot type the arg + // const typedState = state as ReturnType; + + if (action.type.startsWith('api/')) { + // don't log api actions, with manual cache updates they are extremely noisy + return false; + } + + if (actionsDenylist.includes(action.type)) { + // don't log other noisy actions + return false; + } + + return true; + }, }, }); diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 229761dabb..40b8c1c73a 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -94,7 +94,8 @@ export type AppFeature = | 'bugLink' | 'localization' | 'consoleLogging' - | 'dynamicPrompting'; + | 'dynamicPrompting' + | 'batches'; /** * A disable-able Stable Diffusion feature diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 959a70bc29..59a1d281fe 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -6,30 +6,21 @@ import { useColorMode, useColorModeValue, } from '@chakra-ui/react'; -import { useCombinedRefs } from '@dnd-kit/utilities'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { - IAILoadingImageFallback, - IAINoContentFallback, -} from 'common/components/IAIImageFallback'; -import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; -import { AnimatePresence } from 'framer-motion'; -import { MouseEvent, ReactElement, SyntheticEvent } from 'react'; -import { memo, useRef } from 'react'; -import { FaImage, FaUndo, FaUpload } from 'react-icons/fa'; -import { ImageDTO } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; -import IAIDropOverlay from './IAIDropOverlay'; -import { PostUploadAction } from 'services/api/thunks/image'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { mode } from 'theme/util/mode'; import { TypesafeDraggableData, TypesafeDroppableData, - isValidDrop, - useDraggable, - useDroppable, } from 'app/components/ImageDnd/typesafeDnd'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; +import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; +import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react'; +import { FaImage, FaUndo, FaUpload } from 'react-icons/fa'; +import { PostUploadAction } from 'services/api/thunks/image'; +import { ImageDTO } from 'services/api/types'; +import { mode } from 'theme/util/mode'; +import IAIDraggable from './IAIDraggable'; +import IAIDroppable from './IAIDroppable'; type IAIDndImageProps = { imageDTO: ImageDTO | undefined; @@ -83,28 +74,6 @@ const IAIDndImage = (props: IAIDndImageProps) => { const { colorMode } = useColorMode(); - const dndId = useRef(uuidv4()); - - const { - attributes, - listeners, - setNodeRef: setDraggableRef, - isDragging, - active, - } = useDraggable({ - id: dndId.current, - disabled: isDragDisabled || !imageDTO, - data: draggableData, - }); - - const { isOver, setNodeRef: setDroppableRef } = useDroppable({ - id: dndId.current, - disabled: isDropDisabled, - data: droppableData, - }); - - const setDndRef = useCombinedRefs(setDroppableRef, setDraggableRef); - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ postUploadAction, isDisabled: isUploadDisabled, @@ -139,9 +108,6 @@ const IAIDndImage = (props: IAIDndImageProps) => { userSelect: 'none', cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer', }} - {...attributes} - {...listeners} - ref={setDndRef} > {imageDTO && ( { }} > } + // If we fall back to thumbnail, it feels much snappier than the skeleton... + fallbackSrc={imageDTO.thumbnail_url} + // fallback={} + width={imageDTO.width} + height={imageDTO.height} onError={onError} draggable={false} sx={{ @@ -171,30 +140,6 @@ const IAIDndImage = (props: IAIDndImageProps) => { }} /> {withMetadataOverlay && } - {onClickReset && withResetIcon && ( - - )} )} {!imageDTO && !isUploadDisabled && ( @@ -225,11 +170,42 @@ const IAIDndImage = (props: IAIDndImageProps) => { )} {!imageDTO && isUploadDisabled && noContentFallback} - - {isValidDrop(droppableData, active) && !isDragging && ( - - )} - + + {imageDTO && ( + + )} + {onClickReset && withResetIcon && imageDTO && ( + + )} ); }; diff --git a/invokeai/frontend/web/src/common/components/IAIDraggable.tsx b/invokeai/frontend/web/src/common/components/IAIDraggable.tsx new file mode 100644 index 0000000000..482a8ac604 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIDraggable.tsx @@ -0,0 +1,40 @@ +import { Box } from '@chakra-ui/react'; +import { + TypesafeDraggableData, + useDraggable, +} from 'app/components/ImageDnd/typesafeDnd'; +import { MouseEvent, memo, useRef } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +type IAIDraggableProps = { + disabled?: boolean; + data?: TypesafeDraggableData; + onClick?: (event: MouseEvent) => void; +}; + +const IAIDraggable = (props: IAIDraggableProps) => { + const { data, disabled, onClick } = props; + const dndId = useRef(uuidv4()); + + const { attributes, listeners, setNodeRef } = useDraggable({ + id: dndId.current, + disabled, + data, + }); + + return ( + + ); +}; + +export default memo(IAIDraggable); diff --git a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx new file mode 100644 index 0000000000..98093d04e4 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx @@ -0,0 +1,47 @@ +import { Box } from '@chakra-ui/react'; +import { + TypesafeDroppableData, + isValidDrop, + useDroppable, +} from 'app/components/ImageDnd/typesafeDnd'; +import { AnimatePresence } from 'framer-motion'; +import { memo, useRef } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import IAIDropOverlay from './IAIDropOverlay'; + +type IAIDroppableProps = { + dropLabel?: string; + disabled?: boolean; + data?: TypesafeDroppableData; +}; + +const IAIDroppable = (props: IAIDroppableProps) => { + const { dropLabel, data, disabled } = props; + const dndId = useRef(uuidv4()); + + const { isOver, setNodeRef, active } = useDroppable({ + id: dndId.current, + disabled, + data, + }); + + return ( + + + {isValidDrop(data, active) && ( + + )} + + + ); +}; + +export default memo(IAIDroppable); diff --git a/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx new file mode 100644 index 0000000000..2136acc3c3 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIErrorLoadingImageFallback.tsx @@ -0,0 +1,42 @@ +import { Box, Flex, Icon } from '@chakra-ui/react'; +import { FaExclamation } from 'react-icons/fa'; + +const IAIErrorLoadingImageFallback = () => { + return ( + + + + + + ); +}; + +export default IAIErrorLoadingImageFallback; diff --git a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx new file mode 100644 index 0000000000..a3c83cb734 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx @@ -0,0 +1,30 @@ +import { Box, Skeleton } from '@chakra-ui/react'; + +const IAIFillSkeleton = () => { + return ( + + + + ); +}; + +export default IAIFillSkeleton; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts index 605aa8b162..3b1476fb1f 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts @@ -3,36 +3,22 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { validateSeedWeights } from 'common/util/seedWeightPairs'; -import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { systemSelector } from 'features/system/store/systemSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { - modelsApi, - useGetMainModelsQuery, -} from '../../services/api/endpoints/models'; +import { modelsApi } from '../../services/api/endpoints/models'; const readinessSelector = createSelector( [stateSelector, activeTabNameSelector], (state, activeTabName) => { - const { generation, system, batch } = state; + const { generation, system } = state; const { shouldGenerateVariations, seedWeights, initialImage, seed } = generation; const { isProcessing, isConnected } = system; - const { - isEnabled: isBatchEnabled, - asInitialImage, - imageNames: batchImageNames, - } = batch; let isReady = true; const reasonsWhyNotReady: string[] = []; - if ( - activeTabName === 'img2img' && - !initialImage && - !(asInitialImage && batchImageNames.length > 1) - ) { + if (activeTabName === 'img2img' && !initialImage) { isReady = false; reasonsWhyNotReady.push('No initial image selected'); } diff --git a/invokeai/frontend/web/src/features/batch/components/BatchControlNet.tsx b/invokeai/frontend/web/src/features/batch/components/BatchControlNet.tsx deleted file mode 100644 index 4231c84bec..0000000000 --- a/invokeai/frontend/web/src/features/batch/components/BatchControlNet.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { - Flex, - FormControl, - FormLabel, - Heading, - Spacer, - Switch, - Text, -} from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAISwitch from 'common/components/IAISwitch'; -import { ControlNetConfig } from 'features/controlNet/store/controlNetSlice'; -import { ChangeEvent, memo, useCallback } from 'react'; -import { controlNetToggled } from '../store/batchSlice'; - -type Props = { - controlNet: ControlNetConfig; -}; - -const selector = createSelector( - [stateSelector, (state, controlNetId: string) => controlNetId], - (state, controlNetId) => { - const isControlNetEnabled = state.batch.controlNets.includes(controlNetId); - return { isControlNetEnabled }; - }, - defaultSelectorOptions -); - -const BatchControlNet = (props: Props) => { - const dispatch = useAppDispatch(); - const { isControlNetEnabled } = useAppSelector((state) => - selector(state, props.controlNet.controlNetId) - ); - const { processorType, model } = props.controlNet; - - const handleChangeAsControlNet = useCallback(() => { - dispatch(controlNetToggled(props.controlNet.controlNetId)); - }, [dispatch, props.controlNet.controlNetId]); - - return ( - - - - - ControlNet - - - - - - - Model: {model} - - - Processor: {processorType} - - - ); -}; - -export default memo(BatchControlNet); diff --git a/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx b/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx deleted file mode 100644 index 4a6250f93a..0000000000 --- a/invokeai/frontend/web/src/features/batch/components/BatchImage.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Box, Icon, Skeleton } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; -import { stateSelector } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIDndImage from 'common/components/IAIDndImage'; -import { - batchImageRangeEndSelected, - batchImageSelected, - batchImageSelectionToggled, - imageRemovedFromBatch, -} from 'features/batch/store/batchSlice'; -import { MouseEvent, memo, useCallback, useMemo } from 'react'; -import { FaExclamationCircle } from 'react-icons/fa'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const makeSelector = (image_name: string) => - createSelector( - [stateSelector], - (state) => ({ - selectionCount: state.batch.selection.length, - isSelected: state.batch.selection.includes(image_name), - }), - defaultSelectorOptions - ); - -type BatchImageProps = { - imageName: string; -}; - -const BatchImage = (props: BatchImageProps) => { - const { - currentData: imageDTO, - isFetching, - isError, - isSuccess, - } = useGetImageDTOQuery(props.imageName); - const dispatch = useAppDispatch(); - - const selector = useMemo( - () => makeSelector(props.imageName), - [props.imageName] - ); - - const { isSelected, selectionCount } = useAppSelector(selector); - - const handleClickRemove = useCallback(() => { - dispatch(imageRemovedFromBatch(props.imageName)); - }, [dispatch, props.imageName]); - - const handleClick = useCallback( - (e: MouseEvent) => { - if (e.shiftKey) { - dispatch(batchImageRangeEndSelected(props.imageName)); - } else if (e.ctrlKey || e.metaKey) { - dispatch(batchImageSelectionToggled(props.imageName)); - } else { - dispatch(batchImageSelected(props.imageName)); - } - }, - [dispatch, props.imageName] - ); - - const draggableData = useMemo(() => { - if (selectionCount > 1) { - return { - id: 'batch', - payloadType: 'BATCH_SELECTION', - }; - } - - if (imageDTO) { - return { - id: 'batch', - payloadType: 'IMAGE_DTO', - payload: { imageDTO }, - }; - } - }, [imageDTO, selectionCount]); - - if (isError) { - return ; - } - - if (isFetching) { - return ( - - - - ); - } - - return ( - - - - ); -}; - -export default memo(BatchImage); diff --git a/invokeai/frontend/web/src/features/batch/components/BatchImageContainer.tsx b/invokeai/frontend/web/src/features/batch/components/BatchImageContainer.tsx deleted file mode 100644 index 09e6b8afd7..0000000000 --- a/invokeai/frontend/web/src/features/batch/components/BatchImageContainer.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Box } from '@chakra-ui/react'; -import BatchImageGrid from './BatchImageGrid'; -import IAIDropOverlay from 'common/components/IAIDropOverlay'; -import { - AddToBatchDropData, - isValidDrop, - useDroppable, -} from 'app/components/ImageDnd/typesafeDnd'; - -const droppableData: AddToBatchDropData = { - id: 'batch', - actionType: 'ADD_TO_BATCH', -}; - -const BatchImageContainer = () => { - const { isOver, setNodeRef, active } = useDroppable({ - id: 'batch-manager', - data: droppableData, - }); - - return ( - - - {isValidDrop(droppableData, active) && ( - - )} - - ); -}; - -export default BatchImageContainer; diff --git a/invokeai/frontend/web/src/features/batch/components/BatchImageGrid.tsx b/invokeai/frontend/web/src/features/batch/components/BatchImageGrid.tsx deleted file mode 100644 index f61d27d4cf..0000000000 --- a/invokeai/frontend/web/src/features/batch/components/BatchImageGrid.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FaImages } from 'react-icons/fa'; -import { Grid, GridItem } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import BatchImage from './BatchImage'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; - -const selector = createSelector( - stateSelector, - (state) => { - const imageNames = state.batch.imageNames.concat().reverse(); - - return { imageNames }; - }, - defaultSelectorOptions -); - -const BatchImageGrid = () => { - const { imageNames } = useAppSelector(selector); - - if (imageNames.length === 0) { - return ( - - ); - } - - return ( - - {imageNames.map((imageName) => ( - - - - ))} - - ); -}; - -export default BatchImageGrid; diff --git a/invokeai/frontend/web/src/features/batch/components/BatchManager.tsx b/invokeai/frontend/web/src/features/batch/components/BatchManager.tsx deleted file mode 100644 index d7855dd4e2..0000000000 --- a/invokeai/frontend/web/src/features/batch/components/BatchManager.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Flex, Heading, Spacer } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCallback } from 'react'; -import IAISwitch from 'common/components/IAISwitch'; -import { - asInitialImageToggled, - batchReset, - isEnabledChanged, -} from 'features/batch/store/batchSlice'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIButton from 'common/components/IAIButton'; -import BatchImageContainer from './BatchImageGrid'; -import { map } from 'lodash-es'; -import BatchControlNet from './BatchControlNet'; - -const selector = createSelector( - stateSelector, - (state) => { - const { controlNets } = state.controlNet; - const { - imageNames, - asInitialImage, - controlNets: batchControlNets, - isEnabled, - } = state.batch; - - return { - imageCount: imageNames.length, - asInitialImage, - controlNets, - batchControlNets, - isEnabled, - }; - }, - defaultSelectorOptions -); - -const BatchManager = () => { - const dispatch = useAppDispatch(); - const { imageCount, isEnabled, controlNets, batchControlNets } = - useAppSelector(selector); - - const handleResetBatch = useCallback(() => { - dispatch(batchReset()); - }, [dispatch]); - - const handleToggle = useCallback(() => { - dispatch(isEnabledChanged(!isEnabled)); - }, [dispatch, isEnabled]); - - const handleChangeAsInitialImage = useCallback(() => { - dispatch(asInitialImageToggled()); - }, [dispatch]); - - return ( - - - - {imageCount || 'No'} images - - - Reset - - - - {map(controlNets, (controlNet) => { - return ( - - ); - })} - - - - ); -}; - -export default BatchManager; diff --git a/invokeai/frontend/web/src/features/batch/store/batchSlice.ts b/invokeai/frontend/web/src/features/batch/store/batchSlice.ts deleted file mode 100644 index 6a96361d3f..0000000000 --- a/invokeai/frontend/web/src/features/batch/store/batchSlice.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit'; -import { uniq } from 'lodash-es'; -import { imageDeleted } from 'services/api/thunks/image'; - -type BatchState = { - isEnabled: boolean; - imageNames: string[]; - asInitialImage: boolean; - controlNets: string[]; - selection: string[]; -}; - -export const initialBatchState: BatchState = { - isEnabled: false, - imageNames: [], - asInitialImage: false, - controlNets: [], - selection: [], -}; - -const batch = createSlice({ - name: 'batch', - initialState: initialBatchState, - reducers: { - isEnabledChanged: (state, action: PayloadAction) => { - state.isEnabled = action.payload; - }, - imageAddedToBatch: (state, action: PayloadAction) => { - state.imageNames = uniq(state.imageNames.concat(action.payload)); - }, - imagesAddedToBatch: (state, action: PayloadAction) => { - state.imageNames = uniq(state.imageNames.concat(action.payload)); - }, - imageRemovedFromBatch: (state, action: PayloadAction) => { - state.imageNames = state.imageNames.filter( - (imageName) => action.payload !== imageName - ); - state.selection = state.selection.filter( - (imageName) => action.payload !== imageName - ); - }, - imagesRemovedFromBatch: (state, action: PayloadAction) => { - state.imageNames = state.imageNames.filter( - (imageName) => !action.payload.includes(imageName) - ); - state.selection = state.selection.filter( - (imageName) => !action.payload.includes(imageName) - ); - }, - batchImageRangeEndSelected: (state, action: PayloadAction) => { - const rangeEndImageName = action.payload; - const lastSelectedImage = state.selection[state.selection.length - 1]; - const lastClickedIndex = state.imageNames.findIndex( - (n) => n === lastSelectedImage - ); - const currentClickedIndex = state.imageNames.findIndex( - (n) => n === 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 = state.imageNames.slice(start, end + 1); - state.selection = uniq(state.selection.concat(imagesToSelect)); - } - }, - batchImageSelectionToggled: (state, action: PayloadAction) => { - if ( - state.selection.includes(action.payload) && - state.selection.length > 1 - ) { - state.selection = state.selection.filter( - (imageName) => imageName !== action.payload - ); - } else { - state.selection = uniq(state.selection.concat(action.payload)); - } - }, - batchImageSelected: (state, action: PayloadAction) => { - state.selection = action.payload - ? [action.payload] - : [String(state.imageNames[0])]; - }, - batchReset: (state) => { - state.imageNames = []; - state.selection = []; - }, - asInitialImageToggled: (state) => { - state.asInitialImage = !state.asInitialImage; - }, - controlNetAddedToBatch: (state, action: PayloadAction) => { - state.controlNets = uniq(state.controlNets.concat(action.payload)); - }, - controlNetRemovedFromBatch: (state, action: PayloadAction) => { - state.controlNets = state.controlNets.filter( - (controlNetId) => controlNetId !== action.payload - ); - }, - controlNetToggled: (state, action: PayloadAction) => { - if (state.controlNets.includes(action.payload)) { - state.controlNets = state.controlNets.filter( - (controlNetId) => controlNetId !== action.payload - ); - } else { - state.controlNets = uniq(state.controlNets.concat(action.payload)); - } - }, - }, - extraReducers: (builder) => { - builder.addCase(imageDeleted.fulfilled, (state, action) => { - state.imageNames = state.imageNames.filter( - (imageName) => imageName !== action.meta.arg.image_name - ); - state.selection = state.selection.filter( - (imageName) => imageName !== action.meta.arg.image_name - ); - }); - }, -}); - -export const { - isEnabledChanged, - imageAddedToBatch, - imagesAddedToBatch, - imageRemovedFromBatch, - imagesRemovedFromBatch, - asInitialImageToggled, - controlNetAddedToBatch, - controlNetRemovedFromBatch, - batchReset, - controlNetToggled, - batchImageRangeEndSelected, - batchImageSelectionToggled, - batchImageSelected, -} = batch.actions; - -export default batch.reducer; - -export const selectionAddedToBatch = createAction( - 'batch/selectionAddedToBatch' -); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx deleted file mode 100644 index 918e9390f9..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Flex, useColorMode } from '@chakra-ui/react'; -import { FaImages } from 'react-icons/fa'; -import { boardIdSelected } from 'features/gallery/store/gallerySlice'; -import { useDispatch } from 'react-redux'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { AnimatePresence } from 'framer-motion'; -import IAIDropOverlay from 'common/components/IAIDropOverlay'; -import { mode } from 'theme/util/mode'; -import { - MoveBoardDropData, - isValidDrop, - useDroppable, -} from 'app/components/ImageDnd/typesafeDnd'; - -const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => { - const dispatch = useDispatch(); - const { colorMode } = useColorMode(); - - const handleAllImagesBoardClick = () => { - dispatch(boardIdSelected()); - }; - - const droppableData: MoveBoardDropData = { - id: 'all-images-board', - actionType: 'MOVE_BOARD', - context: { boardId: null }, - }; - - const { isOver, setNodeRef, active } = useDroppable({ - id: `board_droppable_all_images`, - data: droppableData, - }); - - return ( - - - - - {isValidDrop(droppableData, active) && ( - - )} - - - - All Images - - - ); -}; - -export default AllImagesBoard; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx rename to invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllImagesBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllImagesBoard.tsx new file mode 100644 index 0000000000..c14ae24483 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllImagesBoard.tsx @@ -0,0 +1,31 @@ +import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd'; +import { boardIdSelected } from 'features/gallery/store/gallerySlice'; +import { FaImages } from 'react-icons/fa'; +import { useDispatch } from 'react-redux'; +import GenericBoard from './GenericBoard'; + +const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => { + const dispatch = useDispatch(); + + const handleAllImagesBoardClick = () => { + dispatch(boardIdSelected('all')); + }; + + const droppableData: MoveBoardDropData = { + id: 'all-images-board', + actionType: 'MOVE_BOARD', + context: { boardId: null }, + }; + + return ( + + ); +}; + +export default AllImagesBoard; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BatchBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BatchBoard.tsx new file mode 100644 index 0000000000..816fba33c1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BatchBoard.tsx @@ -0,0 +1,42 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { AddToBatchDropData } from 'app/components/ImageDnd/typesafeDnd'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { boardIdSelected } from 'features/gallery/store/gallerySlice'; +import { useCallback } from 'react'; +import { FaLayerGroup } from 'react-icons/fa'; +import { useDispatch } from 'react-redux'; +import GenericBoard from './GenericBoard'; + +const selector = createSelector(stateSelector, (state) => { + return { + count: state.gallery.batchImageNames.length, + }; +}); + +const BatchBoard = ({ isSelected }: { isSelected: boolean }) => { + const dispatch = useDispatch(); + const { count } = useAppSelector(selector); + + const handleBatchBoardClick = useCallback(() => { + dispatch(boardIdSelected('batch')); + }, [dispatch]); + + const droppableData: AddToBatchDropData = { + id: 'batch-board', + actionType: 'ADD_TO_BATCH', + }; + + return ( + + ); +}; + +export default BatchBoard; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx similarity index 85% rename from invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx rename to invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx index 5618c5c5c2..b479c46fd9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx @@ -1,3 +1,4 @@ +import { CloseIcon } from '@chakra-ui/icons'; import { Collapse, Flex, @@ -9,17 +10,18 @@ import { InputRightElement, } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { setBoardSearchText } from 'features/gallery/store/boardSlice'; -import { memo, useState } from 'react'; -import HoverableBoard from './HoverableBoard'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { memo, useState } from 'react'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import AddBoardButton from './AddBoardButton'; import AllImagesBoard from './AllImagesBoard'; -import { CloseIcon } from '@chakra-ui/icons'; -import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; -import { stateSelector } from 'app/store/store'; +import BatchBoard from './BatchBoard'; +import GalleryBoard from './GalleryBoard'; +import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; const selector = createSelector( [stateSelector], @@ -42,6 +44,8 @@ const BoardsList = (props: Props) => { const { data: boards } = useListAllBoardsQuery(); + const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled; + const filteredBoards = searchText ? boards?.filter((board) => board.board_name.toLowerCase().includes(searchText.toLowerCase()) @@ -115,14 +119,21 @@ const BoardsList = (props: Props) => { }} > {!searchMode && ( - - - + <> + + + + {isBatchEnabled && ( + + + + )} + )} {filteredBoards && filteredBoards.map((board) => ( - diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx similarity index 76% rename from invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx rename to invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index 035ee77f18..c01113d38a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -12,35 +12,31 @@ import { } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { memo, useCallback, useContext } from 'react'; -import { FaFolder, FaTrash } from 'react-icons/fa'; import { ContextMenu } from 'chakra-ui-contextmenu'; -import { BoardDTO } from 'services/api/types'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { boardIdSelected } from 'features/gallery/store/gallerySlice'; +import { memo, useCallback, useContext, useMemo } from 'react'; +import { FaFolder, FaImages, FaTrash } from 'react-icons/fa'; import { useDeleteBoardMutation, useUpdateBoardMutation, } from 'services/api/endpoints/boards'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { BoardDTO } from 'services/api/types'; import { skipToken } from '@reduxjs/toolkit/dist/query'; -import { AnimatePresence } from 'framer-motion'; -import IAIDropOverlay from 'common/components/IAIDropOverlay'; -import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext'; +import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd'; +// import { boardAddedToBatch } from 'app/store/middleware/listenerMiddleware/listeners/addBoardToBatch'; +import IAIDroppable from 'common/components/IAIDroppable'; import { mode } from 'theme/util/mode'; -import { - MoveBoardDropData, - isValidDrop, - useDroppable, -} from 'app/components/ImageDnd/typesafeDnd'; +import { DeleteBoardImagesContext } from '../../../../../app/contexts/DeleteBoardImagesContext'; -interface HoverableBoardProps { +interface GalleryBoardProps { board: BoardDTO; isSelected: boolean; } -const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { +const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => { const dispatch = useAppDispatch(); const { currentData: coverImage } = useGetImageDTOQuery( @@ -71,21 +67,22 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { deleteBoard(board_id); }, [board_id, deleteBoard]); + const handleAddBoardToBatch = useCallback(() => { + // dispatch(boardAddedToBatch({ board_id })); + }, []); + const handleDeleteBoardAndImages = useCallback(() => { - console.log({ board }); onClickDeleteBoardImages(board); }, [board, onClickDeleteBoardImages]); - const droppableData: MoveBoardDropData = { - id: board_id, - actionType: 'MOVE_BOARD', - context: { boardId: board_id }, - }; - - const { isOver, setNodeRef, active } = useDroppable({ - id: `board_droppable_${board_id}`, - data: droppableData, - }); + const droppableData: MoveBoardDropData = useMemo( + () => ({ + id: board_id, + actionType: 'MOVE_BOARD', + context: { boardId: board_id }, + }), + [board_id] + ); return ( @@ -94,16 +91,25 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { renderMenu={() => ( {board.image_count > 0 && ( - } - onClickCapture={handleDeleteBoardAndImages} - > - Delete Board and Images - + <> + } + onClickCapture={handleAddBoardToBatch} + > + Add Board to Batch + + } + onClickCapture={handleDeleteBoardAndImages} + > + Delete Board and Images + + )} } onClickCapture={handleDeleteBoard} > @@ -127,7 +133,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { }} > { > {board.image_count} - - {isValidDrop(droppableData, active) && ( - - )} - + { ); }); -HoverableBoard.displayName = 'HoverableBoard'; +GalleryBoard.displayName = 'HoverableBoard'; -export default HoverableBoard; +export default GalleryBoard; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx new file mode 100644 index 0000000000..a300c1b18c --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx @@ -0,0 +1,83 @@ +import { As, Badge, Flex } from '@chakra-ui/react'; +import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd'; +import IAIDroppable from 'common/components/IAIDroppable'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; + +type GenericBoardProps = { + droppableData: TypesafeDroppableData; + onClick: () => void; + isSelected: boolean; + icon: As; + label: string; + badgeCount?: number; +}; + +const GenericBoard = (props: GenericBoardProps) => { + const { droppableData, onClick, isSelected, icon, label, badgeCount } = props; + + return ( + + + + + {badgeCount !== undefined && ( + {badgeCount} + )} + + + + + {label} + + + ); +}; + +export default GenericBoard; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx similarity index 88% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx rename to invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 4e227c4a7d..c01a00fafe 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -8,8 +8,6 @@ import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; -import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; - import { skipToken } from '@reduxjs/toolkit/dist/query'; import { useAppToaster } from 'app/components/Toaster'; import { stateSelector } from 'app/store/store'; @@ -36,7 +34,6 @@ import { FaCode, FaCopy, FaDownload, - FaExpand, FaExpandArrowsAlt, FaGrinStars, FaHourglassHalf, @@ -50,11 +47,11 @@ import { useGetImageMetadataQuery, } from 'services/api/endpoints/images'; import { useDebounce } from 'use-debounce'; -import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; +import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; const currentImageButtonsSelector = createSelector( [stateSelector, activeTabNameSelector], - ({ gallery, system, postprocessing, ui, lightbox }, activeTabName) => { + ({ gallery, system, postprocessing, ui }, activeTabName) => { const { isProcessing, isConnected, @@ -66,8 +63,6 @@ const currentImageButtonsSelector = createSelector( const { upscalingLevel, facetoolStrength } = postprocessing; - const { isLightboxOpen } = lightbox; - const { shouldShowImageDetails, shouldHidePreview, @@ -88,7 +83,6 @@ const currentImageButtonsSelector = createSelector( shouldDisableToolbarButtons: Boolean(progressImage) || !lastSelectedImage, shouldShowImageDetails, activeTabName, - isLightboxOpen, shouldHidePreview, shouldShowProgressInViewer, lastSelectedImage, @@ -114,14 +108,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { facetoolStrength, shouldDisableToolbarButtons, shouldShowImageDetails, - isLightboxOpen, activeTabName, - shouldHidePreview, lastSelectedImage, shouldShowProgressInViewer, } = useAppSelector(currentImageButtonsSelector); - const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled; @@ -149,30 +140,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const metadata = metadataData?.metadata; - // const handleCopyImage = useCallback(async () => { - // if (!image?.url) { - // return; - // } - - // const url = getUrl(image.url); - - // if (!url) { - // return; - // } - - // const blob = await fetch(url).then((res) => res.blob()); - // const data = [new ClipboardItem({ [blob.type]: blob })]; - - // await navigator.clipboard.write(data); - - // toast({ - // title: t('toast.imageCopied'), - // status: 'success', - // duration: 2500, - // isClosable: true, - // }); - // }, [getUrl, t, image?.url, toast]); - const handleCopyImageLink = useCallback(() => { const getImageUrl = () => { if (!image) { @@ -318,7 +285,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const handleSendToCanvas = useCallback(() => { if (!image) return; dispatch(sentImageToCanvas()); - if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); dispatch(setInitialCanvasImage(image)); dispatch(requestCanvasRescale()); @@ -333,7 +299,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { duration: 2500, isClosable: true, }); - }, [image, isLightboxOpen, dispatch, activeTabName, toaster, t]); + }, [image, dispatch, activeTabName, toaster, t]); useHotkeys( 'i', @@ -356,10 +322,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); }, [dispatch, shouldShowProgressInViewer]); - const handleLightBox = useCallback(() => { - dispatch(setIsLightboxOpen(!isLightboxOpen)); - }, [dispatch, isLightboxOpen]); - return ( <> { - {isLightboxEnabled && ( - } - tooltip={ - !isLightboxOpen - ? `${t('parameters.openInViewer')} (Z)` - : `${t('parameters.closeViewer')} (Z)` - } - aria-label={ - !isLightboxOpen - ? `${t('parameters.openInViewer')} (Z)` - : `${t('parameters.closeViewer')} (Z)` - } - isChecked={isLightboxOpen} - onClick={handleLightBox} - isDisabled={shouldDisableToolbarButtons} - /> - )} diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx similarity index 100% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx rename to invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageHidden.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx similarity index 100% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImageHidden.tsx rename to invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageHidden.tsx diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx similarity index 70% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx rename to invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx index 9ef12871bb..e143a87fc9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx @@ -8,14 +8,15 @@ import { import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; -import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice'; +import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage'; +import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; +import { AnimatePresence, motion } from 'framer-motion'; import { isEqual } from 'lodash-es'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import { useNextPrevImage } from '../hooks/useNextPrevImage'; -import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; -import NextPrevImageButtons from './NextPrevImageButtons'; +import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer'; +import NextPrevImageButtons from '../NextPrevImageButtons'; export const imagesSelector = createSelector( [stateSelector, selectLastSelectedImage], @@ -115,8 +116,27 @@ const CurrentImagePreview = () => { [] ); + // Show and hide the next/prev buttons on mouse move + const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = + useState(false); + + const timeoutId = useRef(0); + + const handleMouseOver = useCallback(() => { + setShouldShowNextPrevButtons(true); + window.clearTimeout(timeoutId.current); + }, []); + + const handleMouseOut = useCallback(() => { + timeoutId.current = window.setTimeout(() => { + setShouldShowNextPrevButtons(false); + }, 500); + }, []); + return ( { )} - {!shouldShowImageDetails && imageDTO && ( - - - - )} + + {!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && ( + + + + )} + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx index 9ab8ccb5c9..2aa44e50a1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx @@ -5,32 +5,23 @@ import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice import { clamp, isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; -import './ImageGallery.css'; -import ImageGalleryContent from './ImageGalleryContent'; -import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer'; -import { setShouldShowGallery } from 'features/ui/store/uiSlice'; import { createSelector } from '@reduxjs/toolkit'; +import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer'; import { activeTabNameSelector, uiSelector, } from 'features/ui/store/uiSelectors'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; +import { setShouldShowGallery } from 'features/ui/store/uiSlice'; import { memo } from 'react'; +import ImageGalleryContent from './ImageGalleryContent'; const selector = createSelector( - [ - activeTabNameSelector, - uiSelector, - gallerySelector, - isStagingSelector, - lightboxSelector, - ], - (activeTabName, ui, gallery, isStaging, lightbox) => { + [activeTabNameSelector, uiSelector, gallerySelector, isStagingSelector], + (activeTabName, ui, gallery, isStaging) => { const { shouldPinGallery, shouldShowGallery } = ui; const { galleryImageMinimumWidth } = gallery; - const { isLightboxOpen } = lightbox; return { activeTabName, @@ -39,7 +30,6 @@ const selector = createSelector( shouldShowGallery, galleryImageMinimumWidth, isResizable: activeTabName !== 'unifiedCanvas', - isLightboxOpen, }; }, { @@ -58,7 +48,6 @@ const GalleryDrawer = () => { // activeTabName, // isStaging, // isResizable, - // isLightboxOpen, } = useAppSelector(selector); const handleCloseGallery = () => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx new file mode 100644 index 0000000000..44fa964596 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx @@ -0,0 +1,52 @@ +import { MenuList } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; +import { memo, useMemo } from 'react'; +import { ImageDTO } from 'services/api/types'; +import MultipleSelectionMenuItems from './MultipleSelectionMenuItems'; +import SingleSelectionMenuItems from './SingleSelectionMenuItems'; + +type Props = { + imageDTO: ImageDTO; + children: ContextMenuProps['children']; +}; + +const ImageContextMenu = ({ imageDTO, children }: Props) => { + const selector = useMemo( + () => + createSelector( + [stateSelector], + ({ gallery }) => { + const selectionCount = gallery.selection.length; + + return { selectionCount }; + }, + defaultSelectorOptions + ), + [] + ); + + const { selectionCount } = useAppSelector(selector); + + return ( + + menuProps={{ size: 'sm', isLazy: true }} + renderMenu={() => ( + + {selectionCount === 1 ? ( + + ) : ( + + )} + + )} + > + {children} + + ); +}; + +export default memo(ImageContextMenu); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx new file mode 100644 index 0000000000..62d2cb06f4 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx @@ -0,0 +1,40 @@ +import { MenuItem } from '@chakra-ui/react'; +import { useCallback } from 'react'; +import { FaFolder, FaFolderPlus, FaTrash } from 'react-icons/fa'; + +const MultipleSelectionMenuItems = () => { + const handleAddSelectionToBoard = useCallback(() => { + // TODO: add selection to board + }, []); + + const handleDeleteSelection = useCallback(() => { + // TODO: delete all selected images + }, []); + + const handleAddSelectionToBatch = useCallback(() => { + // TODO: add selection to batch + }, []); + + return ( + <> + } onClickCapture={handleAddSelectionToBoard}> + Move Selection to Board + + } + onClickCapture={handleAddSelectionToBatch} + > + Add Selection to Batch + + } + onClickCapture={handleDeleteSelection} + > + Delete Selection + + + ); +}; + +export default MultipleSelectionMenuItems; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx similarity index 56% rename from invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 92da141054..fda984e2c3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,16 +1,15 @@ import { ExternalLinkIcon } from '@chakra-ui/icons'; -import { MenuItem, MenuList } from '@chakra-ui/react'; +import { MenuItem } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppToaster } from 'app/components/Toaster'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; -import { imagesAddedToBatch } from 'features/batch/store/batchSlice'; import { resizeAndScaleCanvas, setInitialCanvasImage, } from 'features/canvas/store/canvasSlice'; +import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice'; import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; @@ -18,109 +17,35 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa'; +import { FaFolder, FaShare, FaTrash } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; -import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext'; -import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; +import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext'; +import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; -type Props = { - image: ImageDTO; - children: ContextMenuProps['children']; +type SingleSelectionMenuItemsProps = { + imageDTO: ImageDTO; }; -const ImageContextMenu = ({ image, children }: Props) => { +const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { + const { imageDTO } = props; + const selector = useMemo( () => createSelector( [stateSelector], ({ gallery }) => { - const selectionCount = gallery.selection.length; - - return { selectionCount }; - }, - defaultSelectorOptions - ), - [] - ); - const { selectionCount } = useAppSelector(selector); - const dispatch = useAppDispatch(); - - const { onClickAddToBoard } = useContext(AddImageToBoardContext); - - const handleDelete = useCallback(() => { - if (!image) { - return; - } - dispatch(imageToDeleteSelected(image)); - }, [dispatch, image]); - - const handleAddToBoard = useCallback(() => { - onClickAddToBoard(image); - }, [image, onClickAddToBoard]); - - return ( - - menuProps={{ size: 'sm', isLazy: true }} - renderMenu={() => ( - - {selectionCount === 1 ? ( - - ) : ( - <> - } - onClickCapture={handleAddToBoard} - > - Move Selection to Board - - {/* } - onClickCapture={handleAddSelectionToBatch} - > - Add Selection to Batch - */} - } - onClickCapture={handleDelete} - > - Delete Selection - - - )} - - )} - > - {children} - - ); -}; - -export default memo(ImageContextMenu); - -type SingleSelectionMenuItemsProps = { - image: ImageDTO; -}; - -const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { - const { image } = props; - - const selector = useMemo( - () => - createSelector( - [stateSelector], - ({ batch }) => { - const isInBatch = batch.imageNames.includes(image.image_name); + const isInBatch = gallery.batchImageNames.includes( + imageDTO.image_name + ); return { isInBatch }; }, defaultSelectorOptions ), - [image.image_name] + [imageDTO.image_name] ); const { isInBatch } = useAppSelector(selector); @@ -129,21 +54,21 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const toaster = useAppToaster(); - const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; + const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled; const { onClickAddToBoard } = useContext(AddImageToBoardContext); - const { currentData } = useGetImageMetadataQuery(image.image_name); + const { currentData } = useGetImageMetadataQuery(imageDTO.image_name); const metadata = currentData?.metadata; const handleDelete = useCallback(() => { - if (!image) { + if (!imageDTO) { return; } - dispatch(imageToDeleteSelected(image)); - }, [dispatch, image]); + dispatch(imageToDeleteSelected(imageDTO)); + }, [dispatch, imageDTO]); const { recallBothPrompts, recallSeed, recallAllParameters } = useRecallParameters(); @@ -161,12 +86,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToImageToImage = useCallback(() => { dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(image)); - }, [dispatch, image]); + dispatch(initialImageSelected(imageDTO)); + }, [dispatch, imageDTO]); - const handleSendToCanvas = () => { + const handleSendToCanvas = useCallback(() => { dispatch(sentImageToCanvas()); - dispatch(setInitialCanvasImage(image)); + dispatch(setInitialCanvasImage(imageDTO)); dispatch(resizeAndScaleCanvas()); dispatch(setActiveTab('unifiedCanvas')); @@ -176,47 +101,40 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { duration: 2500, isClosable: true, }); - }; + }, [dispatch, imageDTO, t, toaster]); const handleUseAllParameters = useCallback(() => { console.log(metadata); recallAllParameters(metadata); }, [metadata, recallAllParameters]); - const handleLightBox = () => { - // dispatch(setCurrentImage(image)); - // dispatch(setIsLightboxOpen(true)); - }; - const handleAddToBoard = useCallback(() => { - onClickAddToBoard(image); - }, [image, onClickAddToBoard]); + onClickAddToBoard(imageDTO); + }, [imageDTO, onClickAddToBoard]); const handleRemoveFromBoard = useCallback(() => { - if (!image.board_id) { + if (!imageDTO.board_id) { return; } - removeFromBoard({ board_id: image.board_id, image_name: image.image_name }); - }, [image.board_id, image.image_name, removeFromBoard]); + removeFromBoard({ + board_id: imageDTO.board_id, + image_name: imageDTO.image_name, + }); + }, [imageDTO.board_id, imageDTO.image_name, removeFromBoard]); - const handleOpenInNewTab = () => { - window.open(image.image_url, '_blank'); - }; + const handleOpenInNewTab = useCallback(() => { + window.open(imageDTO.image_url, '_blank'); + }, [imageDTO.image_url]); const handleAddToBatch = useCallback(() => { - dispatch(imagesAddedToBatch([image.image_name])); - }, [dispatch, image.image_name]); + dispatch(imagesAddedToBatch([imageDTO.image_name])); + }, [dispatch, imageDTO.image_name]); return ( <> } onClickCapture={handleOpenInNewTab}> {t('common.openInNewTab')} - {isLightboxEnabled && ( - } onClickCapture={handleLightBox}> - {t('parameters.openInViewer')} - - )} } onClickCapture={handleRecallPrompt} @@ -258,17 +176,19 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.sendToUnifiedCanvas')} )} - } - isDisabled={isInBatch} - onClickCapture={handleAddToBatch} - > - Add to Batch - + {isBatchEnabled && ( + } + isDisabled={isInBatch} + onClickCapture={handleAddToBatch} + > + Add to Batch + + )} } onClickCapture={handleAddToBoard}> - {image.board_id ? 'Change Board' : 'Add to Board'} + {imageDTO.board_id ? 'Change Board' : 'Add to Board'} - {image.board_id && ( + {imageDTO.board_id && ( } onClickCapture={handleRemoveFromBoard}> Remove from Board @@ -283,3 +203,5 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { ); }; + +export default memo(SingleSelectionMenuItems); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGallery.css b/invokeai/frontend/web/src/features/gallery/components/ImageGallery.css deleted file mode 100644 index 559248dd0f..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGallery.css +++ /dev/null @@ -1,35 +0,0 @@ -.ltr-image-gallery-css-transition-enter { - transform: translateX(150%); -} - -.ltr-image-gallery-css-transition-enter-active { - transform: translateX(0); - transition: all 120ms ease-out; -} - -.ltr-image-gallery-css-transition-exit { - transform: translateX(0); -} - -.ltr-image-gallery-css-transition-exit-active { - transform: translateX(150%); - transition: all 120ms ease-out; -} - -.rtl-image-gallery-css-transition-enter { - transform: translateX(-150%); -} - -.rtl-image-gallery-css-transition-enter-active { - transform: translateX(0); - transition: all 120ms ease-out; -} - -.rtl-image-gallery-css-transition-exit { - transform: translateX(0); -} - -.rtl-image-gallery-css-transition-exit-active { - transform: translateX(-150%); - transition: all 120ms ease-out; -} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 19d48ea910..8badad942e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -19,7 +19,7 @@ import { } from 'features/gallery/store/gallerySlice'; import { togglePinGalleryPanel } from 'features/ui/store/uiSlice'; -import { ChangeEvent, memo, useCallback, useRef } from 'react'; +import { ChangeEvent, memo, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { FaImage, FaServer, FaWrench } from 'react-icons/fa'; @@ -29,16 +29,12 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { - ASSETS_CATEGORIES, - IMAGE_CATEGORIES, - imageCategoriesChanged, - shouldAutoSwitchChanged, -} from 'features/gallery/store/gallerySlice'; +import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { mode } from 'theme/util/mode'; -import BoardsList from './Boards/BoardsList'; -import ImageGalleryGrid from './ImageGalleryGrid'; +import BoardsList from './Boards/BoardsList/BoardsList'; +import BatchImageGrid from './ImageGrid/BatchImageGrid'; +import GalleryImageGrid from './ImageGrid/GalleryImageGrid'; const selector = createSelector( [stateSelector], @@ -66,6 +62,7 @@ const ImageGalleryContent = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const resizeObserverRef = useRef(null); + const galleryGridRef = useRef(null); const { colorMode } = useColorMode(); @@ -83,6 +80,16 @@ const ImageGalleryContent = () => { }), }); + const boardTitle = useMemo(() => { + if (selectedBoardId === 'batch') { + return 'Batch'; + } + if (selectedBoard) { + return selectedBoard.board_name; + } + return 'All Images'; + }, [selectedBoard, selectedBoardId]); + const { isOpen: isBoardListOpen, onToggle } = useDisclosure(); const handleChangeGalleryImageMinimumWidth = (v: number) => { @@ -95,12 +102,10 @@ const ImageGalleryContent = () => { }; const handleClickImagesCategory = useCallback(() => { - dispatch(imageCategoriesChanged(IMAGE_CATEGORIES)); dispatch(setGalleryView('images')); }, [dispatch]); const handleClickAssetsCategory = useCallback(() => { - dispatch(imageCategoriesChanged(ASSETS_CATEGORIES)); dispatch(setGalleryView('assets')); }, [dispatch]); @@ -163,7 +168,7 @@ const ImageGalleryContent = () => { fontWeight: 600, }} > - {selectedBoard ? selectedBoard.board_name : 'All Images'} + {boardTitle} { - - + + {selectedBoardId === 'batch' ? ( + + ) : ( + + )} ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx deleted file mode 100644 index c7d4e5f0f8..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { - Box, - Flex, - FlexProps, - Grid, - Skeleton, - Spinner, - forwardRef, -} from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIButton from 'common/components/IAIButton'; -import { IMAGE_LIMIT } from 'features/gallery/store/gallerySlice'; -import { useOverlayScrollbars } from 'overlayscrollbars-react'; - -import { - PropsWithChildren, - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import { FaImage } from 'react-icons/fa'; -import GalleryImage from './GalleryImage'; - -import { createSelector } from '@reduxjs/toolkit'; -import { RootState, stateSelector } from 'app/store/store'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { selectFilteredImages } from 'features/gallery/store/gallerySlice'; -import { VirtuosoGrid } from 'react-virtuoso'; -import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; -import { receivedPageOfImages } from 'services/api/thunks/image'; -import { ImageDTO } from 'services/api/types'; - -const selector = createSelector( - [stateSelector, selectFilteredImages], - (state, filteredImages) => { - const { - categories, - total: allImagesTotal, - isLoading, - isFetching, - selectedBoardId, - } = state.gallery; - - let images = filteredImages as (ImageDTO | 'loading')[]; - - if (!isLoading && isFetching) { - // loading, not not the initial load - images = images.concat(Array(IMAGE_LIMIT).fill('loading')); - } - - return { - images, - allImagesTotal, - isLoading, - isFetching, - categories, - selectedBoardId, - }; - }, - defaultSelectorOptions -); - -const ImageGalleryGrid = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const rootRef = useRef(null); - const [scroller, setScroller] = useState(null); - const [initialize, osInstance] = useOverlayScrollbars({ - defer: true, - options: { - scrollbars: { - visibility: 'auto', - autoHide: 'leave', - autoHideDelay: 1300, - theme: 'os-theme-dark', - }, - overflow: { x: 'hidden' }, - }, - }); - - const { - images, - isLoading, - isFetching, - allImagesTotal, - categories, - selectedBoardId, - } = useAppSelector(selector); - - const { selectedBoard } = useListAllBoardsQuery(undefined, { - selectFromResult: ({ data }) => ({ - selectedBoard: data?.find((b) => b.board_id === selectedBoardId), - }), - }); - - const filteredImagesTotal = useMemo( - () => selectedBoard?.image_count ?? allImagesTotal, - [allImagesTotal, selectedBoard?.image_count] - ); - - const areMoreAvailable = useMemo(() => { - return images.length < filteredImagesTotal; - }, [images.length, filteredImagesTotal]); - - const handleLoadMoreImages = useCallback(() => { - dispatch( - receivedPageOfImages({ - categories, - board_id: selectedBoardId, - is_intermediate: false, - }) - ); - }, [categories, dispatch, selectedBoardId]); - - const handleEndReached = useMemo(() => { - if (areMoreAvailable && !isLoading) { - return handleLoadMoreImages; - } - return undefined; - }, [areMoreAvailable, handleLoadMoreImages, isLoading]); - - useEffect(() => { - const { current: root } = rootRef; - if (scroller && root) { - initialize({ - target: root, - elements: { - viewport: scroller, - }, - }); - } - return () => osInstance()?.destroy(); - }, [scroller, initialize, osInstance]); - - if (isLoading) { - return ( - - - - ); - } - - if (images.length) { - return ( - <> - - - typeof item === 'string' ? ( - - ) : ( - - ) - } - /> - - - {areMoreAvailable - ? t('gallery.loadMore') - : t('gallery.allImagesLoaded')} - - - ); - } - - return ( - - ); -}; - -type ItemContainerProps = PropsWithChildren & FlexProps; -const ItemContainer = forwardRef((props: ItemContainerProps, ref) => ( - - {props.children} - -)); - -type ListContainerProps = PropsWithChildren & FlexProps; -const ListContainer = forwardRef((props: ListContainerProps, ref) => { - const galleryImageMinimumWidth = useAppSelector( - (state: RootState) => state.gallery.galleryImageMinimumWidth - ); - - return ( - - {props.children} - - ); -}); - -export default memo(ImageGalleryGrid); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImage.tsx new file mode 100644 index 0000000000..a918682ccd --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImage.tsx @@ -0,0 +1,128 @@ +import { Box } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback'; +import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; +import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; +import { + imageRangeEndSelected, + imageSelected, + imageSelectionToggled, + imagesRemovedFromBatch, +} from 'features/gallery/store/gallerySlice'; +import { MouseEvent, memo, useCallback, useMemo } from 'react'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +const makeSelector = (image_name: string) => + createSelector( + [stateSelector], + (state) => ({ + selectionCount: state.gallery.selection.length, + selection: state.gallery.selection, + isSelected: state.gallery.selection.includes(image_name), + }), + defaultSelectorOptions + ); + +type BatchImageProps = { + imageName: string; +}; + +const BatchImage = (props: BatchImageProps) => { + const dispatch = useAppDispatch(); + const { imageName } = props; + const { + currentData: imageDTO, + isLoading, + isError, + isSuccess, + } = useGetImageDTOQuery(imageName); + const selector = useMemo(() => makeSelector(imageName), [imageName]); + + const { isSelected, selectionCount, selection } = useAppSelector(selector); + + const handleClickRemove = useCallback(() => { + dispatch(imagesRemovedFromBatch([imageName])); + }, [dispatch, imageName]); + + const handleClick = useCallback( + (e: MouseEvent) => { + if (e.shiftKey) { + dispatch(imageRangeEndSelected(imageName)); + } else if (e.ctrlKey || e.metaKey) { + dispatch(imageSelectionToggled(imageName)); + } else { + dispatch(imageSelected(imageName)); + } + }, + [dispatch, imageName] + ); + + const draggableData = useMemo(() => { + if (selectionCount > 1) { + return { + id: 'batch', + payloadType: 'IMAGE_NAMES', + payload: { image_names: selection }, + }; + } + + if (imageDTO) { + return { + id: 'batch', + payloadType: 'IMAGE_DTO', + payload: { imageDTO }, + }; + } + }, [imageDTO, selection, selectionCount]); + + if (isLoading) { + return ; + } + + if (isError || !imageDTO) { + return ; + } + + return ( + + + {(ref) => ( + + + + )} + + + ); +}; + +export default memo(BatchImage); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImageGrid.tsx new file mode 100644 index 0000000000..feaa47403d --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/BatchImageGrid.tsx @@ -0,0 +1,87 @@ +import { Box } from '@chakra-ui/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useOverlayScrollbars } from 'overlayscrollbars-react'; + +import { memo, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaImage } from 'react-icons/fa'; + +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { VirtuosoGrid } from 'react-virtuoso'; +import BatchImage from './BatchImage'; +import ItemContainer from './ImageGridItemContainer'; +import ListContainer from './ImageGridListContainer'; + +const selector = createSelector( + [stateSelector], + (state) => { + return { + imageNames: state.gallery.batchImageNames, + }; + }, + defaultSelectorOptions +); + +const BatchImageGrid = () => { + const { t } = useTranslation(); + const rootRef = useRef(null); + const [scroller, setScroller] = useState(null); + const [initialize, osInstance] = useOverlayScrollbars({ + defer: true, + options: { + scrollbars: { + visibility: 'auto', + autoHide: 'leave', + autoHideDelay: 1300, + theme: 'os-theme-dark', + }, + overflow: { x: 'hidden' }, + }, + }); + + const { imageNames } = useAppSelector(selector); + + useEffect(() => { + const { current: root } = rootRef; + if (scroller && root) { + initialize({ + target: root, + elements: { + viewport: scroller, + }, + }); + } + return () => osInstance()?.destroy(); + }, [scroller, initialize, osInstance]); + + if (imageNames.length) { + return ( + + ( + + )} + /> + + ); + } + + return ( + + ); +}; + +export default memo(BatchImageGrid); diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx similarity index 64% rename from invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 468db558b3..eb7428bb69 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -1,69 +1,57 @@ -import { Box } from '@chakra-ui/react'; +import { Box, Spinner } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIDndImage from 'common/components/IAIDndImage'; -import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; -import { MouseEvent, memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { FaTrash } from 'react-icons/fa'; -import { ImageDTO } from 'services/api/types'; +import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { imageRangeEndSelected, imageSelected, imageSelectionToggled, -} from '../store/gallerySlice'; -import ImageContextMenu from './ImageContextMenu'; +} from 'features/gallery/store/gallerySlice'; +import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; +import { MouseEvent, memo, useCallback, useMemo } from 'react'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; export const makeSelector = (image_name: string) => createSelector( [stateSelector], - ({ gallery }) => { - const isSelected = gallery.selection.includes(image_name); - const selectionCount = gallery.selection.length; - - return { - isSelected, - selectionCount, - }; - }, + ({ gallery }) => ({ + isSelected: gallery.selection.includes(image_name), + selectionCount: gallery.selection.length, + selection: gallery.selection, + }), defaultSelectorOptions ); interface HoverableImageProps { - imageDTO: ImageDTO; + imageName: string; } -/** - * Gallery image component with delete/use all/use seed buttons on hover. - */ const GalleryImage = (props: HoverableImageProps) => { - const { imageDTO } = props; - const { image_url, thumbnail_url, image_name } = imageDTO; - - const localSelector = useMemo(() => makeSelector(image_name), [image_name]); - - const { isSelected, selectionCount } = useAppSelector(localSelector); - const dispatch = useAppDispatch(); + const { imageName } = props; + const { currentData: imageDTO } = useGetImageDTOQuery(imageName); + const localSelector = useMemo(() => makeSelector(imageName), [imageName]); - const { t } = useTranslation(); + const { isSelected, selectionCount, selection } = + useAppSelector(localSelector); const handleClick = useCallback( (e: MouseEvent) => { - // multiselect disabled for now + // disable multiselect for now // if (e.shiftKey) { - // dispatch(imageRangeEndSelected(props.imageDTO.image_name)); + // dispatch(imageRangeEndSelected(imageName)); // } else if (e.ctrlKey || e.metaKey) { - // dispatch(imageSelectionToggled(props.imageDTO.image_name)); + // dispatch(imageSelectionToggled(imageName)); // } else { - // dispatch(imageSelected(props.imageDTO.image_name)); + // dispatch(imageSelected(imageName)); // } - dispatch(imageSelected(props.imageDTO.image_name)); + dispatch(imageSelected(imageName)); }, - [dispatch, props.imageDTO.image_name] + [dispatch, imageName] ); const handleDelete = useCallback( @@ -81,7 +69,8 @@ const GalleryImage = (props: HoverableImageProps) => { if (selectionCount > 1) { return { id: 'gallery-image', - payloadType: 'GALLERY_SELECTION', + payloadType: 'IMAGE_NAMES', + payload: { image_names: selection }, }; } @@ -92,15 +81,19 @@ const GalleryImage = (props: HoverableImageProps) => { payload: { imageDTO }, }; } - }, [imageDTO, selectionCount]); + }, [imageDTO, selection, selectionCount]); + + if (!imageDTO) { + return ; + } return ( - + {(ref) => ( { isSelected={isSelected} minSize={0} onClickReset={handleDelete} - resetIcon={} - resetTooltip="Delete image" imageSx={{ w: 'full', h: 'full' }} - // withResetIcon // removed bc it's too easy to accidentally delete images isDropDisabled={true} isUploadDisabled={true} thumbnail={true} + // resetIcon={} + // resetTooltip="Delete image" + // withResetIcon // removed bc it's too easy to accidentally delete images /> )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx new file mode 100644 index 0000000000..858eeedaa3 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx @@ -0,0 +1,204 @@ +import { Box } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import { useOverlayScrollbars } from 'overlayscrollbars-react'; + +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaImage } from 'react-icons/fa'; +import GalleryImage from './GalleryImage'; + +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { + ASSETS_CATEGORIES, + IMAGE_CATEGORIES, + IMAGE_LIMIT, + selectImagesAll, +} from 'features/gallery//store/gallerySlice'; +import { selectFilteredImages } from 'features/gallery/store/gallerySelectors'; +import { VirtuosoGrid } from 'react-virtuoso'; +import { receivedPageOfImages } from 'services/api/thunks/image'; +import ImageGridItemContainer from './ImageGridItemContainer'; +import ImageGridListContainer from './ImageGridListContainer'; +import { useListBoardImagesQuery } from '../../../../services/api/endpoints/boardImages'; + +const selector = createSelector( + [stateSelector, selectFilteredImages], + (state, filteredImages) => { + const { + galleryImageMinimumWidth, + selectedBoardId, + galleryView, + total, + isLoading, + } = state.gallery; + + return { + imageNames: filteredImages.map((i) => i.image_name), + total, + selectedBoardId, + galleryView, + galleryImageMinimumWidth, + isLoading, + }; + }, + defaultSelectorOptions +); + +const GalleryImageGrid = () => { + const { t } = useTranslation(); + const rootRef = useRef(null); + const emptyGalleryRef = useRef(null); + const [scroller, setScroller] = useState(null); + const [initialize, osInstance] = useOverlayScrollbars({ + defer: true, + options: { + scrollbars: { + visibility: 'auto', + autoHide: 'leave', + autoHideDelay: 1300, + theme: 'os-theme-dark', + }, + overflow: { x: 'hidden' }, + }, + }); + + const [didInitialFetch, setDidInitialFetch] = useState(false); + + const dispatch = useAppDispatch(); + + const { + galleryImageMinimumWidth, + imageNames: imageNamesAll, //all images names loaded on main tab, + total: totalAll, + selectedBoardId, + galleryView, + isLoading: isLoadingAll, + } = useAppSelector(selector); + + const { data: imagesForBoard, isLoading: isLoadingImagesForBoard } = + useListBoardImagesQuery( + { board_id: selectedBoardId }, + { skip: selectedBoardId === 'all' } + ); + + const imageNames = useMemo(() => { + if (selectedBoardId === 'all') { + return imageNamesAll; // already sorted by images/uploads in gallery selector + } else { + const categories = + galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES; + const imageList = (imagesForBoard?.items || []).filter((img) => + categories.includes(img.image_category) + ); + return imageList.map((img) => img.image_name); + } + }, [selectedBoardId, galleryView, imagesForBoard, imageNamesAll]); + + const areMoreAvailable = useMemo(() => { + return selectedBoardId === 'all' ? totalAll > imageNamesAll.length : false; + }, [selectedBoardId, imageNamesAll.length, totalAll]); + + const isLoading = useMemo(() => { + return selectedBoardId === 'all' ? isLoadingAll : isLoadingImagesForBoard; + }, [selectedBoardId, isLoadingAll, isLoadingImagesForBoard]); + + const handleLoadMoreImages = useCallback(() => { + dispatch( + receivedPageOfImages({ + categories: + galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES, + is_intermediate: false, + offset: imageNames.length, + limit: IMAGE_LIMIT, + }) + ); + }, [dispatch, imageNames.length, galleryView]); + + const handleEndReached = useMemo(() => { + if (areMoreAvailable) { + return handleLoadMoreImages; + } + return undefined; + }, [areMoreAvailable, handleLoadMoreImages]); + + // useEffect(() => { + // if (!didInitialFetch) { + // return; + // } + // // rough, conservative calculation of how many images fit in the gallery + // // TODO: this gets an incorrect value on first load... + // const galleryHeight = rootRef.current?.clientHeight ?? 0; + // const galleryWidth = rootRef.current?.clientHeight ?? 0; + + // const rows = galleryHeight / galleryImageMinimumWidth; + // const columns = galleryWidth / galleryImageMinimumWidth; + + // const imagesToLoad = Math.ceil(rows * columns); + + // setDidInitialFetch(true); + + // // load up that many images + // dispatch( + // receivedPageOfImages({ + // offset: 0, + // limit: 10, + // }) + // ); + // }, [ + // didInitialFetch, + // dispatch, + // galleryImageMinimumWidth, + // galleryView, + // selectedBoardId, + // ]); + + if (!isLoading && imageNames.length === 0) { + return ( + + + + ); + } + console.log({ selectedBoardId }); + + if (status !== 'rejected') { + return ( + <> + + ( + + )} + /> + + + {areMoreAvailable + ? t('gallery.loadMore') + : t('gallery.allImagesLoaded')} + + + ); + } +}; + +export default memo(GalleryImageGrid); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx new file mode 100644 index 0000000000..a09455ef2c --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridItemContainer.tsx @@ -0,0 +1,11 @@ +import { Box, FlexProps, forwardRef } from '@chakra-ui/react'; +import { PropsWithChildren } from 'react'; + +type ItemContainerProps = PropsWithChildren & FlexProps; +const ItemContainer = forwardRef((props: ItemContainerProps, ref) => ( + + {props.children} + +)); + +export default ItemContainer; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx new file mode 100644 index 0000000000..fbbca2b2cf --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/ImageGridListContainer.tsx @@ -0,0 +1,26 @@ +import { FlexProps, Grid, forwardRef } from '@chakra-ui/react'; +import { RootState } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { PropsWithChildren } from 'react'; + +type ListContainerProps = PropsWithChildren & FlexProps; +const ListContainer = forwardRef((props: ListContainerProps, ref) => { + const galleryImageMinimumWidth = useAppSelector( + (state: RootState) => state.gallery.galleryImageMinimumWidth + ); + + return ( + + {props.children} + + ); +}); + +export default ListContainer; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx similarity index 94% rename from invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 35685bde6f..89cd0a5005 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,7 +1,7 @@ import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useCallback } from 'react'; import { UnsafeImageMetadata } from 'services/api/endpoints/images'; -import MetadataItem from './MetadataItem'; +import ImageMetadataItem from './ImageMetadataItem'; type Props = { metadata?: UnsafeImageMetadata['metadata']; @@ -73,13 +73,13 @@ const ImageMetadataActions = (props: Props) => { return ( <> {metadata.generation_mode && ( - )} {metadata.positive_prompt && ( - { /> )} {metadata.negative_prompt && ( - { /> )} {metadata.seed !== undefined && ( - )} {metadata.model !== undefined && ( - )} {metadata.width && ( - )} {metadata.height && ( - { /> )} */} {metadata.scheduler && ( - )} {metadata.steps && ( - )} {metadata.cfg_scale !== undefined && ( - { /> )} */} {metadata.strength && ( - { +const ImageMetadataJSON = (props: Props) => { const { copyTooltip, jsonObject } = props; const jsonString = useMemo( () => JSON.stringify(jsonObject, null, 2), @@ -67,4 +67,4 @@ const MetadataJSONViewer = (props: Props) => { ); }; -export default MetadataJSONViewer; +export default ImageMetadataJSON; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx similarity index 97% rename from invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index 83be19658f..e1f2a9e46a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -15,7 +15,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; import { useDebounce } from 'use-debounce'; import ImageMetadataActions from './ImageMetadataActions'; -import MetadataJSONViewer from './MetadataJSONViewer'; +import ImageMetadataJSON from './ImageMetadataJSON'; type ImageMetadataViewerProps = { image: ImageDTO; @@ -123,7 +123,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { key={tab.label} sx={{ w: 'full', h: 'full', p: 0, pt: 4 }} > - diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index 5a510616a1..06e2a22cbd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -1,17 +1,12 @@ -import { ChakraProps, Flex, Grid, IconButton, Spinner } from '@chakra-ui/react'; -import { memo, useCallback, useState } from 'react'; +import { Box, ChakraProps, Flex, IconButton, Spinner } from '@chakra-ui/react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa'; import { useNextPrevImage } from '../hooks/useNextPrevImage'; -const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = { - height: '100%', - width: '15%', - alignItems: 'center', - pointerEvents: 'auto', -}; const nextPrevButtonStyles: ChakraProps['sx'] = { color: 'base.100', + pointerEvents: 'auto', }; const NextPrevImageButtons = () => { @@ -27,35 +22,23 @@ const NextPrevImageButtons = () => { isFetching, } = useNextPrevImage(); - const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = - useState(false); - - const handleCurrentImagePreviewMouseOver = useCallback(() => { - setShouldShowNextPrevButtons(true); - }, []); - - const handleCurrentImagePreviewMouseOut = useCallback(() => { - setShouldShowNextPrevButtons(false); - }, []); - return ( - - - {shouldShowNextPrevButtons && !isOnFirstImage && ( + {!isOnFirstImage && ( } @@ -65,16 +48,16 @@ const NextPrevImageButtons = () => { sx={nextPrevButtonStyles} /> )} - - + - {shouldShowNextPrevButtons && !isOnLastImage && ( + {!isOnLastImage && ( } @@ -84,36 +67,30 @@ const NextPrevImageButtons = () => { sx={nextPrevButtonStyles} /> )} - {shouldShowNextPrevButtons && - isOnLastImage && - areMoreImagesAvailable && - !isFetching && ( - } - variant="unstyled" - onClick={handleLoadMoreImages} - boxSize={16} - sx={nextPrevButtonStyles} - /> - )} - {shouldShowNextPrevButtons && - isOnLastImage && - areMoreImagesAvailable && - isFetching && ( - - - - )} - - + {isOnLastImage && areMoreImagesAvailable && !isFetching && ( + } + variant="unstyled" + onClick={handleLoadMoreImages} + boxSize={16} + sx={nextPrevButtonStyles} + /> + )} + {isOnLastImage && areMoreImagesAvailable && isFetching && ( + + + + )} + + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts b/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts index 5beadabbc1..44473bea83 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts @@ -3,12 +3,12 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imageSelected, - selectFilteredImages, selectImagesById, } from 'features/gallery/store/gallerySlice'; import { clamp, isEqual } from 'lodash-es'; import { useCallback } from 'react'; import { receivedPageOfImages } from 'services/api/thunks/image'; +import { selectFilteredImages } from '../store/gallerySelectors'; export const nextPrevImageButtonsSelector = createSelector( [stateSelector, selectFilteredImages], diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts index 5b4a439e38..acef7d6fc1 100644 --- a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts @@ -11,7 +11,6 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [ 'limit', 'offset', 'selectedBoardId', - 'categories', 'galleryView', 'total', 'isInitialized', diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 3c7e366bf7..045fb68737 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -1,3 +1,136 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { clamp, keyBy } from 'lodash-es'; +import { ImageDTO } from 'services/api/types'; +import { + ASSETS_CATEGORIES, + BoardId, + IMAGE_CATEGORIES, + imagesAdapter, + initialGalleryState, +} from './gallerySlice'; export const gallerySelector = (state: RootState) => state.gallery; + +const isInSelectedBoard = ( + selectedBoardId: BoardId, + imageDTO: ImageDTO, + batchImageNames: string[] +) => { + if (selectedBoardId === 'all') { + // all images are in the "All Images" board + return true; + } + + if (selectedBoardId === 'none' && !imageDTO.board_id) { + // Only images without a board are in the "No Board" board + return true; + } + + if ( + selectedBoardId === 'batch' && + batchImageNames.includes(imageDTO.image_name) + ) { + // Only images with is_batch are in the "Batch" board + return true; + } + + return selectedBoardId === imageDTO.board_id; +}; + +export const selectFilteredImagesLocal = createSelector( + [(state: typeof initialGalleryState) => state], + (galleryState) => { + const allImages = imagesAdapter.getSelectors().selectAll(galleryState); + const { galleryView, selectedBoardId } = galleryState; + + const categories = + galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES; + + const filteredImages = allImages.filter((i) => { + const isInCategory = categories.includes(i.image_category); + + const isInBoard = isInSelectedBoard( + selectedBoardId, + i, + galleryState.batchImageNames + ); + return isInCategory && isInBoard; + }); + + return filteredImages; + } +); + +export const selectFilteredImages = createSelector( + (state: RootState) => state, + (state) => { + return selectFilteredImagesLocal(state.gallery); + }, + defaultSelectorOptions +); + +export const selectFilteredImagesAsObject = createSelector( + selectFilteredImages, + (filteredImages) => keyBy(filteredImages, 'image_name') +); + +export const selectFilteredImagesIds = createSelector( + selectFilteredImages, + (filteredImages) => filteredImages.map((i) => i.image_name) +); + +export const selectLastSelectedImage = createSelector( + (state: RootState) => state, + (state) => state.gallery.selection[state.gallery.selection.length - 1], + defaultSelectorOptions +); + +export const selectSelectedImages = createSelector( + (state: RootState) => state, + (state) => + imagesAdapter + .getSelectors() + .selectAll(state.gallery) + .filter((i) => state.gallery.selection.includes(i.image_name)), + defaultSelectorOptions +); + +export const selectNextImageToSelectLocal = createSelector( + [ + (state: typeof initialGalleryState) => state, + (state: typeof initialGalleryState, image_name: string) => image_name, + ], + (state, image_name) => { + const filteredImages = selectFilteredImagesLocal(state); + const ids = filteredImages.map((i) => i.image_name); + + const deletedImageIndex = ids.findIndex( + (result) => result.toString() === image_name + ); + + const filteredIds = ids.filter((id) => id.toString() !== image_name); + + const newSelectedImageIndex = clamp( + deletedImageIndex, + 0, + filteredIds.length - 1 + ); + + const newSelectedImageId = filteredIds[newSelectedImageIndex]; + + return newSelectedImageId; + } +); + +export const selectNextImageToSelect = createSelector( + [ + (state: RootState) => state, + (state: RootState, image_name: string) => image_name, + ], + (state, image_name) => { + return selectNextImageToSelectLocal(state.gallery, image_name); + }, + defaultSelectorOptions +); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 63fd9625a0..fa1f6a6f1a 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,19 +1,15 @@ import type { PayloadAction, Update } from '@reduxjs/toolkit'; -import { - createEntityAdapter, - createSelector, - createSlice, -} from '@reduxjs/toolkit'; +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { dateComparator } from 'common/util/dateComparator'; -import { keyBy, uniq } from 'lodash-es'; +import { uniq } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { imageUrlsReceived, receivedPageOfImages, } from 'services/api/thunks/image'; import { ImageCategory, ImageDTO } from 'services/api/types'; +import { selectFilteredImagesLocal } from './gallerySelectors'; export const imagesAdapter = createEntityAdapter({ selectId: (image) => image.image_name, @@ -27,23 +23,30 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [ 'user', 'other', ]; - export const INITIAL_IMAGE_LIMIT = 100; export const IMAGE_LIMIT = 20; +export type GalleryView = 'images' | 'assets'; +export type BoardId = + | 'all' + | 'none' + | 'batch' + | (string & Record); + type AdditionaGalleryState = { offset: number; limit: number; total: number; isLoading: boolean; isFetching: boolean; - categories: ImageCategory[]; - selectedBoardId?: string; selection: string[]; shouldAutoSwitch: boolean; galleryImageMinimumWidth: number; - galleryView: 'images' | 'assets'; + galleryView: GalleryView; + selectedBoardId: BoardId; isInitialized: boolean; + batchImageNames: string[]; + isBatchEnabled: boolean; }; export const initialGalleryState = @@ -53,12 +56,14 @@ export const initialGalleryState = total: 0, isLoading: true, isFetching: true, - categories: IMAGE_CATEGORIES, selection: [], shouldAutoSwitch: true, galleryImageMinimumWidth: 96, galleryView: 'images', + selectedBoardId: 'all', isInitialized: false, + batchImageNames: [], + isBatchEnabled: false, }); export const gallerySlice = createSlice({ @@ -73,7 +78,7 @@ export const gallerySlice = createSlice({ ) { state.selection = [action.payload.image_name]; state.galleryView = 'images'; - state.categories = IMAGE_CATEGORIES; + state.selectedBoardId = 'all'; } }, imageUpdatedOne: (state, action: PayloadAction>) => { @@ -81,12 +86,15 @@ export const gallerySlice = createSlice({ }, imageRemoved: (state, action: PayloadAction) => { imagesAdapter.removeOne(state, action.payload); + state.batchImageNames = state.batchImageNames.filter( + (name) => name !== action.payload + ); }, imagesRemoved: (state, action: PayloadAction) => { imagesAdapter.removeMany(state, action.payload); - }, - imageCategoriesChanged: (state, action: PayloadAction) => { - state.categories = action.payload; + state.batchImageNames = state.batchImageNames.filter( + (name) => !action.payload.includes(name) + ); }, imageRangeEndSelected: (state, action: PayloadAction) => { const rangeEndImageName = action.payload; @@ -127,9 +135,7 @@ export const gallerySlice = createSlice({ } }, imageSelected: (state, action: PayloadAction) => { - state.selection = action.payload - ? [action.payload] - : [String(state.ids[0])]; + state.selection = action.payload ? [action.payload] : []; }, shouldAutoSwitchChanged: (state, action: PayloadAction) => { state.shouldAutoSwitch = action.payload; @@ -137,15 +143,43 @@ export const gallerySlice = createSlice({ setGalleryImageMinimumWidth: (state, action: PayloadAction) => { state.galleryImageMinimumWidth = action.payload; }, - setGalleryView: (state, action: PayloadAction<'images' | 'assets'>) => { + setGalleryView: (state, action: PayloadAction) => { state.galleryView = action.payload; }, - boardIdSelected: (state, action: PayloadAction) => { + boardIdSelected: (state, action: PayloadAction) => { state.selectedBoardId = action.payload; }, isLoadingChanged: (state, action: PayloadAction) => { state.isLoading = action.payload; }, + isBatchEnabledChanged: (state, action: PayloadAction) => { + state.isBatchEnabled = action.payload; + }, + imagesAddedToBatch: (state, action: PayloadAction) => { + state.batchImageNames = uniq( + state.batchImageNames.concat(action.payload) + ); + }, + imagesRemovedFromBatch: (state, action: PayloadAction) => { + state.batchImageNames = state.batchImageNames.filter( + (imageName) => !action.payload.includes(imageName) + ); + + const newSelection = state.selection.filter( + (imageName) => !action.payload.includes(imageName) + ); + + if (newSelection.length) { + state.selection = newSelection; + return; + } + + state.selection = [state.batchImageNames[0]] ?? []; + }, + batchReset: (state) => { + state.batchImageNames = []; + state.selection = []; + }, }, extraReducers: (builder) => { builder.addCase(receivedPageOfImages.pending, (state) => { @@ -188,7 +222,7 @@ export const gallerySlice = createSlice({ boardsApi.endpoints.deleteBoard.matchFulfilled, (state, action) => { if (action.meta.arg.originalArgs === state.selectedBoardId) { - state.selectedBoardId = undefined; + state.selectedBoardId = 'all'; } } ); @@ -208,7 +242,6 @@ export const { imageUpdatedOne, imageRemoved, imagesRemoved, - imageCategoriesChanged, imageRangeEndSelected, imageSelectionToggled, imageSelected, @@ -217,48 +250,9 @@ export const { setGalleryView, boardIdSelected, isLoadingChanged, + isBatchEnabledChanged, + imagesAddedToBatch, + imagesRemovedFromBatch, } = gallerySlice.actions; export default gallerySlice.reducer; - -export const selectFilteredImagesLocal = createSelector( - (state: typeof initialGalleryState) => state, - (galleryState) => { - const allImages = imagesAdapter.getSelectors().selectAll(galleryState); - const { categories, selectedBoardId } = galleryState; - - const filteredImages = allImages.filter((i) => { - const isInCategory = categories.includes(i.image_category); - const isInSelectedBoard = selectedBoardId - ? i.board_id === selectedBoardId - : true; - return isInCategory && isInSelectedBoard; - }); - - return filteredImages; - } -); - -export const selectFilteredImages = createSelector( - (state: RootState) => state, - (state) => { - return selectFilteredImagesLocal(state.gallery); - }, - defaultSelectorOptions -); - -export const selectFilteredImagesAsObject = createSelector( - selectFilteredImages, - (filteredImages) => keyBy(filteredImages, 'image_name') -); - -export const selectFilteredImagesIds = createSelector( - selectFilteredImages, - (filteredImages) => filteredImages.map((i) => i.image_name) -); - -export const selectLastSelectedImage = createSelector( - (state: RootState) => state, - (state) => state.gallery.selection[state.gallery.selection.length - 1], - defaultSelectorOptions -); diff --git a/invokeai/frontend/web/src/features/lightbox/components/Lightbox.tsx b/invokeai/frontend/web/src/features/lightbox/components/Lightbox.tsx deleted file mode 100644 index cd0ce55b1e..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/components/Lightbox.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Box, Flex } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIIconButton from 'common/components/IAIIconButton'; -import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons'; -import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer'; -import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; -import { gallerySelector } from 'features/gallery/store/gallerySelectors'; -import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; -import { uiSelector } from 'features/ui/store/uiSelectors'; -import { AnimatePresence, motion } from 'framer-motion'; -import { isEqual } from 'lodash-es'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { BiExit } from 'react-icons/bi'; -import { TransformWrapper } from 'react-zoom-pan-pinch'; -import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants'; -import useImageTransform from '../hooks/useImageTransform'; -import ReactPanZoomButtons from './ReactPanZoomButtons'; -import ReactPanZoomImage from './ReactPanZoomImage'; - -export const lightboxSelector = createSelector( - [gallerySelector, uiSelector], - (gallery, ui) => { - const { currentImage } = gallery; - const { shouldShowImageDetails } = ui; - - return { - viewerImageToDisplay: currentImage, - shouldShowImageDetails, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -export default function Lightbox() { - const dispatch = useAppDispatch(); - const isLightBoxOpen = useAppSelector( - (state: RootState) => state.lightbox.isLightboxOpen - ); - - const { - rotation, - scaleX, - scaleY, - flipHorizontally, - flipVertically, - rotateCounterClockwise, - rotateClockwise, - reset, - } = useImageTransform(); - - const { viewerImageToDisplay, shouldShowImageDetails } = - useAppSelector(lightboxSelector); - - useHotkeys( - 'Esc', - () => { - if (isLightBoxOpen) dispatch(setIsLightboxOpen(false)); - }, - [isLightBoxOpen] - ); - - return ( - - {isLightBoxOpen && ( - - - - } - aria-label="Exit Viewer" - className="lightbox-close-btn" - onClick={() => { - dispatch(setIsLightboxOpen(false)); - }} - fontSize={20} - /> - - - - - - - {viewerImageToDisplay && ( - <> - - {shouldShowImageDetails && ( - - )} - - {!shouldShowImageDetails && ( - - - - )} - - )} - - - )} - - ); -} diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomButtons.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomButtons.tsx deleted file mode 100644 index 2e592e83d7..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomButtons.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { ButtonGroup } from '@chakra-ui/react'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { useTranslation } from 'react-i18next'; -import { - BiReset, - BiRotateLeft, - BiRotateRight, - BiZoomIn, - BiZoomOut, -} from 'react-icons/bi'; -import { MdFlip } from 'react-icons/md'; -import { useTransformContext } from 'react-zoom-pan-pinch'; - -type ReactPanZoomButtonsProps = { - flipHorizontally: () => void; - flipVertically: () => void; - rotateCounterClockwise: () => void; - rotateClockwise: () => void; - reset: () => void; -}; - -const ReactPanZoomButtons = ({ - flipHorizontally, - flipVertically, - rotateCounterClockwise, - rotateClockwise, - reset, -}: ReactPanZoomButtonsProps) => { - const { zoomIn, zoomOut, resetTransform } = useTransformContext(); - const { t } = useTranslation(); - - return ( - - } - aria-label={t('accessibility.zoomIn')} - tooltip={t('accessibility.zoomIn')} - onClick={() => zoomIn()} - fontSize={20} - /> - - } - aria-label={t('accessibility.zoomOut')} - tooltip={t('accessibility.zoomOut')} - onClick={() => zoomOut()} - fontSize={20} - /> - - } - aria-label={t('accessibility.rotateCounterClockwise')} - tooltip={t('accessibility.rotateCounterClockwise')} - onClick={rotateCounterClockwise} - fontSize={20} - /> - - } - aria-label={t('accessibility.rotateClockwise')} - tooltip={t('accessibility.rotateClockwise')} - onClick={rotateClockwise} - fontSize={20} - /> - - } - aria-label={t('accessibility.flipHorizontally')} - tooltip={t('accessibility.flipHorizontally')} - onClick={flipHorizontally} - fontSize={20} - /> - - } - aria-label={t('accessibility.flipVertically')} - tooltip={t('accessibility.flipVertically')} - onClick={flipVertically} - fontSize={20} - /> - - } - aria-label={t('accessibility.reset')} - tooltip={t('accessibility.reset')} - onClick={() => { - resetTransform(); - reset(); - }} - fontSize={20} - /> - - ); -}; - -export default ReactPanZoomButtons; diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx deleted file mode 100644 index 73e7144163..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; -import { ImageDTO } from 'services/api/types'; - -type ReactPanZoomProps = { - image: ImageDTO; - styleClass?: string; - alt?: string; - ref?: React.Ref; - rotation: number; - scaleX: number; - scaleY: number; -}; - -export default function ReactPanZoomImage({ - image, - alt, - ref, - styleClass, - rotation, - scaleX, - scaleY, -}: ReactPanZoomProps) { - const { centerView } = useTransformContext(); - - return ( - - {alt} centerView(1, 0, 'easeOut')} - /> - - ); -} diff --git a/invokeai/frontend/web/src/features/lightbox/hooks/useImageTransform.ts b/invokeai/frontend/web/src/features/lightbox/hooks/useImageTransform.ts deleted file mode 100644 index c191d7d1d7..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/hooks/useImageTransform.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useState } from 'react'; - -const useImageTransform = () => { - const [rotation, setRotation] = useState(0); - const [scaleX, setScaleX] = useState(1); - const [scaleY, setScaleY] = useState(1); - - const rotateCounterClockwise = () => { - if (rotation === -270) { - setRotation(0); - } else { - setRotation(rotation - 90); - } - }; - - const rotateClockwise = () => { - if (rotation === 270) { - setRotation(0); - } else { - setRotation(rotation + 90); - } - }; - - const flipHorizontally = () => { - setScaleX(scaleX * -1); - }; - - const flipVertically = () => { - setScaleY(scaleY * -1); - }; - - const reset = () => { - setRotation(0); - setScaleX(1); - setScaleY(1); - }; - - return { - rotation, - scaleX, - scaleY, - flipHorizontally, - flipVertically, - rotateCounterClockwise, - rotateClockwise, - reset, - }; -}; - -export default useImageTransform; diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts deleted file mode 100644 index b8a1d12f44..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LightboxState } from './lightboxSlice'; - -/** - * Lightbox slice persist denylist - */ -export const lightboxPersistDenylist: (keyof LightboxState)[] = [ - 'isLightboxOpen', -]; diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxSelectors.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxSelectors.ts deleted file mode 100644 index f7d7e0129a..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxSelectors.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; -import { isEqual } from 'lodash-es'; - -export const lightboxSelector = createSelector( - (state: RootState) => state.lightbox, - (lightbox) => lightbox, - { - memoizeOptions: { - equalityCheck: isEqual, - }, - } -); diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts deleted file mode 100644 index ea73e5bb13..0000000000 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; - -export interface LightboxState { - isLightboxOpen: boolean; -} - -export const initialLightboxState: LightboxState = { - isLightboxOpen: false, -}; - -const initialState: LightboxState = initialLightboxState; - -export const lightboxSlice = createSlice({ - name: 'lightbox', - initialState, - reducers: { - setIsLightboxOpen: (state, action: PayloadAction) => { - state.isLightboxOpen = action.payload; - }, - }, -}); - -export const { setIsLightboxOpen } = lightboxSlice.actions; - -export default lightboxSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx index 7951df31a7..c95149393e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImage.tsx @@ -1,30 +1,24 @@ -import { Flex, Icon, Text } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useMemo } from 'react'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIDndImage from 'common/components/IAIDndImage'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { skipToken } from '@reduxjs/toolkit/dist/query'; -import { FaImage } from 'react-icons/fa'; -import { stateSelector } from 'app/store/store'; import { TypesafeDraggableData, TypesafeDroppableData, } from 'app/components/ImageDnd/typesafeDnd'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIDndImage from 'common/components/IAIDndImage'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { useMemo } from 'react'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; const selector = createSelector( [stateSelector], (state) => { const { initialImage } = state.generation; - const { asInitialImage: useBatchAsInitialImage, imageNames } = state.batch; return { initialImage, - useBatchAsInitialImage, - isResetButtonDisabled: useBatchAsInitialImage - ? imageNames.length === 0 - : !initialImage, + isResetButtonDisabled: !initialImage, }; }, defaultSelectorOptions diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx index c08f714488..2422e6f542 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -1,22 +1,14 @@ import { Flex, Spacer, Text } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { useCallback, useMemo } from 'react'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import { skipToken } from '@reduxjs/toolkit/dist/query'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa'; -import useImageUploader from 'common/hooks/useImageUploader'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import IAIButton from 'common/components/IAIButton'; import { stateSelector } from 'app/store/store'; -import { - asInitialImageToggled, - batchReset, -} from 'features/batch/store/batchSlice'; -import BatchImageContainer from 'features/batch/components/BatchImageContainer'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; +import useImageUploader from 'common/hooks/useImageUploader'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { useCallback } from 'react'; +import { FaUndo, FaUpload } from 'react-icons/fa'; import { PostUploadAction } from 'services/api/thunks/image'; import InitialImage from './InitialImage'; @@ -24,59 +16,34 @@ const selector = createSelector( [stateSelector], (state) => { const { initialImage } = state.generation; - const { asInitialImage: useBatchAsInitialImage, imageNames } = state.batch; return { - initialImage, - useBatchAsInitialImage, - isResetButtonDisabled: useBatchAsInitialImage - ? imageNames.length === 0 - : !initialImage, + isResetButtonDisabled: !initialImage, }; }, defaultSelectorOptions ); +const postUploadAction: PostUploadAction = { + type: 'SET_INITIAL_IMAGE', +}; + const InitialImageDisplay = () => { - const { initialImage, useBatchAsInitialImage, isResetButtonDisabled } = - useAppSelector(selector); + const { isResetButtonDisabled } = useAppSelector(selector); const dispatch = useAppDispatch(); const { openUploader } = useImageUploader(); - const { - currentData: imageDTO, - isLoading, - isError, - isSuccess, - } = useGetImageDTOQuery(initialImage?.imageName ?? skipToken); - - const postUploadAction = useMemo( - () => - useBatchAsInitialImage - ? { type: 'ADD_TO_BATCH' } - : { type: 'SET_INITIAL_IMAGE' }, - [useBatchAsInitialImage] - ); - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ postUploadAction, }); const handleReset = useCallback(() => { - if (useBatchAsInitialImage) { - dispatch(batchReset()); - } else { - dispatch(clearInitialImage()); - } - }, [dispatch, useBatchAsInitialImage]); + dispatch(clearInitialImage()); + }, [dispatch]); const handleUpload = useCallback(() => { openUploader(); }, [openUploader]); - const handleClickUseBatch = useCallback(() => { - dispatch(asInitialImageToggled()); - }, [dispatch]); - return ( { Initial Image - {/* } - isChecked={useBatchAsInitialImage} - onClick={handleClickUseBatch} - > - {useBatchAsInitialImage ? 'Batch' : 'Single'} - */} } onClick={handleUpload} {...getUploadButtonProps()} /> } onClick={handleReset} isDisabled={isResetButtonDisabled} /> - {/* {useBatchAsInitialImage ? : } */} ); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index c69d596b78..6cff92a136 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -6,7 +6,7 @@ import { merge } from 'lodash-es'; export const initialConfigState: AppConfig = { shouldUpdateImagesOnConnect: false, disabledTabs: [], - disabledFeatures: ['lightbox', 'faceRestore'], + disabledFeatures: ['lightbox', 'faceRestore', 'batches'], disabledSDFeatures: [ 'variation', 'seamless', diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index ef168285ee..a4e0773695 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -15,7 +15,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; -import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { configSelector } from 'features/system/store/configSelectors'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice'; @@ -38,7 +37,6 @@ import NodesTab from './tabs/Nodes/NodesTab'; import ResizeHandle from './tabs/ResizeHandle'; import TextToImageTab from './tabs/TextToImage/TextToImageTab'; import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab'; -import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; export interface InvokeTabInfo { id: InvokeTabName; @@ -105,10 +103,6 @@ const InvokeTabs = () => { const activeTab = useAppSelector(activeTabIndexSelector); const activeTabName = useAppSelector(activeTabNameSelector); const enabledTabs = useAppSelector(enabledTabsSelector); - const isLightBoxOpen = useAppSelector( - (state: RootState) => state.lightbox.isLightboxOpen - ); - const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } = useAppSelector((state: RootState) => state.ui); @@ -117,17 +111,6 @@ const InvokeTabs = () => { const dispatch = useAppDispatch(); - // Lightbox Hotkey - useHotkeys( - 'z', - () => { - if (isLightboxEnabled) { - dispatch(setIsLightboxOpen(!isLightBoxOpen)); - } - }, - [isLightBoxOpen] - ); - useHotkeys( 'f', () => { diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx index 0777463ec4..ee9cc4bbb8 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx @@ -2,7 +2,6 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; import { activeTabNameSelector, @@ -12,19 +11,16 @@ import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice'; import { memo, useMemo } from 'react'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import PinParametersPanelButton from './PinParametersPanelButton'; -import OverlayScrollable from './common/OverlayScrollable'; import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer'; import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters'; import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters'; import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters'; const selector = createSelector( - [uiSelector, activeTabNameSelector, lightboxSelector], - (ui, activeTabName, lightbox) => { + [uiSelector, activeTabNameSelector], + (ui, activeTabName) => { const { shouldPinParametersPanel, shouldShowParametersPanel } = ui; - const { isLightboxOpen } = lightbox; - return { activeTabName, shouldPinParametersPanel, diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx index de21cb14eb..1864e3d043 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx @@ -1,5 +1,5 @@ import { Box, Flex } from '@chakra-ui/react'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; +import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; const TextToImageTabMain = () => { return ( diff --git a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts index a0db3f3dff..f7a486e8fc 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts @@ -1,11 +1,10 @@ import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types'; -import { api } from '..'; +import { ApiFullTagDescription, LIST_TAG, api } from '..'; import { paths } from '../schema'; -import { imagesApi } from './images'; type ListBoardImagesArg = paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] & - paths['/api/v1/board_images/{board_id}']['get']['parameters']['query']; + paths['/api/v1/board_images/{board_id}']['get']['parameters']['query']; type AddImageToBoardArg = paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json']; @@ -25,9 +24,25 @@ export const boardImagesApi = api.injectEndpoints({ >({ query: ({ board_id, offset, limit }) => ({ url: `board_images/${board_id}`, - method: 'DELETE', - body: { offset, limit }, + method: 'GET', + }), + providesTags: (result, error, arg) => { + // any list of boardimages + const tags: ApiFullTagDescription[] = [{ id: 'BoardImage', type: `${arg.board_id}_${LIST_TAG}` }]; + + if (result) { + // and individual tags for each boardimage + tags.push( + ...result.items.map(({ board_id, image_name }) => ({ + type: 'BoardImage' as const, + id: `${board_id}_${image_name}`, + })) + ); + } + + return tags; + }, }), /** @@ -41,23 +56,9 @@ export const boardImagesApi = api.injectEndpoints({ body: { board_id, image_name }, }), invalidatesTags: (result, error, arg) => [ - { type: 'Board', id: arg.board_id }, + { type: 'BoardImage' }, + { type: 'Board', id: arg.board_id } ], - async onQueryStarted( - { image_name, ...patch }, - { dispatch, queryFulfilled } - ) { - const patchResult = dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - Object.assign(draft, patch); - }) - ); - try { - await queryFulfilled; - } catch { - patchResult.undo(); - } - }, }), removeImageFromBoard: build.mutation({ @@ -67,23 +68,9 @@ export const boardImagesApi = api.injectEndpoints({ body: { board_id, image_name }, }), invalidatesTags: (result, error, arg) => [ - { type: 'Board', id: arg.board_id }, + { type: 'BoardImage' }, + { type: 'Board', id: arg.board_id } ], - async onQueryStarted( - { image_name, ...patch }, - { dispatch, queryFulfilled } - ) { - const patchResult = dispatch( - imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => { - Object.assign(draft, { board_id: null }); - }) - ); - try { - await queryFulfilled; - } catch { - patchResult.undo(); - } - }, }), }), }); diff --git a/invokeai/frontend/web/src/services/api/thunks/image.ts b/invokeai/frontend/web/src/services/api/thunks/image.ts index f20eee9420..09271c3625 100644 --- a/invokeai/frontend/web/src/services/api/thunks/image.ts +++ b/invokeai/frontend/web/src/services/api/thunks/image.ts @@ -1,5 +1,9 @@ import { createAppAsyncThunk } from 'app/store/storeUtils'; -import { selectImagesAll } from 'features/gallery/store/gallerySlice'; +import { selectFilteredImages } from 'features/gallery/store/gallerySelectors'; +import { + ASSETS_CATEGORIES, + IMAGE_CATEGORIES, +} from 'features/gallery/store/gallerySlice'; import { size } from 'lodash-es'; import queryString from 'query-string'; import { $client } from 'services/api/client'; @@ -287,15 +291,12 @@ export const receivedPageOfImages = createAppAsyncThunk< const { get } = $client.get(); const state = getState(); - const { categories, selectedBoardId } = state.gallery; - const images = selectImagesAll(state).filter((i) => { - const isInCategory = categories.includes(i.image_category); - const isInSelectedBoard = selectedBoardId - ? i.board_id === selectedBoardId - : true; - return isInCategory && isInSelectedBoard; - }); + const images = selectFilteredImages(state); + const categories = + state.gallery.galleryView === 'images' + ? IMAGE_CATEGORIES + : ASSETS_CATEGORIES; let query: ListImagesArg = {};