diff --git a/invokeai/app/api/routers/board_images.py b/invokeai/app/api/routers/board_images.py index 1e81ef0625..f1ef04a72a 100644 --- a/invokeai/app/api/routers/board_images.py +++ b/invokeai/app/api/routers/board_images.py @@ -1,11 +1,9 @@ -from fastapi import Body, HTTPException, Path, Query +from fastapi import Body, HTTPException, Path from fastapi.routing import APIRouter from invokeai.app.models.image import (AddManyImagesToBoardResult, GetAllBoardImagesForBoardResult, RemoveManyImagesFromBoardResult) -from invokeai.app.services.image_record_storage import OffsetPaginatedResults -from invokeai.app.services.models.image_record import ImageDTO from ..dependencies import ApiDependencies @@ -13,7 +11,7 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"]) @board_images_router.post( - "/", + "/{board_id}", operation_id="create_board_image", responses={ 201: {"description": "The image was added to a board successfully"}, @@ -21,7 +19,7 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"]) status_code=201, ) async def create_board_image( - board_id: str = Body(description="The id of the board to add to"), + board_id: str = Path(description="The id of the board to add to"), image_name: str = Body(description="The name of the image to add"), ): """Creates a board_image""" diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 94d8667ae4..f81c4bef98 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -1,11 +1,13 @@ from typing import Optional, Union + from fastapi import Body, HTTPException, Path, Query from fastapi.routing import APIRouter + +from invokeai.app.models.image import DeleteManyImagesResult from invokeai.app.services.board_record_storage import BoardChanges from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.models.board_record import BoardDTO - from ..dependencies import ApiDependencies boards_router = APIRouter(prefix="/v1/boards", tags=["boards"]) @@ -69,25 +71,26 @@ async def update_board( raise HTTPException(status_code=500, detail="Failed to update board") -@boards_router.delete("/{board_id}", operation_id="delete_board") +@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteManyImagesResult) async def delete_board( board_id: str = Path(description="The id of board to delete"), include_images: Optional[bool] = Query( description="Permanently delete all images on the board", default=False ), -) -> None: +) -> DeleteManyImagesResult: """Deletes a board""" try: if include_images is True: - ApiDependencies.invoker.services.images.delete_images_on_board( + result = ApiDependencies.invoker.services.images.delete_images_on_board( board_id=board_id ) ApiDependencies.invoker.services.boards.delete(board_id=board_id) else: ApiDependencies.invoker.services.boards.delete(board_id=board_id) + result = DeleteManyImagesResult(deleted_images=[]) + return result except Exception as e: - # TODO: Does this need any exception handling at all? - pass + raise HTTPException(status_code=500, detail="Failed to delete images on board") @boards_router.get( diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 4ebea9ad51..d888931978 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -113,7 +113,7 @@ class ImageServiceABC(ABC): pass @abstractmethod - def delete_images_on_board(self, board_id: str): + def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult: """Deletes all images on a board.""" pass @@ -386,7 +386,7 @@ class ImageService(ImageServiceABC): deleted_images.append(image_name) return DeleteManyImagesResult(deleted_images=deleted_images) - def delete_images_on_board(self, board_id: str): + def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult: try: board_images = ( self._services.board_image_records.get_all_board_images_for_board( @@ -397,6 +397,7 @@ class ImageService(ImageServiceABC): for image_name in image_name_list: self._services.image_files.delete(image_name) self._services.image_records.delete_many(image_name_list) + return DeleteManyImagesResult(deleted_images=board_images.image_names) except ImageRecordDeleteException: self._services.logger.error(f"Failed to delete image records") raise diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 921b234aaf..d8f913d1a8 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -128,13 +128,13 @@ "@types/react-redux": "^7.1.25", "@types/react-transition-group": "^4.4.6", "@types/uuid": "^9.0.2", - "@typescript-eslint/eslint-plugin": "^5.60.0", - "@typescript-eslint/parser": "^5.60.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react-swc": "^3.3.2", "axios": "^1.4.0", "babel-plugin-transform-imports": "^2.0.0", "concurrently": "^8.2.0", - "eslint": "^8.43.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", @@ -151,6 +151,8 @@ "rollup-plugin-visualizer": "^5.9.2", "terser": "^5.18.1", "ts-toolbelt": "^9.6.0", + "typescript": "^5.1.6", + "typescript-eslint": "^0.0.1-alpha.0", "vite": "^4.3.9", "vite-plugin-css-injected-by-js": "^3.1.1", "vite-plugin-dts": "^2.3.0", 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 2467cf6009..814a2f577c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -39,10 +39,7 @@ import { addImageUploadedFulfilledListener, addImageUploadedRejectedListener, } from './listeners/imageUploaded'; -import { - addImageUrlsReceivedFulfilledListener, - addImageUrlsReceivedRejectedListener, -} from './listeners/imageUrlsReceived'; +import { addImagesLoadedListener } from './listeners/imagesLoaded'; import { addInitialImageSelectedListener } from './listeners/initialImageSelected'; import { addModelSelectedListener } from './listeners/modelSelected'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; @@ -125,10 +122,6 @@ addImageToDeleteSelectedListener(); addImageDTOReceivedFulfilledListener(); addImageDTOReceivedRejectedListener(); -// Image URLs -addImageUrlsReceivedFulfilledListener(); -addImageUrlsReceivedRejectedListener(); - // User Invoked addUserInvokedCanvasListener(); addUserInvokedNodesListener(); @@ -184,6 +177,7 @@ addSessionCanceledRejectedListener(); // Fetching images addReceivedPageOfImagesListener(); +addImagesLoadedListener(); // ControlNet addControlNetImageProcessedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addBoardApiListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addBoardApiListeners.ts index 5fef8ecc1d..91d62882ee 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addBoardApiListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addBoardApiListeners.ts @@ -1,8 +1,4 @@ import { log } from 'app/logging/useLogger'; -import { - imageUpdatedMany, - imageUpdatedOne, -} from 'features/gallery/store/gallerySlice'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; import { startAppListening } from '..'; @@ -19,13 +15,6 @@ export const addBoardApiListeners = () => { { data: { board_id, image_name } }, 'Image added to board' ); - - dispatch( - imageUpdatedOne({ - id: image_name, - changes: { board_id }, - }) - ); }, }); @@ -49,13 +38,6 @@ export const addBoardApiListeners = () => { const { image_name } = action.meta.arg.originalArgs; moduleLog.debug({ data: { image_name } }, 'Image removed from board'); - - dispatch( - imageUpdatedOne({ - id: image_name, - changes: { board_id: undefined }, - }) - ); }, }); @@ -82,13 +64,6 @@ export const addBoardApiListeners = () => { { data: { board_id, image_names } }, 'Images added to board' ); - - const updates = image_names.map((image_name) => ({ - id: image_name, - changes: { board_id }, - })); - - dispatch(imageUpdatedMany(updates)); }, }); @@ -112,13 +87,6 @@ export const addBoardApiListeners = () => { const { image_names } = action.meta.arg.originalArgs; moduleLog.debug({ data: { image_names } }, 'Images removed from board'); - - const updates = image_names.map((image_name) => ({ - id: image_name, - changes: { board_id: undefined }, - })); - - dispatch(imageUpdatedMany(updates)); }, }); 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..43442a7be4 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,9 +1,4 @@ import { createAction } from '@reduxjs/toolkit'; -import { - INITIAL_IMAGE_LIMIT, - isLoadingChanged, -} from 'features/gallery/store/gallerySlice'; -import { receivedPageOfImages } from 'services/api/thunks/image'; import { startAppListening } from '..'; export const appStarted = createAction('app/appStarted'); @@ -15,29 +10,27 @@ export const addAppStartedListener = () => { action, { getState, dispatch, unsubscribe, cancelActiveListeners } ) => { - cancelActiveListeners(); - unsubscribe(); - // fill up the gallery tab with images - await dispatch( - receivedPageOfImages({ - categories: ['general'], - is_intermediate: false, - offset: 0, - limit: INITIAL_IMAGE_LIMIT, - }) - ); - - // fill up the assets tab with images - await dispatch( - receivedPageOfImages({ - categories: ['control', 'mask', 'user', 'other'], - is_intermediate: false, - offset: 0, - limit: INITIAL_IMAGE_LIMIT, - }) - ); - - dispatch(isLoadingChanged(false)); + // cancelActiveListeners(); + // unsubscribe(); + // // fill up the gallery tab with images + // await dispatch( + // receivedPageOfImages({ + // categories: ['general'], + // is_intermediate: false, + // offset: 0, + // // limit: INITIAL_IMAGE_LIMIT, + // }) + // ); + // // fill up the assets tab with images + // await dispatch( + // receivedPageOfImages({ + // categories: ['control', 'mask', 'user', 'other'], + // is_intermediate: false, + // offset: 0, + // // limit: INITIAL_IMAGE_LIMIT, + // }) + // ); + // dispatch(isLoadingChanged(false)); }, }); }; 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..4ddba29cd1 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,6 @@ import { log } from 'app/logging/useLogger'; +import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { startAppListening } from '..'; -import { - imageSelected, - selectImagesAll, - boardIdSelected, -} from 'features/gallery/store/gallerySlice'; -import { - IMAGES_PER_PAGE, - receivedPageOfImages, -} from 'services/api/thunks/image'; -import { boardsApi } from 'services/api/endpoints/boards'; const moduleLog = log.child({ namespace: 'boards' }); @@ -17,49 +8,40 @@ export const addBoardIdSelectedListener = () => { startAppListening({ actionCreator: boardIdSelected, effect: (action, { getState, dispatch }) => { - const board_id = action.payload; - - // we need to check if we need to fetch more images - - const state = getState(); - const allImages = selectImagesAll(state); - - if (!board_id) { - // a board was unselected - dispatch(imageSelected(allImages[0]?.image_name)); - return; - } - - const { categories } = state.gallery; - - 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; - }); - - // 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... - dispatch(imageSelected(allImages[0]?.image_name)); - return; - } - - dispatch(imageSelected(board.cover_image_name ?? null)); - - // 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 }) - ); - } + // const board_id = action.payload; + // // we need to check if we need to fetch more images + // const state = getState(); + // const allImages = selectImagesAll(state); + // if (!board_id) { + // // a board was unselected + // dispatch(imageSelected(allImages[0]?.image_name)); + // return; + // } + // const { categories } = state.gallery; + // 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; + // }); + // // 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... + // dispatch(imageSelected(allImages[0]?.image_name)); + // return; + // } + // dispatch(imageSelected(board.cover_image_name ?? null)); + // // 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 }) + // ); + // } }, }); }; @@ -68,43 +50,36 @@ 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 }) - ); - } + // 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/boardImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts index a9b4ee3cfa..2bd798e78d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts @@ -1,10 +1,8 @@ import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; -import { requestedBoardImagesDeletion } from 'features/gallery/store/actions'; +import { requestedBoardImagesDeletion as requestedBoardAndImagesDeletion } from 'features/gallery/store/actions'; import { imageSelected, - imagesRemoved, - selectImagesAll, selectImagesById, } from 'features/gallery/store/gallerySlice'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; @@ -15,7 +13,7 @@ import { boardsApi } from '../../../../../services/api/endpoints/boards'; export const addRequestedBoardImageDeletionListener = () => { startAppListening({ - actionCreator: requestedBoardImagesDeletion, + actionCreator: requestedBoardAndImagesDeletion, effect: async (action, { dispatch, getState, condition }) => { const { board, imagesUsage } = action.payload; @@ -51,20 +49,12 @@ export const addRequestedBoardImageDeletionListener = () => { dispatch(nodeEditorReset()); } - // Preemptively remove from gallery - const images = selectImagesAll(state).reduce((acc: string[], img) => { - if (img.board_id === board_id) { - acc.push(img.image_name); - } - return acc; - }, []); - dispatch(imagesRemoved(images)); - // Delete from server dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id)); const result = boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state); - const { isSuccess } = result; + + const { isSuccess, data } = result; // Wait for successful deletion, then trigger boards to re-fetch const wasBoardDeleted = await condition(() => !!isSuccess, 30000); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index 610d89873f..8c34474bdd 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -1,10 +1,10 @@ -import { canvasSavedToGallery } from 'features/canvas/store/actions'; -import { startAppListening } from '..'; import { log } from 'app/logging/useLogger'; -import { imageUploaded } from 'services/api/thunks/image'; +import { canvasSavedToGallery } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; -import { imageUpserted } from 'features/gallery/store/gallerySlice'; +import { imagesApi } from 'services/api/endpoints/images'; +import { imageUploaded } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' }); @@ -49,7 +49,11 @@ export const addCanvasSavedToGalleryListener = () => { uploadedImageAction.meta.requestId === imageUploadedRequest.requestId ); - dispatch(imageUpserted(uploadedImageDTO)); + imagesApi.util.upsertQueryData( + 'getImageDTO', + uploadedImageDTO.image_name, + uploadedImageDTO + ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDTOReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDTOReceived.ts index f55293b0f7..735dd7fe01 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDTOReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDTOReceived.ts @@ -1,5 +1,5 @@ import { log } from 'app/logging/useLogger'; -import { imageUpserted } from 'features/gallery/store/gallerySlice'; +import { imagesApi } from 'services/api/endpoints/images'; import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image'; import { startAppListening } from '..'; @@ -33,7 +33,7 @@ export const addImageDTOReceivedFulfilledListener = () => { } moduleLog.debug({ data: { image } }, 'Image metadata received'); - dispatch(imageUpserted(image)); + imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image); }, }); }; 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 380cc7b95d..8663b9d9d3 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 @@ -2,10 +2,7 @@ import { log } from 'app/logging/useLogger'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; import { selectFilteredImages } from 'features/gallery/store/gallerySelectors'; -import { - imageRemoved, - imageSelected, -} from 'features/gallery/store/gallerySlice'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageDeletionConfirmed, isModalOpenChanged, @@ -80,9 +77,6 @@ export const addRequestedImageDeletionListener = () => { dispatch(nodeEditorReset()); } - // Preemptively remove from gallery - dispatch(imageRemoved(image_name)); - // Delete from server const { requestId } = dispatch(imageDeleted({ image_name })); @@ -91,7 +85,7 @@ export const addRequestedImageDeletionListener = () => { (action): action is ReturnType => imageDeleted.fulfilled.match(action) && action.meta.requestId === requestId, - 30000 + 30_000 ); if (wasImageDeleted) { 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 82149c4ad8..f998bf7673 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 @@ -2,10 +2,10 @@ import { log } from 'app/logging/useLogger'; import { imageAddedToBatch } from 'features/batch/store/batchSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice'; -import { imageUpserted } from 'features/gallery/store/gallerySlice'; import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; +import { imagesApi } from 'services/api/endpoints/images'; import { imageUploaded } from 'services/api/thunks/image'; import { startAppListening } from '..'; @@ -24,7 +24,8 @@ export const addImageUploadedFulfilledListener = () => { return; } - dispatch(imageUpserted(image)); + // update RTK query cache + imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image); const { postUploadAction } = action.meta.arg; @@ -73,7 +74,7 @@ export const addImageUploadedFulfilledListener = () => { } if (postUploadAction?.type === 'ADD_TO_BATCH') { - dispatch(imageAddedToBatch(image)); + dispatch(imageAddedToBatch(image.image_name)); return; } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts deleted file mode 100644 index 0d8aa3d7c9..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { log } from 'app/logging/useLogger'; -import { startAppListening } from '..'; -import { imageUrlsReceived } from 'services/api/thunks/image'; -import { imageUpdatedOne } from 'features/gallery/store/gallerySlice'; - -const moduleLog = log.child({ namespace: 'image' }); - -export const addImageUrlsReceivedFulfilledListener = () => { - startAppListening({ - actionCreator: imageUrlsReceived.fulfilled, - effect: (action, { getState, dispatch }) => { - const image = action.payload; - moduleLog.debug({ data: { image } }, 'Image URLs received'); - - const { image_name, image_url, thumbnail_url } = image; - - dispatch( - imageUpdatedOne({ - id: image_name, - changes: { image_url, thumbnail_url }, - }) - ); - }, - }); -}; - -export const addImageUrlsReceivedRejectedListener = () => { - startAppListening({ - actionCreator: imageUrlsReceived.rejected, - effect: (action, { getState, dispatch }) => { - moduleLog.debug( - { data: { image: action.meta.arg } }, - 'Problem getting image URLs' - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesLoaded.ts new file mode 100644 index 0000000000..6cf28e746b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesLoaded.ts @@ -0,0 +1,38 @@ +import { log } from 'app/logging/useLogger'; +import { serializeError } from 'serialize-error'; +import { imagesApi } from 'services/api/endpoints/images'; +import { imagesLoaded } from 'services/api/thunks/image'; +import { startAppListening } from '..'; + +const moduleLog = log.child({ namespace: 'gallery' }); + +export const addImagesLoadedListener = () => { + startAppListening({ + actionCreator: imagesLoaded.fulfilled, + effect: (action, { getState, dispatch }) => { + const { items } = action.payload; + moduleLog.debug( + { data: { payload: action.payload } }, + `Loaded ${items.length} images` + ); + + items.forEach((image) => { + dispatch( + imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image) + ); + }); + }, + }); + + startAppListening({ + actionCreator: imagesLoaded.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + moduleLog.debug( + { data: { error: serializeError(action.payload) } }, + 'Problem loading images' + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts index 0d1f5ac20b..54a11468ae 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts @@ -16,6 +16,8 @@ export const addReceivedPageOfImagesListener = () => { `Received ${items.length} images` ); + // inject the received images into the RTK Query cache so consumers of the useGetImageDTOQuery + // hook can get their data from the cache instead of fetching the data again items.forEach((image) => { dispatch( imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index af336f0120..b35aaef83b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,6 +2,7 @@ import { log } from 'app/logging/useLogger'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { progressImageSet } from 'features/system/store/systemSlice'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; +import { imagesApi } from 'services/api/endpoints/images'; import { isImageOutput } from 'services/api/guards'; import { imageDTOReceived } from 'services/api/thunks/image'; import { sessionCanceled } from 'services/api/thunks/session'; @@ -41,14 +42,16 @@ export const addInvocationCompleteEventListener = () => { const { image_name } = result.image; // Get its metadata - dispatch( + const { requestId } = dispatch( imageDTOReceived({ image_name, }) ); const [{ payload: imageDTO }] = await take( - imageDTOReceived.fulfilled.match + (action): action is ReturnType => + imageDTOReceived.fulfilled.match(action) && + action.meta.requestId === requestId ); // Handle canvas image @@ -59,6 +62,15 @@ export const addInvocationCompleteEventListener = () => { dispatch(addImageToStagingArea(imageDTO)); } + // Update the RTK Query cache + dispatch( + imagesApi.util.upsertQueryData( + 'getImageDTO', + imageDTO.image_name, + imageDTO + ) + ); + if (boardIdToAddTo && !imageDTO.is_intermediate) { dispatch( boardImagesApi.endpoints.addBoardImage.initiate({ @@ -66,6 +78,17 @@ export const addInvocationCompleteEventListener = () => { image_name, }) ); + + // Set the board_id on the image in the RTK Query cache + dispatch( + imagesApi.util.updateQueryData( + 'getImageDTO', + imageDTO.image_name, + (draft) => { + Object.assign(draft, { board_id: boardIdToAddTo }); + } + ) + ); } dispatch(progressImageSet(null)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts index 36840e5de1..c2a95379bc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts @@ -1,9 +1,9 @@ -import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { startAppListening } from '..'; import { log } from 'app/logging/useLogger'; -import { imageUpdated } from 'services/api/thunks/image'; -import { imageUpserted } from 'features/gallery/store/gallerySlice'; +import { stagingAreaImageSaved } from 'features/canvas/store/actions'; import { addToast } from 'features/system/store/systemSlice'; +import { imagesApi } from 'services/api/endpoints/images'; +import { imageUpdated } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'canvas' }); @@ -43,7 +43,10 @@ export const addStagingAreaImageSavedListener = () => { } if (imageUpdated.fulfilled.match(imageUpdatedAction)) { - dispatch(imageUpserted(imageUpdatedAction.payload)); + // update cache + imagesApi.util.updateQueryData('getImageDTO', imageName, (draft) => { + Object.assign(draft, { is_intermediate: false }); + }); dispatch(addToast({ title: 'Image Saved', status: 'success' })); } }, diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index bdee94ed0f..07058e9121 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -104,10 +104,10 @@ export const store = configureStore({ // 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 (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 diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx index b38aefc7b0..c14ae24483 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx @@ -8,7 +8,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => { const dispatch = useDispatch(); const handleAllImagesBoardClick = () => { - dispatch(boardIdSelected()); + dispatch(boardIdSelected('all')); }; const droppableData: MoveBoardDropData = { diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx index e7dc35ac4d..39058961a6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx @@ -118,7 +118,7 @@ const BoardsList = (props: Props) => { {!searchMode && ( <> - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index cdde89379e..15c99a31f2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -29,16 +29,10 @@ 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 BatchGrid from './BatchGrid'; -import BoardGrid from './BoardGrid'; import BoardsList from './Boards/BoardsList'; import ImageGalleryGrid from './ImageGalleryGrid'; @@ -68,6 +62,7 @@ const ImageGalleryContent = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const resizeObserverRef = useRef(null); + const galleryGridRef = useRef(null); const { colorMode } = useColorMode(); @@ -107,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]); @@ -228,14 +221,8 @@ const ImageGalleryContent = () => { - - {selectedBoardId === 'batch' ? ( - - ) : selectedBoardId ? ( - - ) : ( - - )} + + {selectedBoardId === 'batch' ? : } ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx index aeae5cd4e3..cf590aa093 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryGrid.tsx @@ -1,7 +1,6 @@ -import { Box, Flex, Skeleton, Spinner } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Box } from '@chakra-ui/react'; +import { 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 { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -13,48 +12,27 @@ 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 { selectFilteredImages } from 'features/gallery/store/gallerySelectors'; 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'; +import { useLoadMoreImages } from '../hooks/useLoadMoreImages'; import ItemContainer from './ItemContainer'; import ListContainer from './ListContainer'; 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')); - } + [stateSelector], + (state) => { + const { galleryImageMinimumWidth } = state.gallery; return { - images, - allImagesTotal, - isLoading, - isFetching, - categories, - selectedBoardId, + galleryImageMinimumWidth, }; }, defaultSelectorOptions ); const ImageGalleryGrid = () => { - const dispatch = useAppDispatch(); const { t } = useTranslation(); - const rootRef = useRef(null); + const rootRef = useRef(null); + const emptyGalleryRef = useRef(null); const [scroller, setScroller] = useState(null); const [initialize, osInstance] = useOverlayScrollbars({ defer: true, @@ -69,46 +47,27 @@ const ImageGalleryGrid = () => { }, }); + const { galleryImageMinimumWidth } = useAppSelector(selector); + const { - images, - isLoading, - isFetching, - allImagesTotal, - categories, + imageNames, + galleryView, + loadMoreImages, 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]); + status, + areMoreAvailable, + } = useLoadMoreImages(); const handleLoadMoreImages = useCallback(() => { - dispatch( - receivedPageOfImages({ - categories, - board_id: selectedBoardId, - is_intermediate: false, - }) - ); - }, [categories, dispatch, selectedBoardId]); + loadMoreImages({}); + }, [loadMoreImages]); const handleEndReached = useMemo(() => { - if (areMoreAvailable && !isLoading) { + if (areMoreAvailable && status !== 'pending') { return handleLoadMoreImages; } return undefined; - }, [areMoreAvailable, handleLoadMoreImages, isLoading]); + }, [areMoreAvailable, handleLoadMoreImages, status]); useEffect(() => { const { current: root } = rootRef; @@ -123,53 +82,68 @@ const ImageGalleryGrid = () => { return () => osInstance()?.destroy(); }, [scroller, initialize, osInstance]); - if (isLoading) { + useEffect(() => { + // TODO: this doesn't actually prevent 2 intial image loads... + if (status !== undefined) { + 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); + + // load up that many images + loadMoreImages({ + offset: 0, + limit: imagesToLoad, + }); + }, [ + galleryImageMinimumWidth, + galleryView, + loadMoreImages, + selectedBoardId, + status, + ]); + + if (status === 'fulfilled' && imageNames.length === 0) { return ( - - + - + ); } - if (images.length) { + if (status !== 'rejected') { return ( <> - typeof item === 'string' ? ( - - ) : ( - - ) - } + itemContent={(index, imageName) => ( + + )} /> @@ -180,13 +154,6 @@ const ImageGalleryGrid = () => { ); } - - return ( - - ); }; export default memo(ImageGalleryGrid); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useLoadMoreImages.ts b/invokeai/frontend/web/src/features/gallery/hooks/useLoadMoreImages.ts new file mode 100644 index 0000000000..161a49c490 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useLoadMoreImages.ts @@ -0,0 +1,64 @@ +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 { useCallback } from 'react'; +import { ImagesLoadedArg, imagesLoaded } from 'services/api/thunks/image'; + +const selector = createSelector( + [stateSelector], + (state) => { + const { selectedBoardId, galleryView } = state.gallery; + + const imageNames = + state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView] + .imageNames ?? []; + + const total = + state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView] + .total ?? 0; + + const status = + state.gallery.statusByIdAndView[selectedBoardId]?.[galleryView] ?? + undefined; + + return { + imageNames, + status, + total, + selectedBoardId, + galleryView, + }; + }, + defaultSelectorOptions +); + +export const useLoadMoreImages = () => { + const dispatch = useAppDispatch(); + const { selectedBoardId, imageNames, galleryView, total, status } = + useAppSelector(selector); + + const loadMoreImages = useCallback( + (arg: Partial) => { + dispatch( + imagesLoaded({ + board_id: selectedBoardId, + offset: imageNames.length, + view: galleryView, + ...arg, + }) + ); + }, + [dispatch, galleryView, imageNames.length, selectedBoardId] + ); + + return { + loadMoreImages, + selectedBoardId, + imageNames, + galleryView, + areMoreAvailable: imageNames.length < total, + total, + status, + }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts index 5b4a439e38..28d3b5bce1 100644 --- a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts @@ -15,4 +15,6 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [ 'galleryView', 'total', 'isInitialized', + 'imageNamesByIdAndView', + 'statusByIdAndView', ]; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index a1ef48c567..a6b6e1a811 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,15 +1,12 @@ -import type { PayloadAction, Update } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { dateComparator } from 'common/util/dateComparator'; -import { uniq } from 'lodash-es'; +import { filter, forEach, uniq } from 'lodash-es'; +import { boardImagesApi } from 'services/api/endpoints/boardImages'; import { boardsApi } from 'services/api/endpoints/boards'; -import { - imageUrlsReceived, - receivedPageOfImages, -} from 'services/api/thunks/image'; +import { imageDeleted, imagesLoaded } from 'services/api/thunks/image'; import { ImageCategory, ImageDTO } from 'services/api/types'; -import { selectFilteredImagesLocal } from './gallerySelectors'; export const galleryImagesAdapter = createEntityAdapter({ selectId: (image) => image.image_name, @@ -27,6 +24,40 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [ export const INITIAL_IMAGE_LIMIT = 100; export const IMAGE_LIMIT = 20; +type RequestState = 'pending' | 'fulfilled' | 'rejected'; +type GalleryView = 'images' | 'assets'; + +// dirty hack to get autocompletion while still accepting any string +type BoardPath = + | 'all.images' + | 'all.assets' + | 'none.images' + | 'none.assets' + | 'batch.images' + | 'batch.assets' + | `${string}.${GalleryView}`; + +const systemBoards = [ + 'all.images', + 'all.assets', + 'none.images', + 'none.assets', + 'batch.images', + 'batch.assets', +]; + +type Boards = Record< + BoardPath, + { + path: BoardPath; + id: 'all' | 'none' | 'batch' | (string & Record); + view: GalleryView; + imageNames: string[]; + total: number; + status: RequestState | undefined; + } +>; + type AdditionalGalleryState = { offset: number; limit: number; @@ -34,12 +65,54 @@ type AdditionalGalleryState = { isLoading: boolean; isFetching: boolean; categories: ImageCategory[]; - selectedBoardId?: 'batch' | string; selection: string[]; shouldAutoSwitch: boolean; galleryImageMinimumWidth: number; - galleryView: 'images' | 'assets'; isInitialized: boolean; + galleryView: GalleryView; + selectedBoardId: 'all' | 'none' | 'batch' | (string & Record); + boards: Boards; +}; + +const initialBoardState = { imageNames: [], total: 0, status: undefined }; + +const initialBoards: Boards = { + 'all.images': { + path: 'all.images', + id: 'all', + view: 'images', + ...initialBoardState, + }, + 'all.assets': { + path: 'all.assets', + id: 'all', + view: 'assets', + ...initialBoardState, + }, + 'none.images': { + path: 'none.images', + id: 'none', + view: 'images', + ...initialBoardState, + }, + 'none.assets': { + path: 'none.assets', + id: 'none', + view: 'assets', + ...initialBoardState, + }, + 'batch.images': { + path: 'batch.images', + id: 'batch', + view: 'images', + ...initialBoardState, + }, + 'batch.assets': { + path: 'batch.assets', + id: 'batch', + view: 'assets', + ...initialBoardState, + }, }; export const initialGalleryState = @@ -55,60 +128,45 @@ export const initialGalleryState = galleryImageMinimumWidth: 96, galleryView: 'images', isInitialized: false, + selectedBoardId: 'all', + boards: initialBoards, }); export const gallerySlice = createSlice({ name: 'gallery', initialState: initialGalleryState, reducers: { - imageUpserted: (state, action: PayloadAction) => { - galleryImagesAdapter.upsertOne(state, action.payload); - if ( - state.shouldAutoSwitch && - action.payload.image_category === 'general' - ) { - state.selection = [action.payload.image_name]; - state.galleryView = 'images'; - state.categories = IMAGE_CATEGORIES; - } - }, - imageUpdatedOne: (state, action: PayloadAction>) => { - galleryImagesAdapter.updateOne(state, action.payload); - }, - imageUpdatedMany: (state, action: PayloadAction[]>) => { - galleryImagesAdapter.updateMany(state, action.payload); - }, imageRemoved: (state, action: PayloadAction) => { galleryImagesAdapter.removeOne(state, action.payload); }, imagesRemoved: (state, action: PayloadAction) => { galleryImagesAdapter.removeMany(state, action.payload); }, - imageCategoriesChanged: (state, action: PayloadAction) => { - state.categories = action.payload; - }, imageRangeEndSelected: (state, action: PayloadAction) => { const rangeEndImageName = action.payload; const lastSelectedImage = state.selection[state.selection.length - 1]; - const filteredImages = selectFilteredImagesLocal(state); + // get image names for the current board and view + const imageNames = + state.boards[`${state.selectedBoardId}.${state.galleryView}`] + .imageNames; - const lastClickedIndex = filteredImages.findIndex( - (n) => n.image_name === lastSelectedImage + // get the index of the last selected image + const lastClickedIndex = imageNames.findIndex( + (n) => n === lastSelectedImage ); - const currentClickedIndex = filteredImages.findIndex( - (n) => n.image_name === rangeEndImageName + // get the index of the just-clicked image + const currentClickedIndex = imageNames.findIndex( + (n) => n === rangeEndImageName ); if (lastClickedIndex > -1 && currentClickedIndex > -1) { - // We have a valid range! + // We have a valid range, selected it! const start = Math.min(lastClickedIndex, currentClickedIndex); const end = Math.max(lastClickedIndex, currentClickedIndex); - const imagesToSelect = filteredImages - .slice(start, end + 1) - .map((i) => i.image_name); + const imagesToSelect = imageNames.slice(start, end + 1); state.selection = uniq(state.selection.concat(imagesToSelect)); } @@ -121,9 +179,10 @@ export const gallerySlice = createSlice({ state.selection = state.selection.filter( (imageName) => imageName !== action.payload ); - } else { - state.selection = uniq(state.selection.concat(action.payload)); + return; } + + state.selection = uniq(state.selection.concat(action.payload)); }, imageSelected: (state, action: PayloadAction) => { state.selection = action.payload @@ -136,59 +195,210 @@ 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) => { - state.selectedBoardId = action.payload; + boardIdSelected: (state, action: PayloadAction) => { + const boardId = action.payload; + + if (state.selectedBoardId === boardId) { + // selected same board, no-op + return; + } + + state.selectedBoardId = boardId; + + // handle selecting an unitialized board + const boardImagesId: BoardPath = `${boardId}.images`; + const boardAssetsId: BoardPath = `${boardId}.assets`; + + if (!state.boards[boardImagesId]) { + state.boards[boardImagesId] = { + path: boardImagesId, + id: boardId, + view: 'images', + ...initialBoardState, + }; + } + + if (!state.boards[boardAssetsId]) { + state.boards[boardAssetsId] = { + path: boardAssetsId, + id: boardId, + view: 'assets', + ...initialBoardState, + }; + } + + // set the first image as selected + const firstImageName = + state.boards[`${boardId}.${state.galleryView}`].imageNames[0]; + + state.selection = firstImageName ? [firstImageName] : []; }, isLoadingChanged: (state, action: PayloadAction) => { state.isLoading = action.payload; }, }, extraReducers: (builder) => { - builder.addCase(receivedPageOfImages.pending, (state) => { - state.isFetching = true; + /** + * Image deleted + */ + builder.addCase(imageDeleted.pending, (state, action) => { + // optimistic update, but no undo :/ + const { image_name } = action.meta.arg; + // remove image from all boards + forEach(state.boards, (board) => { + board.imageNames = board.imageNames.filter((n) => n !== image_name); + }); + // and selection + state.selection = state.selection.filter((n) => n !== image_name); }); - builder.addCase(receivedPageOfImages.rejected, (state) => { - state.isFetching = false; + /** + * Images loaded into gallery - PENDING + */ + builder.addCase(imagesLoaded.pending, (state, action) => { + const { board_id, view } = action.meta.arg; + state.boards[`${board_id}.${view}`].status = 'pending'; }); - builder.addCase(receivedPageOfImages.fulfilled, (state, action) => { - state.isFetching = false; - const { board_id, categories, image_origin, is_intermediate } = - action.meta.arg; + /** + * Images loaded into gallery - FULFILLED + */ + builder.addCase(imagesLoaded.fulfilled, (state, action) => { + const { items, total } = action.payload; + const { board_id, view } = action.meta.arg; + const board = state.boards[`${board_id}.${view}`]; - const { items, offset, limit, total } = action.payload; + board.status = 'fulfilled'; - galleryImagesAdapter.upsertMany(state, items); + board.imageNames = uniq( + board.imageNames.concat(items.map((i) => i.image_name)) + ); + + board.total = total; if (state.selection.length === 0 && items.length) { state.selection = [items[0].image_name]; } + }); + /** + * Images loaded into gallery - REJECTED + */ + builder.addCase(imagesLoaded.rejected, (state, action) => { + const { board_id, view } = action.meta.arg; + state.boards[`${board_id}.${view}`].status = 'rejected'; + }); + /** + * Image added to board + */ + builder.addMatcher( + boardImagesApi.endpoints.addBoardImage.matchFulfilled, + (state, action) => { + const { board_id, image_name } = action.meta.arg.originalArgs; + // update user board stores + const userBoards = selectUserBoards(state); + userBoards.forEach((board) => { + // only update the current view + if (board.view !== state.galleryView) { + return; + } - if (!categories?.includes('general') || board_id) { - // need to skip updating the total images count if the images recieved were for a specific board - // TODO: this doesn't work when on the Asset tab/category... - return; + if (board_id === board.id) { + // add image to the board + board.imageNames = uniq(board.imageNames.concat(image_name)); + } else { + // remove image from other boards + board.imageNames = board.imageNames.filter((n) => n !== image_name); + } + }); } - - state.offset = offset; - state.total = total; - }); - builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { - const { image_name, image_url, thumbnail_url } = action.payload; - - galleryImagesAdapter.updateOne(state, { - id: image_name, - changes: { image_url, thumbnail_url }, - }); - }); + ); + /** + * Many images added to board + */ + builder.addMatcher( + boardImagesApi.endpoints.addManyBoardImages.matchFulfilled, + (state, action) => { + const { board_id, image_names } = action.meta.arg.originalArgs; + // update local board stores + forEach(state.boards, (board, board_id) => { + // only update the current view + if (board_id === board.id) { + // add images to the board + board.imageNames = uniq(board.imageNames.concat(image_names)); + } else { + // remove images from other boards + board.imageNames = board.imageNames.filter((n) => + image_names.includes(n) + ); + } + }); + } + ); + /** + * Board deleted (not images) + */ builder.addMatcher( boardsApi.endpoints.deleteBoard.matchFulfilled, (state, action) => { - if (action.meta.arg.originalArgs === state.selectedBoardId) { - state.selectedBoardId = undefined; + const deletedBoardId = action.meta.arg.originalArgs; + if (deletedBoardId === state.selectedBoardId) { + state.selectedBoardId = 'all'; } + // remove board from local store + delete state.boards[`${deletedBoardId}.images`]; + delete state.boards[`${deletedBoardId}.assets`]; + } + ); + /** + * Board deleted (with images) + */ + builder.addMatcher( + boardsApi.endpoints.deleteBoardAndImages.matchFulfilled, + (state, action) => { + const { deleted_images } = action.payload; + const deletedBoardId = action.meta.arg.originalArgs; + // remove images from all boards + forEach(state.boards, (board) => { + // remove images from all boards + board.imageNames = board.imageNames.filter((n) => + deleted_images.includes(n) + ); + }); + + delete state.boards[`${deletedBoardId}.images`]; + delete state.boards[`${deletedBoardId}.assets`]; + } + ); + /** + * Image removed from board; i.e. Board reset for image + */ + builder.addMatcher( + boardImagesApi.endpoints.deleteBoardImage.matchFulfilled, + (state, action) => { + const { image_name } = action.meta.arg.originalArgs; + // remove from all user boards (skip all, none, batch) + const userBoards = selectUserBoards(state); + userBoards.forEach((board) => { + board.imageNames = board.imageNames.filter((n) => n !== image_name); + }); + } + ); + /** + * Many images removed from board; i.e. Board reset for many images + */ + builder.addMatcher( + boardImagesApi.endpoints.deleteManyBoardImages.matchFulfilled, + (state, action) => { + const { image_names } = action.meta.arg.originalArgs; + // remove images from all boards + forEach(state.imageNamesByIdAndView, (board) => { + // only update the current view + const view = board[state.galleryView]; + view.imageNames = view.imageNames.filter((n) => + image_names.includes(n) + ); + }); } ); }, @@ -203,12 +413,7 @@ export const { } = galleryImagesAdapter.getSelectors((state) => state.gallery); export const { - imageUpserted, - imageUpdatedOne, - imageUpdatedMany, - imageRemoved, imagesRemoved, - imageCategoriesChanged, imageRangeEndSelected, imageSelectionToggled, imageSelected, @@ -220,3 +425,13 @@ export const { } = gallerySlice.actions; export default gallerySlice.reducer; + +const selectUserBoards = (state: typeof initialGalleryState) => + filter(state.boards, (board, path) => !systemBoards.includes(path)); + +const selectCurrentBoard = (state: typeof initialGalleryState) => + state.boards[`${state.selectedBoardId}.${state.galleryView}`]; + +const isImagesView = (board: BoardPath) => board.split('.')[1] === 'images'; + +const isAssetsView = (board: BoardPath) => board.split('.')[1] === 'assets'; diff --git a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts index 01c7ff2958..7a04d88534 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts @@ -4,7 +4,7 @@ import { components, paths } from '../schema'; import { imagesApi } from './images'; type AddImageToBoardArg = - paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json']; + paths['/api/v1/board_images/{board_id}']['post']['requestBody']['content']['application/json']; type AddManyImagesToBoardArg = paths['/api/v1/board_images/{board_id}/images']['patch']['requestBody']['content']['application/json']; @@ -44,11 +44,14 @@ export const boardImagesApi = api.injectEndpoints({ * Board Images Mutations */ - addBoardImage: build.mutation({ + addBoardImage: build.mutation< + void, + { board_id: string; image_name: string } + >({ query: ({ board_id, image_name }) => ({ - url: `board_images/`, + url: `board_images/${board_id}`, method: 'POST', - body: { board_id, image_name }, + body: image_name, }), invalidatesTags: (result, error, arg) => [ { type: 'Board', id: arg.board_id }, diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index fc3cb530a4..f9d3df9980 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -1,6 +1,6 @@ import { BoardDTO, OffsetPaginatedResults_BoardDTO_ } from 'services/api/types'; import { ApiFullTagDescription, LIST_TAG, api } from '..'; -import { paths } from '../schema'; +import { components, paths } from '../schema'; type ListBoardsArg = NonNullable< paths['/api/v1/boards/']['get']['parameters']['query'] @@ -86,7 +86,10 @@ export const boardsApi = api.injectEndpoints({ query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }), invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }], }), - deleteBoardAndImages: build.mutation({ + deleteBoardAndImages: build.mutation< + components['schemas']['DeleteManyImagesResult'], + string + >({ query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE', diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index edda31fe75..b3fb3cf918 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -200,24 +200,24 @@ export type paths = { */ patch: operations["update_board"]; }; - "/api/v1/board_images/": { - /** - * Create Board Image - * @description Creates a board_image - */ - post: operations["create_board_image"]; - /** - * Remove Board Image - * @description Deletes a board_image - */ - delete: operations["remove_board_image"]; - }; "/api/v1/board_images/{board_id}": { /** * Get All Board Images For Board * @description Gets all image names for a board */ get: operations["get_all_board_images_for_board"]; + /** + * Create Board Image + * @description Creates a board_image + */ + post: operations["create_board_image"]; + }; + "/api/v1/board_images/": { + /** + * Remove Board Image + * @description Deletes a board_image + */ + delete: operations["remove_board_image"]; }; "/api/v1/board_images/{board_id}/images": { /** @@ -346,19 +346,6 @@ export type components = { */ image_count: number; }; - /** Body_create_board_image */ - Body_create_board_image: { - /** - * Board Id - * @description The id of the board to add to - */ - board_id: string; - /** - * Image Name - * @description The name of the image to add - */ - image_name: string; - }; /** Body_import_model */ Body_import_model: { /** @@ -4478,18 +4465,18 @@ export type components = { */ image?: components["schemas"]["ImageField"]; }; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusion1ModelFormat * @description An enumeration. * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; @@ -5418,7 +5405,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": unknown; + "application/json": components["schemas"]["DeleteManyImagesResult"]; }; }; /** @description Validation Error */ @@ -5460,14 +5447,46 @@ export type operations = { }; }; }; + /** + * Get All Board Images For Board + * @description Gets all image names for a board + */ + get_all_board_images_for_board: { + parameters: { + path: { + /** @description The id of the board */ + board_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["GetAllBoardImagesForBoardResult"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * Create Board Image * @description Creates a board_image */ create_board_image: { + parameters: { + path: { + /** @description The id of the board to add to */ + board_id: string; + }; + }; requestBody: { content: { - "application/json": components["schemas"]["Body_create_board_image"]; + "application/json": string; }; }; responses: { @@ -5510,32 +5529,6 @@ export type operations = { }; }; }; - /** - * Get All Board Images For Board - * @description Gets all image names for a board - */ - get_all_board_images_for_board: { - parameters: { - path: { - /** @description The id of the board */ - board_id: string; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["GetAllBoardImagesForBoardResult"]; - }; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; /** * Create Multiple Board Images * @description Add many images to a board diff --git a/invokeai/frontend/web/src/services/api/thunks/image.ts b/invokeai/frontend/web/src/services/api/thunks/image.ts index fa09699431..bd7ebb5362 100644 --- a/invokeai/frontend/web/src/services/api/thunks/image.ts +++ b/invokeai/frontend/web/src/services/api/thunks/image.ts @@ -4,6 +4,7 @@ import { size } from 'lodash-es'; import queryString from 'query-string'; import { $client } from 'services/api/client'; import { paths } from 'services/api/schema'; +import { ImageCategory, OffsetPaginatedResults_ImageDTO_ } from '../types'; type GetImageUrlsArg = paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path']; @@ -329,6 +330,75 @@ export const receivedPageOfImages = createAppAsyncThunk< } ); +export type ImagesLoadedArg = { + board_id: 'all' | 'none' | (string & Record); + view: 'images' | 'assets'; + offset: number; + limit?: number; +}; + +type ImagesLoadedThunkConfig = { + rejectValue: { + arg: ImagesLoadedArg; + error: unknown; + }; +}; + +const getCategories = (view: 'images' | 'assets'): ImageCategory[] => { + if (view === 'images') { + return ['general']; + } + return ['control', 'mask', 'user', 'other']; +}; + +const getBoardId = ( + board_id: 'all' | 'none' | (string & Record) +) => { + if (board_id === 'all') { + return undefined; + } + if (board_id === 'none') { + return 'none'; + } + return board_id; +}; + +/** + * `ImagesService.listImagesWithMetadata()` thunk + */ +export const imagesLoaded = createAppAsyncThunk< + OffsetPaginatedResults_ImageDTO_, + ImagesLoadedArg, + ImagesLoadedThunkConfig +>( + 'thunkApi/imagesLoaded', + async (arg, { getState, rejectWithValue, requestId }) => { + const { get } = $client.get(); + + // TODO: do not make request if request in progress + + const query = { + categories: getCategories(arg.view), + board_id: getBoardId(arg.board_id), + offset: arg.offset, + limit: arg.limit ?? IMAGES_PER_PAGE, + }; + + const { data, error, response } = await get('/api/v1/images/', { + params: { + query, + }, + querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }), + }); + + if (error) { + return rejectWithValue({ arg, error }); + } + + return data; + } +); + type GetImagesByNamesArg = NonNullable< paths['/api/v1/images/']['post']['requestBody']['content']['application/json'] >; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index c1ac8e9c7a..c6dcddc97a 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -1175,14 +1175,14 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.0": version "4.5.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== @@ -1982,7 +1982,7 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== -"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.11", "@types/json-schema@^7.0.6": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== @@ -2086,49 +2086,53 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== -"@typescript-eslint/eslint-plugin@^5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.0.tgz#2f4bea6a3718bed2ba52905358d0f45cd3620d31" - integrity sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg== +"@typescript-eslint/eslint-plugin@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.0.0.tgz#19ff4f1cab8d6f8c2c1825150f7a840bc5d9bdc4" + integrity sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A== dependencies: - "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.60.0" - "@typescript-eslint/type-utils" "5.60.0" - "@typescript-eslint/utils" "5.60.0" + "@eslint-community/regexpp" "^4.5.0" + "@typescript-eslint/scope-manager" "6.0.0" + "@typescript-eslint/type-utils" "6.0.0" + "@typescript-eslint/utils" "6.0.0" + "@typescript-eslint/visitor-keys" "6.0.0" debug "^4.3.4" grapheme-splitter "^1.0.4" - ignore "^5.2.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" natural-compare-lite "^1.4.0" - semver "^7.3.7" - tsutils "^3.21.0" + semver "^7.5.0" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.0.tgz#08f4daf5fc6548784513524f4f2f359cebb4068a" - integrity sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ== +"@typescript-eslint/parser@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.0.0.tgz#46b2600fd1f67e62fc00a28093a75f41bf7effc4" + integrity sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg== dependencies: - "@typescript-eslint/scope-manager" "5.60.0" - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/typescript-estree" "5.60.0" + "@typescript-eslint/scope-manager" "6.0.0" + "@typescript-eslint/types" "6.0.0" + "@typescript-eslint/typescript-estree" "6.0.0" + "@typescript-eslint/visitor-keys" "6.0.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c" - integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ== +"@typescript-eslint/scope-manager@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.0.0.tgz#8ede47a37cb2b7ed82d329000437abd1113b5e11" + integrity sha512-o4q0KHlgCZTqjuaZ25nw5W57NeykZT9LiMEG4do/ovwvOcPnDO1BI5BQdCsUkjxFyrCL0cSzLjvIMfR9uo7cWg== dependencies: - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/visitor-keys" "5.60.0" + "@typescript-eslint/types" "6.0.0" + "@typescript-eslint/visitor-keys" "6.0.0" -"@typescript-eslint/type-utils@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz#69b09087eb12d7513d5b07747e7d47f5533aa228" - integrity sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g== +"@typescript-eslint/type-utils@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.0.0.tgz#0478d8a94f05e51da2877cc0500f1b3c27ac7e18" + integrity sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ== dependencies: - "@typescript-eslint/typescript-estree" "5.60.0" - "@typescript-eslint/utils" "5.60.0" + "@typescript-eslint/typescript-estree" "6.0.0" + "@typescript-eslint/utils" "6.0.0" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" "@typescript-eslint/types@4.33.0": version "4.33.0" @@ -2140,18 +2144,23 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558" integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA== -"@typescript-eslint/typescript-estree@5.60.0", "@typescript-eslint/typescript-estree@^5.55.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600" - integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ== +"@typescript-eslint/types@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.0.0.tgz#19795f515f8decbec749c448b0b5fc76d82445a1" + integrity sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg== + +"@typescript-eslint/typescript-estree@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.0.0.tgz#1e09aab7320e404fb9f83027ea568ac24e372f81" + integrity sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ== dependencies: - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/visitor-keys" "5.60.0" + "@typescript-eslint/types" "6.0.0" + "@typescript-eslint/visitor-keys" "6.0.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + semver "^7.5.0" + ts-api-utils "^1.0.1" "@typescript-eslint/typescript-estree@^4.33.0": version "4.33.0" @@ -2166,19 +2175,32 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.60.0": +"@typescript-eslint/typescript-estree@^5.55.0": version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c" - integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ== + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600" + integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.60.0" "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/typescript-estree" "5.60.0" - eslint-scope "^5.1.1" + "@typescript-eslint/visitor-keys" "5.60.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.0.0.tgz#27a16d0d8f2719274a39417b9782f7daa3802db0" + integrity sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ== + dependencies: + "@eslint-community/eslint-utils" "^4.3.0" + "@types/json-schema" "^7.0.11" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "6.0.0" + "@typescript-eslint/types" "6.0.0" + "@typescript-eslint/typescript-estree" "6.0.0" + eslint-scope "^5.1.1" + semver "^7.5.0" "@typescript-eslint/visitor-keys@4.33.0": version "4.33.0" @@ -2196,6 +2218,14 @@ "@typescript-eslint/types" "5.60.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.0.0.tgz#0b49026049fbd096d2c00c5e784866bc69532a31" + integrity sha512-cvJ63l8c0yXdeT5POHpL0Q1cZoRcmRKFCtSjNGJxPkcP571EfZMcNbzWAc7oK3D1dRzm/V5EwtkANTZxqvuuUA== + dependencies: + "@typescript-eslint/types" "6.0.0" + eslint-visitor-keys "^3.4.1" + "@vitejs/plugin-react-swc@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz#34a82c1728066f48a86dfecb2f15df60f89207fb" @@ -3426,7 +3456,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.43.0: +eslint@^8.44.0: version "8.44.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== @@ -4069,7 +4099,7 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -5795,6 +5825,13 @@ semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.5.0: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@~7.3.0: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" @@ -6235,6 +6272,11 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-api-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" + integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== + ts-easing@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" @@ -6338,6 +6380,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typescript-eslint@^0.0.1-alpha.0: + version "0.0.1-alpha.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz#285d68a4e96588295cd436278801bcb6a6b916c1" + integrity sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg== + typescript@^3.9.10, typescript@^3.9.7: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" @@ -6348,6 +6395,11 @@ typescript@^4.0.0, typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + typescript@~5.0.4: version "5.0.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"