diff --git a/invokeai/app/api/routers/board_images.py b/invokeai/app/api/routers/board_images.py
index b206ab500d..651310af24 100644
--- a/invokeai/app/api/routers/board_images.py
+++ b/invokeai/app/api/routers/board_images.py
@@ -24,11 +24,14 @@ async def create_board_image(
):
"""Creates a board_image"""
try:
- result = ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
+ result = ApiDependencies.invoker.services.board_images.add_image_to_board(
+ board_id=board_id, image_name=image_name
+ )
return result
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to add to board")
-
+
+
@board_images_router.delete(
"/",
operation_id="remove_board_image",
@@ -43,27 +46,10 @@ async def remove_board_image(
):
"""Deletes a board_image"""
try:
- result = ApiDependencies.invoker.services.board_images.remove_image_from_board(board_id=board_id, image_name=image_name)
+ result = ApiDependencies.invoker.services.board_images.remove_image_from_board(
+ board_id=board_id, image_name=image_name
+ )
return result
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to update board")
-
-
-@board_images_router.get(
- "/{board_id}",
- operation_id="list_board_images",
- response_model=OffsetPaginatedResults[ImageDTO],
-)
-async def list_board_images(
- board_id: str = Path(description="The id of the board"),
- offset: int = Query(default=0, description="The page offset"),
- limit: int = Query(default=10, description="The number of boards per page"),
-) -> OffsetPaginatedResults[ImageDTO]:
- """Gets a list of images for a board"""
-
- results = ApiDependencies.invoker.services.board_images.get_images_for_board(
- board_id,
- )
- return results
-
diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
index 94d8667ae4..f3de7f4952 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -1,16 +1,28 @@
from typing import Optional, Union
+
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+
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"])
+class DeleteBoardResult(BaseModel):
+ board_id: str = Field(description="The id of the board that was deleted.")
+ deleted_board_images: list[str] = Field(
+ description="The image names of the board-images relationships that were deleted."
+ )
+ deleted_images: list[str] = Field(
+ description="The names of the images that were deleted."
+ )
+
+
@boards_router.post(
"/",
operation_id="create_board",
@@ -69,25 +81,42 @@ 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=DeleteBoardResult
+)
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:
+) -> DeleteBoardResult:
"""Deletes a board"""
try:
if include_images is True:
+ deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id=board_id
+ )
ApiDependencies.invoker.services.images.delete_images_on_board(
board_id=board_id
)
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ return DeleteBoardResult(
+ board_id=board_id,
+ deleted_board_images=[],
+ deleted_images=deleted_images,
+ )
else:
+ deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id=board_id
+ )
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ return DeleteBoardResult(
+ board_id=board_id,
+ deleted_board_images=deleted_board_images,
+ deleted_images=[],
+ )
except Exception as e:
- # TODO: Does this need any exception handling at all?
- pass
+ raise HTTPException(status_code=500, detail="Failed to delete board")
@boards_router.get(
@@ -115,3 +144,19 @@ async def list_boards(
status_code=400,
detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'",
)
+
+
+@boards_router.get(
+ "/{board_id}/image_names",
+ operation_id="list_all_board_image_names",
+ response_model=list[str],
+)
+async def list_all_board_image_names(
+ board_id: str = Path(description="The id of the board"),
+) -> list[str]:
+ """Gets a list of images for a board"""
+
+ image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id,
+ )
+ return image_names
diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py
index 3da94df7f4..559afa3b37 100644
--- a/invokeai/app/api/routers/images.py
+++ b/invokeai/app/api/routers/images.py
@@ -84,6 +84,17 @@ async def delete_image(
# TODO: Does this need any exception handling at all?
pass
+@images_router.post("/clear-intermediates", operation_id="clear_intermediates")
+async def clear_intermediates() -> int:
+ """Clears first 100 intermediates"""
+
+ try:
+ count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True)
+ return count_deleted
+ except Exception as e:
+ # TODO: Does this need any exception handling at all?
+ pass
+
@images_router.patch(
"/{image_name}",
@@ -234,16 +245,16 @@ async def get_image_urls(
)
async def list_image_dtos(
image_origin: Optional[ResourceOrigin] = Query(
- default=None, description="The origin of images to list"
+ default=None, description="The origin of images to list."
),
categories: Optional[list[ImageCategory]] = Query(
- default=None, description="The categories of image to include"
+ default=None, description="The categories of image to include."
),
is_intermediate: Optional[bool] = Query(
- default=None, description="Whether to list intermediate images"
+ default=None, description="Whether to list intermediate images."
),
board_id: Optional[str] = Query(
- default=None, description="The board id to filter by"
+ default=None, description="The board id to filter by. Use 'none' to find images without a board."
),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"),
diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py
index 2800624235..743a648785 100644
--- a/invokeai/app/invocations/latent.py
+++ b/invokeai/app/invocations/latent.py
@@ -22,7 +22,7 @@ from ...backend.stable_diffusion.diffusers_pipeline import (
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
PostprocessingSettings
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
-from ...backend.util.devices import torch_dtype
+from ...backend.util.devices import choose_torch_device, torch_dtype
from ..models.image import ImageCategory, ImageField, ResourceOrigin
from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext)
@@ -38,7 +38,6 @@ from diffusers.models.attention_processor import (
XFormersAttnProcessor,
)
-
class LatentsField(BaseModel):
"""A latents field used for passing latents between invocations"""
diff --git a/invokeai/app/services/board_image_record_storage.py b/invokeai/app/services/board_image_record_storage.py
index 197a639157..491972bd32 100644
--- a/invokeai/app/services/board_image_record_storage.py
+++ b/invokeai/app/services/board_image_record_storage.py
@@ -32,11 +32,11 @@ class BoardImageRecordStorageBase(ABC):
pass
@abstractmethod
- def get_images_for_board(
+ def get_all_board_image_names_for_board(
self,
board_id: str,
- ) -> OffsetPaginatedResults[ImageRecord]:
- """Gets images for a board."""
+ ) -> list[str]:
+ """Gets all board images for a board, as a list of the image names."""
pass
@abstractmethod
@@ -211,6 +211,26 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
items=images, offset=offset, limit=limit, total=count
)
+ def get_all_board_image_names_for_board(self, board_id: str) -> list[str]:
+ try:
+ self._lock.acquire()
+ self._cursor.execute(
+ """--sql
+ SELECT image_name
+ FROM board_images
+ WHERE board_id = ?;
+ """,
+ (board_id,),
+ )
+ result = cast(list[sqlite3.Row], self._cursor.fetchall())
+ image_names = list(map(lambda r: r[0], result))
+ return image_names
+ except sqlite3.Error as e:
+ self._conn.rollback()
+ raise e
+ finally:
+ self._lock.release()
+
def get_board_for_image(
self,
image_name: str,
diff --git a/invokeai/app/services/board_images.py b/invokeai/app/services/board_images.py
index 1ba225338b..b9f9663603 100644
--- a/invokeai/app/services/board_images.py
+++ b/invokeai/app/services/board_images.py
@@ -38,11 +38,11 @@ class BoardImagesServiceABC(ABC):
pass
@abstractmethod
- def get_images_for_board(
+ def get_all_board_image_names_for_board(
self,
board_id: str,
- ) -> OffsetPaginatedResults[ImageDTO]:
- """Gets images for a board."""
+ ) -> list[str]:
+ """Gets all board images for a board, as a list of the image names."""
pass
@abstractmethod
@@ -98,30 +98,13 @@ class BoardImagesService(BoardImagesServiceABC):
) -> None:
self._services.board_image_records.remove_image_from_board(board_id, image_name)
- def get_images_for_board(
+ def get_all_board_image_names_for_board(
self,
board_id: str,
- ) -> OffsetPaginatedResults[ImageDTO]:
- image_records = self._services.board_image_records.get_images_for_board(
+ ) -> list[str]:
+ return self._services.board_image_records.get_all_board_image_names_for_board(
board_id
)
- image_dtos = list(
- map(
- lambda r: image_record_to_dto(
- r,
- self._services.urls.get_image_url(r.image_name),
- self._services.urls.get_image_url(r.image_name, True),
- board_id,
- ),
- image_records.items,
- )
- )
- return OffsetPaginatedResults[ImageDTO](
- items=image_dtos,
- offset=image_records.offset,
- limit=image_records.limit,
- total=image_records.total,
- )
def get_board_for_image(
self,
@@ -136,7 +119,7 @@ def board_record_to_dto(
) -> BoardDTO:
"""Converts a board record to a board DTO."""
return BoardDTO(
- **board_record.dict(exclude={'cover_image_name'}),
+ **board_record.dict(exclude={"cover_image_name"}),
cover_image_name=cover_image_name,
image_count=image_count,
)
diff --git a/invokeai/app/services/events.py b/invokeai/app/services/events.py
index 30d1b5e7a9..35003536e6 100644
--- a/invokeai/app/services/events.py
+++ b/invokeai/app/services/events.py
@@ -141,7 +141,7 @@ class EventServiceBase:
model_type=model_type,
submodel=submodel,
hash=model_info.hash,
- location=model_info.location,
+ location=str(model_info.location),
precision=str(model_info.precision),
),
)
diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py
index 7b37307ce8..09c3bdcc3e 100644
--- a/invokeai/app/services/image_record_storage.py
+++ b/invokeai/app/services/image_record_storage.py
@@ -10,7 +10,10 @@ from pydantic.generics import GenericModel
from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.services.models.image_record import (
- ImageRecord, ImageRecordChanges, deserialize_image_record)
+ ImageRecord,
+ ImageRecordChanges,
+ deserialize_image_record,
+)
T = TypeVar("T", bound=BaseModel)
@@ -97,8 +100,8 @@ class ImageRecordStorageBase(ABC):
@abstractmethod
def get_many(
self,
- offset: int = 0,
- limit: int = 10,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
image_origin: Optional[ResourceOrigin] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
@@ -322,8 +325,8 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
def get_many(
self,
- offset: int = 0,
- limit: int = 10,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
image_origin: Optional[ResourceOrigin] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
@@ -377,11 +380,15 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
query_params.append(is_intermediate)
- if board_id is not None:
+ # board_id of "none" is reserved for images without a board
+ if board_id == "none":
+ query_conditions += """--sql
+ AND board_images.board_id IS NULL
+ """
+ elif board_id is not None:
query_conditions += """--sql
AND board_images.board_id = ?
"""
-
query_params.append(board_id)
query_pagination = """--sql
@@ -392,8 +399,12 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
images_query += query_conditions + query_pagination + ";"
# Add all the parameters
images_params = query_params.copy()
- images_params.append(limit)
- images_params.append(offset)
+
+ if limit is not None:
+ images_params.append(limit)
+ if offset is not None:
+ images_params.append(offset)
+
# Build the list of images, deserializing each row
self._cursor.execute(images_query, images_params)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py
index a7d0b6ddee..13c6c04719 100644
--- a/invokeai/app/services/images.py
+++ b/invokeai/app/services/images.py
@@ -11,7 +11,6 @@ from invokeai.app.models.image import (ImageCategory,
InvalidOriginException, ResourceOrigin)
from invokeai.app.services.board_image_record_storage import \
BoardImageRecordStorageBase
-from invokeai.app.services.graph import Graph
from invokeai.app.services.image_file_storage import (
ImageFileDeleteException, ImageFileNotFoundException,
ImageFileSaveException, ImageFileStorageBase)
@@ -109,6 +108,13 @@ class ImageServiceABC(ABC):
"""Deletes an image."""
pass
+ @abstractmethod
+ def delete_many(self, is_intermediate: bool) -> int:
+ """Deletes many images."""
+ pass
+
+
+
@abstractmethod
def delete_images_on_board(self, board_id: str):
"""Deletes all images on a board."""
@@ -378,16 +384,39 @@ class ImageService(ImageServiceABC):
def delete_images_on_board(self, board_id: str):
try:
- images = self._services.board_image_records.get_images_for_board(board_id)
- image_name_list = list(
- map(
- lambda r: r.image_name,
- images.items,
+ image_names = (
+ self._services.board_image_records.get_all_board_image_names_for_board(
+ board_id
)
)
- for image_name in image_name_list:
+ for image_name in image_names:
self._services.image_files.delete(image_name)
- self._services.image_records.delete_many(image_name_list)
+ self._services.image_records.delete_many(image_names)
+ except ImageRecordDeleteException:
+ self._services.logger.error(f"Failed to delete image records")
+ raise
+ except ImageFileDeleteException:
+ self._services.logger.error(f"Failed to delete image files")
+ raise
+ except Exception as e:
+ self._services.logger.error("Problem deleting image records and files")
+ raise e
+
+ def delete_many(self, is_intermediate: bool):
+ try:
+ # only clears 100 at a time
+ images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,)
+ count = len(images.items)
+ image_name_list = list(
+ map(
+ lambda r: r.image_name,
+ images.items,
+ )
+ )
+ for image_name in image_name_list:
+ self._services.image_files.delete(image_name)
+ self._services.image_records.delete_many(image_name_list)
+ return count
except ImageRecordDeleteException:
self._services.logger.error(f"Failed to delete image records")
raise
diff --git a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py
index e3e64940de..d9d4262e47 100644
--- a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py
+++ b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py
@@ -21,6 +21,7 @@ import re
import warnings
from pathlib import Path
from typing import Union
+from packaging import version
import torch
from safetensors.torch import load_file
@@ -63,6 +64,7 @@ from diffusers.pipelines.stable_diffusion.safety_checker import (
StableDiffusionSafetyChecker,
)
from diffusers.utils import is_safetensors_available
+import transformers
from transformers import (
AutoFeatureExtractor,
BertTokenizerFast,
@@ -841,7 +843,16 @@ def convert_ldm_clip_checkpoint(checkpoint):
key
]
- text_model.load_state_dict(text_model_dict)
+ # transformers 4.31.0 and higher - this key no longer in state dict
+ if version.parse(transformers.__version__) >= version.parse("4.31.0"):
+ position_ids = text_model_dict.pop("text_model.embeddings.position_ids", None)
+ text_model.load_state_dict(text_model_dict)
+ if position_ids is not None:
+ text_model.text_model.embeddings.position_ids.copy_(position_ids)
+
+ # transformers 4.30.2 and lower - position_ids is part of state_dict
+ else:
+ text_model.load_state_dict(text_model_dict)
return text_model
@@ -947,7 +958,16 @@ def convert_open_clip_checkpoint(checkpoint):
text_model_dict[new_key] = checkpoint[key]
- text_model.load_state_dict(text_model_dict)
+ # transformers 4.31.0 and higher - this key no longer in state dict
+ if version.parse(transformers.__version__) >= version.parse("4.31.0"):
+ position_ids = text_model_dict.pop("text_model.embeddings.position_ids", None)
+ text_model.load_state_dict(text_model_dict)
+ if position_ids is not None:
+ text_model.text_model.embeddings.position_ids.copy_(position_ids)
+
+ # transformers 4.30.2 and lower - position_ids is part of state_dict
+ else:
+ text_model.load_state_dict(text_model_dict)
return text_model
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index a05266d5f2..092f1b0f89 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -15,7 +15,6 @@ import InvokeTabs from 'features/ui/components/InvokeTabs';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import i18n from 'i18n';
import { ReactNode, memo, useEffect } from 'react';
-import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster';
@@ -84,7 +83,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
-
>
diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/DragPreview.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/DragPreview.tsx
index 942365848e..e8fa949e9a 100644
--- a/invokeai/frontend/web/src/app/components/ImageDnd/DragPreview.tsx
+++ b/invokeai/frontend/web/src/app/components/ImageDnd/DragPreview.tsx
@@ -15,10 +15,7 @@ const STYLES: ChakraProps['sx'] = {
maxH: BOX_SIZE,
shadow: 'dark-lg',
borderRadius: 'lg',
- borderWidth: 2,
- borderStyle: 'dashed',
- borderColor: 'base.100',
- opacity: 0.5,
+ opacity: 0.3,
bg: 'base.800',
color: 'base.50',
_dark: {
diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx
index 6ce9b06bd9..91e274930c 100644
--- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx
+++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx
@@ -28,6 +28,7 @@ const ImageDndContext = (props: ImageDndContextProps) => {
const dispatch = useAppDispatch();
const handleDragStart = useCallback((event: DragStartEvent) => {
+ console.log('dragStart', event.active.data.current);
const activeData = event.active.data.current;
if (!activeData) {
return;
@@ -37,15 +38,16 @@ const ImageDndContext = (props: ImageDndContextProps) => {
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
+ console.log('dragEnd', event.active.data.current);
const activeData = event.active.data.current;
const overData = event.over?.data.current;
- if (!activeData || !overData) {
+ if (!activeDragData || !overData) {
return;
}
- dispatch(dndDropped({ overData, activeData }));
+ dispatch(dndDropped({ overData, activeData: activeDragData }));
setActiveDragData(null);
},
- [dispatch]
+ [activeDragData, dispatch]
);
const mouseSensor = useSensor(MouseSensor, {
diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx
index 003142390f..af4b5bbe3b 100644
--- a/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx
+++ b/invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx
@@ -11,6 +11,7 @@ import {
useDraggable as useOriginalDraggable,
useDroppable as useOriginalDroppable,
} from '@dnd-kit/core';
+import { BoardId } from 'features/gallery/store/gallerySlice';
import { ImageDTO } from 'services/api/types';
type BaseDropData = {
@@ -55,7 +56,7 @@ export type AddToBatchDropData = BaseDropData & {
export type MoveBoardDropData = BaseDropData & {
actionType: 'MOVE_BOARD';
- context: { boardId: string | null };
+ context: { boardId: BoardId };
};
export type TypesafeDroppableData =
@@ -158,8 +159,36 @@ export const isValidDrop = (
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
case 'ADD_TO_BATCH':
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
- case 'MOVE_BOARD':
- return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
+ case 'MOVE_BOARD': {
+ // If the board is the same, don't allow the drop
+
+ // Check the payload types
+ const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
+ if (!isPayloadValid) {
+ return false;
+ }
+
+ // Check if the image's board is the board we are dragging onto
+ if (payloadType === 'IMAGE_DTO') {
+ const { imageDTO } = active.data.current.payload;
+ const currentBoard = imageDTO.board_id;
+ const destinationBoard = overData.context.boardId;
+
+ const isSameBoard = currentBoard === destinationBoard;
+ const isDestinationValid = !currentBoard
+ ? destinationBoard !== 'no_board'
+ : true;
+
+ return !isSameBoard && isDestinationValid;
+ }
+
+ if (payloadType === 'IMAGE_NAMES') {
+ // TODO (multi-select)
+ return false;
+ }
+
+ return true;
+ }
default:
return false;
}
diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
index 105f8f18d7..3136354730 100644
--- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
+++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
@@ -18,7 +18,6 @@ import { Middleware } from '@reduxjs/toolkit';
import ImageDndContext from './ImageDnd/ImageDndContext';
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
import { $authToken, $baseUrl } from 'services/api/client';
-import { DeleteBoardImagesContextProvider } from '../contexts/DeleteBoardImagesContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@@ -78,9 +77,7 @@ const InvokeAIUI = ({
-
-
-
+
diff --git a/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
index f37f06d4b1..d5b3b746f1 100644
--- a/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
+++ b/invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx
@@ -1,7 +1,8 @@
import { useDisclosure } from '@chakra-ui/react';
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api/types';
-import { useAddImageToBoardMutation } from 'services/api/endpoints/boardImages';
+import { imagesApi } from 'services/api/endpoints/images';
+import { useAppDispatch } from '../store/storeHooks';
export type ImageUsage = {
isInitialImage: boolean;
@@ -40,8 +41,7 @@ type Props = PropsWithChildren;
export const AddImageToBoardContextProvider = (props: Props) => {
const [imageToMove, setImageToMove] = useState();
const { isOpen, onOpen, onClose } = useDisclosure();
-
- const [addImageToBoard, result] = useAddImageToBoardMutation();
+ const dispatch = useAppDispatch();
// Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => {
@@ -63,14 +63,16 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const handleAddToBoard = useCallback(
(boardId: string) => {
if (imageToMove) {
- addImageToBoard({
- board_id: boardId,
- image_name: imageToMove.image_name,
- });
+ dispatch(
+ imagesApi.endpoints.addImageToBoard.initiate({
+ imageDTO: imageToMove,
+ board_id: boardId,
+ })
+ );
closeAndClearImageToDelete();
}
},
- [addImageToBoard, closeAndClearImageToDelete, imageToMove]
+ [dispatch, closeAndClearImageToDelete, imageToMove]
);
return (
diff --git a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx
deleted file mode 100644
index 15f9fab282..0000000000
--- a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import { useDisclosure } from '@chakra-ui/react';
-import { PropsWithChildren, createContext, useCallback, useState } from 'react';
-import { BoardDTO } from 'services/api/types';
-import { useDeleteBoardMutation } from '../../services/api/endpoints/boards';
-import { defaultSelectorOptions } from '../store/util/defaultMemoizeOptions';
-import { createSelector } from '@reduxjs/toolkit';
-import { some } from 'lodash-es';
-import { canvasSelector } from 'features/canvas/store/canvasSelectors';
-import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
-import { selectImagesById } from 'features/gallery/store/gallerySlice';
-import { nodesSelector } from 'features/nodes/store/nodesSlice';
-import { generationSelector } from 'features/parameters/store/generationSelectors';
-import { RootState } from '../store/store';
-import { useAppDispatch, useAppSelector } from '../store/storeHooks';
-import { ImageUsage } from './DeleteImageContext';
-import { requestedBoardImagesDeletion } from 'features/gallery/store/actions';
-
-export const selectBoardImagesUsage = createSelector(
- [
- (state: RootState) => state,
- generationSelector,
- canvasSelector,
- nodesSelector,
- controlNetSelector,
- (state: RootState, board_id?: string) => board_id,
- ],
- (state, generation, canvas, nodes, controlNet, board_id) => {
- const initialImage = generation.initialImage
- ? selectImagesById(state, generation.initialImage.imageName)
- : undefined;
- const isInitialImage = initialImage?.board_id === board_id;
-
- const isCanvasImage = canvas.layerState.objects.some((obj) => {
- if (obj.kind === 'image') {
- const image = selectImagesById(state, obj.imageName);
- return image?.board_id === board_id;
- }
- return false;
- });
-
- const isNodesImage = nodes.nodes.some((node) => {
- return some(node.data.inputs, (input) => {
- if (input.type === 'image' && input.value) {
- const image = selectImagesById(state, input.value.image_name);
- return image?.board_id === board_id;
- }
- return false;
- });
- });
-
- const isControlNetImage = some(controlNet.controlNets, (c) => {
- const controlImage = c.controlImage
- ? selectImagesById(state, c.controlImage)
- : undefined;
- const processedControlImage = c.processedControlImage
- ? selectImagesById(state, c.processedControlImage)
- : undefined;
- return (
- controlImage?.board_id === board_id ||
- processedControlImage?.board_id === board_id
- );
- });
-
- const imageUsage: ImageUsage = {
- isInitialImage,
- isCanvasImage,
- isNodesImage,
- isControlNetImage,
- };
-
- return imageUsage;
- },
- defaultSelectorOptions
-);
-
-type DeleteBoardImagesContextValue = {
- /**
- * Whether the move image dialog is open.
- */
- isOpen: boolean;
- /**
- * Closes the move image dialog.
- */
- onClose: () => void;
- imagesUsage?: ImageUsage;
- board?: BoardDTO;
- onClickDeleteBoardImages: (board: BoardDTO) => void;
- handleDeleteBoardImages: (boardId: string) => void;
- handleDeleteBoardOnly: (boardId: string) => void;
-};
-
-export const DeleteBoardImagesContext =
- createContext({
- isOpen: false,
- onClose: () => undefined,
- onClickDeleteBoardImages: () => undefined,
- handleDeleteBoardImages: () => undefined,
- handleDeleteBoardOnly: () => undefined,
- });
-
-type Props = PropsWithChildren;
-
-export const DeleteBoardImagesContextProvider = (props: Props) => {
- const [boardToDelete, setBoardToDelete] = useState();
- const { isOpen, onOpen, onClose } = useDisclosure();
- const dispatch = useAppDispatch();
-
- // Check where the board images to be deleted are used (eg init image, controlnet, etc.)
- const imagesUsage = useAppSelector((state) =>
- selectBoardImagesUsage(state, boardToDelete?.board_id)
- );
-
- const [deleteBoard] = useDeleteBoardMutation();
-
- // Clean up after deleting or dismissing the modal
- const closeAndClearBoardToDelete = useCallback(() => {
- setBoardToDelete(undefined);
- onClose();
- }, [onClose]);
-
- const onClickDeleteBoardImages = useCallback(
- (board?: BoardDTO) => {
- console.log({ board });
- if (!board) {
- return;
- }
- setBoardToDelete(board);
- onOpen();
- },
- [setBoardToDelete, onOpen]
- );
-
- const handleDeleteBoardImages = useCallback(
- (boardId: string) => {
- if (boardToDelete) {
- dispatch(
- requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage })
- );
- closeAndClearBoardToDelete();
- }
- },
- [dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage]
- );
-
- const handleDeleteBoardOnly = useCallback(
- (boardId: string) => {
- if (boardToDelete) {
- deleteBoard(boardId);
- closeAndClearBoardToDelete();
- }
- },
- [deleteBoard, closeAndClearBoardToDelete, boardToDelete]
- );
-
- return (
-
- {props.children}
-
- );
-};
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 35bfde2bff..6c3a4508b4 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -11,7 +11,7 @@ import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingA
import { addAppConfigReceivedListener } from './listeners/appConfigReceived';
import { addAppStartedListener } from './listeners/appStarted';
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
-import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
+import { addDeleteBoardAndImagesFulfilledListener } from './listeners/boardAndImagesDeleted';
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
import { addCanvasMergedListener } from './listeners/canvasMerged';
@@ -29,10 +29,6 @@ import {
addRequestedImageDeletionListener,
} from './listeners/imageDeleted';
import { addImageDroppedListener } from './listeners/imageDropped';
-import {
- addImageMetadataReceivedFulfilledListener,
- addImageMetadataReceivedRejectedListener,
-} from './listeners/imageMetadataReceived';
import {
addImageRemovedFromBoardFulfilledListener,
addImageRemovedFromBoardRejectedListener,
@@ -46,18 +42,10 @@ import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
} from './listeners/imageUploaded';
-import {
- addImageUrlsReceivedFulfilledListener,
- addImageUrlsReceivedRejectedListener,
-} from './listeners/imageUrlsReceived';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addModelSelectedListener } from './listeners/modelSelected';
import { addModelsLoadedListener } from './listeners/modelsLoaded';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
-import {
- addReceivedPageOfImagesFulfilledListener,
- addReceivedPageOfImagesRejectedListener,
-} from './listeners/receivedPageOfImages';
import {
addSessionCanceledFulfilledListener,
addSessionCanceledPendingListener,
@@ -91,6 +79,7 @@ import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextTo
import { addModelLoadStartedEventListener } from './listeners/socketio/socketModelLoadStarted';
import { addModelLoadCompletedEventListener } from './listeners/socketio/socketModelLoadCompleted';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
+import { addFirstListImagesListener } from './listeners/addFirstListImagesListener.ts';
export const listenerMiddleware = createListenerMiddleware();
@@ -132,17 +121,9 @@ addRequestedImageDeletionListener();
addImageDeletedPendingListener();
addImageDeletedFulfilledListener();
addImageDeletedRejectedListener();
-addRequestedBoardImageDeletionListener();
+addDeleteBoardAndImagesFulfilledListener();
addImageToDeleteSelectedListener();
-// Image metadata
-addImageMetadataReceivedFulfilledListener();
-addImageMetadataReceivedRejectedListener();
-
-// Image URLs
-addImageUrlsReceivedFulfilledListener();
-addImageUrlsReceivedRejectedListener();
-
// User Invoked
addUserInvokedCanvasListener();
addUserInvokedNodesListener();
@@ -198,17 +179,10 @@ addSessionCanceledPendingListener();
addSessionCanceledFulfilledListener();
addSessionCanceledRejectedListener();
-// Fetching images
-addReceivedPageOfImagesFulfilledListener();
-addReceivedPageOfImagesRejectedListener();
-
// ControlNet
addControlNetImageProcessedListener();
addControlNetAutoProcessListener();
-// Update image URLs on connect
-// addUpdateImageUrlsOnConnectListener();
-
// Boards
addImageAddedToBoardFulfilledListener();
addImageAddedToBoardRejectedListener();
@@ -229,5 +203,7 @@ addModelSelectedListener();
addAppStartedListener();
addModelsLoadedListener();
addAppConfigReceivedListener();
+addFirstListImagesListener();
+// Ad-hoc upscale workflwo
addUpscaleRequestedListener();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts
new file mode 100644
index 0000000000..d01a6440a8
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts
@@ -0,0 +1,43 @@
+import { createAction } from '@reduxjs/toolkit';
+import {
+ IMAGE_CATEGORIES,
+ imageSelected,
+} from 'features/gallery/store/gallerySlice';
+import {
+ ImageCache,
+ getListImagesUrl,
+ imagesApi,
+} from 'services/api/endpoints/images';
+import { startAppListening } from '..';
+
+export const appStarted = createAction('app/appStarted');
+
+export const addFirstListImagesListener = () => {
+ startAppListening({
+ matcher: imagesApi.endpoints.listImages.matchFulfilled,
+ effect: async (
+ action,
+ { getState, dispatch, unsubscribe, cancelActiveListeners }
+ ) => {
+ // Only run this listener on the first listImages request for `images` categories
+ if (
+ action.meta.arg.queryCacheKey !==
+ getListImagesUrl({ categories: IMAGE_CATEGORIES })
+ ) {
+ return;
+ }
+
+ // this should only run once
+ cancelActiveListeners();
+ unsubscribe();
+
+ // TODO: figure out how to type the predicate
+ const data = action.payload as ImageCache;
+
+ if (data.ids.length > 0) {
+ // Select the first image
+ dispatch(imageSelected(data.ids[0] as string));
+ }
+ },
+ });
+};
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 9f7085db6f..cfe9fd4a1c 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,11 +1,4 @@
import { createAction } from '@reduxjs/toolkit';
-import {
- ASSETS_CATEGORIES,
- IMAGE_CATEGORIES,
- 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');
@@ -17,29 +10,9 @@ export const addAppStartedListener = () => {
action,
{ getState, dispatch, unsubscribe, cancelActiveListeners }
) => {
+ // this should only run once
cancelActiveListeners();
unsubscribe();
- // fill up the gallery tab with images
- await dispatch(
- receivedPageOfImages({
- categories: IMAGE_CATEGORIES,
- is_intermediate: false,
- offset: 0,
- limit: INITIAL_IMAGE_LIMIT,
- })
- );
-
- // fill up the assets tab with images
- await dispatch(
- receivedPageOfImages({
- categories: ASSETS_CATEGORIES,
- is_intermediate: false,
- offset: 0,
- limit: INITIAL_IMAGE_LIMIT,
- })
- );
-
- dispatch(isLoadingChanged(false));
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
new file mode 100644
index 0000000000..8c5572f399
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
@@ -0,0 +1,48 @@
+import { resetCanvas } from 'features/canvas/store/canvasSlice';
+import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
+import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSlice';
+import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
+import { clearInitialImage } from 'features/parameters/store/generationSlice';
+import { startAppListening } from '..';
+import { boardsApi } from '../../../../../services/api/endpoints/boards';
+
+export const addDeleteBoardAndImagesFulfilledListener = () => {
+ startAppListening({
+ matcher: boardsApi.endpoints.deleteBoardAndImages.matchFulfilled,
+ effect: async (action, { dispatch, getState, condition }) => {
+ const { board_id, deleted_board_images, deleted_images } = action.payload;
+
+ // Remove all deleted images from the UI
+
+ let wasInitialImageReset = false;
+ let wasCanvasReset = false;
+ let wasNodeEditorReset = false;
+ let wasControlNetReset = false;
+
+ const state = getState();
+ deleted_images.forEach((image_name) => {
+ const imageUsage = getImageUsage(state, image_name);
+
+ if (imageUsage.isInitialImage && !wasInitialImageReset) {
+ dispatch(clearInitialImage());
+ wasInitialImageReset = true;
+ }
+
+ if (imageUsage.isCanvasImage && !wasCanvasReset) {
+ dispatch(resetCanvas());
+ wasCanvasReset = true;
+ }
+
+ if (imageUsage.isNodesImage && !wasNodeEditorReset) {
+ dispatch(nodeEditorReset());
+ wasNodeEditorReset = true;
+ }
+
+ if (imageUsage.isControlNetImage && !wasControlNetReset) {
+ dispatch(controlNetReset());
+ wasControlNetReset = true;
+ }
+ });
+ },
+ });
+};
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 9ce17e3099..c3e789ff6e 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,17 +1,13 @@
import { log } from 'app/logging/useLogger';
-import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
import {
- ASSETS_CATEGORIES,
- IMAGE_CATEGORIES,
boardIdSelected,
imageSelected,
- selectImagesAll,
} from 'features/gallery/store/gallerySlice';
-import { boardsApi } from 'services/api/endpoints/boards';
import {
- IMAGES_PER_PAGE,
- receivedPageOfImages,
-} from 'services/api/thunks/image';
+ getBoardIdQueryParamForBoard,
+ getCategoriesQueryParamForBoard,
+} from 'features/gallery/store/util';
+import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
@@ -19,54 +15,44 @@ const moduleLog = log.child({ namespace: 'boards' });
export const addBoardIdSelectedListener = () => {
startAppListening({
actionCreator: boardIdSelected,
- effect: (action, { getState, dispatch }) => {
- const board_id = action.payload;
+ effect: async (
+ action,
+ { getState, dispatch, condition, cancelActiveListeners }
+ ) => {
+ // Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
+ cancelActiveListeners();
- // we need to check if we need to fetch more images
+ const _board_id = action.payload;
+ // when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
- const state = getState();
- const allImages = selectImagesAll(state);
+ const categories = getCategoriesQueryParamForBoard(_board_id);
+ const board_id = getBoardIdQueryParamForBoard(_board_id);
+ const queryArgs = { board_id, categories };
- if (board_id === 'all') {
- // Selected all images
- dispatch(imageSelected(allImages[0]?.image_name ?? null));
- return;
- }
+ // wait until the board has some images - maybe it already has some from a previous fetch
+ // must use getState() to ensure we do not have stale state
+ const isSuccess = await condition(
+ () =>
+ imagesApi.endpoints.listImages.select(queryArgs)(getState())
+ .isSuccess,
+ 1000
+ );
- if (board_id === 'batch') {
- // Selected the batch
- dispatch(imageSelected(state.gallery.batchImageNames[0] ?? null));
- return;
- }
+ if (isSuccess) {
+ // the board was just changed - we can select the first image
+ const { data: boardImagesData } = imagesApi.endpoints.listImages.select(
+ queryArgs
+ )(getState());
- const filteredImages = selectFilteredImages(state);
-
- const categories =
- state.gallery.galleryView === 'images'
- ? IMAGE_CATEGORIES
- : ASSETS_CATEGORIES;
-
- // 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(boardIdSelected('all'));
- 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 })
- );
+ if (boardImagesData?.ids.length) {
+ dispatch(imageSelected((boardImagesData.ids[0] as string) ?? null));
+ } else {
+ // board has no images - deselect
+ dispatch(imageSelected(null));
+ }
+ } else {
+ // fallback - deselect
+ dispatch(imageSelected(null));
}
},
});
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
deleted file mode 100644
index 4b48aa4626..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { requestedBoardImagesDeletion } from 'features/gallery/store/actions';
-import { startAppListening } from '..';
-import {
- imageSelected,
- imagesRemoved,
- selectImagesAll,
- selectImagesById,
-} from 'features/gallery/store/gallerySlice';
-import { resetCanvas } from 'features/canvas/store/canvasSlice';
-import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
-import { clearInitialImage } from 'features/parameters/store/generationSlice';
-import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
-import { LIST_TAG, api } from 'services/api';
-import { boardsApi } from '../../../../../services/api/endpoints/boards';
-
-export const addRequestedBoardImageDeletionListener = () => {
- startAppListening({
- actionCreator: requestedBoardImagesDeletion,
- effect: async (action, { dispatch, getState, condition }) => {
- const { board, imagesUsage } = action.payload;
-
- const { board_id } = board;
-
- const state = getState();
- const selectedImageName =
- state.gallery.selection[state.gallery.selection.length - 1];
-
- const selectedImage = selectedImageName
- ? selectImagesById(state, selectedImageName)
- : undefined;
-
- if (selectedImage && selectedImage.board_id === board_id) {
- dispatch(imageSelected(null));
- }
-
- // We need to reset the features where the board images are in use - none of these work if their image(s) don't exist
-
- if (imagesUsage.isCanvasImage) {
- dispatch(resetCanvas());
- }
-
- if (imagesUsage.isControlNetImage) {
- dispatch(controlNetReset());
- }
-
- if (imagesUsage.isInitialImage) {
- dispatch(clearInitialImage());
- }
-
- if (imagesUsage.isNodesImage) {
- 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;
-
- // Wait for successful deletion, then trigger boards to re-fetch
- const wasBoardDeleted = await condition(() => !!isSuccess, 30000);
-
- if (wasBoardDeleted) {
- dispatch(
- api.util.invalidateTags([
- { type: 'Board', id: board_id },
- { type: 'Image', id: LIST_TAG },
- ])
- );
- }
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
index ce135ab3d0..0d0192143f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
@@ -1,11 +1,11 @@
-import { canvasMerged } from 'features/canvas/store/actions';
-import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
-import { addToast } from 'features/system/store/systemSlice';
-import { imageUploaded } from 'services/api/thunks/image';
+import { canvasMerged } from 'features/canvas/store/actions';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
-import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
+import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
+import { addToast } from 'features/system/store/systemSlice';
+import { imagesApi } from 'services/api/endpoints/images';
+import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
@@ -46,27 +46,28 @@ export const addCanvasMergedListener = () => {
});
const imageUploadedRequest = dispatch(
- imageUploaded({
+ imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'mergedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: true,
postUploadAction: {
- type: 'TOAST_CANVAS_MERGED',
+ type: 'TOAST',
+ toastOptions: { title: 'Canvas Merged' },
},
})
);
const [{ payload }] = await take(
- (
- uploadedImageAction
- ): uploadedImageAction is ReturnType =>
- imageUploaded.fulfilled.match(uploadedImageAction) &&
+ (uploadedImageAction) =>
+ imagesApi.endpoints.uploadImage.matchFulfilled(uploadedImageAction) &&
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
);
- const { image_name } = payload;
+ // TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
+ const { image_name } =
+ payload as typeof imagesApi.endpoints.uploadImage.Types.ResultType;
dispatch(
setMergedCanvas({
@@ -76,13 +77,6 @@ export const addCanvasMergedListener = () => {
...baseLayerRect,
})
);
-
- dispatch(
- addToast({
- title: 'Canvas Merged',
- status: 'success',
- })
- );
},
});
};
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..3b7b8e7b75 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,9 @@
-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 { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
@@ -28,28 +27,19 @@ export const addCanvasSavedToGalleryListener = () => {
return;
}
- const imageUploadedRequest = dispatch(
- imageUploaded({
+ dispatch(
+ imagesApi.endpoints.uploadImage.initiate({
file: new File([blob], 'savedCanvas.png', {
type: 'image/png',
}),
image_category: 'general',
is_intermediate: false,
postUploadAction: {
- type: 'TOAST_CANVAS_SAVED_TO_GALLERY',
+ type: 'TOAST',
+ toastOptions: { title: 'Canvas Saved to Gallery' },
},
})
);
-
- const [{ payload: uploadedImageDTO }] = await take(
- (
- uploadedImageAction
- ): uploadedImageAction is ReturnType =>
- imageUploaded.fulfilled.match(uploadedImageAction) &&
- uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
- );
-
- dispatch(imageUpserted(uploadedImageDTO));
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts
index 42387b8078..8d369a021f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts
@@ -2,10 +2,10 @@ import { log } from 'app/logging/useLogger';
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
import { sessionReadyToInvoke } from 'features/system/store/actions';
+import { imagesApi } from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards';
-import { imageDTOReceived } from 'services/api/thunks/image';
import { sessionCreated } from 'services/api/thunks/session';
-import { Graph } from 'services/api/types';
+import { Graph, ImageDTO } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
import { startAppListening } from '..';
@@ -62,12 +62,13 @@ export const addControlNetImageProcessedListener = () => {
invocationCompleteAction.payload.data.result.image;
// Wait for the ImageDTO to be received
- const [imageMetadataReceivedAction] = await take(
- (action): action is ReturnType =>
- imageDTOReceived.fulfilled.match(action) &&
+ const [{ payload }] = await take(
+ (action) =>
+ imagesApi.endpoints.getImageDTO.matchFulfilled(action) &&
action.payload.image_name === image_name
);
- const processedControlImage = imageMetadataReceivedAction.payload;
+
+ const processedControlImage = payload as ImageDTO;
moduleLog.debug(
{ data: { arg: action.payload, processedControlImage } },
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 c92eeac0db..6e1c34a04d 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,31 +1,30 @@
import { log } from 'app/logging/useLogger';
-import { boardImagesApi } from 'services/api/endpoints/boardImages';
+import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageAddedToBoardFulfilledListener = () => {
startAppListening({
- matcher: boardImagesApi.endpoints.addImageToBoard.matchFulfilled,
+ matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
- const { board_id, image_name } = action.meta.arg.originalArgs;
+ const { board_id, imageDTO } = action.meta.arg.originalArgs;
- moduleLog.debug(
- { data: { board_id, image_name } },
- 'Image added to board'
- );
+ // TODO: update listImages cache for this board
+
+ moduleLog.debug({ data: { board_id, imageDTO } }, 'Image added to board');
},
});
};
export const addImageAddedToBoardRejectedListener = () => {
startAppListening({
- matcher: boardImagesApi.endpoints.addImageToBoard.matchRejected,
+ matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
- const { board_id, image_name } = action.meta.arg.originalArgs;
+ const { board_id, imageDTO } = action.meta.arg.originalArgs;
moduleLog.debug(
- { data: { board_id, image_name } },
+ { data: { board_id, imageDTO } },
'Problem adding image to board'
);
},
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 c90c08d94a..f179530045 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,19 +1,17 @@
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,
-} from 'features/gallery/store/gallerySlice';
+import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
imageDeletionConfirmed,
isModalOpenChanged,
} 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 { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' });
@@ -36,10 +34,28 @@ export const addRequestedImageDeletionListener = () => {
state.gallery.selection[state.gallery.selection.length - 1];
if (lastSelectedImage === image_name) {
- const newSelectedImageId = selectNextImageToSelect(state, image_name);
+ const baseQueryArgs = selectListImagesBaseQueryArgs(state);
+ const { data } =
+ imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
+
+ const ids = data?.ids ?? [];
+
+ 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];
if (newSelectedImageId) {
- dispatch(imageSelected(newSelectedImageId));
+ dispatch(imageSelected(newSelectedImageId as string));
} else {
dispatch(imageSelected(null));
}
@@ -63,16 +79,15 @@ export const addRequestedImageDeletionListener = () => {
dispatch(nodeEditorReset());
}
- // Preemptively remove from gallery
- dispatch(imageRemoved(image_name));
-
// Delete from server
- const { requestId } = dispatch(imageDeleted({ image_name }));
+ const { requestId } = dispatch(
+ imagesApi.endpoints.deleteImage.initiate(imageDTO)
+ );
// Wait for successful deletion, then trigger boards to re-fetch
const wasImageDeleted = await condition(
- (action): action is ReturnType =>
- imageDeleted.fulfilled.match(action) &&
+ (action) =>
+ imagesApi.endpoints.deleteImage.matchFulfilled(action) &&
action.meta.requestId === requestId,
30000
);
@@ -91,7 +106,7 @@ export const addRequestedImageDeletionListener = () => {
*/
export const addImageDeletedPendingListener = () => {
startAppListening({
- actionCreator: imageDeleted.pending,
+ matcher: imagesApi.endpoints.deleteImage.matchPending,
effect: (action, { dispatch, getState }) => {
//
},
@@ -103,9 +118,12 @@ export const addImageDeletedPendingListener = () => {
*/
export const addImageDeletedFulfilledListener = () => {
startAppListening({
- actionCreator: imageDeleted.fulfilled,
+ matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
- moduleLog.debug({ data: { image: action.meta.arg } }, 'Image deleted');
+ moduleLog.debug(
+ { data: { image: action.meta.arg.originalArgs } },
+ 'Image deleted'
+ );
},
});
};
@@ -115,10 +133,10 @@ export const addImageDeletedFulfilledListener = () => {
*/
export const addImageDeletedRejectedListener = () => {
startAppListening({
- actionCreator: imageDeleted.rejected,
+ matcher: imagesApi.endpoints.deleteImage.matchRejected,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
- { data: { image: action.meta.arg } },
+ { data: { image: action.meta.arg.originalArgs } },
'Unable to delete image'
);
},
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 51894d50de..4da7264cbb 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
@@ -10,12 +10,9 @@ import {
imageSelected,
imagesAddedToBatch,
} from 'features/gallery/store/gallerySlice';
-import {
- fieldValueChanged,
- imageCollectionFieldValueChanged,
-} from 'features/nodes/store/nodesSlice';
+import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
-import { boardImagesApi } from 'services/api/endpoints/boardImages';
+import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '../';
const moduleLog = log.child({ namespace: 'dnd' });
@@ -137,23 +134,23 @@ export const addImageDroppedListener = () => {
return;
}
- // set multiple nodes images (multiple images handler)
- if (
- overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
- activeData.payloadType === 'IMAGE_NAMES'
- ) {
- const { fieldName, nodeId } = overData.context;
- dispatch(
- imageCollectionFieldValueChanged({
- nodeId,
- fieldName,
- value: activeData.payload.image_names.map((image_name) => ({
- image_name,
- })),
- })
- );
- return;
- }
+ // // set multiple nodes images (multiple images handler)
+ // if (
+ // overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
+ // activeData.payloadType === 'IMAGE_NAMES'
+ // ) {
+ // const { fieldName, nodeId } = overData.context;
+ // dispatch(
+ // imageCollectionFieldValueChanged({
+ // nodeId,
+ // fieldName,
+ // value: activeData.payload.image_names.map((image_name) => ({
+ // image_name,
+ // })),
+ // })
+ // );
+ // return;
+ // }
// add image to board
if (
@@ -162,97 +159,95 @@ export const addImageDroppedListener = () => {
activeData.payload.imageDTO &&
overData.context.boardId
) {
- const { image_name } = activeData.payload.imageDTO;
+ const { imageDTO } = activeData.payload;
const { boardId } = overData.context;
+
+ // if the board is "No Board", this is a remove action
+ if (boardId === 'no_board') {
+ dispatch(
+ imagesApi.endpoints.removeImageFromBoard.initiate({
+ imageDTO,
+ })
+ );
+ return;
+ }
+
+ // Handle adding image to batch
+ if (boardId === 'batch') {
+ // TODO
+ }
+
+ // Otherwise, add the image to the board
dispatch(
- boardImagesApi.endpoints.addImageToBoard.initiate({
- image_name,
+ imagesApi.endpoints.addImageToBoard.initiate({
+ imageDTO,
board_id: boardId,
})
);
return;
}
- // 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;
+ // }
- // 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;
+ // }
- // 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;
+ // }
- // 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;
- }
+ // // 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/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
deleted file mode 100644
index 8a6d069ab0..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { log } from 'app/logging/useLogger';
-import { imageUpserted } from 'features/gallery/store/gallerySlice';
-import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image';
-import { startAppListening } from '..';
-
-const moduleLog = log.child({ namespace: 'image' });
-
-export const addImageMetadataReceivedFulfilledListener = () => {
- startAppListening({
- actionCreator: imageDTOReceived.fulfilled,
- effect: (action, { getState, dispatch }) => {
- const image = action.payload;
-
- const state = getState();
-
- if (
- image.session_id === state.canvas.layerState.stagingArea.sessionId &&
- state.canvas.shouldAutoSave
- ) {
- dispatch(
- imageUpdated({
- image_name: image.image_name,
- is_intermediate: image.is_intermediate,
- })
- );
- } else if (image.is_intermediate) {
- // No further actions needed for intermediate images
- moduleLog.trace(
- { data: { image } },
- 'Image metadata received (intermediate), skipping'
- );
- return;
- }
-
- moduleLog.debug({ data: { image } }, 'Image metadata received');
- dispatch(imageUpserted(image));
- },
- });
-};
-
-export const addImageMetadataReceivedRejectedListener = () => {
- startAppListening({
- actionCreator: imageDTOReceived.rejected,
- effect: (action, { getState, dispatch }) => {
- moduleLog.debug(
- { data: { image: action.meta.arg } },
- 'Problem receiving image metadata'
- );
- },
- });
-};
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 3c6731bb31..a9dd6eda3c 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,12 +1,12 @@
import { log } from 'app/logging/useLogger';
-import { boardImagesApi } from 'services/api/endpoints/boardImages';
+import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageRemovedFromBoardFulfilledListener = () => {
startAppListening({
- matcher: boardImagesApi.endpoints.removeImageFromBoard.matchFulfilled,
+ matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
@@ -20,7 +20,7 @@ export const addImageRemovedFromBoardFulfilledListener = () => {
export const addImageRemovedFromBoardRejectedListener = () => {
startAppListening({
- matcher: boardImagesApi.endpoints.removeImageFromBoard.matchRejected,
+ matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts
index 2e235aeb33..d6a24cda24 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts
@@ -1,15 +1,20 @@
-import { startAppListening } from '..';
-import { imageUpdated } from 'services/api/thunks/image';
import { log } from 'app/logging/useLogger';
+import { imagesApi } from 'services/api/endpoints/images';
+import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' });
export const addImageUpdatedFulfilledListener = () => {
startAppListening({
- actionCreator: imageUpdated.fulfilled,
+ matcher: imagesApi.endpoints.updateImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
- { oldImage: action.meta.arg, updatedImage: action.payload },
+ {
+ data: {
+ oldImage: action.meta.arg.originalArgs,
+ updatedImage: action.payload,
+ },
+ },
'Image updated'
);
},
@@ -18,9 +23,12 @@ export const addImageUpdatedFulfilledListener = () => {
export const addImageUpdatedRejectedListener = () => {
startAppListening({
- actionCreator: imageUpdated.rejected,
+ matcher: imagesApi.endpoints.updateImage.matchRejected,
effect: (action, { dispatch }) => {
- moduleLog.debug({ oldImage: action.meta.arg }, 'Image update failed');
+ moduleLog.debug(
+ { data: action.meta.arg.originalArgs },
+ 'Image update failed'
+ );
},
});
};
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 cca01354b5..1f24bdba2a 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,49 +1,87 @@
+import { UseToastOptions } from '@chakra-ui/react';
import { log } from 'app/logging/useLogger';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
-import {
- imageUpserted,
- imagesAddedToBatch,
-} from 'features/gallery/store/gallerySlice';
+import { imagesAddedToBatch } 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 { imageUploaded } from 'services/api/thunks/image';
+import { boardsApi } from 'services/api/endpoints/boards';
import { startAppListening } from '..';
+import {
+ SYSTEM_BOARDS,
+ imagesApi,
+} from '../../../../../services/api/endpoints/images';
const moduleLog = log.child({ namespace: 'image' });
+const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
+ title: 'Image Uploaded',
+ status: 'success',
+};
+
export const addImageUploadedFulfilledListener = () => {
startAppListening({
- actionCreator: imageUploaded.fulfilled,
+ matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
- const image = action.payload;
+ const imageDTO = action.payload;
+ const state = getState();
+ const { selectedBoardId } = state.gallery;
- moduleLog.debug({ arg: '', image }, 'Image uploaded');
+ moduleLog.debug({ arg: '', imageDTO }, 'Image uploaded');
- if (action.payload.is_intermediate) {
- // No further actions needed for intermediate images
+ const { postUploadAction } = action.meta.arg.originalArgs;
+
+ if (
+ // No further actions needed for intermediate images,
+ action.payload.is_intermediate &&
+ // unless they have an explicit post-upload action
+ !postUploadAction
+ ) {
return;
}
- dispatch(imageUpserted(image));
+ // default action - just upload and alert user
+ if (postUploadAction?.type === 'TOAST') {
+ const { toastOptions } = postUploadAction;
+ if (SYSTEM_BOARDS.includes(selectedBoardId)) {
+ dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
+ } else {
+ // Add this image to the board
+ dispatch(
+ imagesApi.endpoints.addImageToBoard.initiate({
+ board_id: selectedBoardId,
+ imageDTO,
+ })
+ );
- const { postUploadAction } = action.meta.arg;
+ // Attempt to get the board's name for the toast
+ const { data } = boardsApi.endpoints.listAllBoards.select()(state);
- if (postUploadAction?.type === 'TOAST_CANVAS_SAVED_TO_GALLERY') {
- dispatch(
- addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
- );
- return;
- }
+ // Fall back to just the board id if we can't find the board for some reason
+ const board = data?.find((b) => b.board_id === selectedBoardId);
+ const description = board
+ ? `Added to board ${board.board_name}`
+ : `Added to board ${selectedBoardId}`;
- if (postUploadAction?.type === 'TOAST_CANVAS_MERGED') {
- dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description,
+ })
+ );
+ }
return;
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
- dispatch(setInitialCanvasImage(image));
+ dispatch(setInitialCanvasImage(imageDTO));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: 'Set as canvas initial image',
+ })
+ );
return;
}
@@ -52,30 +90,49 @@ export const addImageUploadedFulfilledListener = () => {
dispatch(
controlNetImageChanged({
controlNetId,
- controlImage: image.image_name,
+ controlImage: imageDTO.image_name,
+ })
+ );
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: 'Set as control image',
})
);
return;
}
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
- dispatch(initialImageChanged(image));
+ dispatch(initialImageChanged(imageDTO));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: 'Set as initial image',
+ })
+ );
return;
}
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction;
- dispatch(fieldValueChanged({ nodeId, fieldName, value: image }));
- return;
- }
-
- if (postUploadAction?.type === 'TOAST_UPLOADED') {
- dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
+ dispatch(fieldValueChanged({ nodeId, fieldName, value: imageDTO }));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: `Set as node field ${fieldName}`,
+ })
+ );
return;
}
if (postUploadAction?.type === 'ADD_TO_BATCH') {
- dispatch(imagesAddedToBatch([image.image_name]));
+ dispatch(imagesAddedToBatch([imageDTO.image_name]));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: 'Added to batch',
+ })
+ );
return;
}
},
@@ -84,10 +141,10 @@ export const addImageUploadedFulfilledListener = () => {
export const addImageUploadedRejectedListener = () => {
startAppListening({
- actionCreator: imageUploaded.rejected,
+ matcher: imagesApi.endpoints.uploadImage.matchRejected,
effect: (action, { dispatch }) => {
- const { formData, ...rest } = action.meta.arg;
- const sanitizedData = { arg: { ...rest, formData: { file: '' } } };
+ const { file, postUploadAction, ...rest } = action.meta.arg.originalArgs;
+ const sanitizedData = { arg: { ...rest, file: '' } };
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
dispatch(
addToast({
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/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
index fe1a9bd806..0cd68cf6fa 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
@@ -1,11 +1,9 @@
-import { initialImageChanged } from 'features/parameters/store/generationSlice';
-import { t } from 'i18next';
-import { addToast } from 'features/system/store/systemSlice';
-import { startAppListening } from '..';
-import { initialImageSelected } from 'features/parameters/store/actions';
import { makeToast } from 'app/components/Toaster';
-import { selectImagesById } from 'features/gallery/store/gallerySlice';
-import { isImageDTO } from 'services/api/guards';
+import { initialImageSelected } from 'features/parameters/store/actions';
+import { initialImageChanged } from 'features/parameters/store/generationSlice';
+import { addToast } from 'features/system/store/systemSlice';
+import { t } from 'i18next';
+import { startAppListening } from '..';
export const addInitialImageSelectedListener = () => {
startAppListening({
@@ -20,25 +18,7 @@ export const addInitialImageSelectedListener = () => {
return;
}
- if (isImageDTO(action.payload)) {
- dispatch(initialImageChanged(action.payload));
- dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
- return;
- }
-
- const imageName = action.payload;
- const image = selectImagesById(getState(), imageName);
-
- if (!image) {
- dispatch(
- addToast(
- makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
- )
- );
- return;
- }
-
- dispatch(initialImageChanged(image));
+ dispatch(initialImageChanged(action.payload));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
},
});
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
deleted file mode 100644
index 3c11916be0..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { log } from 'app/logging/useLogger';
-import { startAppListening } from '..';
-import { serializeError } from 'serialize-error';
-import { receivedPageOfImages } from 'services/api/thunks/image';
-import { imagesApi } from 'services/api/endpoints/images';
-
-const moduleLog = log.child({ namespace: 'gallery' });
-
-export const addReceivedPageOfImagesFulfilledListener = () => {
- startAppListening({
- actionCreator: receivedPageOfImages.fulfilled,
- effect: (action, { getState, dispatch }) => {
- const { items } = action.payload;
- moduleLog.debug(
- { data: { payload: action.payload } },
- `Received ${items.length} images`
- );
-
- items.forEach((image) => {
- dispatch(
- imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
- );
- });
- },
- });
-};
-
-export const addReceivedPageOfImagesRejectedListener = () => {
- startAppListening({
- actionCreator: receivedPageOfImages.rejected,
- effect: (action, { getState, dispatch }) => {
- if (action.payload) {
- moduleLog.debug(
- { data: { error: serializeError(action.payload) } },
- 'Problem receiving images'
- );
- }
- },
- });
-};
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 2d091af0b6..c2c57e0913 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
@@ -1,9 +1,17 @@
import { log } from 'app/logging/useLogger';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
+import {
+ IMAGE_CATEGORIES,
+ boardIdSelected,
+ imageSelected,
+} from 'features/gallery/store/gallerySlice';
import { progressImageSet } from 'features/system/store/systemSlice';
-import { boardImagesApi } from 'services/api/endpoints/boardImages';
+import {
+ SYSTEM_BOARDS,
+ imagesAdapter,
+ 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';
import {
appSocketInvocationComplete,
@@ -22,7 +30,6 @@ export const addInvocationCompleteEventListener = () => {
{ data: action.payload },
`Invocation complete (${action.payload.data.node.type})`
);
-
const session_id = action.payload.data.graph_execution_state_id;
const { cancelType, isCancelScheduled, boardIdToAddTo } =
@@ -39,33 +46,70 @@ export const addInvocationCompleteEventListener = () => {
// This complete event has an associated image output
if (isImageOutput(result) && !nodeDenylist.includes(node.type)) {
const { image_name } = result.image;
+ const { canvas, gallery } = getState();
- // Get its metadata
- dispatch(
- imageDTOReceived({
- image_name,
- })
- );
+ const imageDTO = await dispatch(
+ imagesApi.endpoints.getImageDTO.initiate(image_name)
+ ).unwrap();
- const [{ payload: imageDTO }] = await take(
- imageDTOReceived.fulfilled.match
- );
-
- // Handle canvas image
+ // Add canvas images to the staging area
if (
- graph_execution_state_id ===
- getState().canvas.layerState.stagingArea.sessionId
+ graph_execution_state_id === canvas.layerState.stagingArea.sessionId
) {
dispatch(addImageToStagingArea(imageDTO));
}
- if (boardIdToAddTo && !imageDTO.is_intermediate) {
+ if (!imageDTO.is_intermediate) {
+ // update the cache for 'All Images'
dispatch(
- boardImagesApi.endpoints.addImageToBoard.initiate({
- board_id: boardIdToAddTo,
- image_name,
- })
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ {
+ categories: IMAGE_CATEGORIES,
+ },
+ (draft) => {
+ imagesAdapter.addOne(draft, imageDTO);
+ draft.total = draft.total + 1;
+ }
+ )
);
+
+ // update the cache for 'No Board'
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ {
+ board_id: 'none',
+ },
+ (draft) => {
+ imagesAdapter.addOne(draft, imageDTO);
+ draft.total = draft.total + 1;
+ }
+ )
+ );
+
+ // add image to the board if we had one selected
+ if (boardIdToAddTo && !SYSTEM_BOARDS.includes(boardIdToAddTo)) {
+ dispatch(
+ imagesApi.endpoints.addImageToBoard.initiate({
+ board_id: boardIdToAddTo,
+ imageDTO,
+ })
+ );
+ }
+
+ const { selectedBoardId } = gallery;
+
+ if (boardIdToAddTo && boardIdToAddTo !== selectedBoardId) {
+ dispatch(boardIdSelected(boardIdToAddTo));
+ } else if (!boardIdToAddTo) {
+ dispatch(boardIdSelected('all'));
+ }
+
+ // If auto-switch is enabled, select the new image
+ if (getState().gallery.shouldAutoSwitch) {
+ dispatch(imageSelected(imageDTO.image_name));
+ }
}
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..903d2472b2 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,8 @@
-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 { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvas' });
@@ -11,41 +10,27 @@ export const addStagingAreaImageSavedListener = () => {
startAppListening({
actionCreator: stagingAreaImageSaved,
effect: async (action, { dispatch, getState, take }) => {
- const { imageName } = action.payload;
+ const { imageDTO } = action.payload;
dispatch(
- imageUpdated({
- image_name: imageName,
- is_intermediate: false,
+ imagesApi.endpoints.updateImage.initiate({
+ imageDTO,
+ changes: { is_intermediate: false },
})
- );
-
- const [imageUpdatedAction] = await take(
- (action) =>
- (imageUpdated.fulfilled.match(action) ||
- imageUpdated.rejected.match(action)) &&
- action.meta.arg.image_name === imageName
- );
-
- if (imageUpdated.rejected.match(imageUpdatedAction)) {
- moduleLog.error(
- { data: { arg: imageUpdatedAction.meta.arg } },
- 'Image saving failed'
- );
- dispatch(
- addToast({
- title: 'Image Saving Failed',
- description: imageUpdatedAction.error.message,
- status: 'error',
- })
- );
- return;
- }
-
- if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
- dispatch(imageUpserted(imageUpdatedAction.payload));
- dispatch(addToast({ title: 'Image Saved', status: 'success' }));
- }
+ )
+ .unwrap()
+ .then((image) => {
+ dispatch(addToast({ title: 'Image Saved', status: 'success' }));
+ })
+ .catch((error) => {
+ dispatch(
+ addToast({
+ title: 'Image Saving Failed',
+ description: error.message,
+ status: 'error',
+ })
+ );
+ });
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts
deleted file mode 100644
index 490d99290d..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { socketConnected } from 'services/events/actions';
-import { startAppListening } from '..';
-import { createSelector } from '@reduxjs/toolkit';
-import { generationSelector } from 'features/parameters/store/generationSelectors';
-import { canvasSelector } from 'features/canvas/store/canvasSelectors';
-import { nodesSelector } from 'features/nodes/store/nodesSlice';
-import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
-import { forEach, uniqBy } from 'lodash-es';
-import { imageUrlsReceived } from 'services/api/thunks/image';
-import { log } from 'app/logging/useLogger';
-import { selectImagesEntities } from 'features/gallery/store/gallerySlice';
-
-const moduleLog = log.child({ namespace: 'images' });
-
-const selectAllUsedImages = createSelector(
- [
- generationSelector,
- canvasSelector,
- nodesSelector,
- controlNetSelector,
- selectImagesEntities,
- ],
- (generation, canvas, nodes, controlNet, imageEntities) => {
- const allUsedImages: string[] = [];
-
- if (generation.initialImage) {
- allUsedImages.push(generation.initialImage.imageName);
- }
-
- canvas.layerState.objects.forEach((obj) => {
- if (obj.kind === 'image') {
- allUsedImages.push(obj.imageName);
- }
- });
-
- nodes.nodes.forEach((node) => {
- forEach(node.data.inputs, (input) => {
- if (input.type === 'image' && input.value) {
- allUsedImages.push(input.value.image_name);
- }
- });
- });
-
- forEach(controlNet.controlNets, (c) => {
- if (c.controlImage) {
- allUsedImages.push(c.controlImage);
- }
- if (c.processedControlImage) {
- allUsedImages.push(c.processedControlImage);
- }
- });
-
- forEach(imageEntities, (image) => {
- if (image) {
- allUsedImages.push(image.image_name);
- }
- });
-
- const uniqueImages = uniqBy(allUsedImages, 'image_name');
-
- return uniqueImages;
- }
-);
-
-export const addUpdateImageUrlsOnConnectListener = () => {
- startAppListening({
- actionCreator: socketConnected,
- effect: async (action, { dispatch, getState, take }) => {
- const state = getState();
-
- if (!state.config.shouldUpdateImagesOnConnect) {
- return;
- }
-
- const allUsedImages = selectAllUsedImages(state);
-
- moduleLog.trace(
- { data: allUsedImages },
- `Fetching new image URLs for ${allUsedImages.length} images`
- );
-
- allUsedImages.forEach((image_name) => {
- dispatch(
- imageUrlsReceived({
- image_name,
- })
- );
- });
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts
index 1f9f773392..afddaf8bea 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts
@@ -1,20 +1,20 @@
-import { startAppListening } from '..';
-import { sessionCreated } from 'services/api/thunks/session';
-import { buildCanvasGraph } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
import { log } from 'app/logging/useLogger';
-import { canvasGraphBuilt } from 'features/nodes/store/actions';
-import { imageUpdated, imageUploaded } from 'services/api/thunks/image';
-import { ImageDTO } from 'services/api/types';
+import { userInvoked } from 'app/store/actions';
+import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import {
canvasSessionIdChanged,
stagingAreaInitialized,
} from 'features/canvas/store/canvasSlice';
-import { userInvoked } from 'app/store/actions';
+import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
-import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
-import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
+import { canvasGraphBuilt } from 'features/nodes/store/actions';
+import { buildCanvasGraph } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
import { sessionReadyToInvoke } from 'features/system/store/actions';
+import { imagesApi } from 'services/api/endpoints/images';
+import { sessionCreated } from 'services/api/thunks/session';
+import { ImageDTO } from 'services/api/types';
+import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'invoke' });
@@ -74,7 +74,7 @@ export const addUserInvokedCanvasListener = () => {
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id
const { requestId: initImageUploadedRequestId } = dispatch(
- imageUploaded({
+ imagesApi.endpoints.uploadImage.initiate({
file: new File([baseBlob], 'canvasInitImage.png', {
type: 'image/png',
}),
@@ -85,19 +85,20 @@ export const addUserInvokedCanvasListener = () => {
// Wait for the image to be uploaded, matching by request id
const [{ payload }] = await take(
- (action): action is ReturnType =>
- imageUploaded.fulfilled.match(action) &&
+ // TODO: figure out how to narrow this action's type
+ (action) =>
+ imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
action.meta.requestId === initImageUploadedRequestId
);
- canvasInitImage = payload;
+ canvasInitImage = payload as ImageDTO;
}
// For inpaint/outpaint, we also need to upload the mask layer
if (['inpaint', 'outpaint'].includes(generationMode)) {
// upload the image, saving the request id
const { requestId: maskImageUploadedRequestId } = dispatch(
- imageUploaded({
+ imagesApi.endpoints.uploadImage.initiate({
file: new File([maskBlob], 'canvasMaskImage.png', {
type: 'image/png',
}),
@@ -108,12 +109,13 @@ export const addUserInvokedCanvasListener = () => {
// Wait for the image to be uploaded, matching by request id
const [{ payload }] = await take(
- (action): action is ReturnType =>
- imageUploaded.fulfilled.match(action) &&
+ // TODO: figure out how to narrow this action's type
+ (action) =>
+ imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
action.meta.requestId === maskImageUploadedRequestId
);
- canvasMaskImage = payload;
+ canvasMaskImage = payload as ImageDTO;
}
const graph = buildCanvasGraph(
@@ -144,9 +146,9 @@ export const addUserInvokedCanvasListener = () => {
// Associate the init image with the session, now that we have the session ID
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
dispatch(
- imageUpdated({
- image_name: canvasInitImage.image_name,
- session_id: sessionId,
+ imagesApi.endpoints.updateImage.initiate({
+ imageDTO: canvasInitImage,
+ changes: { session_id: sessionId },
})
);
}
@@ -154,9 +156,9 @@ export const addUserInvokedCanvasListener = () => {
// Associate the mask image with the session, now that we have the session ID
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
dispatch(
- imageUpdated({
- image_name: canvasMaskImage.image_name,
- session_id: sessionId,
+ imagesApi.endpoints.updateImage.initiate({
+ imageDTO: canvasMaskImage,
+ changes: { session_id: sessionId },
})
);
}
diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
index c024622d2e..6082843c55 100644
--- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
@@ -11,13 +11,15 @@ import {
TypesafeDroppableData,
} from 'app/components/ImageDnd/typesafeDnd';
import IAIIconButton from 'common/components/IAIIconButton';
-import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import {
+ IAILoadingImageFallback,
+ 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 { ImageDTO, PostUploadAction } from 'services/api/types';
import { mode } from 'theme/util/mode';
import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable';
@@ -46,6 +48,7 @@ type IAIDndImageProps = {
isSelected?: boolean;
thumbnail?: boolean;
noContentFallback?: ReactElement;
+ useThumbailFallback?: boolean;
};
const IAIDndImage = (props: IAIDndImageProps) => {
@@ -71,6 +74,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
resetTooltip = 'Reset',
resetIcon = ,
noContentFallback = ,
+ useThumbailFallback,
} = props;
const { colorMode } = useColorMode();
@@ -126,9 +130,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
}
+ fallbackSrc={
+ useThumbailFallback ? imageDTO.thumbnail_url : undefined
+ }
+ fallback={
+ useThumbailFallback ? undefined : (
+
+ )
+ }
width={imageDTO.width}
height={imageDTO.height}
onError={onError}
diff --git a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx
index 573a900fef..7601758409 100644
--- a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx
@@ -1,12 +1,12 @@
import { Flex, Text, useColorMode } from '@chakra-ui/react';
import { motion } from 'framer-motion';
-import { memo, useRef } from 'react';
+import { ReactNode, memo, useRef } from 'react';
import { mode } from 'theme/util/mode';
import { v4 as uuidv4 } from 'uuid';
type Props = {
isOver: boolean;
- label?: string;
+ label?: ReactNode;
};
export const IAIDropOverlay = (props: Props) => {
@@ -57,16 +57,16 @@ export const IAIDropOverlay = (props: Props) => {
{
sx={{
fontSize: '2xl',
fontWeight: 600,
- transform: isOver ? 'scale(1.02)' : 'scale(1)',
+ transform: isOver ? 'scale(1.1)' : 'scale(1)',
color: isOver
? mode('base.50', 'base.50')(colorMode)
- : mode('base.100', 'base.200')(colorMode),
+ : mode('base.200', 'base.300')(colorMode),
transitionProperty: 'common',
transitionDuration: '0.1s',
}}
diff --git a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx
index 98093d04e4..1038f36840 100644
--- a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx
@@ -5,12 +5,12 @@ import {
useDroppable,
} from 'app/components/ImageDnd/typesafeDnd';
import { AnimatePresence } from 'framer-motion';
-import { memo, useRef } from 'react';
+import { ReactNode, memo, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import IAIDropOverlay from './IAIDropOverlay';
type IAIDroppableProps = {
- dropLabel?: string;
+ dropLabel?: ReactNode;
disabled?: boolean;
data?: TypesafeDroppableData;
};
diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
index a07071ee79..2057525b7a 100644
--- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
@@ -68,6 +68,7 @@ export const IAINoContentFallback = (props: IAINoImageFallbackProps) => {
flexDir: 'column',
gap: 2,
userSelect: 'none',
+ opacity: 0.7,
color: 'base.700',
_dark: {
color: 'base.500',
diff --git a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx
index 862d806eb1..b2d5ddb2da 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx
@@ -32,27 +32,57 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
>
+
- {isDragAccept ? (
- Drop to Upload
- ) : (
- <>
- Invalid Upload
- Must be single JPEG or PNG image
- >
- )}
+
+ {isDragAccept ? (
+ Drop to Upload
+ ) : (
+ <>
+ Invalid Upload
+ Must be single JPEG or PNG image
+ >
+ )}
+
);
diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
index d3565ff5ec..dbdaf26c5b 100644
--- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx
+++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx
@@ -1,35 +1,43 @@
import { Box } from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import useImageUploader from 'common/hooks/useImageUploader';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppToaster } from 'app/components/Toaster';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { selectIsBusy } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import {
KeyboardEvent,
- memo,
ReactNode,
+ memo,
useCallback,
useEffect,
useState,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
-import { imageUploaded } from 'services/api/thunks/image';
+import { useUploadImageMutation } from 'services/api/endpoints/images';
+import { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay';
-import { useAppToaster } from 'app/components/Toaster';
-import { createSelector } from '@reduxjs/toolkit';
-import { systemSelector } from 'features/system/store/systemSelectors';
+import { AnimatePresence, motion } from 'framer-motion';
const selector = createSelector(
- [systemSelector, activeTabNameSelector],
- (system, activeTabName) => {
- const { isConnected, isUploading } = system;
+ [activeTabNameSelector],
+ (activeTabName) => {
+ let postUploadAction: PostUploadAction = { type: 'TOAST' };
- const isUploaderDisabled = !isConnected || isUploading;
+ if (activeTabName === 'unifiedCanvas') {
+ postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
+ }
+
+ if (activeTabName === 'img2img') {
+ postUploadAction = { type: 'SET_INITIAL_IMAGE' };
+ }
return {
- isUploaderDisabled,
- activeTabName,
+ postUploadAction,
};
- }
+ },
+ defaultSelectorOptions
);
type ImageUploaderProps = {
@@ -38,12 +46,13 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
- const dispatch = useAppDispatch();
- const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
+ const { postUploadAction } = useAppSelector(selector);
+ const isBusy = useAppSelector(selectIsBusy);
const toaster = useAppToaster();
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState(false);
- const { setOpenUploaderFunction } = useImageUploader();
+
+ const [uploadImage] = useUploadImageMutation();
const fileRejectionCallback = useCallback(
(rejection: FileRejection) => {
@@ -60,16 +69,14 @@ const ImageUploader = (props: ImageUploaderProps) => {
const fileAcceptedCallback = useCallback(
async (file: File) => {
- dispatch(
- imageUploaded({
- file,
- image_category: 'user',
- is_intermediate: false,
- postUploadAction: { type: 'TOAST_UPLOADED' },
- })
- );
+ uploadImage({
+ file,
+ image_category: 'user',
+ is_intermediate: false,
+ postUploadAction,
+ });
},
- [dispatch]
+ [postUploadAction, uploadImage]
);
const onDrop = useCallback(
@@ -101,13 +108,12 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragReject,
isDragActive,
inputRef,
- open,
} = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true,
onDrop,
onDragOver: () => setIsHandlingUpload(true),
- disabled: isUploaderDisabled,
+ disabled: isBusy,
multiple: false,
});
@@ -126,19 +132,13 @@ const ImageUploader = (props: ImageUploaderProps) => {
}
};
- // Set the open function so we can open the uploader from anywhere
- setOpenUploaderFunction(open);
-
// Add the paste event listener
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('paste', handlePaste);
- setOpenUploaderFunction(() => {
- return;
- });
};
- }, [inputRef, open, setOpenUploaderFunction]);
+ }, [inputRef]);
return (
{
>
{children}
- {isDragActive && isHandlingUpload && (
-
- )}
+
+ {isDragActive && isHandlingUpload && (
+
+
+
+ )}
+
);
};
diff --git a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx b/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
deleted file mode 100644
index bb24ce6e18..0000000000
--- a/invokeai/frontend/web/src/common/components/ImageUploaderButton.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Flex, Heading, Icon } from '@chakra-ui/react';
-import useImageUploader from 'common/hooks/useImageUploader';
-import { FaUpload } from 'react-icons/fa';
-
-type ImageUploaderButtonProps = {
- styleClass?: string;
-};
-
-const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
- const { styleClass } = props;
- const { openUploader } = useImageUploader();
-
- return (
-
-
-
- Click or Drag and Drop
-
-
- );
-};
-
-export default ImageUploaderButton;
diff --git a/invokeai/frontend/web/src/common/components/ImageUploaderIconButton.tsx b/invokeai/frontend/web/src/common/components/ImageUploaderIconButton.tsx
deleted file mode 100644
index af5eb8dbb5..0000000000
--- a/invokeai/frontend/web/src/common/components/ImageUploaderIconButton.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { FaUpload } from 'react-icons/fa';
-import IAIIconButton from './IAIIconButton';
-import useImageUploader from 'common/hooks/useImageUploader';
-
-const ImageUploaderIconButton = () => {
- const { t } = useTranslation();
- const { openUploader } = useImageUploader();
-
- return (
- }
- onClick={openUploader}
- />
- );
-};
-
-export default ImageUploaderIconButton;
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
index 0712daf742..fad6deb350 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
@@ -1,7 +1,7 @@
-import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
-import { PostUploadAction, imageUploaded } from 'services/api/thunks/image';
+import { useUploadImageMutation } from 'services/api/endpoints/images';
+import { PostUploadAction } from 'services/api/types';
type UseImageUploadButtonArgs = {
postUploadAction?: PostUploadAction;
@@ -12,7 +12,7 @@ type UseImageUploadButtonArgs = {
* Provides image uploader functionality to any component.
*
* @example
- * const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
+ * const { getUploadButtonProps, getUploadInputProps, openUploader } = useImageUploadButton({
* postUploadAction: {
* type: 'SET_CONTROLNET_IMAGE',
* controlNetId: '12345',
@@ -20,6 +20,9 @@ type UseImageUploadButtonArgs = {
* isDisabled: getIsUploadDisabled(),
* });
*
+ * // open the uploaded directly
+ * const handleSomething = () => { openUploader() }
+ *
* // in the render function
* // will open the file dialog on click
* // hidden, handles native upload functionality
@@ -28,24 +31,23 @@ export const useImageUploadButton = ({
postUploadAction,
isDisabled,
}: UseImageUploadButtonArgs) => {
- const dispatch = useAppDispatch();
+ const [uploadImage] = useUploadImageMutation();
const onDropAccepted = useCallback(
(files: File[]) => {
const file = files[0];
+
if (!file) {
return;
}
- dispatch(
- imageUploaded({
- file,
- image_category: 'user',
- is_intermediate: false,
- postUploadAction,
- })
- );
+ uploadImage({
+ file,
+ image_category: 'user',
+ is_intermediate: false,
+ postUploadAction: postUploadAction ?? { type: 'TOAST' },
+ });
},
- [dispatch, postUploadAction]
+ [postUploadAction, uploadImage]
);
const {
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts b/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
deleted file mode 100644
index 2b04ac9530..0000000000
--- a/invokeai/frontend/web/src/common/hooks/useImageUploader.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useCallback } from 'react';
-
-let openUploader = () => {
- return;
-};
-
-const useImageUploader = () => {
- const setOpenUploaderFunction = useCallback(
- (openUploaderFunction?: () => void) => {
- if (openUploaderFunction) {
- openUploader = openUploaderFunction;
- }
- },
- []
- );
-
- return {
- setOpenUploaderFunction,
- openUploader,
- };
-};
-
-export default useImageUploader;
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
index eb9129e4c1..1929bff8f9 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
@@ -26,6 +26,8 @@ import {
FaSave,
} from 'react-icons/fa';
import { stagingAreaImageSaved } from '../store/actions';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { skipToken } from '@reduxjs/toolkit/dist/query';
const selector = createSelector(
[canvasSelector],
@@ -123,6 +125,10 @@ const IAICanvasStagingAreaToolbar = () => {
[dispatch, sessionId]
);
+ const { data: imageDTO } = useGetImageDTOQuery(
+ currentStagingAreaImage?.imageName ?? skipToken
+ );
+
if (!currentStagingAreaImage) return null;
return (
@@ -173,14 +179,19 @@ const IAICanvasStagingAreaToolbar = () => {
}
- onClick={() =>
+ onClick={() => {
+ if (!imageDTO) {
+ return;
+ }
+
dispatch(
stagingAreaImageSaved({
- imageName: currentStagingAreaImage.imageName,
+ imageDTO,
})
- )
- }
+ );
+ }}
colorScheme="accent"
/>
{
const { t } = useTranslation();
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
- const { openUploader } = useImageUploader();
+ const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
+ postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' },
+ });
useHotkeys(
['v'],
@@ -288,9 +290,10 @@ const IAICanvasToolbar = () => {
aria-label={`${t('common.upload')}`}
tooltip={`${t('common.upload')}`}
icon={}
- onClick={openUploader}
isDisabled={isStaging}
+ {...getUploadButtonProps()}
/>
+
(
+export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>(
'canvas/stagingAreaImageSaved'
);
diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
index 2faec4737d..1bdb2fd034 100644
--- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
@@ -11,8 +11,8 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage';
import { memo, useCallback, useMemo, useState } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-import { PostUploadAction } from 'services/api/thunks/image';
import { controlNetImageChanged } from '../store/controlNetSlice';
+import { PostUploadAction } from 'services/api/types';
type Props = {
controlNetId: string;
diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
index 8e6f96add3..663edfd65f 100644
--- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
+++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
@@ -2,7 +2,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { ControlNetModelParam } from 'features/parameters/types/parameterSchemas';
import { cloneDeep, forEach } from 'lodash-es';
-import { imageDeleted } from 'services/api/thunks/image';
+import { imagesApi } from 'services/api/endpoints/images';
import { isAnySessionRejected } from 'services/api/thunks/session';
import { appSocketInvocationError } from 'services/events/actions';
import { controlNetImageProcessed } from './actions';
@@ -300,21 +300,6 @@ export const controlNetSlice = createSlice({
}
});
- builder.addCase(imageDeleted.pending, (state, action) => {
- // Preemptively remove the image from all controlnets
- // TODO: doesn't the imageusage stuff do this for us?
- const { image_name } = action.meta.arg;
- forEach(state.controlNets, (c) => {
- if (c.controlImage === image_name) {
- c.controlImage = null;
- c.processedControlImage = null;
- }
- if (c.processedControlImage === image_name) {
- c.processedControlImage = null;
- }
- });
- });
-
builder.addCase(appSocketInvocationError, (state, action) => {
state.pendingControlImages = [];
});
@@ -322,6 +307,24 @@ export const controlNetSlice = createSlice({
builder.addMatcher(isAnySessionRejected, (state, action) => {
state.pendingControlImages = [];
});
+
+ builder.addMatcher(
+ imagesApi.endpoints.deleteImage.matchFulfilled,
+ (state, action) => {
+ // Preemptively remove the image from all controlnets
+ // TODO: doesn't the imageusage stuff do this for us?
+ const { image_name } = action.meta.arg.originalArgs;
+ forEach(state.controlNets, (c) => {
+ if (c.controlImage === image_name) {
+ c.controlImage = null;
+ c.processedControlImage = null;
+ }
+ if (c.processedControlImage === image_name) {
+ c.processedControlImage = null;
+ }
+ });
+ }
+ );
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllAssetsBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllAssetsBoard.tsx
new file mode 100644
index 0000000000..5f4f1cbeb0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllAssetsBoard.tsx
@@ -0,0 +1,50 @@
+import {
+ ASSETS_CATEGORIES,
+ INITIAL_IMAGE_LIMIT,
+ boardIdSelected,
+} from 'features/gallery/store/gallerySlice';
+import { FaFileImage } from 'react-icons/fa';
+import { useDispatch } from 'react-redux';
+import {
+ ListImagesArgs,
+ useListImagesQuery,
+} from 'services/api/endpoints/images';
+import GenericBoard from './GenericBoard';
+
+const baseQueryArg: ListImagesArgs = {
+ categories: ASSETS_CATEGORIES,
+ offset: 0,
+ limit: INITIAL_IMAGE_LIMIT,
+ is_intermediate: false,
+};
+
+const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
+ const dispatch = useDispatch();
+
+ const handleClick = () => {
+ dispatch(boardIdSelected('assets'));
+ };
+
+ const { total } = useListImagesQuery(baseQueryArg, {
+ selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
+ });
+
+ // TODO: Do we support making 'images' 'assets? if yes, we need to handle this
+ // const droppableData: MoveBoardDropData = {
+ // id: 'all-images-board',
+ // actionType: 'MOVE_BOARD',
+ // context: { boardId: 'assets' },
+ // };
+
+ return (
+
+ );
+};
+
+export default AllAssetsBoard;
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
index c14ae24483..9c34c64e7e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllImagesBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AllImagesBoard.tsx
@@ -1,29 +1,48 @@
-import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
-import { boardIdSelected } from 'features/gallery/store/gallerySlice';
+import {
+ IMAGE_CATEGORIES,
+ INITIAL_IMAGE_LIMIT,
+ boardIdSelected,
+} from 'features/gallery/store/gallerySlice';
import { FaImages } from 'react-icons/fa';
import { useDispatch } from 'react-redux';
+import {
+ ListImagesArgs,
+ useListImagesQuery,
+} from 'services/api/endpoints/images';
import GenericBoard from './GenericBoard';
+const baseQueryArg: ListImagesArgs = {
+ categories: IMAGE_CATEGORIES,
+ offset: 0,
+ limit: INITIAL_IMAGE_LIMIT,
+ is_intermediate: false,
+};
+
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
- const handleAllImagesBoardClick = () => {
- dispatch(boardIdSelected('all'));
+ const handleClick = () => {
+ dispatch(boardIdSelected('images'));
};
- const droppableData: MoveBoardDropData = {
- id: 'all-images-board',
- actionType: 'MOVE_BOARD',
- context: { boardId: null },
- };
+ const { total } = useListImagesQuery(baseQueryArg, {
+ selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
+ });
+
+ // TODO: Do we support making 'images' 'assets? if yes, we need to handle this
+ // const droppableData: MoveBoardDropData = {
+ // id: 'all-images-board',
+ // actionType: 'MOVE_BOARD',
+ // context: { boardId: 'images' },
+ // };
return (
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
index b479c46fd9..61b8856ff9 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx
@@ -1,27 +1,27 @@
-import { CloseIcon } from '@chakra-ui/icons';
import {
Collapse,
Flex,
Grid,
GridItem,
- IconButton,
- Input,
- InputGroup,
- InputRightElement,
+ useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
-import { setBoardSearchText } from 'features/gallery/store/boardSlice';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo, useState } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
+import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
import AddBoardButton from './AddBoardButton';
+import AllAssetsBoard from './AllAssetsBoard';
import AllImagesBoard from './AllImagesBoard';
import BatchBoard from './BatchBoard';
+import BoardsSearch from './BoardsSearch';
import GalleryBoard from './GalleryBoard';
-import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
+import NoBoardBoard from './NoBoardBoard';
+import DeleteBoardModal from '../DeleteBoardModal';
+import { BoardDTO } from 'services/api/types';
const selector = createSelector(
[stateSelector],
@@ -39,110 +39,91 @@ type Props = {
const BoardsList = (props: Props) => {
const { isOpen } = props;
- const dispatch = useAppDispatch();
const { selectedBoardId, searchText } = useAppSelector(selector);
-
const { data: boards } = useListAllBoardsQuery();
-
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
-
const filteredBoards = searchText
? boards?.filter((board) =>
board.board_name.toLowerCase().includes(searchText.toLowerCase())
)
: boards;
-
+ const [boardToDelete, setBoardToDelete] = useState();
const [searchMode, setSearchMode] = useState(false);
- const handleBoardSearch = (searchTerm: string) => {
- setSearchMode(searchTerm.length > 0);
- dispatch(setBoardSearchText(searchTerm));
- };
- const clearBoardSearch = () => {
- setSearchMode(false);
- dispatch(setBoardSearchText(''));
- };
-
return (
-
-
-
-
- {
- handleBoardSearch(e.target.value);
- }}
- />
- {searchText && searchText.length && (
-
- }
- />
-
- )}
-
-
-
-
+
+
-
+
+
+
+
- {!searchMode && (
- <>
-
-
-
- {isBatchEnabled && (
+
+ {!searchMode && (
+ <>
-
+
- )}
- >
- )}
- {filteredBoards &&
- filteredBoards.map((board) => (
-
-
-
- ))}
-
-
-
-
+
+
+
+
+
+
+ {isBatchEnabled && (
+
+
+
+ )}
+ >
+ )}
+ {filteredBoards &&
+ filteredBoards.map((board) => (
+
+
+
+ ))}
+
+
+
+
+
+ >
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
new file mode 100644
index 0000000000..fffe50f6a7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
@@ -0,0 +1,66 @@
+import { CloseIcon } from '@chakra-ui/icons';
+import {
+ IconButton,
+ Input,
+ InputGroup,
+ 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 } from 'react';
+
+const selector = createSelector(
+ [stateSelector],
+ ({ boards }) => {
+ const { searchText } = boards;
+ return { searchText };
+ },
+ defaultSelectorOptions
+);
+
+type Props = {
+ setSearchMode: (searchMode: boolean) => void;
+};
+
+const BoardsSearch = (props: Props) => {
+ const { setSearchMode } = props;
+ const dispatch = useAppDispatch();
+ const { searchText } = useAppSelector(selector);
+
+ const handleBoardSearch = (searchTerm: string) => {
+ setSearchMode(searchTerm.length > 0);
+ dispatch(setBoardSearchText(searchTerm));
+ };
+ const clearBoardSearch = () => {
+ setSearchMode(false);
+ dispatch(setBoardSearchText(''));
+ };
+
+ return (
+
+ {
+ handleBoardSearch(e.target.value);
+ }}
+ />
+ {searchText && searchText.length && (
+
+ }
+ />
+
+ )}
+
+ );
+};
+
+export default memo(BoardsSearch);
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
index c01113d38a..1b542eee4d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
@@ -8,217 +8,208 @@ import {
Image,
MenuItem,
MenuList,
+ Text,
useColorMode,
} from '@chakra-ui/react';
-
-import { useAppDispatch } from 'app/store/storeHooks';
-import { ContextMenu } from 'chakra-ui-contextmenu';
-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 { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
-// import { boardAddedToBatch } from 'app/store/middleware/listenerMiddleware/listeners/addBoardToBatch';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { ContextMenu } from 'chakra-ui-contextmenu';
import IAIDroppable from 'common/components/IAIDroppable';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { boardIdSelected } from 'features/gallery/store/gallerySlice';
+import { memo, useCallback, useMemo } from 'react';
+import { FaTrash, FaUser } from 'react-icons/fa';
+import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { BoardDTO } from 'services/api/types';
+import { menuListMotionProps } from 'theme/components/menu';
import { mode } from 'theme/util/mode';
-import { DeleteBoardImagesContext } from '../../../../../app/contexts/DeleteBoardImagesContext';
interface GalleryBoardProps {
board: BoardDTO;
isSelected: boolean;
+ setBoardToDelete: (board?: BoardDTO) => void;
}
-const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
- const dispatch = useAppDispatch();
+const GalleryBoard = memo(
+ ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
+ const dispatch = useAppDispatch();
- const { currentData: coverImage } = useGetImageDTOQuery(
- board.cover_image_name ?? skipToken
- );
+ const { currentData: coverImage } = useGetImageDTOQuery(
+ board.cover_image_name ?? skipToken
+ );
- const { colorMode } = useColorMode();
+ const { colorMode } = useColorMode();
+ const { board_name, board_id } = board;
+ const handleSelectBoard = useCallback(() => {
+ dispatch(boardIdSelected(board_id));
+ }, [board_id, dispatch]);
- const { board_name, board_id } = board;
+ const [updateBoard, { isLoading: isUpdateBoardLoading }] =
+ useUpdateBoardMutation();
- const { onClickDeleteBoardImages } = useContext(DeleteBoardImagesContext);
+ const handleUpdateBoardName = (newBoardName: string) => {
+ updateBoard({ board_id, changes: { board_name: newBoardName } });
+ };
- const handleSelectBoard = useCallback(() => {
- dispatch(boardIdSelected(board_id));
- }, [board_id, dispatch]);
+ const handleDeleteBoard = useCallback(() => {
+ setBoardToDelete(board);
+ }, [board, setBoardToDelete]);
- const [updateBoard, { isLoading: isUpdateBoardLoading }] =
- useUpdateBoardMutation();
+ const droppableData: MoveBoardDropData = useMemo(
+ () => ({
+ id: board_id,
+ actionType: 'MOVE_BOARD',
+ context: { boardId: board_id },
+ }),
+ [board_id]
+ );
- const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
- useDeleteBoardMutation();
-
- const handleUpdateBoardName = (newBoardName: string) => {
- updateBoard({ board_id, changes: { board_name: newBoardName } });
- };
-
- const handleDeleteBoard = useCallback(() => {
- deleteBoard(board_id);
- }, [board_id, deleteBoard]);
-
- const handleAddBoardToBatch = useCallback(() => {
- // dispatch(boardAddedToBatch({ board_id }));
- }, []);
-
- const handleDeleteBoardAndImages = useCallback(() => {
- onClickDeleteBoardImages(board);
- }, [board, onClickDeleteBoardImages]);
-
- const droppableData: MoveBoardDropData = useMemo(
- () => ({
- id: board_id,
- actionType: 'MOVE_BOARD',
- context: { boardId: board_id },
- }),
- [board_id]
- );
-
- return (
-
-
- menuProps={{ size: 'sm', isLazy: true }}
- renderMenu={() => (
-
- {board.image_count > 0 && (
- <>
- }
- onClickCapture={handleAddBoardToBatch}
- >
- Add Board to Batch
-
- }
- onClickCapture={handleDeleteBoardAndImages}
- >
- Delete Board and Images
-
- >
- )}
- }
- onClickCapture={handleDeleteBoard}
+ return (
+
+
+ menuProps={{ size: 'sm', isLazy: true }}
+ menuButtonProps={{
+ bg: 'transparent',
+ _hover: { bg: 'transparent' },
+ }}
+ renderMenu={() => (
+
- Delete Board
-
-
- )}
- >
- {(ref) => (
-
+ {board.image_count > 0 && (
+ <>
+ {/* }
+ onClickCapture={handleAddBoardToBatch}
+ >
+ Add Board to Batch
+ */}
+ >
+ )}
+ }
+ onClickCapture={handleDeleteBoard}
+ >
+ Delete Board
+
+
+ )}
+ >
+ {(ref) => (
- {board.cover_image_name && coverImage?.image_url && (
-
- )}
- {!(board.cover_image_name && coverImage?.image_url) && (
-
+ {board.cover_image_name && coverImage?.thumbnail_url && (
+
+ )}
+ {!(board.cover_image_name && coverImage?.thumbnail_url) && (
+
+ )}
+
+ {board.image_count}
+
+ Move}
/>
- )}
+
+
- {board.image_count}
+ {
+ handleUpdateBoardName(nextValue);
+ }}
+ sx={{ maxW: 'full' }}
+ >
+
+
+
-
-
-
- {
- handleUpdateBoardName(nextValue);
- }}
- >
-
-
-
-
-
- )}
-
-
- );
-});
+ )}
+
+
+ );
+ }
+);
GalleryBoard.displayName = 'HoverableBoard';
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
index a300c1b18c..5067dac33a 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GenericBoard.tsx
@@ -2,18 +2,34 @@ 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';
+import { ReactNode } from 'react';
type GenericBoardProps = {
- droppableData: TypesafeDroppableData;
+ droppableData?: TypesafeDroppableData;
onClick: () => void;
isSelected: boolean;
icon: As;
label: string;
+ dropLabel?: ReactNode;
badgeCount?: number;
};
+const formatBadgeCount = (count: number) =>
+ Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(count);
+
const GenericBoard = (props: GenericBoardProps) => {
- const { droppableData, onClick, isSelected, icon, label, badgeCount } = props;
+ const {
+ droppableData,
+ onClick,
+ isSelected,
+ icon,
+ label,
+ badgeCount,
+ dropLabel,
+ } = props;
return (
{
}}
>
{badgeCount !== undefined && (
- {badgeCount}
+ {formatBadgeCount(badgeCount)}
)}
-
+
{
+ const dispatch = useDispatch();
+
+ const handleClick = () => {
+ dispatch(boardIdSelected('no_board'));
+ };
+
+ const { total } = useListImagesQuery(baseQueryArg, {
+ selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
+ });
+
+ // TODO: Do we support making 'images' 'assets? if yes, we need to handle this
+ const droppableData: MoveBoardDropData = {
+ id: 'all-images-board',
+ actionType: 'MOVE_BOARD',
+ context: { boardId: 'no_board' },
+ };
+
+ return (
+ Move}
+ onClick={handleClick}
+ isSelected={isSelected}
+ icon={FaFolderOpen}
+ label="No Board"
+ badgeCount={total}
+ />
+ );
+};
+
+export default NoBoardBoard;
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx
deleted file mode 100644
index 736d72f862..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import {
- AlertDialog,
- AlertDialogBody,
- AlertDialogContent,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogOverlay,
- Divider,
- Flex,
- ListItem,
- Text,
- UnorderedList,
-} from '@chakra-ui/react';
-import IAIButton from 'common/components/IAIButton';
-import { memo, useContext, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
-import { some } from 'lodash-es';
-import { ImageUsage } from '../../../../app/contexts/DeleteImageContext';
-
-const BoardImageInUseMessage = (props: { imagesUsage?: ImageUsage }) => {
- const { imagesUsage } = props;
-
- if (!imagesUsage) {
- return null;
- }
-
- if (!some(imagesUsage)) {
- return null;
- }
-
- return (
- <>
-
- An image from this board is currently in use in the following features:
-
-
- {imagesUsage.isInitialImage && Image to Image}
- {imagesUsage.isCanvasImage && Unified Canvas}
- {imagesUsage.isControlNetImage && ControlNet}
- {imagesUsage.isNodesImage && Node Editor}
-
-
- If you delete images from this board, those features will immediately be
- reset.
-
- >
- );
-};
-
-const DeleteBoardImagesModal = () => {
- const { t } = useTranslation();
-
- const {
- isOpen,
- onClose,
- board,
- handleDeleteBoardImages,
- handleDeleteBoardOnly,
- imagesUsage,
- } = useContext(DeleteBoardImagesContext);
-
- const cancelRef = useRef(null);
-
- return (
-
-
- {board && (
-
-
- Delete Board
-
-
-
-
-
-
- {t('common.areYouSure')}
-
- This board has {board.image_count} image(s) that will be
- deleted.
-
-
-
-
-
- Cancel
-
- handleDeleteBoardOnly(board.board_id)}
- >
- Delete Board Only
-
- handleDeleteBoardImages(board.board_id)}
- >
- Delete Board and Images
-
-
-
- )}
-
-
- );
-};
-
-export default memo(DeleteBoardImagesModal);
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
new file mode 100644
index 0000000000..3824fd6e0d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -0,0 +1,181 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Flex,
+ Skeleton,
+ Text,
+} from '@chakra-ui/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { skipToken } from '@reduxjs/toolkit/dist/query';
+import { stateSelector } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import IAIButton from 'common/components/IAIButton';
+import ImageUsageMessage from 'features/imageDeletion/components/ImageUsageMessage';
+import {
+ ImageUsage,
+ getImageUsage,
+} from 'features/imageDeletion/store/imageDeletionSlice';
+import { some } from 'lodash-es';
+import { memo, useCallback, useMemo, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ useDeleteBoardAndImagesMutation,
+ useDeleteBoardMutation,
+ useListAllImageNamesForBoardQuery,
+} from 'services/api/endpoints/boards';
+import { BoardDTO } from 'services/api/types';
+
+type Props = {
+ boardToDelete?: BoardDTO;
+ setBoardToDelete: (board?: BoardDTO) => void;
+};
+
+const DeleteImageModal = (props: Props) => {
+ const { boardToDelete, setBoardToDelete } = props;
+ const { t } = useTranslation();
+ const canRestoreDeletedImagesFromBin = useAppSelector(
+ (state) => state.config.canRestoreDeletedImagesFromBin
+ );
+ const { currentData: boardImageNames, isFetching: isFetchingBoardNames } =
+ useListAllImageNamesForBoardQuery(boardToDelete?.board_id ?? skipToken);
+
+ const selectImageUsageSummary = useMemo(
+ () =>
+ createSelector([stateSelector], (state) => {
+ const allImageUsage = (boardImageNames ?? []).map((imageName) =>
+ getImageUsage(state, imageName)
+ );
+
+ const imageUsageSummary: ImageUsage = {
+ isInitialImage: some(allImageUsage, (usage) => usage.isInitialImage),
+ isCanvasImage: some(allImageUsage, (usage) => usage.isCanvasImage),
+ isNodesImage: some(allImageUsage, (usage) => usage.isNodesImage),
+ isControlNetImage: some(
+ allImageUsage,
+ (usage) => usage.isControlNetImage
+ ),
+ };
+ return { imageUsageSummary };
+ }),
+ [boardImageNames]
+ );
+
+ const [deleteBoardOnly, { isLoading: isDeleteBoardOnlyLoading }] =
+ useDeleteBoardMutation();
+
+ const [deleteBoardAndImages, { isLoading: isDeleteBoardAndImagesLoading }] =
+ useDeleteBoardAndImagesMutation();
+
+ const { imageUsageSummary } = useAppSelector(selectImageUsageSummary);
+
+ const handleDeleteBoardOnly = useCallback(() => {
+ if (!boardToDelete) {
+ return;
+ }
+ deleteBoardOnly(boardToDelete.board_id);
+ setBoardToDelete(undefined);
+ }, [boardToDelete, deleteBoardOnly, setBoardToDelete]);
+
+ const handleDeleteBoardAndImages = useCallback(() => {
+ if (!boardToDelete) {
+ return;
+ }
+ deleteBoardAndImages(boardToDelete.board_id);
+ setBoardToDelete(undefined);
+ }, [boardToDelete, deleteBoardAndImages, setBoardToDelete]);
+
+ const handleClose = useCallback(() => {
+ setBoardToDelete(undefined);
+ }, [setBoardToDelete]);
+
+ const cancelRef = useRef(null);
+
+ const isLoading = useMemo(
+ () =>
+ isDeleteBoardAndImagesLoading ||
+ isDeleteBoardOnlyLoading ||
+ isFetchingBoardNames,
+ [
+ isDeleteBoardAndImagesLoading,
+ isDeleteBoardOnlyLoading,
+ isFetchingBoardNames,
+ ]
+ );
+
+ if (!boardToDelete) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ Delete {boardToDelete.board_name}
+
+
+
+
+ {isFetchingBoardNames ? (
+
+
+
+ ) : (
+
+ )}
+ Deleted boards cannot be restored.
+
+ {canRestoreDeletedImagesFromBin
+ ? t('gallery.deleteImageBin')
+ : t('gallery.deleteImagePermanent')}
+
+
+
+
+
+
+ Cancel
+
+
+ Delete Board Only
+
+
+ Delete Board and Images
+
+
+
+
+
+
+ );
+};
+
+export default memo(DeleteImageModal);
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx
index e143a87fc9..52e26c55e7 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx
@@ -17,6 +17,8 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from '../NextPrevImageButtons';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { FaImage } from 'react-icons/fa';
export const imagesSelector = createSelector(
[stateSelector, selectLastSelectedImage],
@@ -168,7 +170,11 @@ const CurrentImagePreview = () => {
draggableData={draggableData}
isUploadDisabled={true}
fitContainer
+ useThumbailFallback
dropLabel="Set as Current Image"
+ noContentFallback={
+
+ }
/>
)}
{shouldShowImageDetails && imageDTO && (
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx
new file mode 100644
index 0000000000..4aa65b234e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx
@@ -0,0 +1,91 @@
+import { ChevronUpIcon } from '@chakra-ui/icons';
+import { Button, Flex, Text } 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 { memo } from 'react';
+import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
+
+const selector = createSelector(
+ [stateSelector],
+ (state) => {
+ const { selectedBoardId } = state.gallery;
+
+ return {
+ selectedBoardId,
+ };
+ },
+ defaultSelectorOptions
+);
+
+type Props = {
+ isOpen: boolean;
+ onToggle: () => void;
+};
+
+const GalleryBoardName = (props: Props) => {
+ const { isOpen, onToggle } = props;
+ const { selectedBoardId } = useAppSelector(selector);
+ const { selectedBoardName } = useListAllBoardsQuery(undefined, {
+ selectFromResult: ({ data }) => {
+ let selectedBoardName = '';
+ if (selectedBoardId === 'images') {
+ selectedBoardName = 'All Images';
+ } else if (selectedBoardId === 'assets') {
+ selectedBoardName = 'All Assets';
+ } else if (selectedBoardId === 'no_board') {
+ selectedBoardName = 'No Board';
+ } else if (selectedBoardId === 'batch') {
+ selectedBoardName = 'Batch';
+ } else {
+ const selectedBoard = data?.find((b) => b.board_id === selectedBoardId);
+ selectedBoardName = selectedBoard?.board_name || 'Unknown Board';
+ }
+
+ return { selectedBoardName };
+ },
+ });
+
+ return (
+
+
+ {selectedBoardName}
+
+
+
+ );
+};
+
+export default memo(GalleryBoardName);
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx
new file mode 100644
index 0000000000..916dec69a2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPinButton.tsx
@@ -0,0 +1,44 @@
+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 IAIIconButton from 'common/components/IAIIconButton';
+import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
+import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
+import { useTranslation } from 'react-i18next';
+import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
+
+const selector = createSelector(
+ [stateSelector],
+ (state) => {
+ const { shouldPinGallery } = state.ui;
+
+ return {
+ shouldPinGallery,
+ };
+ },
+ defaultSelectorOptions
+);
+
+const GalleryPinButton = () => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+
+ const { shouldPinGallery } = useAppSelector(selector);
+
+ const handleSetShouldPinGallery = () => {
+ dispatch(togglePinGalleryPanel());
+ dispatch(requestCanvasRescale());
+ };
+ return (
+ : }
+ />
+ );
+};
+
+export default GalleryPinButton;
diff --git a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx
new file mode 100644
index 0000000000..0677364400
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx
@@ -0,0 +1,76 @@
+import { Flex } from '@chakra-ui/react';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import IAIIconButton from 'common/components/IAIIconButton';
+import IAIPopover from 'common/components/IAIPopover';
+import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
+import IAISlider from 'common/components/IAISlider';
+import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
+
+import { ChangeEvent } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaWrench } from 'react-icons/fa';
+
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
+
+const selector = createSelector(
+ [stateSelector],
+ (state) => {
+ const { galleryImageMinimumWidth, shouldAutoSwitch } = state.gallery;
+
+ return {
+ galleryImageMinimumWidth,
+ shouldAutoSwitch,
+ };
+ },
+ defaultSelectorOptions
+);
+
+const GallerySettingsPopover = () => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+
+ const { galleryImageMinimumWidth, shouldAutoSwitch } =
+ useAppSelector(selector);
+
+ const handleChangeGalleryImageMinimumWidth = (v: number) => {
+ dispatch(setGalleryImageMinimumWidth(v));
+ };
+
+ return (
+ }
+ />
+ }
+ >
+
+ dispatch(setGalleryImageMinimumWidth(64))}
+ />
+ ) =>
+ dispatch(shouldAutoSwitchChanged(e.target.checked))
+ }
+ />
+
+
+ );
+};
+
+export default GallerySettingsPopover;
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
index a903b36caf..565012a36f 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx
@@ -1,13 +1,8 @@
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 { MouseEvent, memo, useCallback, useMemo } from 'react';
+import { MouseEvent, memo, useCallback } from 'react';
import { ImageDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
-import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
type Props = {
@@ -16,23 +11,23 @@ type Props = {
};
const ImageContextMenu = ({ imageDTO, children }: Props) => {
- const selector = useMemo(
- () =>
- createSelector(
- [stateSelector],
- ({ gallery }) => {
- const selectionCount = gallery.selection.length;
+ // const selector = useMemo(
+ // () =>
+ // createSelector(
+ // [stateSelector],
+ // ({ gallery }) => {
+ // const selectionCount = gallery.selection.length;
- return { selectionCount };
- },
- defaultSelectorOptions
- ),
- []
- );
+ // return { selectionCount };
+ // },
+ // defaultSelectorOptions
+ // ),
+ // []
+ // );
- const { selectionCount } = useAppSelector(selector);
+ // const { selectionCount } = useAppSelector(selector);
- const handleContextMenu = useCallback((e: MouseEvent) => {
+ const skipEvent = useCallback((e: MouseEvent) => {
e.preventDefault();
}, []);
@@ -48,13 +43,9 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
- {selectionCount === 1 ? (
-
- ) : (
-
- )}
+
) : null
}
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
index 95872495df..538af11d73 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
@@ -28,8 +28,10 @@ import {
FaShare,
FaTrash,
} from 'react-icons/fa';
-import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
-import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
+import {
+ useGetImageMetadataQuery,
+ useRemoveImageFromBoardMutation,
+} from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types';
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
@@ -128,15 +130,8 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
if (!imageDTO.board_id) {
return;
}
- removeFromBoard({
- board_id: imageDTO.board_id,
- image_name: imageDTO.image_name,
- });
- }, [imageDTO.board_id, imageDTO.image_name, removeFromBoard]);
-
- const handleOpenInNewTab = useCallback(() => {
- window.open(imageDTO.image_url, '_blank');
- }, [imageDTO.image_url]);
+ removeFromBoard({ imageDTO });
+ }, [imageDTO, removeFromBoard]);
const handleAddToBatch = useCallback(() => {
dispatch(imagesAddedToBatch([imageDTO.image_name]));
@@ -149,10 +144,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
return (
<>
- }
- onClickCapture={handleOpenInNewTab}
- >
+ }>
{t('common.openInNewTab')}
@@ -161,6 +153,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
{t('parameters.copyImage')}
)}
+
+ } w="100%">
+ {t('parameters.downloadImage')}
+
+
}
onClickCapture={handleRecallPrompt}
@@ -219,11 +216,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
Remove from Board
)}
-
- } w="100%">
- {t('parameters.downloadImage')}
-
-
}
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index 8badad942e..98b4c33408 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -1,113 +1,34 @@
-import {
- Box,
- Button,
- ButtonGroup,
- Flex,
- Text,
- VStack,
- useColorMode,
- useDisclosure,
-} from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import IAIIconButton from 'common/components/IAIIconButton';
-import IAIPopover from 'common/components/IAIPopover';
-import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
-import IAISlider from 'common/components/IAISlider';
-import {
- setGalleryImageMinimumWidth,
- setGalleryView,
-} from 'features/gallery/store/gallerySlice';
-import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
-
-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';
-
-import { ChevronUpIcon } from '@chakra-ui/icons';
+import { Box, Flex, VStack, useDisclosure } 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 { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
-import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
-import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
-import { mode } from 'theme/util/mode';
+import { memo, useRef } from 'react';
import BoardsList from './Boards/BoardsList/BoardsList';
+import GalleryBoardName from './GalleryBoardName';
+import GalleryPinButton from './GalleryPinButton';
+import GallerySettingsPopover from './GallerySettingsPopover';
import BatchImageGrid from './ImageGrid/BatchImageGrid';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
const selector = createSelector(
[stateSelector],
(state) => {
- const {
- selectedBoardId,
- galleryImageMinimumWidth,
- galleryView,
- shouldAutoSwitch,
- } = state.gallery;
- const { shouldPinGallery } = state.ui;
+ const { selectedBoardId } = state.gallery;
return {
selectedBoardId,
- shouldPinGallery,
- galleryImageMinimumWidth,
- shouldAutoSwitch,
- galleryView,
};
},
defaultSelectorOptions
);
const ImageGalleryContent = () => {
- const dispatch = useAppDispatch();
- const { t } = useTranslation();
const resizeObserverRef = useRef(null);
const galleryGridRef = useRef(null);
-
- const { colorMode } = useColorMode();
-
- const {
- selectedBoardId,
- shouldPinGallery,
- galleryImageMinimumWidth,
- shouldAutoSwitch,
- galleryView,
- } = useAppSelector(selector);
-
- const { selectedBoard } = useListAllBoardsQuery(undefined, {
- selectFromResult: ({ data }) => ({
- selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
- }),
- });
-
- 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) => {
- dispatch(setGalleryImageMinimumWidth(v));
- };
-
- const handleSetShouldPinGallery = () => {
- dispatch(togglePinGalleryPanel());
- dispatch(requestCanvasRescale());
- };
-
- const handleClickImagesCategory = useCallback(() => {
- dispatch(setGalleryView('images'));
- }, [dispatch]);
-
- const handleClickAssetsCategory = useCallback(() => {
- dispatch(setGalleryView('assets'));
- }, [dispatch]);
+ const { selectedBoardId } = useAppSelector(selector);
+ const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
+ useDisclosure();
return (
{
gap: 2,
}}
>
-
- }
- />
- }
- />
-
-
-
- {boardTitle}
-
-
-
- }
- />
- }
- >
-
- dispatch(setGalleryImageMinimumWidth(64))}
- />
- ) =>
- dispatch(shouldAutoSwitchChanged(e.target.checked))
- }
- />
-
-
-
- : }
+
+
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index eb7428bb69..dcce3a1b18 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -1,16 +1,13 @@
-import { Box, Spinner } from '@chakra-ui/react';
+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 IAIFillSkeleton from 'common/components/IAIFillSkeleton';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
-import {
- imageRangeEndSelected,
- imageSelected,
- imageSelectionToggled,
-} from 'features/gallery/store/gallerySlice';
+import { imageSelected } 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';
@@ -84,7 +81,7 @@ const GalleryImage = (props: HoverableImageProps) => {
}, [imageDTO, selection, selectionCount]);
if (!imageDTO) {
- return ;
+ return ;
}
return (
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
index 7811e5f1c4..a43f9ce07b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
@@ -1,124 +1,70 @@
-import { Box } from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { Box, Spinner } from '@chakra-ui/react';
+import { useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
-import { useOverlayScrollbars } from 'overlayscrollbars-react';
-
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { IMAGE_LIMIT } from 'features/gallery//store/gallerySlice';
+import {
+ UseOverlayScrollbarsParams,
+ 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,
-} from 'features/gallery//store/gallerySlice';
-import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
+import { FaExclamationCircle, FaImage } from 'react-icons/fa';
import { VirtuosoGrid } from 'react-virtuoso';
-import { receivedPageOfImages } from 'services/api/thunks/image';
-import { useListBoardImagesQuery } from '../../../../services/api/endpoints/boardImages';
+import {
+ useLazyListImagesQuery,
+ useListImagesQuery,
+} from 'services/api/endpoints/images';
+import GalleryImage from './GalleryImage';
import ImageGridItemContainer from './ImageGridItemContainer';
import ImageGridListContainer from './ImageGridListContainer';
+import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
-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,
- };
+const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
+ defer: true,
+ options: {
+ scrollbars: {
+ visibility: 'auto',
+ autoHide: 'leave',
+ autoHideDelay: 1300,
+ theme: 'os-theme-dark',
+ },
+ overflow: { x: 'hidden' },
},
- 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 [initialize, osInstance] = useOverlayScrollbars(
+ overlayScrollbarsConfig
+ );
- const [didInitialFetch, setDidInitialFetch] = useState(false);
+ const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
- const dispatch = useAppDispatch();
+ const { currentData, isFetching, isSuccess, isError } =
+ useListImagesQuery(queryArgs);
- 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 [listImages] = useLazyListImagesQuery();
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]);
+ if (!currentData) {
+ return false;
+ }
+ return currentData.ids.length < currentData.total;
+ }, [currentData]);
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]);
+ listImages({
+ ...queryArgs,
+ offset: currentData?.ids.length ?? 0,
+ limit: IMAGE_LIMIT,
+ });
+ }, [listImages, queryArgs, currentData?.ids.length]);
useEffect(() => {
- // Set up gallery scroler
+ // Initialize the gallery's custom scrollbar
const { current: root } = rootRef;
if (scroller && root) {
initialize({
@@ -131,47 +77,17 @@ const GalleryImageGrid = () => {
return () => osInstance()?.destroy();
}, [scroller, initialize, osInstance]);
- 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) {
+ if (!currentData) {
return (
-
+
+
+
+ );
+ }
+
+ if (isSuccess && currentData?.ids.length === 0) {
+ return (
+
{
);
}
- if (status !== 'rejected') {
+ if (isSuccess && currentData) {
return (
<>
(
-
+
)}
/>
@@ -211,6 +128,17 @@ const GalleryImageGrid = () => {
>
);
}
+
+ if (isError) {
+ return (
+
+
+
+ );
+ }
};
export default memo(GalleryImageGrid);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
index 89cd0a5005..df574c860b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
@@ -11,11 +11,9 @@ const ImageMetadataActions = (props: Props) => {
const { metadata } = props;
const {
- recallBothPrompts,
recallPositivePrompt,
recallNegativePrompt,
recallSeed,
- recallInitialImage,
recallCfgScale,
recallModel,
recallScheduler,
@@ -23,7 +21,6 @@ const ImageMetadataActions = (props: Props) => {
recallWidth,
recallHeight,
recallStrength,
- recallAllParameters,
} = useRecallParameters();
const handleRecallPositivePrompt = useCallback(() => {
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts b/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts
index 44473bea83..b389ffff50 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts
@@ -2,61 +2,76 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
+ IMAGE_LIMIT,
imageSelected,
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';
+import {
+ ListImagesArgs,
+ imagesAdapter,
+ imagesApi,
+ useLazyListImagesQuery,
+} from 'services/api/endpoints/images';
+import { selectListImagesBaseQueryArgs } from '../store/gallerySelectors';
export const nextPrevImageButtonsSelector = createSelector(
- [stateSelector, selectFilteredImages],
- (state, filteredImages) => {
- const { total, isFetching } = state.gallery;
+ [stateSelector, selectListImagesBaseQueryArgs],
+ (state, baseQueryArgs) => {
+ const { data, status } =
+ imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
+
const lastSelectedImage =
state.gallery.selection[state.gallery.selection.length - 1];
- if (!lastSelectedImage || filteredImages.length === 0) {
+ const isFetching = status === 'pending';
+
+ if (!data || !lastSelectedImage || data.total === 0) {
return {
+ isFetching,
+ queryArgs: baseQueryArgs,
isOnFirstImage: true,
isOnLastImage: true,
};
}
- const currentImageIndex = filteredImages.findIndex(
+ const queryArgs: ListImagesArgs = {
+ ...baseQueryArgs,
+ offset: data.ids.length,
+ limit: IMAGE_LIMIT,
+ };
+
+ const selectors = imagesAdapter.getSelectors();
+
+ const images = selectors.selectAll(data);
+
+ const currentImageIndex = images.findIndex(
(i) => i.image_name === lastSelectedImage
);
- const nextImageIndex = clamp(
- currentImageIndex + 1,
- 0,
- filteredImages.length - 1
- );
+ const nextImageIndex = clamp(currentImageIndex + 1, 0, images.length - 1);
- const prevImageIndex = clamp(
- currentImageIndex - 1,
- 0,
- filteredImages.length - 1
- );
+ const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
- const nextImageId = filteredImages[nextImageIndex].image_name;
- const prevImageId = filteredImages[prevImageIndex].image_name;
+ const nextImageId = images[nextImageIndex].image_name;
+ const prevImageId = images[prevImageIndex].image_name;
- const nextImage = selectImagesById(state, nextImageId);
- const prevImage = selectImagesById(state, prevImageId);
+ const nextImage = selectors.selectById(data, nextImageId);
+ const prevImage = selectors.selectById(data, prevImageId);
- const imagesLength = filteredImages.length;
+ const imagesLength = images.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
- areMoreImagesAvailable: total > imagesLength,
- isFetching,
+ areMoreImagesAvailable: data?.total ?? 0 > imagesLength,
+ isFetching: status === 'pending',
nextImage,
prevImage,
nextImageId,
prevImageId,
+ queryArgs,
};
},
{
@@ -76,6 +91,7 @@ export const useNextPrevImage = () => {
prevImageId,
areMoreImagesAvailable,
isFetching,
+ queryArgs,
} = useAppSelector(nextPrevImageButtonsSelector);
const handlePrevImage = useCallback(() => {
@@ -86,13 +102,11 @@ export const useNextPrevImage = () => {
nextImageId && dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
+ const [listImages] = useLazyListImagesQuery();
+
const handleLoadMoreImages = useCallback(() => {
- dispatch(
- receivedPageOfImages({
- is_intermediate: false,
- })
- );
- }, [dispatch]);
+ listImages(queryArgs);
+ }, [listImages, queryArgs]);
return {
handlePrevImage,
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
index 045fb68737..19da92e083 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
@@ -1,136 +1,38 @@
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 { ListImagesArgs } from 'services/api/endpoints/images';
+import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
import {
- ASSETS_CATEGORIES,
- BoardId,
- IMAGE_CATEGORIES,
- imagesAdapter,
- initialGalleryState,
-} from './gallerySlice';
+ getBoardIdQueryParamForBoard,
+ getCategoriesQueryParamForBoard,
+} from './util';
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 selectListImagesBaseQueryArgs = createSelector(
+ [(state: RootState) => state],
+ (state) => {
+ const { selectedBoardId } = state.gallery;
-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 categories = getCategoriesQueryParamForBoard(selectedBoardId);
+ const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
- const deletedImageIndex = ids.findIndex(
- (result) => result.toString() === image_name
- );
+ const listImagesBaseQueryArgs: ListImagesArgs = {
+ categories,
+ board_id,
+ offset: 0,
+ limit: INITIAL_IMAGE_LIMIT,
+ is_intermediate: false,
+ };
- 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);
+ return listImagesBaseQueryArgs;
},
defaultSelectorOptions
);
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index fa1f6a6f1a..340559561f 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -1,20 +1,8 @@
-import type { PayloadAction, Update } from '@reduxjs/toolkit';
-import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
-import { RootState } from 'app/store/store';
-import { dateComparator } from 'common/util/dateComparator';
+import type { PayloadAction } from '@reduxjs/toolkit';
+import { createSlice } from '@reduxjs/toolkit';
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,
- sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
-});
+import { ImageCategory } from 'services/api/types';
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
export const ASSETS_CATEGORIES: ImageCategory[] = [
@@ -26,113 +14,74 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
export const INITIAL_IMAGE_LIMIT = 100;
export const IMAGE_LIMIT = 20;
-export type GalleryView = 'images' | 'assets';
+// export type GalleryView = 'images' | 'assets';
export type BoardId =
- | 'all'
- | 'none'
+ | 'images'
+ | 'assets'
+ | 'no_board'
| 'batch'
| (string & Record);
-type AdditionaGalleryState = {
- offset: number;
- limit: number;
- total: number;
- isLoading: boolean;
- isFetching: boolean;
+type GalleryState = {
selection: string[];
shouldAutoSwitch: boolean;
galleryImageMinimumWidth: number;
- galleryView: GalleryView;
selectedBoardId: BoardId;
- isInitialized: boolean;
batchImageNames: string[];
isBatchEnabled: boolean;
};
-export const initialGalleryState =
- imagesAdapter.getInitialState({
- offset: 0,
- limit: 0,
- total: 0,
- isLoading: true,
- isFetching: true,
- selection: [],
- shouldAutoSwitch: true,
- galleryImageMinimumWidth: 96,
- galleryView: 'images',
- selectedBoardId: 'all',
- isInitialized: false,
- batchImageNames: [],
- isBatchEnabled: false,
- });
+export const initialGalleryState: GalleryState = {
+ selection: [],
+ shouldAutoSwitch: true,
+ galleryImageMinimumWidth: 96,
+ selectedBoardId: 'images',
+ batchImageNames: [],
+ isBatchEnabled: false,
+};
export const gallerySlice = createSlice({
name: 'gallery',
initialState: initialGalleryState,
reducers: {
- imageUpserted: (state, action: PayloadAction) => {
- imagesAdapter.upsertOne(state, action.payload);
- if (
- state.shouldAutoSwitch &&
- action.payload.image_category === 'general'
- ) {
- state.selection = [action.payload.image_name];
- state.galleryView = 'images';
- state.selectedBoardId = 'all';
- }
- },
- imageUpdatedOne: (state, action: PayloadAction>) => {
- imagesAdapter.updateOne(state, action.payload);
- },
- 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);
- state.batchImageNames = state.batchImageNames.filter(
- (name) => !action.payload.includes(name)
- );
+ // TODO: port all instances of this to use RTK Query cache
+ // imagesAdapter.removeMany(state, action.payload);
+ // state.batchImageNames = state.batchImageNames.filter(
+ // (name) => !action.payload.includes(name)
+ // );
},
imageRangeEndSelected: (state, action: PayloadAction) => {
- const rangeEndImageName = action.payload;
- const lastSelectedImage = state.selection[state.selection.length - 1];
-
- const filteredImages = selectFilteredImagesLocal(state);
-
- const lastClickedIndex = filteredImages.findIndex(
- (n) => n.image_name === lastSelectedImage
- );
-
- const currentClickedIndex = filteredImages.findIndex(
- (n) => n.image_name === rangeEndImageName
- );
-
- if (lastClickedIndex > -1 && currentClickedIndex > -1) {
- // We have a valid range!
- const start = Math.min(lastClickedIndex, currentClickedIndex);
- const end = Math.max(lastClickedIndex, currentClickedIndex);
-
- const imagesToSelect = filteredImages
- .slice(start, end + 1)
- .map((i) => i.image_name);
-
- state.selection = uniq(state.selection.concat(imagesToSelect));
- }
+ // const rangeEndImageName = action.payload;
+ // const lastSelectedImage = state.selection[state.selection.length - 1];
+ // const filteredImages = selectFilteredImagesLocal(state);
+ // const lastClickedIndex = filteredImages.findIndex(
+ // (n) => n.image_name === lastSelectedImage
+ // );
+ // const currentClickedIndex = filteredImages.findIndex(
+ // (n) => n.image_name === rangeEndImageName
+ // );
+ // if (lastClickedIndex > -1 && currentClickedIndex > -1) {
+ // // We have a valid range!
+ // const start = Math.min(lastClickedIndex, currentClickedIndex);
+ // const end = Math.max(lastClickedIndex, currentClickedIndex);
+ // const imagesToSelect = filteredImages
+ // .slice(start, end + 1)
+ // .map((i) => i.image_name);
+ // state.selection = uniq(state.selection.concat(imagesToSelect));
+ // }
},
imageSelectionToggled: (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));
- }
+ // 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));
+ // }
},
imageSelected: (state, action: PayloadAction) => {
state.selection = action.payload ? [action.payload] : [];
@@ -143,15 +92,9 @@ export const gallerySlice = createSlice({
setGalleryImageMinimumWidth: (state, action: PayloadAction) => {
state.galleryImageMinimumWidth = action.payload;
},
- setGalleryView: (state, action: PayloadAction) => {
- state.galleryView = action.payload;
- },
boardIdSelected: (state, action: PayloadAction) => {
state.selectedBoardId = action.payload;
},
- isLoadingChanged: (state, action: PayloadAction) => {
- state.isLoading = action.payload;
- },
isBatchEnabledChanged: (state, action: PayloadAction) => {
state.isBatchEnabled = action.payload;
},
@@ -182,47 +125,11 @@ export const gallerySlice = createSlice({
},
},
extraReducers: (builder) => {
- builder.addCase(receivedPageOfImages.pending, (state) => {
- state.isFetching = true;
- });
- builder.addCase(receivedPageOfImages.rejected, (state) => {
- state.isFetching = false;
- });
- builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
- state.isFetching = false;
- const { board_id, categories, image_origin, is_intermediate } =
- action.meta.arg;
-
- const { items, offset, limit, total } = action.payload;
-
- imagesAdapter.upsertMany(state, items);
-
- if (state.selection.length === 0 && items.length) {
- state.selection = [items[0].image_name];
- }
-
- 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;
- }
-
- state.offset = offset;
- state.total = total;
- });
- builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
- const { image_name, image_url, thumbnail_url } = action.payload;
-
- imagesAdapter.updateOne(state, {
- id: image_name,
- changes: { image_url, thumbnail_url },
- });
- });
builder.addMatcher(
boardsApi.endpoints.deleteBoard.matchFulfilled,
(state, action) => {
if (action.meta.arg.originalArgs === state.selectedBoardId) {
- state.selectedBoardId = 'all';
+ state.selectedBoardId = 'images';
}
}
);
@@ -230,26 +137,13 @@ export const gallerySlice = createSlice({
});
export const {
- selectAll: selectImagesAll,
- selectById: selectImagesById,
- selectEntities: selectImagesEntities,
- selectIds: selectImagesIds,
- selectTotal: selectImagesTotal,
-} = imagesAdapter.getSelectors((state) => state.gallery);
-
-export const {
- imageUpserted,
- imageUpdatedOne,
- imageRemoved,
imagesRemoved,
imageRangeEndSelected,
imageSelectionToggled,
imageSelected,
shouldAutoSwitchChanged,
setGalleryImageMinimumWidth,
- setGalleryView,
boardIdSelected,
- isLoadingChanged,
isBatchEnabledChanged,
imagesAddedToBatch,
imagesRemovedFromBatch,
diff --git a/invokeai/frontend/web/src/features/gallery/store/util.ts b/invokeai/frontend/web/src/features/gallery/store/util.ts
new file mode 100644
index 0000000000..fcc39bae82
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/store/util.ts
@@ -0,0 +1,54 @@
+import { SYSTEM_BOARDS } from 'services/api/endpoints/images';
+import { ASSETS_CATEGORIES, BoardId, IMAGE_CATEGORIES } from './gallerySlice';
+import { ImageCategory } from 'services/api/types';
+import { isEqual } from 'lodash-es';
+
+export const getCategoriesQueryParamForBoard = (
+ board_id: BoardId
+): ImageCategory[] | undefined => {
+ if (board_id === 'assets') {
+ return ASSETS_CATEGORIES;
+ }
+
+ if (board_id === 'images') {
+ return IMAGE_CATEGORIES;
+ }
+
+ // 'no_board' board, 'batch' board, user boards
+ return undefined;
+};
+
+export const getBoardIdQueryParamForBoard = (
+ board_id: BoardId
+): string | undefined => {
+ if (board_id === 'no_board') {
+ return 'none';
+ }
+
+ // system boards besides 'no_board'
+ if (SYSTEM_BOARDS.includes(board_id)) {
+ return undefined;
+ }
+
+ // user boards
+ return board_id;
+};
+
+export const getBoardIdFromBoardAndCategoriesQueryParam = (
+ board_id: string | undefined,
+ categories: ImageCategory[] | undefined
+): BoardId => {
+ if (board_id === undefined && isEqual(categories, IMAGE_CATEGORIES)) {
+ return 'images';
+ }
+
+ if (board_id === undefined && isEqual(categories, ASSETS_CATEGORIES)) {
+ return 'assets';
+ }
+
+ if (board_id === 'none') {
+ return 'no_board';
+ }
+
+ return board_id ?? 'UNKNOWN_BOARD';
+};
diff --git a/invokeai/frontend/web/src/features/imageDeletion/components/ImageUsageMessage.tsx b/invokeai/frontend/web/src/features/imageDeletion/components/ImageUsageMessage.tsx
index 9bd4ca5198..64d001f8ba 100644
--- a/invokeai/frontend/web/src/features/imageDeletion/components/ImageUsageMessage.tsx
+++ b/invokeai/frontend/web/src/features/imageDeletion/components/ImageUsageMessage.tsx
@@ -2,9 +2,17 @@ import { some } from 'lodash-es';
import { memo } from 'react';
import { ImageUsage } from '../store/imageDeletionSlice';
import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
-
-const ImageUsageMessage = (props: { imageUsage?: ImageUsage }) => {
- const { imageUsage } = props;
+type Props = {
+ imageUsage?: ImageUsage;
+ topMessage?: string;
+ bottomMessage?: string;
+};
+const ImageUsageMessage = (props: Props) => {
+ const {
+ imageUsage,
+ topMessage = 'This image is currently in use in the following features:',
+ bottomMessage = 'If you delete this image, those features will immediately be reset.',
+ } = props;
if (!imageUsage) {
return null;
@@ -16,16 +24,14 @@ const ImageUsageMessage = (props: { imageUsage?: ImageUsage }) => {
return (
<>
- This image is currently in use in the following features:
+ {topMessage}
{imageUsage.isInitialImage && Image to Image}
{imageUsage.isCanvasImage && Unified Canvas}
{imageUsage.isControlNetImage && ControlNet}
{imageUsage.isNodesImage && Node Editor}
-
- If you delete this image, those features will immediately be reset.
-
+ {bottomMessage}
>
);
};
diff --git a/invokeai/frontend/web/src/features/imageDeletion/store/imageDeletionSlice.ts b/invokeai/frontend/web/src/features/imageDeletion/store/imageDeletionSlice.ts
index 49630bcdb4..df90da5088 100644
--- a/invokeai/frontend/web/src/features/imageDeletion/store/imageDeletionSlice.ts
+++ b/invokeai/frontend/web/src/features/imageDeletion/store/imageDeletionSlice.ts
@@ -51,10 +51,42 @@ export type ImageUsage = {
isControlNetImage: boolean;
};
+export const getImageUsage = (state: RootState, image_name: string) => {
+ const { generation, canvas, nodes, controlNet } = state;
+ const isInitialImage = generation.initialImage?.imageName === image_name;
+
+ const isCanvasImage = canvas.layerState.objects.some(
+ (obj) => obj.kind === 'image' && obj.imageName === image_name
+ );
+
+ const isNodesImage = nodes.nodes.some((node) => {
+ return some(
+ node.data.inputs,
+ (input) =>
+ input.type === 'image' && input.value?.image_name === image_name
+ );
+ });
+
+ const isControlNetImage = some(
+ controlNet.controlNets,
+ (c) =>
+ c.controlImage === image_name || c.processedControlImage === image_name
+ );
+
+ const imageUsage: ImageUsage = {
+ isInitialImage,
+ isCanvasImage,
+ isNodesImage,
+ isControlNetImage,
+ };
+
+ return imageUsage;
+};
+
export const selectImageUsage = createSelector(
[(state: RootState) => state],
- ({ imageDeletion, generation, canvas, nodes, controlNet }) => {
- const { imageToDelete } = imageDeletion;
+ (state) => {
+ const { imageToDelete } = state.imageDeletion;
if (!imageToDelete) {
return;
@@ -62,32 +94,7 @@ export const selectImageUsage = createSelector(
const { image_name } = imageToDelete;
- const isInitialImage = generation.initialImage?.imageName === image_name;
-
- const isCanvasImage = canvas.layerState.objects.some(
- (obj) => obj.kind === 'image' && obj.imageName === image_name
- );
-
- const isNodesImage = nodes.nodes.some((node) => {
- return some(
- node.data.inputs,
- (input) =>
- input.type === 'image' && input.value?.image_name === image_name
- );
- });
-
- const isControlNetImage = some(
- controlNet.controlNets,
- (c) =>
- c.controlImage === image_name || c.processedControlImage === image_name
- );
-
- const imageUsage: ImageUsage = {
- isInitialImage,
- isCanvasImage,
- isNodesImage,
- isControlNetImage,
- };
+ const imageUsage = getImageUsage(state, image_name);
return imageUsage;
},
diff --git a/invokeai/frontend/web/src/features/nodes/components/ProgressImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/ProgressImageNode.tsx
index faaec06e49..142e2a2990 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ProgressImageNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ProgressImageNode.tsx
@@ -1,33 +1,46 @@
import { Flex, Image } from '@chakra-ui/react';
-import { NodeProps } from 'reactflow';
-import { InvocationValue } from '../types/types';
-
-import { useAppSelector } from 'app/store/storeHooks';
+import { RootState } from 'app/store/store';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { memo } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { NodeProps, OnResize } from 'reactflow';
+import { setProgressNodeSize } from '../store/nodesSlice';
import IAINodeHeader from './IAINode/IAINodeHeader';
import IAINodeResizer from './IAINode/IAINodeResizer';
import NodeWrapper from './NodeWrapper';
-const ProgressImageNode = (props: NodeProps) => {
- const progressImage = useAppSelector((state) => state.system.progressImage);
+const ProgressImageNode = (props: NodeProps) => {
+ const progressImage = useSelector(
+ (state: RootState) => state.system.progressImage
+ );
+ const progressNodeSize = useSelector(
+ (state: RootState) => state.nodes.progressNodeSize
+ );
+ const dispatch = useDispatch();
const { selected } = props;
+ const handleResize: OnResize = (_, newSize) => {
+ dispatch(setProgressNodeSize(newSize));
+ };
+
return (
-
{progressImage ? (
@@ -42,22 +55,17 @@ const ProgressImageNode = (props: NodeProps) => {
) : (
)}
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
index 34e403f9cc..3d2ef7f59b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
@@ -15,8 +15,8 @@ import {
} from 'app/components/ImageDnd/typesafeDnd';
import IAIDndImage from 'common/components/IAIDndImage';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-import { PostUploadAction } from 'services/api/thunks/image';
import { FieldComponentProps } from './types';
+import { PostUploadAction } from 'services/api/types';
const ImageInputFieldComponent = (
props: FieldComponentProps
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 833a220336..997d0493dd 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -35,6 +35,7 @@ export type NodesState = {
shouldShowFieldTypeLegend: boolean;
shouldShowMinimapPanel: boolean;
editorInstance: ReactFlowInstance | undefined;
+ progressNodeSize: { width: number; height: number };
};
export const initialNodesState: NodesState = {
@@ -47,6 +48,7 @@ export const initialNodesState: NodesState = {
shouldShowFieldTypeLegend: false,
shouldShowMinimapPanel: true,
editorInstance: undefined,
+ progressNodeSize: { width: 512, height: 512 },
};
const nodesSlice = createSlice({
@@ -157,6 +159,12 @@ const nodesSlice = createSlice({
loadFileEdges: (state, action: PayloadAction) => {
state.edges = action.payload;
},
+ setProgressNodeSize: (
+ state,
+ action: PayloadAction<{ width: number; height: number }>
+ ) => {
+ state.progressNodeSize = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
@@ -182,6 +190,7 @@ export const {
setEditorInstance,
loadFileNodes,
loadFileEdges,
+ setProgressNodeSize,
} = nodesSlice.actions;
export default nodesSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts
index 2b7ccccda2..0f882f248d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts
@@ -29,6 +29,7 @@ export const addControlNetToLinearGraph = (
const controlNetIterateNode: CollectInvocation = {
id: CONTROL_NET_COLLECT,
type: 'collect',
+ is_intermediate: true,
};
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
graph.edges.push({
@@ -55,6 +56,7 @@ export const addControlNetToLinearGraph = (
const controlNetNode: ControlNetInvocation = {
id: `control_net_${controlNetId}`,
type: 'controlnet',
+ is_intermediate: true,
begin_step_percent: beginStepPct,
end_step_percent: endStepPct,
control_mode: controlMode,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts
index 90011ff4c2..ae3b31c2ad 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts
@@ -43,6 +43,7 @@ export const addDynamicPromptsToGraph = (
const dynamicPromptNode: DynamicPromptInvocation = {
id: DYNAMIC_PROMPT,
type: 'dynamic_prompt',
+ is_intermediate: true,
max_prompts: combinatorial ? maxPrompts : iterations,
combinatorial,
prompt: positivePrompt,
@@ -51,6 +52,7 @@ export const addDynamicPromptsToGraph = (
const iterateNode: IterateInvocation = {
id: ITERATE,
type: 'iterate',
+ is_intermediate: true,
};
graph.nodes[DYNAMIC_PROMPT] = dynamicPromptNode;
@@ -99,6 +101,7 @@ export const addDynamicPromptsToGraph = (
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
+ is_intermediate: true,
};
graph.nodes[RANDOM_INT] = randomIntNode;
@@ -133,6 +136,7 @@ export const addDynamicPromptsToGraph = (
const rangeOfSizeNode: RangeOfSizeInvocation = {
id: RANGE_OF_SIZE,
type: 'range_of_size',
+ is_intermediate: true,
size: iterations,
step: 1,
};
@@ -140,6 +144,7 @@ export const addDynamicPromptsToGraph = (
const iterateNode: IterateInvocation = {
id: ITERATE,
type: 'iterate',
+ is_intermediate: true,
};
graph.nodes[ITERATE] = iterateNode;
@@ -186,6 +191,7 @@ export const addDynamicPromptsToGraph = (
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
+ is_intermediate: true,
};
graph.nodes[RANDOM_INT] = randomIntNode;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts
index a2cf1477f2..d38c09cd75 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts
@@ -60,6 +60,7 @@ export const addLoRAsToGraph = (
const loraLoaderNode: LoraLoaderInvocation = {
type: 'lora_loader',
id: currentLoraNodeId,
+ is_intermediate: true,
lora: { model_name, base_model },
weight,
};
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts
index 8574dc4e46..d4e21f547d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts
@@ -28,6 +28,7 @@ export const addVAEToGraph = (
graph.nodes[VAE_LOADER] = {
type: 'vae_loader',
id: VAE_LOADER,
+ is_intermediate: true,
vae_model: vae,
};
}
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
index 3b2816844b..5d6d7d141b 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
@@ -1,10 +1,9 @@
-import { RootState } from 'app/store/store';
-import { ImageDTO } from 'services/api/types';
import { log } from 'app/logging/useLogger';
-import { forEach } from 'lodash-es';
-import { buildCanvasInpaintGraph } from './buildCanvasInpaintGraph';
+import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types';
+import { ImageDTO } from 'services/api/types';
import { buildCanvasImageToImageGraph } from './buildCanvasImageToImageGraph';
+import { buildCanvasInpaintGraph } from './buildCanvasInpaintGraph';
import { buildCanvasTextToImageGraph } from './buildCanvasTextToImageGraph';
const moduleLog = log.child({ namespace: 'nodes' });
@@ -31,9 +30,5 @@ export const buildCanvasGraph = (
graph = buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage);
}
- forEach(graph.nodes, (node) => {
- graph.nodes[node.id].is_intermediate = true;
- });
-
return graph;
};
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts
index 821df8fe6e..fef44bd549 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts
@@ -50,6 +50,8 @@ export const buildCanvasImageToImageGraph = (
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
+ const { shouldAutoSave } = state.canvas;
+
if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
@@ -75,35 +77,42 @@ export const buildCanvasImageToImageGraph = (
[POSITIVE_CONDITIONING]: {
type: 'compel',
id: POSITIVE_CONDITIONING,
+ is_intermediate: true,
prompt: positivePrompt,
},
[NEGATIVE_CONDITIONING]: {
type: 'compel',
id: NEGATIVE_CONDITIONING,
+ is_intermediate: true,
prompt: negativePrompt,
},
[NOISE]: {
type: 'noise',
id: NOISE,
+ is_intermediate: true,
use_cpu,
},
[MAIN_MODEL_LOADER]: {
type: 'main_model_loader',
id: MAIN_MODEL_LOADER,
+ is_intermediate: true,
model,
},
[CLIP_SKIP]: {
type: 'clip_skip',
id: CLIP_SKIP,
+ is_intermediate: true,
skipped_layers: clipSkip,
},
[LATENTS_TO_IMAGE]: {
+ is_intermediate: !shouldAutoSave,
type: 'l2i',
id: LATENTS_TO_IMAGE,
},
[LATENTS_TO_LATENTS]: {
type: 'l2l',
id: LATENTS_TO_LATENTS,
+ is_intermediate: true,
cfg_scale,
scheduler,
steps,
@@ -112,6 +121,7 @@ export const buildCanvasImageToImageGraph = (
[IMAGE_TO_LATENTS]: {
type: 'i2l',
id: IMAGE_TO_LATENTS,
+ is_intermediate: true,
// must be set manually later, bc `fit` parameter may require a resize node inserted
// image: {
// image_name: initialImage.image_name,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
index 9ad976b01f..d2abff1028 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
@@ -61,12 +61,17 @@ export const buildCanvasInpaintGraph = (
const { width, height } = state.canvas.boundingBoxDimensions;
// We may need to set the inpaint width and height to scale the image
- const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas;
+ const {
+ scaledBoundingBoxDimensions,
+ boundingBoxScaleMethod,
+ shouldAutoSave,
+ } = state.canvas;
const graph: NonNullableGraph = {
id: INPAINT_GRAPH,
nodes: {
[INPAINT]: {
+ is_intermediate: !shouldAutoSave,
type: 'inpaint',
id: INPAINT,
steps,
@@ -100,26 +105,31 @@ export const buildCanvasInpaintGraph = (
[POSITIVE_CONDITIONING]: {
type: 'compel',
id: POSITIVE_CONDITIONING,
+ is_intermediate: true,
prompt: positivePrompt,
},
[NEGATIVE_CONDITIONING]: {
type: 'compel',
id: NEGATIVE_CONDITIONING,
+ is_intermediate: true,
prompt: negativePrompt,
},
[MAIN_MODEL_LOADER]: {
type: 'main_model_loader',
id: MAIN_MODEL_LOADER,
+ is_intermediate: true,
model,
},
[CLIP_SKIP]: {
type: 'clip_skip',
id: CLIP_SKIP,
+ is_intermediate: true,
skipped_layers: clipSkip,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',
id: RANGE_OF_SIZE,
+ is_intermediate: true,
// seed - must be connected manually
// start: 0,
size: iterations,
@@ -128,6 +138,7 @@ export const buildCanvasInpaintGraph = (
[ITERATE]: {
type: 'iterate',
id: ITERATE,
+ is_intermediate: true,
},
},
edges: [
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts
index f6fd43b0a5..a5ebbde289 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts
@@ -41,6 +41,8 @@ export const buildCanvasTextToImageGraph = (
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
+ const { shouldAutoSave } = state.canvas;
+
if (!model) {
moduleLog.error('No model found in state');
throw new Error('No model found in state');
@@ -66,16 +68,19 @@ export const buildCanvasTextToImageGraph = (
[POSITIVE_CONDITIONING]: {
type: 'compel',
id: POSITIVE_CONDITIONING,
+ is_intermediate: true,
prompt: positivePrompt,
},
[NEGATIVE_CONDITIONING]: {
type: 'compel',
id: NEGATIVE_CONDITIONING,
+ is_intermediate: true,
prompt: negativePrompt,
},
[NOISE]: {
type: 'noise',
id: NOISE,
+ is_intermediate: true,
width,
height,
use_cpu,
@@ -83,6 +88,7 @@ export const buildCanvasTextToImageGraph = (
[TEXT_TO_LATENTS]: {
type: 't2l',
id: TEXT_TO_LATENTS,
+ is_intermediate: true,
cfg_scale,
scheduler,
steps,
@@ -90,16 +96,19 @@ export const buildCanvasTextToImageGraph = (
[MAIN_MODEL_LOADER]: {
type: 'main_model_loader',
id: MAIN_MODEL_LOADER,
+ is_intermediate: true,
model,
},
[CLIP_SKIP]: {
type: 'clip_skip',
id: CLIP_SKIP,
+ is_intermediate: true,
skipped_layers: clipSkip,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
id: LATENTS_TO_IMAGE,
+ is_intermediate: !shouldAutoSave,
},
},
edges: [
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 2422e6f542..9e23a1a243 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
@@ -5,12 +5,11 @@ 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';
+import { PostUploadAction } from 'services/api/types';
const selector = createSelector(
[stateSelector],
@@ -30,7 +29,6 @@ const postUploadAction: PostUploadAction = {
const InitialImageDisplay = () => {
const { isResetButtonDisabled } = useAppSelector(selector);
const dispatch = useAppDispatch();
- const { openUploader } = useImageUploader();
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
@@ -40,10 +38,6 @@ const InitialImageDisplay = () => {
dispatch(clearInitialImage());
}, [dispatch]);
- const handleUpload = useCallback(() => {
- openUploader();
- }, [openUploader]);
-
return (
{
tooltip={'Upload Initial Image'}
aria-label={'Upload Initial Image'}
icon={}
- onClick={handleUpload}
{...getUploadButtonProps()}
/>
{
[dispatch, parameterSetToast, parameterNotSetToast]
);
- /**
- * Sets initial image with toast
- */
- const recallInitialImage = useCallback(
- async (image: unknown) => {
- if (!isImageField(image)) {
- parameterNotSetToast();
- return;
- }
- dispatch(initialImageSelected(image.image_name));
- parameterSetToast();
- },
- [dispatch, parameterSetToast, parameterNotSetToast]
- );
-
- /**
+ /*
* Sets image as initial image with toast
*/
const sendToImageToImage = useCallback(
@@ -330,7 +315,6 @@ export const useRecallParameters = () => {
recallPositivePrompt,
recallNegativePrompt,
recallSeed,
- recallInitialImage,
recallCfgScale,
recallModel,
recallScheduler,
diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts
index 7a4f86d681..076350ea67 100644
--- a/invokeai/frontend/web/src/features/parameters/store/actions.ts
+++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts
@@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import { ImageDTO, MainModelField } from 'services/api/types';
-export const initialImageSelected = createAction(
+export const initialImageSelected = createAction(
'generation/initialImageSelected'
);
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx
new file mode 100644
index 0000000000..d75eb4d4c2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx
@@ -0,0 +1,60 @@
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useCallback, useEffect, useState } from 'react';
+import { StyledFlex } from './SettingsModal';
+import { Heading, Text } from '@chakra-ui/react';
+import IAIButton from '../../../../common/components/IAIButton';
+import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images';
+import { addToast } from '../../store/systemSlice';
+import { resetCanvas } from '../../../canvas/store/canvasSlice';
+
+export default function SettingsClearIntermediates() {
+ const dispatch = useAppDispatch();
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
+ useClearIntermediatesMutation();
+
+ const handleClickClearIntermediates = useCallback(() => {
+ clearIntermediates({})
+ .unwrap()
+ .then((response) => {
+ dispatch(resetCanvas());
+ dispatch(
+ addToast({
+ title:
+ response === 0
+ ? `No intermediates to clear`
+ : `Successfully cleared ${response} intermediates`,
+ status: 'info',
+ })
+ );
+ if (response < 100) {
+ setIsDisabled(true);
+ }
+ });
+ }, [clearIntermediates, dispatch]);
+
+ return (
+
+ Clear Intermediates
+
+ {isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'}
+
+
+ Will permanently delete first 100 intermediates found on disk and in
+ database
+
+ This will also clear your canvas state.
+
+ Intermediate images are byproducts of generation, different from the
+ result images in the gallery. Purging intermediates will free disk
+ space. Your gallery images will not be deleted.
+
+
+ );
+}
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
index 890ceb1f48..ccc4a9aa24 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
@@ -11,7 +11,7 @@ import {
Text,
useDisclosure,
} from '@chakra-ui/react';
-import { createSelector } from '@reduxjs/toolkit';
+import { createSelector, current } from '@reduxjs/toolkit';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@@ -23,6 +23,7 @@ import {
SystemState,
consoleLogLevelChanged,
setEnableImageDebugging,
+ setIsNodesEnabled,
setShouldConfirmOnDelete,
setShouldDisplayGuides,
shouldAntialiasProgressImageChanged,
@@ -48,6 +49,7 @@ import {
import { useTranslation } from 'react-i18next';
import { LogLevelName } from 'roarr';
import SettingsSchedulers from './SettingsSchedulers';
+import SettingsClearIntermediates from './SettingsClearIntermediates';
const selector = createSelector(
[systemSelector, uiSelector],
@@ -59,6 +61,7 @@ const selector = createSelector(
consoleLogLevel,
shouldLogToConsole,
shouldAntialiasProgressImage,
+ isNodesEnabled,
} = system;
const {
@@ -79,6 +82,7 @@ const selector = createSelector(
shouldLogToConsole,
shouldAntialiasProgressImage,
shouldShowAdvancedOptions,
+ isNodesEnabled,
};
},
{
@@ -91,6 +95,8 @@ type ConfigOptions = {
shouldShowResetWebUiText: boolean;
shouldShowBetaLayout: boolean;
shouldShowAdvancedOptionsSettings: boolean;
+ shouldShowClearIntermediates: boolean;
+ shouldShowNodesToggle: boolean;
};
type SettingsModalProps = {
@@ -109,6 +115,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
const shouldShowAdvancedOptionsSettings =
config?.shouldShowAdvancedOptionsSettings ?? true;
+ const shouldShowClearIntermediates =
+ config?.shouldShowClearIntermediates ?? true;
+ const shouldShowNodesToggle = config?.shouldShowNodesToggle ?? true;
useEffect(() => {
if (!shouldShowDeveloperSettings) {
@@ -139,6 +148,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
shouldLogToConsole,
shouldAntialiasProgressImage,
shouldShowAdvancedOptions,
+ isNodesEnabled,
} = useAppSelector(selector);
const handleClickResetWebUI = useCallback(() => {
@@ -169,6 +179,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
[dispatch]
);
+ const handleToggleNodes = useCallback(
+ (e: ChangeEvent) => {
+ dispatch(setIsNodesEnabled(e.target.checked));
+ },
+ [dispatch]
+ );
+
return (
<>
{cloneElement(children, {
@@ -253,6 +270,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
)
}
/>
+ {shouldShowNodesToggle && (
+
+ )}
{shouldShowDeveloperSettings && (
@@ -280,6 +304,8 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
)}
+ {shouldShowClearIntermediates && }
+
{t('settings.resetWebUI')}
@@ -328,7 +354,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
export default SettingsModal;
-const StyledFlex = (props: PropsWithChildren) => {
+export const StyledFlex = (props: PropsWithChildren) => {
return (
) {
state.progressImage = action.payload;
},
+ setIsNodesEnabled(state, action: PayloadAction) {
+ state.isNodesEnabled = action.payload;
+ },
},
extraReducers(builder) {
/**
@@ -360,27 +364,6 @@ export const systemSlice = createSlice({
state.wasSchemaParsed = true;
});
- /**
- * Image Uploading Started
- */
- builder.addCase(imageUploaded.pending, (state) => {
- state.isUploading = true;
- });
-
- /**
- * Image Uploading Complete
- */
- builder.addCase(imageUploaded.rejected, (state) => {
- state.isUploading = false;
- });
-
- /**
- * Image Uploading Complete
- */
- builder.addCase(imageUploaded.fulfilled, (state) => {
- state.isUploading = false;
- });
-
// *** Matchers - must be after all cases ***
/**
@@ -422,6 +405,7 @@ export const {
shouldAntialiasProgressImageChanged,
languageChanged,
progressImageSet,
+ setIsNodesEnabled,
} = systemSlice.actions;
export default systemSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
index a4e0773695..94195a27c1 100644
--- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
@@ -37,6 +37,7 @@ import NodesTab from './tabs/Nodes/NodesTab';
import ResizeHandle from './tabs/ResizeHandle';
import TextToImageTab from './tabs/TextToImage/TextToImageTab';
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
+import { systemSelector } from '../../system/store/systemSelectors';
export interface InvokeTabInfo {
id: InvokeTabName;
@@ -84,11 +85,20 @@ const tabs: InvokeTabInfo[] = [
];
const enabledTabsSelector = createSelector(
- configSelector,
- (config) => {
+ [configSelector, systemSelector],
+ (config, system) => {
const { disabledTabs } = config;
+ const { isNodesEnabled } = system;
- return tabs.filter((tab) => !disabledTabs.includes(tab.id));
+ const enabledTabs = tabs.filter((tab) => {
+ if (tab.id === 'nodes') {
+ return isNodesEnabled && !disabledTabs.includes(tab.id);
+ } else {
+ return !disabledTabs.includes(tab.id);
+ }
+ });
+
+ return enabledTabs;
},
{
memoizeOptions: { resultEqualityCheck: isEqual },
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx
index 93cef98f80..c9f8384b9a 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/ModelManagerPanel/ModelList.tsx
@@ -17,14 +17,14 @@ type ModelListProps = {
setSelectedModelId: (name: string | undefined) => void;
};
-type ModelFormat = 'all' | 'checkpoint' | 'diffusers';
+type ModelFormat = 'images' | 'checkpoint' | 'diffusers';
const ModelList = (props: ModelListProps) => {
const { selectedModelId, setSelectedModelId } = props;
const { t } = useTranslation();
const [nameFilter, setNameFilter] = useState('');
const [modelFormatFilter, setModelFormatFilter] =
- useState('all');
+ useState('images');
const { filteredDiffusersModels } = useGetMainModelsQuery(undefined, {
selectFromResult: ({ data }) => ({
@@ -47,8 +47,8 @@ const ModelList = (props: ModelListProps) => {
setModelFormatFilter('all')}
- isChecked={modelFormatFilter === 'all'}
+ onClick={() => setModelFormatFilter('images')}
+ isChecked={modelFormatFilter === 'images'}
size="sm"
>
{t('modelManager.allModels')}
@@ -75,7 +75,7 @@ const ModelList = (props: ModelListProps) => {
labelPos="side"
/>
- {['all', 'diffusers'].includes(modelFormatFilter) &&
+ {['images', 'diffusers'].includes(modelFormatFilter) &&
filteredDiffusersModels.length > 0 && (
@@ -93,7 +93,7 @@ const ModelList = (props: ModelListProps) => {
)}
- {['all', 'checkpoint'].includes(modelFormatFilter) &&
+ {['images', 'checkpoint'].includes(modelFormatFilter) &&
filteredCheckpointModels.length > 0 && (
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasFileUploader.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasFileUploader.tsx
index 0e8761111c..ac83e7f20b 100644
--- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasFileUploader.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasFileUploader.tsx
@@ -1,22 +1,28 @@
import { useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
-import useImageUploader from 'common/hooks/useImageUploader';
+import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
export default function UnifiedCanvasFileUploader() {
const isStaging = useAppSelector(isStagingSelector);
- const { openUploader } = useImageUploader();
+
+ const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
+ postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' },
+ });
const { t } = useTranslation();
return (
- }
- onClick={openUploader}
- isDisabled={isStaging}
- />
+ <>
+ }
+ isDisabled={isStaging}
+ {...getUploadButtonProps()}
+ />
+
+ >
);
}
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts
index 39deaf4172..368303c7c5 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boardImages.ts
@@ -1,6 +1,7 @@
-import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
+import { ImageDTO, OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
+import { BoardId } from 'features/gallery/store/gallerySlice';
type ListBoardImagesArg =
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
@@ -45,39 +46,7 @@ export const boardImagesApi = api.injectEndpoints({
return tags;
},
}),
-
- /**
- * Board Images Mutations
- */
-
- addImageToBoard: build.mutation({
- query: ({ board_id, image_name }) => ({
- url: `board_images/`,
- method: 'POST',
- body: { board_id, image_name },
- }),
- invalidatesTags: (result, error, arg) => [
- { type: 'BoardImage' },
- { type: 'Board', id: arg.board_id },
- ],
- }),
-
- removeImageFromBoard: build.mutation({
- query: ({ board_id, image_name }) => ({
- url: `board_images/`,
- method: 'DELETE',
- body: { board_id, image_name },
- }),
- invalidatesTags: (result, error, arg) => [
- { type: 'BoardImage' },
- { type: 'Board', id: arg.board_id },
- ],
- }),
}),
});
-export const {
- useAddImageToBoardMutation,
- useRemoveImageFromBoardMutation,
- useListBoardImagesQuery,
-} = boardImagesApi;
+export const { useListBoardImagesQuery } = boardImagesApi;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index fc3cb530a4..b019652ce5 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
@@ -1,6 +1,17 @@
-import { BoardDTO, OffsetPaginatedResults_BoardDTO_ } from 'services/api/types';
+import { Update } from '@reduxjs/toolkit';
+import {
+ ASSETS_CATEGORIES,
+ IMAGE_CATEGORIES,
+ boardIdSelected,
+} from 'features/gallery/store/gallerySlice';
+import {
+ BoardDTO,
+ ImageDTO,
+ OffsetPaginatedResults_BoardDTO_,
+} from 'services/api/types';
import { ApiFullTagDescription, LIST_TAG, api } from '..';
import { paths } from '../schema';
+import { getListImagesUrl, imagesAdapter, imagesApi } from './images';
type ListBoardsArg = NonNullable<
paths['/api/v1/boards/']['get']['parameters']['query']
@@ -11,6 +22,9 @@ type UpdateBoardArg =
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
};
+type DeleteBoardResult =
+ paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
+
export const boardsApi = api.injectEndpoints({
endpoints: (build) => ({
/**
@@ -59,6 +73,16 @@ export const boardsApi = api.injectEndpoints({
},
}),
+ listAllImageNamesForBoard: build.query, string>({
+ query: (board_id) => ({
+ url: `boards/${board_id}/image_names`,
+ }),
+ providesTags: (result, error, arg) => [
+ { type: 'ImageNameList', id: arg },
+ ],
+ keepUnusedDataFor: 0,
+ }),
+
/**
* Boards Mutations
*/
@@ -82,11 +106,92 @@ export const boardsApi = api.injectEndpoints({
{ type: 'Board', id: arg.board_id },
],
}),
- deleteBoard: build.mutation({
+
+ deleteBoard: build.mutation({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
- invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
+ invalidatesTags: (result, error, arg) => [
+ { type: 'Board', id: arg },
+ // invalidate the 'No Board' cache
+ { type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
+ ],
+ async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
+ /**
+ * Cache changes for deleteBoard:
+ * - Update every image in the 'getImageDTO' cache that has the board_id
+ * - Update every image in the 'All Images' cache that has the board_id
+ * - Update every image in the 'All Assets' cache that has the board_id
+ * - Invalidate the 'No Board' cache:
+ * Ideally we'd be able to insert all deleted images into the cache, but we don't
+ * have access to the deleted images DTOs - only the names, and a network request
+ * for all of a board's DTOs could be very large. Instead, we invalidate the 'No Board'
+ * cache.
+ */
+
+ try {
+ const { data } = await queryFulfilled;
+ const { deleted_board_images } = data;
+
+ // update getImageDTO caches
+ deleted_board_images.forEach((image_id) => {
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'getImageDTO',
+ image_id,
+ (draft) => {
+ draft.board_id = undefined;
+ }
+ )
+ );
+ });
+
+ // update 'All Images' & 'All Assets' caches
+ const queryArgsToUpdate = [
+ {
+ categories: IMAGE_CATEGORIES,
+ },
+ {
+ categories: ASSETS_CATEGORIES,
+ },
+ ];
+
+ const updates: Update[] = deleted_board_images.map(
+ (image_name) => ({
+ id: image_name,
+ changes: { board_id: undefined },
+ })
+ );
+
+ queryArgsToUpdate.forEach((queryArgs) => {
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ queryArgs,
+ (draft) => {
+ const oldCount = imagesAdapter
+ .getSelectors()
+ .selectTotal(draft);
+ const newState = imagesAdapter.updateMany(draft, updates);
+ const newCount = imagesAdapter
+ .getSelectors()
+ .selectTotal(newState);
+ draft.total = Math.max(
+ draft.total - (oldCount - newCount),
+ 0
+ );
+ }
+ )
+ );
+ });
+
+ // after deleting a board, select the 'All Images' board
+ dispatch(boardIdSelected('images'));
+ } catch {
+ //no-op
+ }
+ },
}),
- deleteBoardAndImages: build.mutation({
+
+ deleteBoardAndImages: build.mutation({
query: (board_id) => ({
url: `boards/${board_id}`,
method: 'DELETE',
@@ -94,8 +199,63 @@ export const boardsApi = api.injectEndpoints({
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg },
- { type: 'Image', id: LIST_TAG },
+ { type: 'ImageList', id: getListImagesUrl({ board_id: 'none' }) },
],
+ async onQueryStarted(board_id, { dispatch, queryFulfilled, getState }) {
+ /**
+ * Cache changes for deleteBoardAndImages:
+ * - ~~Remove every image in the 'getImageDTO' cache that has the board_id~~
+ * This isn't actually possible, you cannot remove cache entries with RTK Query.
+ * Instead, we rely on the UI to remove all components that use the deleted images.
+ * - Remove every image in the 'All Images' cache that has the board_id
+ * - Remove every image in the 'All Assets' cache that has the board_id
+ */
+
+ try {
+ const { data } = await queryFulfilled;
+ const { deleted_images } = data;
+
+ // update 'All Images' & 'All Assets' caches
+ const queryArgsToUpdate = [
+ {
+ categories: IMAGE_CATEGORIES,
+ },
+ {
+ categories: ASSETS_CATEGORIES,
+ },
+ ];
+
+ queryArgsToUpdate.forEach((queryArgs) => {
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ queryArgs,
+ (draft) => {
+ const oldCount = imagesAdapter
+ .getSelectors()
+ .selectTotal(draft);
+ const newState = imagesAdapter.removeMany(
+ draft,
+ deleted_images
+ );
+ const newCount = imagesAdapter
+ .getSelectors()
+ .selectTotal(newState);
+ draft.total = Math.max(
+ draft.total - (oldCount - newCount),
+ 0
+ );
+ }
+ )
+ );
+ });
+
+ // after deleting a board, select the 'All Images' board
+ dispatch(boardIdSelected('images'));
+ } catch {
+ //no-op
+ }
+ },
}),
}),
});
@@ -107,4 +267,5 @@ export const {
useUpdateBoardMutation,
useDeleteBoardMutation,
useDeleteBoardAndImagesMutation,
+ useListAllImageNamesForBoardQuery,
} = boardsApi;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index d49ab5f131..a37edd48aa 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -1,6 +1,27 @@
+import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';
+import { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks';
+import { dateComparator } from 'common/util/dateComparator';
+import {
+ ASSETS_CATEGORIES,
+ BoardId,
+ IMAGE_CATEGORIES,
+} from 'features/gallery/store/gallerySlice';
+import { omit } from 'lodash-es';
+import queryString from 'query-string';
import { ApiFullTagDescription, api } from '..';
-import { components } from '../schema';
-import { ImageDTO } from '../types';
+import { components, paths } from '../schema';
+import {
+ ImageCategory,
+ ImageChanges,
+ ImageDTO,
+ OffsetPaginatedResults_ImageDTO_,
+ PostUploadAction,
+} from '../types';
+import { getCacheAction } from './util';
+
+export type ListImagesArgs = NonNullable<
+ paths['/api/v1/images/']['get']['parameters']['query']
+>;
/**
* This is an unsafe type; the object inside is not guaranteed to be valid.
@@ -10,11 +31,102 @@ export type UnsafeImageMetadata = {
graph: NonNullable;
};
+export type ImageCache = EntityState & { total: number };
+
+// The adapter is not actually the data store - it just provides helper functions to interact
+// with some other store of data. We will use the RTK Query cache as that store.
+export const imagesAdapter = createEntityAdapter({
+ selectId: (image) => image.image_name,
+ sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
+});
+
+// We want to also store the images total in the cache. When we initialize the cache state,
+// we will provide this type arg so the adapter knows we want the total.
+export type AdditionalImagesAdapterState = { total: number };
+
+// Create selectors for the adapter.
+export const imagesSelectors = imagesAdapter.getSelectors();
+
+// Helper to create the url for the listImages endpoint. Also we use it to create the cache key.
+export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
+ `images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`;
+
+export const SYSTEM_BOARDS = ['images', 'assets', 'no_board', 'batch'];
+
export const imagesApi = api.injectEndpoints({
endpoints: (build) => ({
/**
* Image Queries
*/
+ listImages: build.query<
+ EntityState & { total: number },
+ ListImagesArgs
+ >({
+ query: (queryArgs) => ({
+ // Use the helper to create the URL.
+ url: getListImagesUrl(queryArgs),
+ method: 'GET',
+ }),
+ providesTags: (result, error, { board_id, categories }) => [
+ // Make the tags the same as the cache key
+ { type: 'ImageList', id: getListImagesUrl({ board_id, categories }) },
+ ],
+ serializeQueryArgs: ({ queryArgs }) => {
+ // Create cache & key based on board_id and categories - skip the other args.
+ // Offset is the size of the cache, and limit is always the same. Both are provided by
+ // the consumer of the query.
+ const { board_id, categories } = queryArgs;
+
+ // Just use the same fn used to create the url; it makes an understandable cache key.
+ // This cache key is the same for any combo of board_id and categories, doesn't change
+ // when offset & limit change.
+ const cacheKey = getListImagesUrl({ board_id, categories });
+ return cacheKey;
+ },
+ transformResponse(response: OffsetPaginatedResults_ImageDTO_) {
+ const { total, items: images } = response;
+ // Use the adapter to convert the response to the right shape, and adding the new total.
+ // The trick is to just provide an empty state and add the images array to it. This returns
+ // a properly shaped EntityState.
+ return imagesAdapter.addMany(
+ imagesAdapter.getInitialState({
+ total,
+ }),
+ images
+ );
+ },
+ merge: (cache, response) => {
+ // Here we actually update the cache. `response` here is the output of `transformResponse`
+ // above. In a similar vein to `transformResponse`, we can use the imagesAdapter to get
+ // things in the right shape. Also update the total image count.
+ imagesAdapter.addMany(cache, imagesSelectors.selectAll(response));
+ cache.total = response.total;
+ },
+ forceRefetch({ currentArg, previousArg }) {
+ // Refetch when the offset changes (which means we are on a new page).
+ return currentArg?.offset !== previousArg?.offset;
+ },
+ async onQueryStarted(_, { dispatch, queryFulfilled }) {
+ try {
+ const { data } = await queryFulfilled;
+
+ // update the `getImageDTO` cache for each image
+ imagesSelectors.selectAll(data).forEach((imageDTO) => {
+ dispatch(
+ imagesApi.util.upsertQueryData(
+ 'getImageDTO',
+ imageDTO.image_name,
+ imageDTO
+ )
+ );
+ });
+ } catch {
+ // no-op
+ }
+ },
+ // 24 hours - reducing this to a few minutes would reduce memory usage.
+ keepUnusedDataFor: 86400,
+ }),
getImageDTO: build.query({
query: (image_name) => ({ url: `images/${image_name}` }),
providesTags: (result, error, arg) => {
@@ -36,7 +148,483 @@ export const imagesApi = api.injectEndpoints({
},
keepUnusedDataFor: 86400, // 24 hours
}),
+ clearIntermediates: build.mutation({
+ query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
+ }),
+ deleteImage: build.mutation({
+ query: ({ image_name }) => ({
+ url: `images/${image_name}`,
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result, error, arg) => [
+ { type: 'Image', id: arg.image_name },
+ ],
+ async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) {
+ /**
+ * Cache changes for deleteImage:
+ * - Remove from "All Images"
+ * - Remove from image's `board_id` if it has one, or "No Board" if not
+ * - Remove from "Batch"
+ */
+
+ const { image_name, board_id, image_category } = imageDTO;
+
+ // Figure out the `listImages` caches that we need to update
+ // That means constructing the possible query args that are serialized into the cache key...
+
+ const removeFromCacheKeys: ListImagesArgs[] = [];
+ const categories = IMAGE_CATEGORIES.includes(image_category)
+ ? IMAGE_CATEGORIES
+ : ASSETS_CATEGORIES;
+
+ // All Images board (e.g. no board)
+ removeFromCacheKeys.push({ categories });
+
+ // Board specific
+ if (board_id) {
+ removeFromCacheKeys.push({ board_id });
+ } else {
+ // TODO: No Board
+ }
+
+ // TODO: Batch
+
+ const patches: PatchCollection[] = [];
+ removeFromCacheKeys.forEach((cacheKey) => {
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ cacheKey,
+ (draft) => {
+ imagesAdapter.removeOne(draft, image_name);
+ draft.total = Math.max(draft.total - 1, 0);
+ }
+ )
+ )
+ );
+ });
+
+ try {
+ await queryFulfilled;
+ } catch {
+ patches.forEach((patchResult) => patchResult.undo());
+ }
+ },
+ }),
+ updateImage: build.mutation<
+ ImageDTO,
+ {
+ imageDTO: ImageDTO;
+ // For now, we will not allow image categories to change
+ changes: Omit;
+ }
+ >({
+ query: ({ imageDTO, changes }) => ({
+ url: `images/${imageDTO.image_name}`,
+ method: 'PATCH',
+ body: changes,
+ }),
+ invalidatesTags: (result, error, { imageDTO }) => [
+ { type: 'Image', id: imageDTO.image_name },
+ ],
+ async onQueryStarted(
+ { imageDTO: oldImageDTO, changes: _changes },
+ { dispatch, queryFulfilled, getState }
+ ) {
+ // TODO: Should we handle changes to boards via this mutation? Seems reasonable...
+
+ // let's be extra-sure we do not accidentally change categories
+ const changes = omit(_changes, 'image_category');
+
+ /**
+ * Cache changes for `updateImage`:
+ * - Update the ImageDTO
+ * - Update the image in "All Images" board:
+ * - IF it is in the date range represented by the cache:
+ * - add the image IF it is not already in the cache & update the total
+ * - ELSE update the image IF it is already in the cache
+ * - IF the image has a board:
+ * - Update the image in it's own board
+ * - ELSE Update the image in the "No Board" board (TODO)
+ */
+
+ const patches: PatchCollection[] = [];
+ const { image_name, board_id, image_category } = oldImageDTO;
+ const categories = IMAGE_CATEGORIES.includes(image_category)
+ ? IMAGE_CATEGORIES
+ : ASSETS_CATEGORIES;
+
+ // TODO: No Board
+
+ // Update `getImageDTO` cache
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'getImageDTO',
+ image_name,
+ (draft) => {
+ Object.assign(draft, changes);
+ }
+ )
+ )
+ );
+
+ // Update the "All Image" or "All Assets" board
+ const queryArgsToUpdate: ListImagesArgs[] = [{ categories }];
+
+ if (board_id) {
+ // We also need to update the user board
+ queryArgsToUpdate.push({ board_id });
+ }
+
+ queryArgsToUpdate.forEach((queryArg) => {
+ const { data } = imagesApi.endpoints.listImages.select(queryArg)(
+ getState()
+ );
+
+ const cacheAction = getCacheAction(data, oldImageDTO);
+
+ if (['update', 'add'].includes(cacheAction)) {
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ queryArg,
+ (draft) => {
+ // One of the common changes is to make a canvas intermediate a non-intermediate,
+ // i.e. save a canvas image to the gallery.
+ // If that was the change, need to add the image to the cache instead of updating
+ // the existing cache entry.
+ if (
+ changes.is_intermediate === false ||
+ cacheAction === 'add'
+ ) {
+ // add it to the cache
+ imagesAdapter.addOne(draft, {
+ ...oldImageDTO,
+ ...changes,
+ });
+ draft.total += 1;
+ } else if (cacheAction === 'update') {
+ // just update it
+ imagesAdapter.updateOne(draft, {
+ id: image_name,
+ changes,
+ });
+ }
+ }
+ )
+ )
+ );
+ }
+ });
+
+ try {
+ await queryFulfilled;
+ } catch {
+ patches.forEach((patchResult) => patchResult.undo());
+ }
+ },
+ }),
+ uploadImage: build.mutation<
+ ImageDTO,
+ {
+ file: File;
+ image_category: ImageCategory;
+ is_intermediate: boolean;
+ postUploadAction?: PostUploadAction;
+ session_id?: string;
+ }
+ >({
+ query: ({ file, image_category, is_intermediate, session_id }) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ return {
+ url: `images/`,
+ method: 'POST',
+ body: formData,
+ params: {
+ image_category,
+ is_intermediate,
+ session_id,
+ },
+ };
+ },
+ async onQueryStarted(
+ { file, image_category, is_intermediate, postUploadAction },
+ { dispatch, queryFulfilled }
+ ) {
+ try {
+ const { data: imageDTO } = await queryFulfilled;
+
+ if (imageDTO.is_intermediate) {
+ // Don't add it to anything
+ return;
+ }
+
+ // Add the image to the "All Images" / "All Assets" board
+ const queryArg = {
+ categories: IMAGE_CATEGORIES.includes(image_category)
+ ? IMAGE_CATEGORIES
+ : ASSETS_CATEGORIES,
+ };
+
+ dispatch(
+ imagesApi.util.updateQueryData('listImages', queryArg, (draft) => {
+ imagesAdapter.addOne(draft, imageDTO);
+ draft.total = draft.total + 1;
+ })
+ );
+ } catch {
+ // no-op
+ }
+ },
+ }),
+ addImageToBoard: build.mutation<
+ void,
+ { board_id: BoardId; imageDTO: ImageDTO }
+ >({
+ query: ({ board_id, imageDTO }) => {
+ const { image_name } = imageDTO;
+ return {
+ url: `board_images/`,
+ method: 'POST',
+ body: { board_id, image_name },
+ };
+ },
+ invalidatesTags: (result, error, arg) => [
+ { type: 'BoardImage' },
+ { type: 'Board', id: arg.board_id },
+ ],
+ async onQueryStarted(
+ { board_id, imageDTO: oldImageDTO },
+ { dispatch, queryFulfilled, getState }
+ ) {
+ /**
+ * Cache changes for addImageToBoard:
+ * - Remove from "No Board"
+ * - Remove from `old_board_id` if it has one
+ * - Add to new `board_id`
+ * - IF the image's `created_at` is within the range of the board's cached images
+ * - OR the board cache has length of 0 or 1
+ * - Update the `total` for each board whose cache is updated
+ * - Update the ImageDTO
+ *
+ * TODO: maybe total should just be updated in the boards endpoints?
+ */
+
+ const { image_name, board_id: old_board_id } = oldImageDTO;
+
+ // Figure out the `listImages` caches that we need to update
+ const removeFromQueryArgs: ListImagesArgs[] = [];
+
+ // TODO: No Board
+ // TODO: Batch
+
+ // Remove from No Board
+ removeFromQueryArgs.push({ board_id: 'none' });
+
+ // Remove from old board
+ if (old_board_id) {
+ removeFromQueryArgs.push({ board_id: old_board_id });
+ }
+
+ // Store all patch results in case we need to roll back
+ const patches: PatchCollection[] = [];
+
+ // Updated imageDTO with new board_id
+ const newImageDTO = { ...oldImageDTO, board_id };
+
+ // Update getImageDTO cache
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'getImageDTO',
+ image_name,
+ (draft) => {
+ Object.assign(draft, newImageDTO);
+ }
+ )
+ )
+ );
+
+ // Do the "Remove from" cache updates
+ removeFromQueryArgs.forEach((queryArgs) => {
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ queryArgs,
+ (draft) => {
+ // sanity check
+ if (draft.ids.includes(image_name)) {
+ imagesAdapter.removeOne(draft, image_name);
+ draft.total = Math.max(draft.total - 1, 0);
+ }
+ }
+ )
+ )
+ );
+ });
+
+ // We only need to add to the cache if the board is not a system board
+ if (!SYSTEM_BOARDS.includes(board_id)) {
+ const queryArgs = { board_id };
+ const { data } = imagesApi.endpoints.listImages.select(queryArgs)(
+ getState()
+ );
+
+ const cacheAction = getCacheAction(data, oldImageDTO);
+
+ if (['add', 'update'].includes(cacheAction)) {
+ // Do the "Add to" cache updates
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ queryArgs,
+ (draft) => {
+ if (cacheAction === 'add') {
+ imagesAdapter.addOne(draft, newImageDTO);
+ draft.total += 1;
+ } else {
+ imagesAdapter.updateOne(draft, {
+ id: image_name,
+ changes: { board_id },
+ });
+ }
+ }
+ )
+ )
+ );
+ }
+ }
+
+ try {
+ await queryFulfilled;
+ } catch {
+ patches.forEach((patchResult) => patchResult.undo());
+ }
+ },
+ }),
+ removeImageFromBoard: build.mutation({
+ query: ({ imageDTO }) => {
+ const { board_id, image_name } = imageDTO;
+ return {
+ url: `board_images/`,
+ method: 'DELETE',
+ body: { board_id, image_name },
+ };
+ },
+ invalidatesTags: (result, error, arg) => [
+ { type: 'BoardImage' },
+ { type: 'Board', id: arg.imageDTO.board_id },
+ ],
+ async onQueryStarted(
+ { imageDTO },
+ { dispatch, queryFulfilled, getState }
+ ) {
+ /**
+ * Cache changes for removeImageFromBoard:
+ * - Add to "No Board"
+ * - IF the image's `created_at` is within the range of the board's cached images
+ * - Remove from `old_board_id`
+ * - Update the ImageDTO
+ */
+
+ const { image_name, board_id: old_board_id } = imageDTO;
+
+ // TODO: Batch
+
+ const patches: PatchCollection[] = [];
+
+ // Updated imageDTO with new board_id
+ const newImageDTO = { ...imageDTO, board_id: undefined };
+
+ // Update getImageDTO cache
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'getImageDTO',
+ image_name,
+ (draft) => {
+ Object.assign(draft, newImageDTO);
+ }
+ )
+ )
+ );
+
+ // Remove from old board
+ if (old_board_id) {
+ const oldBoardQueryArgs = { board_id: old_board_id };
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ oldBoardQueryArgs,
+ (draft) => {
+ // sanity check
+ if (draft.ids.includes(image_name)) {
+ imagesAdapter.removeOne(draft, image_name);
+ draft.total = Math.max(draft.total - 1, 0);
+ }
+ }
+ )
+ )
+ );
+ }
+
+ // Add to "No Board"
+ const noBoardQueryArgs = { board_id: 'none' };
+ const { data } = imagesApi.endpoints.listImages.select(
+ noBoardQueryArgs
+ )(getState());
+
+ // Check if we need to make any cache changes
+ const cacheAction = getCacheAction(data, imageDTO);
+
+ if (['add', 'update'].includes(cacheAction)) {
+ patches.push(
+ dispatch(
+ imagesApi.util.updateQueryData(
+ 'listImages',
+ noBoardQueryArgs,
+ (draft) => {
+ if (cacheAction === 'add') {
+ imagesAdapter.addOne(draft, imageDTO);
+ draft.total += 1;
+ } else {
+ imagesAdapter.updateOne(draft, {
+ id: image_name,
+ changes: { board_id: undefined },
+ });
+ }
+ }
+ )
+ )
+ );
+ }
+
+ try {
+ await queryFulfilled;
+ } catch {
+ patches.forEach((patchResult) => patchResult.undo());
+ }
+ },
+ }),
}),
});
-export const { useGetImageDTOQuery, useGetImageMetadataQuery } = imagesApi;
+export const {
+ useListImagesQuery,
+ useLazyListImagesQuery,
+ useGetImageDTOQuery,
+ useGetImageMetadataQuery,
+ useDeleteImageMutation,
+ useUpdateImageMutation,
+ useUploadImageMutation,
+ useAddImageToBoardMutation,
+ useRemoveImageFromBoardMutation,
+ useClearIntermediatesMutation,
+} = imagesApi;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/util.ts b/invokeai/frontend/web/src/services/api/endpoints/util.ts
new file mode 100644
index 0000000000..d613711dc2
--- /dev/null
+++ b/invokeai/frontend/web/src/services/api/endpoints/util.ts
@@ -0,0 +1,51 @@
+import { ImageDTO } from '../types';
+import { ImageCache, imagesSelectors } from './images';
+
+export const getIsImageInDateRange = (
+ data: ImageCache | undefined,
+ imageDTO: ImageDTO
+) => {
+ if (!data) {
+ return false;
+ }
+ const cacheImageDTOS = imagesSelectors.selectAll(data);
+
+ if (cacheImageDTOS.length > 1) {
+ // Images are sorted by `created_at` DESC
+ // check if the image is newer than the oldest image in the cache
+ const createdDate = new Date(imageDTO.created_at);
+ const oldestDate = new Date(
+ cacheImageDTOS[cacheImageDTOS.length - 1].created_at
+ );
+ return createdDate >= oldestDate;
+ } else if ([0, 1].includes(cacheImageDTOS.length)) {
+ // if there are only 1 or 0 images in the cache, we consider the image to be in the date range
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Determines the action we should take when an image may need to be added or updated in a cache.
+ */
+export const getCacheAction = (
+ data: ImageCache | undefined,
+ imageDTO: ImageDTO
+): 'add' | 'update' | 'none' => {
+ const isInDateRange = getIsImageInDateRange(data, imageDTO);
+ const isCacheFullyPopulated = data && data.total === data.ids.length;
+ const shouldUpdateCache =
+ Boolean(isInDateRange) || Boolean(isCacheFullyPopulated);
+
+ const isImageInCache = data && data.ids.includes(imageDTO.image_name);
+
+ if (shouldUpdateCache && isImageInCache) {
+ return 'update';
+ }
+
+ if (shouldUpdateCache && !isImageInCache) {
+ return 'add';
+ }
+
+ return 'none';
+};
diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts
index 33eb7c35c6..e10c96543e 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -8,7 +8,14 @@ import {
} from '@reduxjs/toolkit/query/react';
import { $authToken, $baseUrl } from 'services/api/client';
-export const tagTypes = ['Board', 'Image', 'ImageMetadata', 'Model'];
+export const tagTypes = [
+ 'Board',
+ 'Image',
+ 'ImageNameList',
+ 'ImageList',
+ 'ImageMetadata',
+ 'Model',
+];
export type ApiFullTagDescription = FullTagDescription<
(typeof tagTypes)[number]
>;
diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts
index 805afe54b3..26b2e8e37f 100644
--- a/invokeai/frontend/web/src/services/api/schema.d.ts
+++ b/invokeai/frontend/web/src/services/api/schema.d.ts
@@ -164,6 +164,13 @@ export type paths = {
*/
patch: operations["update_image"];
};
+ "/api/v1/images/clear-intermediates": {
+ /**
+ * Clear Intermediates
+ * @description Clears first 100 intermediates
+ */
+ post: operations["clear_intermediates"];
+ };
"/api/v1/images/{image_name}/metadata": {
/**
* Get Image Metadata
@@ -221,6 +228,13 @@ export type paths = {
*/
patch: operations["update_board"];
};
+ "/api/v1/boards/{board_id}/image_names": {
+ /**
+ * List All Board Image Names
+ * @description Gets a list of images for a board
+ */
+ get: operations["list_all_board_image_names"];
+ };
"/api/v1/board_images/": {
/**
* Create Board Image
@@ -233,13 +247,6 @@ export type paths = {
*/
delete: operations["remove_board_image"];
};
- "/api/v1/board_images/{board_id}": {
- /**
- * List Board Images
- * @description Gets a list of images for a board
- */
- get: operations["list_board_images"];
- };
"/api/v1/app/version": {
/** Get Version */
get: operations["app_version"];
@@ -1030,6 +1037,24 @@ export type components = {
*/
mask?: components["schemas"]["ImageField"];
};
+ /** DeleteBoardResult */
+ DeleteBoardResult: {
+ /**
+ * Board Id
+ * @description The id of the board that was deleted.
+ */
+ board_id: string;
+ /**
+ * Deleted Board Images
+ * @description The image names of the board-images relationships that were deleted.
+ */
+ deleted_board_images: (string)[];
+ /**
+ * Deleted Images
+ * @description The names of the images that were deleted.
+ */
+ deleted_images: (string)[];
+ };
/**
* DivideInvocation
* @description Divides two numbers
@@ -1254,7 +1279,7 @@ export type components = {
* @description The nodes in this graph
*/
nodes?: {
- [key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
+ [key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
};
/**
* Edges
@@ -1297,7 +1322,7 @@ export type components = {
* @description The results of node executions
*/
results: {
- [key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
+ [key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
};
/**
* Errors
@@ -5298,6 +5323,12 @@ export type components = {
*/
image?: components["schemas"]["ImageField"];
};
+ /**
+ * StableDiffusion1ModelFormat
+ * @description An enumeration.
+ * @enum {string}
+ */
+ StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
@@ -5310,12 +5341,6 @@ export type components = {
* @enum {string}
*/
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
- /**
- * StableDiffusion1ModelFormat
- * @description An enumeration.
- * @enum {string}
- */
- StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
};
responses: never;
parameters: never;
@@ -5426,7 +5451,7 @@ export type operations = {
};
requestBody: {
content: {
- "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
+ "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
};
};
responses: {
@@ -5463,7 +5488,7 @@ export type operations = {
};
requestBody: {
content: {
- "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
+ "application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRawPromptInvocation"] | components["schemas"]["SDXLRefinerRawPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SDXLTextToLatentsInvocation"] | components["schemas"]["SDXLLatentsToLatentsInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["ParamStringInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
};
};
responses: {
@@ -5949,13 +5974,13 @@ export type operations = {
list_image_dtos: {
parameters: {
query?: {
- /** @description The origin of images to list */
+ /** @description The origin of images to list. */
image_origin?: components["schemas"]["ResourceOrigin"];
- /** @description The categories of image to include */
+ /** @description The categories of image to include. */
categories?: (components["schemas"]["ImageCategory"])[];
- /** @description Whether to list intermediate images */
+ /** @description Whether to list intermediate images. */
is_intermediate?: boolean;
- /** @description The board id to filter by */
+ /** @description The board id to filter by. Use 'none' to find images without a board. */
board_id?: string;
/** @description The page offset */
offset?: number;
@@ -6098,6 +6123,20 @@ export type operations = {
};
};
};
+ /**
+ * Clear Intermediates
+ * @description Clears first 100 intermediates
+ */
+ clear_intermediates: {
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ content: {
+ "application/json": unknown;
+ };
+ };
+ };
+ };
/**
* Get Image Metadata
* @description Gets an image's metadata
@@ -6307,7 +6346,7 @@ export type operations = {
/** @description Successful Response */
200: {
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["DeleteBoardResult"];
};
};
/** @description Validation Error */
@@ -6349,6 +6388,32 @@ export type operations = {
};
};
};
+ /**
+ * List All Board Image Names
+ * @description Gets a list of images for a board
+ */
+ list_all_board_image_names: {
+ parameters: {
+ path: {
+ /** @description The id of the board */
+ board_id: string;
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ content: {
+ "application/json": (string)[];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
/**
* Create Board Image
* @description Creates a board_image
@@ -6399,38 +6464,6 @@ export type operations = {
};
};
};
- /**
- * List Board Images
- * @description Gets a list of images for a board
- */
- list_board_images: {
- parameters: {
- query?: {
- /** @description The page offset */
- offset?: number;
- /** @description The number of boards per page */
- limit?: number;
- };
- path: {
- /** @description The id of the board */
- board_id: string;
- };
- };
- responses: {
- /** @description Successful Response */
- 200: {
- content: {
- "application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
- };
- };
- /** @description Validation Error */
- 422: {
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
- };
- };
- };
- };
/** Get Version */
app_version: {
responses: {
diff --git a/invokeai/frontend/web/src/services/api/thunks/image.ts b/invokeai/frontend/web/src/services/api/thunks/image.ts
deleted file mode 100644
index 09271c3625..0000000000
--- a/invokeai/frontend/web/src/services/api/thunks/image.ts
+++ /dev/null
@@ -1,330 +0,0 @@
-import { createAppAsyncThunk } from 'app/store/storeUtils';
-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';
-import { paths } from 'services/api/schema';
-
-type GetImageUrlsArg =
- paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path'];
-
-type GetImageUrlsResponse =
- paths['/api/v1/images/{image_name}/urls']['get']['responses']['200']['content']['application/json'];
-
-type GetImageUrlsThunkConfig = {
- rejectValue: {
- arg: GetImageUrlsArg;
- error: unknown;
- };
-};
-/**
- * Thunk to get image URLs
- */
-export const imageUrlsReceived = createAppAsyncThunk<
- GetImageUrlsResponse,
- GetImageUrlsArg,
- GetImageUrlsThunkConfig
->('thunkApi/imageUrlsReceived', async (arg, { rejectWithValue }) => {
- const { image_name } = arg;
- const { get } = $client.get();
- const { data, error, response } = await get(
- '/api/v1/images/{image_name}/urls',
- {
- params: {
- path: {
- image_name,
- },
- },
- }
- );
-
- if (error) {
- return rejectWithValue({ arg, error });
- }
-
- return data;
-});
-
-type GetImageMetadataArg =
- paths['/api/v1/images/{image_name}']['get']['parameters']['path'];
-
-type GetImageMetadataResponse =
- paths['/api/v1/images/{image_name}']['get']['responses']['200']['content']['application/json'];
-
-type GetImageMetadataThunkConfig = {
- rejectValue: {
- arg: GetImageMetadataArg;
- error: unknown;
- };
-};
-
-export const imageDTOReceived = createAppAsyncThunk<
- GetImageMetadataResponse,
- GetImageMetadataArg,
- GetImageMetadataThunkConfig
->('thunkApi/imageMetadataReceived', async (arg, { rejectWithValue }) => {
- const { image_name } = arg;
- const { get } = $client.get();
- const { data, error, response } = await get('/api/v1/images/{image_name}', {
- params: {
- path: { image_name },
- },
- });
-
- if (error) {
- return rejectWithValue({ arg, error });
- }
-
- return data;
-});
-
-type ControlNetAction = {
- type: 'SET_CONTROLNET_IMAGE';
- controlNetId: string;
-};
-
-type InitialImageAction = {
- type: 'SET_INITIAL_IMAGE';
-};
-
-type NodesAction = {
- type: 'SET_NODES_IMAGE';
- nodeId: string;
- fieldName: string;
-};
-
-type CanvasInitialImageAction = {
- type: 'SET_CANVAS_INITIAL_IMAGE';
-};
-
-type CanvasMergedAction = {
- type: 'TOAST_CANVAS_MERGED';
-};
-
-type CanvasSavedToGalleryAction = {
- type: 'TOAST_CANVAS_SAVED_TO_GALLERY';
-};
-
-type UploadedToastAction = {
- type: 'TOAST_UPLOADED';
-};
-
-type AddToBatchAction = {
- type: 'ADD_TO_BATCH';
-};
-
-export type PostUploadAction =
- | ControlNetAction
- | InitialImageAction
- | NodesAction
- | CanvasInitialImageAction
- | CanvasMergedAction
- | CanvasSavedToGalleryAction
- | UploadedToastAction
- | AddToBatchAction;
-
-type UploadImageArg =
- paths['/api/v1/images/']['post']['parameters']['query'] & {
- file: File;
- postUploadAction?: PostUploadAction;
- };
-
-type UploadImageResponse =
- paths['/api/v1/images/']['post']['responses']['201']['content']['application/json'];
-
-type UploadImageThunkConfig = {
- rejectValue: {
- arg: UploadImageArg;
- error: unknown;
- };
-};
-/**
- * `ImagesService.uploadImage()` thunk
- */
-export const imageUploaded = createAppAsyncThunk<
- UploadImageResponse,
- UploadImageArg,
- UploadImageThunkConfig
->('thunkApi/imageUploaded', async (arg, { rejectWithValue }) => {
- const {
- postUploadAction,
- file,
- image_category,
- is_intermediate,
- session_id,
- } = arg;
- const { post } = $client.get();
- const { data, error, response } = await post('/api/v1/images/', {
- params: {
- query: {
- image_category,
- is_intermediate,
- session_id,
- },
- },
- body: { file },
- bodySerializer: (body) => {
- const formData = new FormData();
- formData.append('file', body.file);
- return formData;
- },
- });
-
- if (error) {
- return rejectWithValue({ arg, error });
- }
-
- return data;
-});
-
-type DeleteImageArg =
- paths['/api/v1/images/{image_name}']['delete']['parameters']['path'];
-
-type DeleteImageResponse =
- paths['/api/v1/images/{image_name}']['delete']['responses']['200']['content']['application/json'];
-
-type DeleteImageThunkConfig = {
- rejectValue: {
- arg: DeleteImageArg;
- error: unknown;
- };
-};
-/**
- * `ImagesService.deleteImage()` thunk
- */
-export const imageDeleted = createAppAsyncThunk<
- DeleteImageResponse,
- DeleteImageArg,
- DeleteImageThunkConfig
->('thunkApi/imageDeleted', async (arg, { rejectWithValue }) => {
- const { image_name } = arg;
- const { del } = $client.get();
- const { data, error, response } = await del('/api/v1/images/{image_name}', {
- params: {
- path: {
- image_name,
- },
- },
- });
-
- if (error) {
- return rejectWithValue({ arg, error });
- }
-});
-
-type UpdateImageArg =
- paths['/api/v1/images/{image_name}']['patch']['requestBody']['content']['application/json'] &
- paths['/api/v1/images/{image_name}']['patch']['parameters']['path'];
-
-type UpdateImageResponse =
- paths['/api/v1/images/{image_name}']['patch']['responses']['200']['content']['application/json'];
-
-type UpdateImageThunkConfig = {
- rejectValue: {
- arg: UpdateImageArg;
- error: unknown;
- };
-};
-/**
- * `ImagesService.updateImage()` thunk
- */
-export const imageUpdated = createAppAsyncThunk<
- UpdateImageResponse,
- UpdateImageArg,
- UpdateImageThunkConfig
->('thunkApi/imageUpdated', async (arg, { rejectWithValue }) => {
- const { image_name, image_category, is_intermediate, session_id } = arg;
- const { patch } = $client.get();
- const { data, error, response } = await patch('/api/v1/images/{image_name}', {
- params: {
- path: {
- image_name,
- },
- },
- body: {
- image_category,
- is_intermediate,
- session_id,
- },
- });
-
- if (error) {
- return rejectWithValue({ arg, error });
- }
-
- return data;
-});
-
-export const IMAGES_PER_PAGE = 20;
-
-const DEFAULT_IMAGES_LISTED_ARG = {
- limit: IMAGES_PER_PAGE,
-};
-
-type ListImagesArg = NonNullable<
- paths['/api/v1/images/']['get']['parameters']['query']
->;
-
-type ListImagesResponse =
- paths['/api/v1/images/']['get']['responses']['200']['content']['application/json'];
-
-type ListImagesThunkConfig = {
- rejectValue: {
- arg: ListImagesArg;
- error: unknown;
- };
-};
-/**
- * `ImagesService.listImagesWithMetadata()` thunk
- */
-export const receivedPageOfImages = createAppAsyncThunk<
- ListImagesResponse,
- ListImagesArg,
- ListImagesThunkConfig
->(
- 'thunkApi/receivedPageOfImages',
- async (arg, { getState, rejectWithValue }) => {
- const { get } = $client.get();
-
- const state = getState();
-
- const images = selectFilteredImages(state);
- const categories =
- state.gallery.galleryView === 'images'
- ? IMAGE_CATEGORIES
- : ASSETS_CATEGORIES;
-
- let query: ListImagesArg = {};
-
- if (size(arg)) {
- query = {
- ...DEFAULT_IMAGES_LISTED_ARG,
- offset: images.length,
- ...arg,
- };
- } else {
- query = {
- ...DEFAULT_IMAGES_LISTED_ARG,
- categories,
- offset: images.length,
- };
- }
-
- 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;
- }
-);
diff --git a/invokeai/frontend/web/src/services/api/types.d.ts b/invokeai/frontend/web/src/services/api/types.d.ts
index 829b96840e..3e945691f1 100644
--- a/invokeai/frontend/web/src/services/api/types.d.ts
+++ b/invokeai/frontend/web/src/services/api/types.d.ts
@@ -1,3 +1,4 @@
+import { UseToastOptions } from '@chakra-ui/react';
import { O } from 'ts-toolbelt';
import { components } from './schema';
@@ -186,3 +187,41 @@ export type CollectInvocationOutput =
export type LatentsOutput = components['schemas']['LatentsOutput'];
export type GraphInvocationOutput =
components['schemas']['GraphInvocationOutput'];
+
+// Post-image upload actions, controls workflows when images are uploaded
+
+export type ControlNetAction = {
+ type: 'SET_CONTROLNET_IMAGE';
+ controlNetId: string;
+};
+
+export type InitialImageAction = {
+ type: 'SET_INITIAL_IMAGE';
+};
+
+export type NodesAction = {
+ type: 'SET_NODES_IMAGE';
+ nodeId: string;
+ fieldName: string;
+};
+
+export type CanvasInitialImageAction = {
+ type: 'SET_CANVAS_INITIAL_IMAGE';
+};
+
+export type ToastAction = {
+ type: 'TOAST';
+ toastOptions?: UseToastOptions;
+};
+
+export type AddToBatchAction = {
+ type: 'ADD_TO_BATCH';
+};
+
+export type PostUploadAction =
+ | ControlNetAction
+ | InitialImageAction
+ | NodesAction
+ | CanvasInitialImageAction
+ | ToastAction
+ | AddToBatchAction;
diff --git a/pyproject.toml b/pyproject.toml
index 0ff9ea1ac0..5cc6971df6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,7 +83,7 @@ dependencies = [
"torchvision>=0.14.1",
"torchmetrics==0.11.4",
"torchsde==0.2.5",
- "transformers==4.30.2",
+ "transformers~=4.31.0",
"uvicorn[standard]==0.21.1",
"windows-curses; sys_platform=='win32'",
]