mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): another go at gallery (#3791)
* feat(ui): migrate listImages to RTK query using createEntityAdapter - see comments in `endpoints/images.ts` for explanation of the caching - so far, only manually updating `all` images when new image is generated. no other manual cache updates are implemented, but will be needed. - fixed some weirdness with loading state components (like the spinners in gallery) - added `useThumbnailFallback` for `IAIDndImage`, this displays the tiny webp thumbnail while the full-size images load - comment out some old thunk related stuff in gallerySlice, which is no longer needed * feat(ui): add manual cache updates for board changes (wip) - update RTK Query caches when adding/removing single image to/from board - work more on migrating all image-related operations to RTK Query * update AddImagesToBoardContext so that it works when user uses context menu + modal * handle case where no image is selected * get assets working for main list and boards - dnd only * feat(ui): migrate image uploads to RTK Query - minor refactor of `ImageUploader` and `useImageUploadButton` hooks, simplify some logic - style filesystem upload overlay to match existing UI - replace all old `imageUploaded` thunks with `uploadImage` RTK Query calls, update associated logic including canvas related uploads - simplify `PostUploadAction`s that only need to display user input * feat(ui): remove `receivedPageOfImages` thunks * feat(ui): remove `receivedImageUrls` thunk * feat(ui): finish removing all images thunks stuff now broken: - image usage - delete board images - on first load, no image selected * feat(ui): simplify `updateImage` cache manipulation - we don't actually ever change categories, so we can remove a lot of logic * feat(ui): simplify canvas autosave - instead of using a network request to set the canvas generation as not intermediate, we can just do that in the graph * feat(ui): simplify & handle edge cases in cache updates * feat(db, api): support `board_id='none'` for `get_many` images queries This allows us to get all images that are not on a board. * chore(ui): regen types * feat(ui): add `All Assets`, `No Board` boards Restructure boards: - `all images` is all images - `all assets` is all assets - `no board` is all images/assets without a board set - user boards may have images and assets Update caching logic - much simpler without every board having sub-views of images and assets - update drag and drop operations for all possible interactions * chore(ui): regen types * feat(ui): move download to top of context menu * feat(ui): improve drop overlay styles * fix(ui): fix image not selected on first load - listen for first load of all images board, then select the first image * feat(ui): refactor board deletion api changes: - add route to list all image names for a board. this is required to handle board + image deletion. we need to know every image in the board to determine the image usage across the app. this is fetched only when the delete board and images modal is opened so it's as efficient as it can be. - update the delete board route to respond with a list of deleted `board_images` and `images`, as image names. this is needed to perform accurate clientside state & cache updates after deleting. db changes: - remove unused `board_images` service method to get paginated images dtos for a board. this is now done thru the list images endpoint & images service. needs a small logic change on `images.delete_images_on_board` ui changes: - simplify the delete board modal - no context, just minor prop drilling. this is feasible for boards only because the components that need to trigger and manipulate the modal are very close together in the tree - add cache updates for `deleteBoard` & `deleteBoardAndImages` mutations - the only thing we cannot do directly is on `deleteBoardAndImages`, update the `No Board` board. we'd need to insert image dtos that we may not have loaded. instead, i am just invalidating the tags for that `listImages` cache. so when you `deleteBoardAndImages`, the `No Board` will re-fetch the initial image limit. i think this is more efficient than e.g. fetching all image dtos to insert then inserting them. - handle image usage for `deleteBoardAndImages` - update all (i think/hope) the little bits and pieces in the UI to accomodate these changes * fix(ui): fix board selection logic * feat(ui): add delete board modal loading state * fix(ui): use thumbnails for board cover images * fix(ui): fix race condition with board selection when selecting a board that doesn't have any images loaded, we need to wait until the images haveloaded before selecting the first image. this logic is debounced to ~1000ms. * feat(ui): name 'No Board' correctly, change icon * fix(ui): do not cache listAllImageNames query if we cache it, we can end up with stale image usage during deletion. we could of course manually update the cache as we are doing elsewhere. but because this is a relatively infrequent network request, i'd like to trade increased cache mgmt complexity here for increased resource usage. * feat(ui): reduce drag preview opacity, remove border * fix(ui): fix incorrect queryArg used in `deleteImage` and `updateImage` cache updates * fix(ui): fix doubled open in new tab * fix(ui): fix new generations not getting added to 'No Board' * fix(ui): fix board id not changing on new image when autosave enabled * fix(ui): context menu when selection is 0 need to revise how context menu is triggered later, when we approach multi select * fix(ui): fix deleting does not update counts for all images and all assets * fix(ui): fix all assets board name in boards list collapse button * fix(ui): ensure we never go under 0 for total board count * fix(ui): fix text overflow on board names --------- Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
This commit is contained in:
parent
055f5b2d4b
commit
0724eb9e0a
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -245,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"),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
@ -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
|
||||
|
@ -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)
|
||||
@ -385,16 +384,14 @@ 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
|
||||
|
@ -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) => {
|
||||
</Grid>
|
||||
<DeleteImageModal />
|
||||
<UpdateImageBoardModal />
|
||||
<DeleteBoardImagesModal />
|
||||
<Toaster />
|
||||
<GlobalHotkeys />
|
||||
</>
|
||||
|
@ -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: {
|
||||
|
@ -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, {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 = ({
|
||||
<ThemeLocaleProvider>
|
||||
<ImageDndContext>
|
||||
<AddImageToBoardContextProvider>
|
||||
<DeleteBoardImagesContextProvider>
|
||||
<App config={config} headerComponent={headerComponent} />
|
||||
</DeleteBoardImagesContextProvider>
|
||||
<App config={config} headerComponent={headerComponent} />
|
||||
</AddImageToBoardContextProvider>
|
||||
</ImageDndContext>
|
||||
</ThemeLocaleProvider>
|
||||
|
@ -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<ImageDTO>();
|
||||
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 (
|
||||
|
@ -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<DeleteBoardImagesContextValue>({
|
||||
isOpen: false,
|
||||
onClose: () => undefined,
|
||||
onClickDeleteBoardImages: () => undefined,
|
||||
handleDeleteBoardImages: () => undefined,
|
||||
handleDeleteBoardOnly: () => undefined,
|
||||
});
|
||||
|
||||
type Props = PropsWithChildren;
|
||||
|
||||
export const DeleteBoardImagesContextProvider = (props: Props) => {
|
||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||
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 (
|
||||
<DeleteBoardImagesContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
board: boardToDelete,
|
||||
onClose: closeAndClearBoardToDelete,
|
||||
onClickDeleteBoardImages,
|
||||
handleDeleteBoardImages,
|
||||
handleDeleteBoardOnly,
|
||||
imagesUsage,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</DeleteBoardImagesContext.Provider>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -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));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -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));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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 },
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -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<typeof imageUploaded.fulfilled> =>
|
||||
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',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||
);
|
||||
|
||||
dispatch(imageUpserted(uploadedImageDTO));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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<typeof imageDTOReceived.fulfilled> =>
|
||||
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 } },
|
||||
|
@ -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'
|
||||
);
|
||||
},
|
||||
|
@ -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<typeof imageDeleted.fulfilled> =>
|
||||
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'
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
// }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -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;
|
||||
|
||||
|
@ -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'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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: '<Blob>', image }, 'Image uploaded');
|
||||
moduleLog.debug({ arg: '<Blob>', 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: '<Blob>' } } };
|
||||
const { file, postUploadAction, ...rest } = action.meta.arg.originalArgs;
|
||||
const sanitizedData = { arg: { ...rest, file: '<Blob>' } };
|
||||
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
|
||||
dispatch(
|
||||
addToast({
|
||||
|
@ -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'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -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'))));
|
||||
},
|
||||
});
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -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));
|
||||
|
@ -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',
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -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<typeof imageUploaded.fulfilled> =>
|
||||
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<typeof imageUploaded.fulfilled> =>
|
||||
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 },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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 = <FaUndo />,
|
||||
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
||||
useThumbailFallback,
|
||||
} = props;
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
@ -126,9 +130,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
<Image
|
||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
// If we fall back to thumbnail, it feels much snappier than the skeleton...
|
||||
fallbackSrc={imageDTO.thumbnail_url}
|
||||
// fallback={<IAILoadingImageFallback image={imageDTO} />}
|
||||
fallbackSrc={
|
||||
useThumbailFallback ? imageDTO.thumbnail_url : undefined
|
||||
}
|
||||
fallback={
|
||||
useThumbailFallback ? undefined : (
|
||||
<IAILoadingImageFallback image={imageDTO} />
|
||||
)
|
||||
}
|
||||
width={imageDTO.width}
|
||||
height={imageDTO.height}
|
||||
onError={onError}
|
||||
|
@ -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) => {
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
top: 0.5,
|
||||
insetInlineStart: 0.5,
|
||||
insetInlineEnd: 0.5,
|
||||
bottom: 0.5,
|
||||
opacity: 1,
|
||||
borderWidth: 3,
|
||||
borderWidth: 2,
|
||||
borderColor: isOver
|
||||
? mode('base.50', 'base.200')(colorMode)
|
||||
: mode('base.100', 'base.500')(colorMode),
|
||||
borderRadius: 'base',
|
||||
? mode('base.50', 'base.50')(colorMode)
|
||||
: mode('base.200', 'base.300')(colorMode),
|
||||
borderRadius: 'lg',
|
||||
borderStyle: 'dashed',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
@ -78,10 +78,10 @@ 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',
|
||||
}}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -32,27 +32,57 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
opacity: 0.4,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
rowGap: 4,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.700',
|
||||
_dark: { bg: 'base.900' },
|
||||
opacity: 0.7,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bg: 'base.900',
|
||||
boxShadow: `inset 0 0 20rem 1rem var(--invokeai-colors-${
|
||||
isDragAccept ? 'accent' : 'error'
|
||||
}-500)`,
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
{isDragAccept ? (
|
||||
<Heading size="lg">Drop to Upload</Heading>
|
||||
) : (
|
||||
<>
|
||||
<Heading size="lg">Invalid Upload</Heading>
|
||||
<Heading size="md">Must be single JPEG or PNG image</Heading>
|
||||
</>
|
||||
)}
|
||||
<Flex
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDir: 'column',
|
||||
gap: 4,
|
||||
borderWidth: 3,
|
||||
borderRadius: 'xl',
|
||||
borderStyle: 'dashed',
|
||||
color: 'base.100',
|
||||
borderColor: 'base.100',
|
||||
_dark: { borderColor: 'base.200' },
|
||||
}}
|
||||
>
|
||||
{isDragAccept ? (
|
||||
<Heading size="lg">Drop to Upload</Heading>
|
||||
) : (
|
||||
<>
|
||||
<Heading size="lg">Invalid Upload</Heading>
|
||||
<Heading size="md">Must be single JPEG or PNG image</Heading>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
@ -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<boolean>(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 (
|
||||
<Box
|
||||
@ -150,13 +150,30 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{isDragActive && isHandlingUpload && (
|
||||
<ImageUploadOverlay
|
||||
isDragAccept={isDragAccept}
|
||||
isDragReject={isDragReject}
|
||||
setIsHandlingUpload={setIsHandlingUpload}
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isDragActive && isHandlingUpload && (
|
||||
<motion.div
|
||||
key="image-upload-overlay"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
<ImageUploadOverlay
|
||||
isDragAccept={isDragAccept}
|
||||
isDragReject={isDragReject}
|
||||
setIsHandlingUpload={setIsHandlingUpload}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
className={styleClass}
|
||||
>
|
||||
<Flex
|
||||
onClick={openUploader}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 8,
|
||||
p: 8,
|
||||
borderRadius: 'base',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
color: 'base.600',
|
||||
bg: 'base.800',
|
||||
_hover: {
|
||||
bg: 'base.700',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon as={FaUpload} boxSize={24} />
|
||||
<Heading size="md">Click or Drag and Drop</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploaderButton;
|
@ -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 (
|
||||
<IAIIconButton
|
||||
aria-label={t('accessibility.uploadImage')}
|
||||
tooltip="Upload Image"
|
||||
icon={<FaUpload />}
|
||||
onClick={openUploader}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploaderIconButton;
|
@ -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
|
||||
* <Button {...getUploadButtonProps()} /> // will open the file dialog on click
|
||||
* <input {...getUploadInputProps()} /> // 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 {
|
||||
|
@ -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;
|
@ -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 = () => {
|
||||
<IAIIconButton
|
||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
isDisabled={!imageDTO || !imageDTO.is_intermediate}
|
||||
icon={<FaSave />}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
stagingAreaImageSaved({
|
||||
imageName: currentStagingAreaImage.imageName,
|
||||
imageDTO,
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
colorScheme="accent"
|
||||
/>
|
||||
<IAIIconButton
|
||||
|
@ -2,7 +2,6 @@ import { Box, ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick';
|
||||
import {
|
||||
canvasSelector,
|
||||
@ -25,6 +24,7 @@ import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import {
|
||||
canvasCopiedToClipboard,
|
||||
canvasDownloadedAsImage,
|
||||
@ -82,7 +82,9 @@ const IAICanvasToolbar = () => {
|
||||
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={<FaUpload />}
|
||||
onClick={openUploader}
|
||||
isDisabled={isStaging}
|
||||
{...getUploadButtonProps()}
|
||||
/>
|
||||
<input {...getUploadInputProps()} />
|
||||
<IAIIconButton
|
||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||
|
||||
@ -12,6 +13,6 @@ export const canvasDownloadedAsImage = createAction(
|
||||
|
||||
export const canvasMerged = createAction('canvas/canvasMerged');
|
||||
|
||||
export const stagingAreaImageSaved = createAction<{ imageName: string }>(
|
||||
export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>(
|
||||
'canvas/stagingAreaImageSaved'
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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 (
|
||||
<GenericBoard
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaFileImage}
|
||||
label="All Assets"
|
||||
badgeCount={total}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllAssetsBoard;
|
@ -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 (
|
||||
<GenericBoard
|
||||
droppableData={droppableData}
|
||||
onClick={handleAllImagesBoardClick}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaImages}
|
||||
label="All Images"
|
||||
badgeCount={total}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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<BoardDTO>();
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
|
||||
const handleBoardSearch = (searchTerm: string) => {
|
||||
setSearchMode(searchTerm.length > 0);
|
||||
dispatch(setBoardSearchText(searchTerm));
|
||||
};
|
||||
const clearBoardSearch = () => {
|
||||
setSearchMode(false);
|
||||
dispatch(setBoardSearchText(''));
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Flex
|
||||
layerStyle={'first'}
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
mt: 2,
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<InputGroup>
|
||||
<Input
|
||||
placeholder="Search Boards..."
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
handleBoardSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{searchText && searchText.length && (
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
onClick={clearBoardSearch}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
aria-label="Clear Search"
|
||||
icon={<CloseIcon boxSize={3} />}
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
<AddBoardButton />
|
||||
</Flex>
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
options={{
|
||||
scrollbars: {
|
||||
visibility: 'auto',
|
||||
autoHide: 'move',
|
||||
autoHideDelay: 1300,
|
||||
theme: 'os-theme-dark',
|
||||
},
|
||||
<>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Flex
|
||||
layerStyle={'first'}
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
mt: 2,
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
className="list-container"
|
||||
sx={{
|
||||
gridTemplateRows: '6.5rem 6.5rem',
|
||||
gridAutoFlow: 'column dense',
|
||||
gridAutoColumns: '5rem',
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<BoardsSearch setSearchMode={setSearchMode} />
|
||||
<AddBoardButton />
|
||||
</Flex>
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
options={{
|
||||
scrollbars: {
|
||||
visibility: 'auto',
|
||||
autoHide: 'move',
|
||||
autoHideDelay: 1300,
|
||||
theme: 'os-theme-dark',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!searchMode && (
|
||||
<>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<AllImagesBoard isSelected={selectedBoardId === 'all'} />
|
||||
</GridItem>
|
||||
{isBatchEnabled && (
|
||||
<Grid
|
||||
className="list-container"
|
||||
sx={{
|
||||
gridTemplateRows: '6.5rem 6.5rem',
|
||||
gridAutoFlow: 'column dense',
|
||||
gridAutoColumns: '5rem',
|
||||
}}
|
||||
>
|
||||
{!searchMode && (
|
||||
<>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<BatchBoard isSelected={selectedBoardId === 'batch'} />
|
||||
<AllImagesBoard isSelected={selectedBoardId === 'images'} />
|
||||
</GridItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||
<GalleryBoard
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
/>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Flex>
|
||||
</Collapse>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<AllAssetsBoard isSelected={selectedBoardId === 'assets'} />
|
||||
</GridItem>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<NoBoardBoard isSelected={selectedBoardId === 'no_board'} />
|
||||
</GridItem>
|
||||
{isBatchEnabled && (
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<BatchBoard isSelected={selectedBoardId === 'batch'} />
|
||||
</GridItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||
<GalleryBoard
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Flex>
|
||||
</Collapse>
|
||||
<DeleteBoardModal
|
||||
boardToDelete={boardToDelete}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
<InputGroup>
|
||||
<Input
|
||||
placeholder="Search Boards..."
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
handleBoardSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{searchText && searchText.length && (
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
onClick={clearBoardSearch}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
aria-label="Clear Search"
|
||||
icon={<CloseIcon boxSize={3} />}
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardsSearch);
|
@ -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 (
|
||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
renderMenu={() => (
|
||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
||||
{board.image_count > 0 && (
|
||||
<>
|
||||
<MenuItem
|
||||
isDisabled={!board.image_count}
|
||||
icon={<FaImages />}
|
||||
onClickCapture={handleAddBoardToBatch}
|
||||
>
|
||||
Add Board to Batch
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoardAndImages}
|
||||
>
|
||||
Delete Board and Images
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoard}
|
||||
return (
|
||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
menuButtonProps={{
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
key={board_id}
|
||||
userSelect="none"
|
||||
ref={ref}
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{board.image_count > 0 && (
|
||||
<>
|
||||
{/* <MenuItem
|
||||
isDisabled={!board.image_count}
|
||||
icon={<FaImages />}
|
||||
onClickCapture={handleAddBoardToBatch}
|
||||
>
|
||||
Add Board to Batch
|
||||
</MenuItem> */}
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoard}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
onClick={handleSelectBoard}
|
||||
key={board_id}
|
||||
userSelect="none"
|
||||
ref={ref}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{board.cover_image_name && coverImage?.image_url && (
|
||||
<Image src={coverImage?.image_url} draggable={false} />
|
||||
)}
|
||||
{!(board.cover_image_name && coverImage?.image_url) && (
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={FaFolder}
|
||||
<Flex
|
||||
onClick={handleSelectBoard}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{board.cover_image_name && coverImage?.thumbnail_url && (
|
||||
<Image src={coverImage?.thumbnail_url} draggable={false} />
|
||||
)}
|
||||
{!(board.cover_image_name && coverImage?.thumbnail_url) && (
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={FaUser}
|
||||
sx={{
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'base.200',
|
||||
_dark: {
|
||||
borderColor: 'base.800',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
sx={{
|
||||
border: '2px solid var(--invokeai-colors-base-200)',
|
||||
_dark: {
|
||||
border: '2px solid var(--invokeai-colors-base-800)',
|
||||
},
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge variant="solid">{board.image_count}</Badge>
|
||||
</Flex>
|
||||
<IAIDroppable
|
||||
data={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Badge variant="solid">{board.image_count}</Badge>
|
||||
<Editable
|
||||
defaultValue={board_name}
|
||||
submitOnBlur={false}
|
||||
onSubmit={(nextValue) => {
|
||||
handleUpdateBoardName(nextValue);
|
||||
}}
|
||||
sx={{ maxW: 'full' }}
|
||||
>
|
||||
<EditablePreview
|
||||
sx={{
|
||||
color: isSelected
|
||||
? mode('base.900', 'base.50')(colorMode)
|
||||
: mode('base.700', 'base.200')(colorMode),
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
textAlign: 'center',
|
||||
p: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
noOfLines={1}
|
||||
/>
|
||||
<EditableInput
|
||||
sx={{
|
||||
color: mode('base.900', 'base.50')(colorMode),
|
||||
fontSize: 'xs',
|
||||
borderColor: mode('base.500', 'base.500')(colorMode),
|
||||
p: 0,
|
||||
outline: 0,
|
||||
}}
|
||||
/>
|
||||
</Editable>
|
||||
</Flex>
|
||||
<IAIDroppable data={droppableData} />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
defaultValue={board_name}
|
||||
submitOnBlur={false}
|
||||
onSubmit={(nextValue) => {
|
||||
handleUpdateBoardName(nextValue);
|
||||
}}
|
||||
>
|
||||
<EditablePreview
|
||||
sx={{
|
||||
color: isSelected
|
||||
? mode('base.900', 'base.50')(colorMode)
|
||||
: mode('base.700', 'base.200')(colorMode),
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
textAlign: 'center',
|
||||
p: 0,
|
||||
}}
|
||||
noOfLines={1}
|
||||
/>
|
||||
<EditableInput
|
||||
sx={{
|
||||
color: mode('base.900', 'base.50')(colorMode),
|
||||
fontSize: 'xs',
|
||||
borderColor: mode('base.500', 'base.500')(colorMode),
|
||||
p: 0,
|
||||
outline: 0,
|
||||
}}
|
||||
/>
|
||||
</Editable>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
)}
|
||||
</ContextMenu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GalleryBoard.displayName = 'HoverableBoard';
|
||||
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
@ -59,10 +75,10 @@ const GenericBoard = (props: GenericBoardProps) => {
|
||||
}}
|
||||
>
|
||||
{badgeCount !== undefined && (
|
||||
<Badge variant="solid">{badgeCount}</Badge>
|
||||
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<IAIDroppable data={droppableData} />
|
||||
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
|
@ -0,0 +1,53 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import {
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
boardIdSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
ListImagesArgs,
|
||||
useListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import GenericBoard from './GenericBoard';
|
||||
|
||||
const baseQueryArg: ListImagesArgs = {
|
||||
board_id: 'none',
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
is_intermediate: false,
|
||||
};
|
||||
|
||||
const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
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 (
|
||||
<GenericBoard
|
||||
droppableData={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaFolderOpen}
|
||||
label="No Board"
|
||||
badgeCount={total}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoBoardBoard;
|
@ -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 (
|
||||
<>
|
||||
<Text>
|
||||
An image from this board is currently in use in the following features:
|
||||
</Text>
|
||||
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
||||
{imagesUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
||||
{imagesUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
||||
{imagesUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
||||
{imagesUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
||||
</UnorderedList>
|
||||
<Text>
|
||||
If you delete images from this board, those features will immediately be
|
||||
reset.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteBoardImagesModal = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
onClose,
|
||||
board,
|
||||
handleDeleteBoardImages,
|
||||
handleDeleteBoardOnly,
|
||||
imagesUsage,
|
||||
} = useContext(DeleteBoardImagesContext);
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
isCentered
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
{board && (
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete Board
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Flex direction="column" gap={3}>
|
||||
<BoardImageInUseMessage imagesUsage={imagesUsage} />
|
||||
<Divider />
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<Text fontWeight="bold">
|
||||
This board has {board.image_count} image(s) that will be
|
||||
deleted.
|
||||
</Text>
|
||||
</Flex>
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter gap={3}>
|
||||
<IAIButton ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="warning"
|
||||
onClick={() => handleDeleteBoardOnly(board.board_id)}
|
||||
>
|
||||
Delete Board Only
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
onClick={() => handleDeleteBoardImages(board.board_id)}
|
||||
>
|
||||
Delete Board and Images
|
||||
</IAIButton>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
)}
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeleteBoardImagesModal);
|
@ -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<HTMLButtonElement>(null);
|
||||
|
||||
const isLoading = useMemo(
|
||||
() =>
|
||||
isDeleteBoardAndImagesLoading ||
|
||||
isDeleteBoardOnlyLoading ||
|
||||
isFetchingBoardNames,
|
||||
[
|
||||
isDeleteBoardAndImagesLoading,
|
||||
isDeleteBoardOnlyLoading,
|
||||
isFetchingBoardNames,
|
||||
]
|
||||
);
|
||||
|
||||
if (!boardToDelete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
isOpen={Boolean(boardToDelete)}
|
||||
onClose={handleClose}
|
||||
leastDestructiveRef={cancelRef}
|
||||
isCentered
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete {boardToDelete.board_name}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Flex direction="column" gap={3}>
|
||||
{isFetchingBoardNames ? (
|
||||
<Skeleton>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 32,
|
||||
}}
|
||||
/>
|
||||
</Skeleton>
|
||||
) : (
|
||||
<ImageUsageMessage
|
||||
imageUsage={imageUsageSummary}
|
||||
topMessage="This board contains images used in the following features:"
|
||||
bottomMessage="Deleting this board and its images will reset any features currently using them."
|
||||
/>
|
||||
)}
|
||||
<Text>Deleted boards cannot be restored.</Text>
|
||||
<Text>
|
||||
{canRestoreDeletedImagesFromBin
|
||||
? t('gallery.deleteImageBin')
|
||||
: t('gallery.deleteImagePermanent')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Flex
|
||||
sx={{ justifyContent: 'space-between', width: 'full', gap: 2 }}
|
||||
>
|
||||
<IAIButton ref={cancelRef} onClick={handleClose}>
|
||||
Cancel
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="warning"
|
||||
isLoading={isLoading}
|
||||
onClick={handleDeleteBoardOnly}
|
||||
>
|
||||
Delete Board Only
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
isLoading={isLoading}
|
||||
onClick={handleDeleteBoardAndImages}
|
||||
>
|
||||
Delete Board and Images
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeleteImageModal);
|
@ -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={
|
||||
<IAINoContentFallback icon={FaImage} label="No image selected" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shouldShowImageDetails && imageDTO && (
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
as={Button}
|
||||
onClick={onToggle}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
sx={{
|
||||
w: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
_hover: {
|
||||
bg: 'base.100',
|
||||
_dark: { bg: 'base.800' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
w: 'full',
|
||||
fontWeight: 600,
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{selectedBoardName}
|
||||
</Text>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GalleryBoardName);
|
@ -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 (
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('gallery.pinGallery')}
|
||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryPinButton;
|
@ -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 (
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
tooltip={t('gallery.gallerySettings')}
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
size="sm"
|
||||
icon={<FaWrench />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAISlider
|
||||
value={galleryImageMinimumWidth}
|
||||
onChange={handleChangeGalleryImageMinimumWidth}
|
||||
min={32}
|
||||
max={256}
|
||||
hideTooltip={true}
|
||||
label={t('gallery.galleryImageSize')}
|
||||
withReset
|
||||
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||
/>
|
||||
<IAISimpleCheckbox
|
||||
label={t('gallery.autoSwitchNewImages')}
|
||||
isChecked={shouldAutoSwitch}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(shouldAutoSwitchChanged(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default GallerySettingsPopover;
|
@ -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<HTMLDivElement>) => {
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
@ -48,13 +43,9 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={handleContextMenu}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
{selectionCount === 1 ? (
|
||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||
) : (
|
||||
<MultipleSelectionMenuItems />
|
||||
)}
|
||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||
</MenuList>
|
||||
) : null
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Link href={imageDTO.image_url} target="_blank">
|
||||
<MenuItem
|
||||
icon={<FaExternalLinkAlt />}
|
||||
onClickCapture={handleOpenInNewTab}
|
||||
>
|
||||
<MenuItem icon={<FaExternalLinkAlt />}>
|
||||
{t('common.openInNewTab')}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
@ -161,6 +153,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
{t('parameters.copyImage')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<Link download={true} href={imageDTO.image_url} target="_blank">
|
||||
<MenuItem icon={<FaDownload />} w="100%">
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<MenuItem
|
||||
icon={<FaQuoteRight />}
|
||||
onClickCapture={handleRecallPrompt}
|
||||
@ -219,11 +216,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
Remove from Board
|
||||
</MenuItem>
|
||||
)}
|
||||
<Link download={true} href={imageDTO.image_url} target="_blank">
|
||||
<MenuItem icon={<FaDownload />} w="100%">
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
|
@ -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<HTMLDivElement>(null);
|
||||
const galleryGridRef = useRef<HTMLDivElement>(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 (
|
||||
<VStack
|
||||
@ -127,95 +48,12 @@ const ImageGalleryContent = () => {
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<ButtonGroup isAttached>
|
||||
<IAIIconButton
|
||||
tooltip={t('gallery.images')}
|
||||
aria-label={t('gallery.images')}
|
||||
onClick={handleClickImagesCategory}
|
||||
isChecked={galleryView === 'images'}
|
||||
size="sm"
|
||||
icon={<FaImage />}
|
||||
/>
|
||||
<IAIIconButton
|
||||
tooltip={t('gallery.assets')}
|
||||
aria-label={t('gallery.assets')}
|
||||
onClick={handleClickAssetsCategory}
|
||||
isChecked={galleryView === 'assets'}
|
||||
size="sm"
|
||||
icon={<FaServer />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<Flex
|
||||
as={Button}
|
||||
onClick={onToggle}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
sx={{
|
||||
w: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
_hover: {
|
||||
bg: mode('base.100', 'base.800')(colorMode),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
w: 'full',
|
||||
color: mode('base.800', 'base.200')(colorMode),
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{boardTitle}
|
||||
</Text>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
transform: isBoardListOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
tooltip={t('gallery.gallerySettings')}
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
size="sm"
|
||||
icon={<FaWrench />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAISlider
|
||||
value={galleryImageMinimumWidth}
|
||||
onChange={handleChangeGalleryImageMinimumWidth}
|
||||
min={32}
|
||||
max={256}
|
||||
hideTooltip={true}
|
||||
label={t('gallery.galleryImageSize')}
|
||||
withReset
|
||||
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||
/>
|
||||
<IAISimpleCheckbox
|
||||
label={t('gallery.autoSwitchNewImages')}
|
||||
isChecked={shouldAutoSwitch}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(shouldAutoSwitchChanged(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label={t('gallery.pinGallery')}
|
||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
<GallerySettingsPopover />
|
||||
<GalleryBoardName
|
||||
isOpen={isBoardListOpen}
|
||||
onToggle={onToggleBoardList}
|
||||
/>
|
||||
<GalleryPinButton />
|
||||
</Flex>
|
||||
<Box>
|
||||
<BoardsList isOpen={isBoardListOpen} />
|
||||
|
@ -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 <Spinner />;
|
||||
return <IAIFillSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -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<HTMLDivElement>(null);
|
||||
const emptyGalleryRef = useRef<HTMLDivElement>(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(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 (
|
||||
<Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}>
|
||||
<Box sx={{ w: 'full', h: 'full' }}>
|
||||
<Spinner size="2xl" opacity={0.5} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSuccess && currentData?.ids.length === 0) {
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full' }}>
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
@ -180,27 +96,28 @@ const GalleryImageGrid = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (status !== 'rejected') {
|
||||
if (isSuccess && currentData) {
|
||||
return (
|
||||
<>
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={imageNames}
|
||||
data={currentData.ids}
|
||||
endReached={handleLoadMoreImages}
|
||||
components={{
|
||||
Item: ImageGridItemContainer,
|
||||
List: ImageGridListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, imageName) => (
|
||||
<GalleryImage key={imageName} imageName={imageName} />
|
||||
<GalleryImage key={imageName} imageName={imageName as string} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<IAIButton
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreAvailable}
|
||||
isLoading={status === 'pending'}
|
||||
isLoading={isFetching}
|
||||
loadingText="Loading"
|
||||
flexShrink={0}
|
||||
>
|
||||
@ -211,6 +128,17 @@ const GalleryImageGrid = () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full' }}>
|
||||
<IAINoContentFallback
|
||||
label="Unable to load Gallery"
|
||||
icon={FaExclamationCircle}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(GalleryImageGrid);
|
||||
|
@ -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(() => {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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<ImageDTO>({
|
||||
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<never, never>);
|
||||
|
||||
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<AdditionaGalleryState>({
|
||||
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<ImageDTO>) => {
|
||||
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<Update<ImageDTO>>) => {
|
||||
imagesAdapter.updateOne(state, action.payload);
|
||||
},
|
||||
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||
imagesAdapter.removeOne(state, action.payload);
|
||||
state.batchImageNames = state.batchImageNames.filter(
|
||||
(name) => name !== action.payload
|
||||
);
|
||||
},
|
||||
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
||||
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<string>) => {
|
||||
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<string>) => {
|
||||
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<string | null>) => {
|
||||
state.selection = action.payload ? [action.payload] : [];
|
||||
@ -143,15 +92,9 @@ export const gallerySlice = createSlice({
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
setGalleryView: (state, action: PayloadAction<GalleryView>) => {
|
||||
state.galleryView = action.payload;
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
},
|
||||
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||
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<RootState>((state) => state.gallery);
|
||||
|
||||
export const {
|
||||
imageUpserted,
|
||||
imageUpdatedOne,
|
||||
imageRemoved,
|
||||
imagesRemoved,
|
||||
imageRangeEndSelected,
|
||||
imageSelectionToggled,
|
||||
imageSelected,
|
||||
shouldAutoSwitchChanged,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryView,
|
||||
boardIdSelected,
|
||||
isLoadingChanged,
|
||||
isBatchEnabledChanged,
|
||||
imagesAddedToBatch,
|
||||
imagesRemovedFromBatch,
|
||||
|
54
invokeai/frontend/web/src/features/gallery/store/util.ts
Normal file
54
invokeai/frontend/web/src/features/gallery/store/util.ts
Normal file
@ -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';
|
||||
};
|
@ -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 (
|
||||
<>
|
||||
<Text>This image is currently in use in the following features:</Text>
|
||||
<Text>{topMessage}</Text>
|
||||
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
||||
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
||||
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
||||
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
||||
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
||||
</UnorderedList>
|
||||
<Text>
|
||||
If you delete this image, those features will immediately be reset.
|
||||
</Text>
|
||||
<Text>{bottomMessage}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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<ImageInputFieldValue, ImageInputFieldTemplate>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -60,6 +60,7 @@ export const addLoRAsToGraph = (
|
||||
const loraLoaderNode: LoraLoaderInvocation = {
|
||||
type: 'lora_loader',
|
||||
id: currentLoraNodeId,
|
||||
is_intermediate: true,
|
||||
lora: { model_name, base_model },
|
||||
weight,
|
||||
};
|
||||
|
@ -28,6 +28,7 @@ export const addVAEToGraph = (
|
||||
graph.nodes[VAE_LOADER] = {
|
||||
type: 'vae_loader',
|
||||
id: VAE_LOADER,
|
||||
is_intermediate: true,
|
||||
vae_model: vae,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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: [
|
||||
|
@ -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: [
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
layerStyle={'first'}
|
||||
@ -85,7 +79,6 @@ const InitialImageDisplay = () => {
|
||||
tooltip={'Upload Initial Image'}
|
||||
aria-label={'Upload Initial Image'}
|
||||
icon={<FaUpload />}
|
||||
onClick={handleUpload}
|
||||
{...getUploadButtonProps()}
|
||||
/>
|
||||
<IAIIconButton
|
||||
|
@ -244,22 +244,7 @@ export const useRecallParameters = () => {
|
||||
[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,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ImageDTO, MainModelField } from 'services/api/types';
|
||||
|
||||
export const initialImageSelected = createAction<ImageDTO | string | undefined>(
|
||||
export const initialImageSelected = createAction<ImageDTO | undefined>(
|
||||
'generation/initialImageSelected'
|
||||
);
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { userInvoked } from 'app/store/actions';
|
||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
||||
import { t } from 'i18next';
|
||||
import { LogLevelName } from 'roarr';
|
||||
import { imageUploaded } from 'services/api/thunks/image';
|
||||
import {
|
||||
isAnySessionRejected,
|
||||
sessionCanceled,
|
||||
@ -360,27 +359,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 ***
|
||||
|
||||
/**
|
||||
|
@ -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<string>('');
|
||||
const [modelFormatFilter, setModelFormatFilter] =
|
||||
useState<ModelFormat>('all');
|
||||
useState<ModelFormat>('images');
|
||||
|
||||
const { filteredDiffusersModels } = useGetMainModelsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => ({
|
||||
@ -47,8 +47,8 @@ const ModelList = (props: ModelListProps) => {
|
||||
<Flex flexDirection="column" gap={4} paddingInlineEnd={4}>
|
||||
<ButtonGroup isAttached>
|
||||
<IAIButton
|
||||
onClick={() => 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 && (
|
||||
<StyledModelContainer>
|
||||
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||
@ -93,7 +93,7 @@ const ModelList = (props: ModelListProps) => {
|
||||
</Flex>
|
||||
</StyledModelContainer>
|
||||
)}
|
||||
{['all', 'checkpoint'].includes(modelFormatFilter) &&
|
||||
{['images', 'checkpoint'].includes(modelFormatFilter) &&
|
||||
filteredCheckpointModels.length > 0 && (
|
||||
<StyledModelContainer>
|
||||
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||
|
@ -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 (
|
||||
<IAIIconButton
|
||||
aria-label={t('common.upload')}
|
||||
tooltip={t('common.upload')}
|
||||
icon={<FaUpload />}
|
||||
onClick={openUploader}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
<>
|
||||
<IAIIconButton
|
||||
aria-label={t('common.upload')}
|
||||
tooltip={t('common.upload')}
|
||||
icon={<FaUpload />}
|
||||
isDisabled={isStaging}
|
||||
{...getUploadButtonProps()}
|
||||
/>
|
||||
<input {...getUploadInputProps()} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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<void, AddImageToBoardArg>({
|
||||
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<void, RemoveImageFromBoardArg>({
|
||||
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;
|
||||
|
@ -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<Array<string>, 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<void, string>({
|
||||
|
||||
deleteBoard: build.mutation<DeleteBoardResult, string>({
|
||||
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<ImageDTO>[] = 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<void, string>({
|
||||
|
||||
deleteBoardAndImages: build.mutation<DeleteBoardResult, string>({
|
||||
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;
|
||||
|
@ -1,7 +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.
|
||||
@ -11,11 +31,102 @@ export type UnsafeImageMetadata = {
|
||||
graph: NonNullable<components['schemas']['Graph']>;
|
||||
};
|
||||
|
||||
export type ImageCache = EntityState<ImageDTO> & { 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<ImageDTO>({
|
||||
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<ImageDTO> & { 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<AdditionalImagesAdapterState>({
|
||||
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<ImageDTO, string>({
|
||||
query: (image_name) => ({ url: `images/${image_name}` }),
|
||||
providesTags: (result, error, arg) => {
|
||||
@ -40,7 +151,480 @@ export const imagesApi = api.injectEndpoints({
|
||||
clearIntermediates: build.mutation({
|
||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||
}),
|
||||
deleteImage: build.mutation<void, ImageDTO>({
|
||||
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<ImageChanges, 'image_category'>;
|
||||
}
|
||||
>({
|
||||
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<void, { imageDTO: ImageDTO }>({
|
||||
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, useClearIntermediatesMutation } = imagesApi;
|
||||
export const {
|
||||
useListImagesQuery,
|
||||
useLazyListImagesQuery,
|
||||
useGetImageDTOQuery,
|
||||
useGetImageMetadataQuery,
|
||||
useDeleteImageMutation,
|
||||
useUpdateImageMutation,
|
||||
useUploadImageMutation,
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
useClearIntermediatesMutation,
|
||||
} = imagesApi;
|
||||
|
51
invokeai/frontend/web/src/services/api/endpoints/util.ts
Normal file
51
invokeai/frontend/web/src/services/api/endpoints/util.ts
Normal file
@ -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';
|
||||
};
|
@ -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]
|
||||
>;
|
||||
|
116
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
116
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
@ -228,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
|
||||
@ -240,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"];
|
||||
@ -1037,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
|
||||
@ -1261,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
|
||||
@ -1304,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
|
||||
@ -5306,11 +5324,11 @@ export type components = {
|
||||
image?: components["schemas"]["ImageField"];
|
||||
};
|
||||
/**
|
||||
* StableDiffusionXLModelFormat
|
||||
* StableDiffusion1ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusion2ModelFormat
|
||||
* @description An enumeration.
|
||||
@ -5318,11 +5336,11 @@ export type components = {
|
||||
*/
|
||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusion1ModelFormat
|
||||
* StableDiffusionXLModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -5433,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: {
|
||||
@ -5470,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: {
|
||||
@ -5956,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;
|
||||
@ -6328,7 +6346,7 @@ export type operations = {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
"application/json": components["schemas"]["DeleteBoardResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -6370,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
|
||||
@ -6420,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: {
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user