mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into bugfix/ImageToLatentsInvocation_fp32_precision
This commit is contained in:
commit
2fbc6dc315
@ -24,11 +24,14 @@ async def create_board_image(
|
|||||||
):
|
):
|
||||||
"""Creates a board_image"""
|
"""Creates a board_image"""
|
||||||
try:
|
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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail="Failed to add to board")
|
raise HTTPException(status_code=500, detail="Failed to add to board")
|
||||||
|
|
||||||
|
|
||||||
@board_images_router.delete(
|
@board_images_router.delete(
|
||||||
"/",
|
"/",
|
||||||
operation_id="remove_board_image",
|
operation_id="remove_board_image",
|
||||||
@ -43,27 +46,10 @@ async def remove_board_image(
|
|||||||
):
|
):
|
||||||
"""Deletes a board_image"""
|
"""Deletes a board_image"""
|
||||||
try:
|
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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail="Failed to update board")
|
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 typing import Optional, Union
|
||||||
|
|
||||||
from fastapi import Body, HTTPException, Path, Query
|
from fastapi import Body, HTTPException, Path, Query
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from invokeai.app.services.board_record_storage import BoardChanges
|
from invokeai.app.services.board_record_storage import BoardChanges
|
||||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||||
from invokeai.app.services.models.board_record import BoardDTO
|
from invokeai.app.services.models.board_record import BoardDTO
|
||||||
|
|
||||||
|
|
||||||
from ..dependencies import ApiDependencies
|
from ..dependencies import ApiDependencies
|
||||||
|
|
||||||
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
|
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(
|
@boards_router.post(
|
||||||
"/",
|
"/",
|
||||||
operation_id="create_board",
|
operation_id="create_board",
|
||||||
@ -69,25 +81,42 @@ async def update_board(
|
|||||||
raise HTTPException(status_code=500, detail="Failed to 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(
|
async def delete_board(
|
||||||
board_id: str = Path(description="The id of board to delete"),
|
board_id: str = Path(description="The id of board to delete"),
|
||||||
include_images: Optional[bool] = Query(
|
include_images: Optional[bool] = Query(
|
||||||
description="Permanently delete all images on the board", default=False
|
description="Permanently delete all images on the board", default=False
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> DeleteBoardResult:
|
||||||
"""Deletes a board"""
|
"""Deletes a board"""
|
||||||
try:
|
try:
|
||||||
if include_images is True:
|
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(
|
ApiDependencies.invoker.services.images.delete_images_on_board(
|
||||||
board_id=board_id
|
board_id=board_id
|
||||||
)
|
)
|
||||||
ApiDependencies.invoker.services.boards.delete(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:
|
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)
|
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:
|
except Exception as e:
|
||||||
# TODO: Does this need any exception handling at all?
|
raise HTTPException(status_code=500, detail="Failed to delete board")
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@boards_router.get(
|
@boards_router.get(
|
||||||
@ -115,3 +144,19 @@ async def list_boards(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'",
|
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(
|
async def list_image_dtos(
|
||||||
image_origin: Optional[ResourceOrigin] = Query(
|
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(
|
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(
|
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(
|
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"),
|
offset: int = Query(default=0, description="The page offset"),
|
||||||
limit: int = Query(default=10, description="The number of images per page"),
|
limit: int = Query(default=10, description="The number of images per page"),
|
||||||
|
@ -32,11 +32,11 @@ class BoardImageRecordStorageBase(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_images_for_board(
|
def get_all_board_image_names_for_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
board_id: str,
|
||||||
) -> OffsetPaginatedResults[ImageRecord]:
|
) -> list[str]:
|
||||||
"""Gets images for a board."""
|
"""Gets all board images for a board, as a list of the image names."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -211,6 +211,26 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
|||||||
items=images, offset=offset, limit=limit, total=count
|
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(
|
def get_board_for_image(
|
||||||
self,
|
self,
|
||||||
image_name: str,
|
image_name: str,
|
||||||
|
@ -38,11 +38,11 @@ class BoardImagesServiceABC(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_images_for_board(
|
def get_all_board_image_names_for_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
board_id: str,
|
||||||
) -> OffsetPaginatedResults[ImageDTO]:
|
) -> list[str]:
|
||||||
"""Gets images for a board."""
|
"""Gets all board images for a board, as a list of the image names."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -98,30 +98,13 @@ class BoardImagesService(BoardImagesServiceABC):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._services.board_image_records.remove_image_from_board(board_id, image_name)
|
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,
|
self,
|
||||||
board_id: str,
|
board_id: str,
|
||||||
) -> OffsetPaginatedResults[ImageDTO]:
|
) -> list[str]:
|
||||||
image_records = self._services.board_image_records.get_images_for_board(
|
return self._services.board_image_records.get_all_board_image_names_for_board(
|
||||||
board_id
|
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(
|
def get_board_for_image(
|
||||||
self,
|
self,
|
||||||
@ -136,7 +119,7 @@ def board_record_to_dto(
|
|||||||
) -> BoardDTO:
|
) -> BoardDTO:
|
||||||
"""Converts a board record to a board DTO."""
|
"""Converts a board record to a board DTO."""
|
||||||
return BoardDTO(
|
return BoardDTO(
|
||||||
**board_record.dict(exclude={'cover_image_name'}),
|
**board_record.dict(exclude={"cover_image_name"}),
|
||||||
cover_image_name=cover_image_name,
|
cover_image_name=cover_image_name,
|
||||||
image_count=image_count,
|
image_count=image_count,
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,10 @@ from pydantic.generics import GenericModel
|
|||||||
|
|
||||||
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
||||||
from invokeai.app.services.models.image_record import (
|
from invokeai.app.services.models.image_record import (
|
||||||
ImageRecord, ImageRecordChanges, deserialize_image_record)
|
ImageRecord,
|
||||||
|
ImageRecordChanges,
|
||||||
|
deserialize_image_record,
|
||||||
|
)
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
@ -377,11 +380,15 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
|||||||
|
|
||||||
query_params.append(is_intermediate)
|
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
|
query_conditions += """--sql
|
||||||
AND board_images.board_id = ?
|
AND board_images.board_id = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query_params.append(board_id)
|
query_params.append(board_id)
|
||||||
|
|
||||||
query_pagination = """--sql
|
query_pagination = """--sql
|
||||||
|
@ -11,7 +11,6 @@ from invokeai.app.models.image import (ImageCategory,
|
|||||||
InvalidOriginException, ResourceOrigin)
|
InvalidOriginException, ResourceOrigin)
|
||||||
from invokeai.app.services.board_image_record_storage import \
|
from invokeai.app.services.board_image_record_storage import \
|
||||||
BoardImageRecordStorageBase
|
BoardImageRecordStorageBase
|
||||||
from invokeai.app.services.graph import Graph
|
|
||||||
from invokeai.app.services.image_file_storage import (
|
from invokeai.app.services.image_file_storage import (
|
||||||
ImageFileDeleteException, ImageFileNotFoundException,
|
ImageFileDeleteException, ImageFileNotFoundException,
|
||||||
ImageFileSaveException, ImageFileStorageBase)
|
ImageFileSaveException, ImageFileStorageBase)
|
||||||
@ -385,16 +384,14 @@ class ImageService(ImageServiceABC):
|
|||||||
|
|
||||||
def delete_images_on_board(self, board_id: str):
|
def delete_images_on_board(self, board_id: str):
|
||||||
try:
|
try:
|
||||||
images = self._services.board_image_records.get_images_for_board(board_id)
|
image_names = (
|
||||||
image_name_list = list(
|
self._services.board_image_records.get_all_board_image_names_for_board(
|
||||||
map(
|
board_id
|
||||||
lambda r: r.image_name,
|
|
||||||
images.items,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for image_name in image_name_list:
|
for image_name in image_names:
|
||||||
self._services.image_files.delete(image_name)
|
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:
|
except ImageRecordDeleteException:
|
||||||
self._services.logger.error(f"Failed to delete image records")
|
self._services.logger.error(f"Failed to delete image records")
|
||||||
raise
|
raise
|
||||||
|
@ -15,7 +15,6 @@ import InvokeTabs from 'features/ui/components/InvokeTabs';
|
|||||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { ReactNode, memo, useEffect } from 'react';
|
import { ReactNode, memo, useEffect } from 'react';
|
||||||
import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal';
|
|
||||||
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
|
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
|
||||||
import GlobalHotkeys from './GlobalHotkeys';
|
import GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
@ -84,7 +83,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<DeleteImageModal />
|
<DeleteImageModal />
|
||||||
<UpdateImageBoardModal />
|
<UpdateImageBoardModal />
|
||||||
<DeleteBoardImagesModal />
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<GlobalHotkeys />
|
<GlobalHotkeys />
|
||||||
</>
|
</>
|
||||||
|
@ -15,10 +15,7 @@ const STYLES: ChakraProps['sx'] = {
|
|||||||
maxH: BOX_SIZE,
|
maxH: BOX_SIZE,
|
||||||
shadow: 'dark-lg',
|
shadow: 'dark-lg',
|
||||||
borderRadius: 'lg',
|
borderRadius: 'lg',
|
||||||
borderWidth: 2,
|
opacity: 0.3,
|
||||||
borderStyle: 'dashed',
|
|
||||||
borderColor: 'base.100',
|
|
||||||
opacity: 0.5,
|
|
||||||
bg: 'base.800',
|
bg: 'base.800',
|
||||||
color: 'base.50',
|
color: 'base.50',
|
||||||
_dark: {
|
_dark: {
|
||||||
|
@ -28,6 +28,7 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
|
console.log('dragStart', event.active.data.current);
|
||||||
const activeData = event.active.data.current;
|
const activeData = event.active.data.current;
|
||||||
if (!activeData) {
|
if (!activeData) {
|
||||||
return;
|
return;
|
||||||
@ -37,15 +38,16 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
|||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
|
console.log('dragEnd', event.active.data.current);
|
||||||
const activeData = event.active.data.current;
|
const activeData = event.active.data.current;
|
||||||
const overData = event.over?.data.current;
|
const overData = event.over?.data.current;
|
||||||
if (!activeData || !overData) {
|
if (!activeDragData || !overData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(dndDropped({ overData, activeData }));
|
dispatch(dndDropped({ overData, activeData: activeDragData }));
|
||||||
setActiveDragData(null);
|
setActiveDragData(null);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[activeDragData, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseSensor = useSensor(MouseSensor, {
|
const mouseSensor = useSensor(MouseSensor, {
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
useDraggable as useOriginalDraggable,
|
useDraggable as useOriginalDraggable,
|
||||||
useDroppable as useOriginalDroppable,
|
useDroppable as useOriginalDroppable,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
|
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
type BaseDropData = {
|
type BaseDropData = {
|
||||||
@ -55,7 +56,7 @@ export type AddToBatchDropData = BaseDropData & {
|
|||||||
|
|
||||||
export type MoveBoardDropData = BaseDropData & {
|
export type MoveBoardDropData = BaseDropData & {
|
||||||
actionType: 'MOVE_BOARD';
|
actionType: 'MOVE_BOARD';
|
||||||
context: { boardId: string | null };
|
context: { boardId: BoardId };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDroppableData =
|
export type TypesafeDroppableData =
|
||||||
@ -158,8 +159,36 @@ export const isValidDrop = (
|
|||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||||
case 'ADD_TO_BATCH':
|
case 'ADD_TO_BATCH':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||||
case 'MOVE_BOARD':
|
case 'MOVE_BOARD': {
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
// 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:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import { Middleware } from '@reduxjs/toolkit';
|
|||||||
import ImageDndContext from './ImageDnd/ImageDndContext';
|
import ImageDndContext from './ImageDnd/ImageDndContext';
|
||||||
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
|
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
|
||||||
import { $authToken, $baseUrl } from 'services/api/client';
|
import { $authToken, $baseUrl } from 'services/api/client';
|
||||||
import { DeleteBoardImagesContextProvider } from '../contexts/DeleteBoardImagesContext';
|
|
||||||
|
|
||||||
const App = lazy(() => import('./App'));
|
const App = lazy(() => import('./App'));
|
||||||
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
|
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
|
||||||
@ -78,9 +77,7 @@ const InvokeAIUI = ({
|
|||||||
<ThemeLocaleProvider>
|
<ThemeLocaleProvider>
|
||||||
<ImageDndContext>
|
<ImageDndContext>
|
||||||
<AddImageToBoardContextProvider>
|
<AddImageToBoardContextProvider>
|
||||||
<DeleteBoardImagesContextProvider>
|
|
||||||
<App config={config} headerComponent={headerComponent} />
|
<App config={config} headerComponent={headerComponent} />
|
||||||
</DeleteBoardImagesContextProvider>
|
|
||||||
</AddImageToBoardContextProvider>
|
</AddImageToBoardContextProvider>
|
||||||
</ImageDndContext>
|
</ImageDndContext>
|
||||||
</ThemeLocaleProvider>
|
</ThemeLocaleProvider>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||||
import { ImageDTO } from 'services/api/types';
|
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 = {
|
export type ImageUsage = {
|
||||||
isInitialImage: boolean;
|
isInitialImage: boolean;
|
||||||
@ -40,8 +41,7 @@ type Props = PropsWithChildren;
|
|||||||
export const AddImageToBoardContextProvider = (props: Props) => {
|
export const AddImageToBoardContextProvider = (props: Props) => {
|
||||||
const [imageToMove, setImageToMove] = useState<ImageDTO>();
|
const [imageToMove, setImageToMove] = useState<ImageDTO>();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [addImageToBoard, result] = useAddImageToBoardMutation();
|
|
||||||
|
|
||||||
// Clean up after deleting or dismissing the modal
|
// Clean up after deleting or dismissing the modal
|
||||||
const closeAndClearImageToDelete = useCallback(() => {
|
const closeAndClearImageToDelete = useCallback(() => {
|
||||||
@ -63,14 +63,16 @@ export const AddImageToBoardContextProvider = (props: Props) => {
|
|||||||
const handleAddToBoard = useCallback(
|
const handleAddToBoard = useCallback(
|
||||||
(boardId: string) => {
|
(boardId: string) => {
|
||||||
if (imageToMove) {
|
if (imageToMove) {
|
||||||
addImageToBoard({
|
dispatch(
|
||||||
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
|
imageDTO: imageToMove,
|
||||||
board_id: boardId,
|
board_id: boardId,
|
||||||
image_name: imageToMove.image_name,
|
})
|
||||||
});
|
);
|
||||||
closeAndClearImageToDelete();
|
closeAndClearImageToDelete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addImageToBoard, closeAndClearImageToDelete, imageToMove]
|
[dispatch, closeAndClearImageToDelete, imageToMove]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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 { addAppConfigReceivedListener } from './listeners/appConfigReceived';
|
||||||
import { addAppStartedListener } from './listeners/appStarted';
|
import { addAppStartedListener } from './listeners/appStarted';
|
||||||
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
|
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
|
||||||
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
|
import { addDeleteBoardAndImagesFulfilledListener } from './listeners/boardAndImagesDeleted';
|
||||||
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
||||||
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
||||||
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
||||||
@ -29,10 +29,6 @@ import {
|
|||||||
addRequestedImageDeletionListener,
|
addRequestedImageDeletionListener,
|
||||||
} from './listeners/imageDeleted';
|
} from './listeners/imageDeleted';
|
||||||
import { addImageDroppedListener } from './listeners/imageDropped';
|
import { addImageDroppedListener } from './listeners/imageDropped';
|
||||||
import {
|
|
||||||
addImageMetadataReceivedFulfilledListener,
|
|
||||||
addImageMetadataReceivedRejectedListener,
|
|
||||||
} from './listeners/imageMetadataReceived';
|
|
||||||
import {
|
import {
|
||||||
addImageRemovedFromBoardFulfilledListener,
|
addImageRemovedFromBoardFulfilledListener,
|
||||||
addImageRemovedFromBoardRejectedListener,
|
addImageRemovedFromBoardRejectedListener,
|
||||||
@ -46,18 +42,10 @@ import {
|
|||||||
addImageUploadedFulfilledListener,
|
addImageUploadedFulfilledListener,
|
||||||
addImageUploadedRejectedListener,
|
addImageUploadedRejectedListener,
|
||||||
} from './listeners/imageUploaded';
|
} from './listeners/imageUploaded';
|
||||||
import {
|
|
||||||
addImageUrlsReceivedFulfilledListener,
|
|
||||||
addImageUrlsReceivedRejectedListener,
|
|
||||||
} from './listeners/imageUrlsReceived';
|
|
||||||
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||||
import { addModelSelectedListener } from './listeners/modelSelected';
|
import { addModelSelectedListener } from './listeners/modelSelected';
|
||||||
import { addModelsLoadedListener } from './listeners/modelsLoaded';
|
import { addModelsLoadedListener } from './listeners/modelsLoaded';
|
||||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||||
import {
|
|
||||||
addReceivedPageOfImagesFulfilledListener,
|
|
||||||
addReceivedPageOfImagesRejectedListener,
|
|
||||||
} from './listeners/receivedPageOfImages';
|
|
||||||
import {
|
import {
|
||||||
addSessionCanceledFulfilledListener,
|
addSessionCanceledFulfilledListener,
|
||||||
addSessionCanceledPendingListener,
|
addSessionCanceledPendingListener,
|
||||||
@ -91,6 +79,7 @@ import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextTo
|
|||||||
import { addModelLoadStartedEventListener } from './listeners/socketio/socketModelLoadStarted';
|
import { addModelLoadStartedEventListener } from './listeners/socketio/socketModelLoadStarted';
|
||||||
import { addModelLoadCompletedEventListener } from './listeners/socketio/socketModelLoadCompleted';
|
import { addModelLoadCompletedEventListener } from './listeners/socketio/socketModelLoadCompleted';
|
||||||
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
||||||
|
import { addFirstListImagesListener } from './listeners/addFirstListImagesListener.ts';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -132,17 +121,9 @@ addRequestedImageDeletionListener();
|
|||||||
addImageDeletedPendingListener();
|
addImageDeletedPendingListener();
|
||||||
addImageDeletedFulfilledListener();
|
addImageDeletedFulfilledListener();
|
||||||
addImageDeletedRejectedListener();
|
addImageDeletedRejectedListener();
|
||||||
addRequestedBoardImageDeletionListener();
|
addDeleteBoardAndImagesFulfilledListener();
|
||||||
addImageToDeleteSelectedListener();
|
addImageToDeleteSelectedListener();
|
||||||
|
|
||||||
// Image metadata
|
|
||||||
addImageMetadataReceivedFulfilledListener();
|
|
||||||
addImageMetadataReceivedRejectedListener();
|
|
||||||
|
|
||||||
// Image URLs
|
|
||||||
addImageUrlsReceivedFulfilledListener();
|
|
||||||
addImageUrlsReceivedRejectedListener();
|
|
||||||
|
|
||||||
// User Invoked
|
// User Invoked
|
||||||
addUserInvokedCanvasListener();
|
addUserInvokedCanvasListener();
|
||||||
addUserInvokedNodesListener();
|
addUserInvokedNodesListener();
|
||||||
@ -198,17 +179,10 @@ addSessionCanceledPendingListener();
|
|||||||
addSessionCanceledFulfilledListener();
|
addSessionCanceledFulfilledListener();
|
||||||
addSessionCanceledRejectedListener();
|
addSessionCanceledRejectedListener();
|
||||||
|
|
||||||
// Fetching images
|
|
||||||
addReceivedPageOfImagesFulfilledListener();
|
|
||||||
addReceivedPageOfImagesRejectedListener();
|
|
||||||
|
|
||||||
// ControlNet
|
// ControlNet
|
||||||
addControlNetImageProcessedListener();
|
addControlNetImageProcessedListener();
|
||||||
addControlNetAutoProcessListener();
|
addControlNetAutoProcessListener();
|
||||||
|
|
||||||
// Update image URLs on connect
|
|
||||||
// addUpdateImageUrlsOnConnectListener();
|
|
||||||
|
|
||||||
// Boards
|
// Boards
|
||||||
addImageAddedToBoardFulfilledListener();
|
addImageAddedToBoardFulfilledListener();
|
||||||
addImageAddedToBoardRejectedListener();
|
addImageAddedToBoardRejectedListener();
|
||||||
@ -229,5 +203,7 @@ addModelSelectedListener();
|
|||||||
addAppStartedListener();
|
addAppStartedListener();
|
||||||
addModelsLoadedListener();
|
addModelsLoadedListener();
|
||||||
addAppConfigReceivedListener();
|
addAppConfigReceivedListener();
|
||||||
|
addFirstListImagesListener();
|
||||||
|
|
||||||
|
// Ad-hoc upscale workflwo
|
||||||
addUpscaleRequestedListener();
|
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 { 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 '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
export const appStarted = createAction('app/appStarted');
|
export const appStarted = createAction('app/appStarted');
|
||||||
@ -17,29 +10,9 @@ export const addAppStartedListener = () => {
|
|||||||
action,
|
action,
|
||||||
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
||||||
) => {
|
) => {
|
||||||
|
// this should only run once
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
unsubscribe();
|
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 { log } from 'app/logging/useLogger';
|
||||||
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
|
|
||||||
import {
|
import {
|
||||||
ASSETS_CATEGORIES,
|
|
||||||
IMAGE_CATEGORIES,
|
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectImagesAll,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
|
||||||
import {
|
import {
|
||||||
IMAGES_PER_PAGE,
|
getBoardIdQueryParamForBoard,
|
||||||
receivedPageOfImages,
|
getCategoriesQueryParamForBoard,
|
||||||
} from 'services/api/thunks/image';
|
} from 'features/gallery/store/util';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
@ -19,54 +15,44 @@ const moduleLog = log.child({ namespace: 'boards' });
|
|||||||
export const addBoardIdSelectedListener = () => {
|
export const addBoardIdSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: boardIdSelected,
|
actionCreator: boardIdSelected,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: async (
|
||||||
const board_id = action.payload;
|
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 categories = getCategoriesQueryParamForBoard(_board_id);
|
||||||
const allImages = selectImagesAll(state);
|
const board_id = getBoardIdQueryParamForBoard(_board_id);
|
||||||
|
const queryArgs = { board_id, categories };
|
||||||
|
|
||||||
if (board_id === 'all') {
|
// wait until the board has some images - maybe it already has some from a previous fetch
|
||||||
// Selected all images
|
// must use getState() to ensure we do not have stale state
|
||||||
dispatch(imageSelected(allImages[0]?.image_name ?? null));
|
const isSuccess = await condition(
|
||||||
return;
|
() =>
|
||||||
}
|
imagesApi.endpoints.listImages.select(queryArgs)(getState())
|
||||||
|
.isSuccess,
|
||||||
if (board_id === 'batch') {
|
1000
|
||||||
// Selected the batch
|
|
||||||
dispatch(imageSelected(state.gallery.batchImageNames[0] ?? null));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (isSuccess) {
|
||||||
|
// the board was just changed - we can select the first image
|
||||||
|
const { data: boardImagesData } = imagesApi.endpoints.listImages.select(
|
||||||
|
queryArgs
|
||||||
|
)(getState());
|
||||||
|
|
||||||
|
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 { log } from 'app/logging/useLogger';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { canvasMerged } from 'features/canvas/store/actions';
|
||||||
import { imageUploaded } from 'services/api/thunks/image';
|
|
||||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
|
||||||
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
|
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' });
|
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||||
|
|
||||||
@ -46,27 +46,28 @@ export const addCanvasMergedListener = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const imageUploadedRequest = dispatch(
|
const imageUploadedRequest = dispatch(
|
||||||
imageUploaded({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([blob], 'mergedCanvas.png', {
|
file: new File([blob], 'mergedCanvas.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
}),
|
}),
|
||||||
image_category: 'general',
|
image_category: 'general',
|
||||||
is_intermediate: true,
|
is_intermediate: true,
|
||||||
postUploadAction: {
|
postUploadAction: {
|
||||||
type: 'TOAST_CANVAS_MERGED',
|
type: 'TOAST',
|
||||||
|
toastOptions: { title: 'Canvas Merged' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const [{ payload }] = await take(
|
const [{ payload }] = await take(
|
||||||
(
|
(uploadedImageAction) =>
|
||||||
uploadedImageAction
|
imagesApi.endpoints.uploadImage.matchFulfilled(uploadedImageAction) &&
|
||||||
): uploadedImageAction is ReturnType<typeof imageUploaded.fulfilled> =>
|
|
||||||
imageUploaded.fulfilled.match(uploadedImageAction) &&
|
|
||||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
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(
|
dispatch(
|
||||||
setMergedCanvas({
|
setMergedCanvas({
|
||||||
@ -76,13 +77,6 @@ export const addCanvasMergedListener = () => {
|
|||||||
...baseLayerRect,
|
...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 { 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 { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
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' });
|
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||||
|
|
||||||
@ -28,28 +27,19 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUploadedRequest = dispatch(
|
dispatch(
|
||||||
imageUploaded({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([blob], 'savedCanvas.png', {
|
file: new File([blob], 'savedCanvas.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
}),
|
}),
|
||||||
image_category: 'general',
|
image_category: 'general',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction: {
|
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 { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||||
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { isImageOutput } from 'services/api/guards';
|
import { isImageOutput } from 'services/api/guards';
|
||||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
|
||||||
import { sessionCreated } from 'services/api/thunks/session';
|
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 { socketInvocationComplete } from 'services/events/actions';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
@ -62,12 +62,13 @@ export const addControlNetImageProcessedListener = () => {
|
|||||||
invocationCompleteAction.payload.data.result.image;
|
invocationCompleteAction.payload.data.result.image;
|
||||||
|
|
||||||
// Wait for the ImageDTO to be received
|
// Wait for the ImageDTO to be received
|
||||||
const [imageMetadataReceivedAction] = await take(
|
const [{ payload }] = await take(
|
||||||
(action): action is ReturnType<typeof imageDTOReceived.fulfilled> =>
|
(action) =>
|
||||||
imageDTOReceived.fulfilled.match(action) &&
|
imagesApi.endpoints.getImageDTO.matchFulfilled(action) &&
|
||||||
action.payload.image_name === image_name
|
action.payload.image_name === image_name
|
||||||
);
|
);
|
||||||
const processedControlImage = imageMetadataReceivedAction.payload;
|
|
||||||
|
const processedControlImage = payload as ImageDTO;
|
||||||
|
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ data: { arg: action.payload, processedControlImage } },
|
{ data: { arg: action.payload, processedControlImage } },
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
|
|
||||||
export const addImageAddedToBoardFulfilledListener = () => {
|
export const addImageAddedToBoardFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: boardImagesApi.endpoints.addImageToBoard.matchFulfilled,
|
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||||
|
|
||||||
moduleLog.debug(
|
// TODO: update listImages cache for this board
|
||||||
{ data: { board_id, image_name } },
|
|
||||||
'Image added to board'
|
moduleLog.debug({ data: { board_id, imageDTO } }, 'Image added to board');
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addImageAddedToBoardRejectedListener = () => {
|
export const addImageAddedToBoardRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: boardImagesApi.endpoints.addImageToBoard.matchRejected,
|
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||||
|
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ data: { board_id, image_name } },
|
{ data: { board_id, imageDTO } },
|
||||||
'Problem adding image to board'
|
'Problem adding image to board'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { selectNextImageToSelect } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
imageRemoved,
|
|
||||||
imageSelected,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import {
|
import {
|
||||||
imageDeletionConfirmed,
|
imageDeletionConfirmed,
|
||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
} from 'features/imageDeletion/store/imageDeletionSlice';
|
} from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
import { api } from 'services/api';
|
import { api } from 'services/api';
|
||||||
import { imageDeleted } from 'services/api/thunks/image';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
@ -36,10 +34,28 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
state.gallery.selection[state.gallery.selection.length - 1];
|
state.gallery.selection[state.gallery.selection.length - 1];
|
||||||
|
|
||||||
if (lastSelectedImage === image_name) {
|
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) {
|
if (newSelectedImageId) {
|
||||||
dispatch(imageSelected(newSelectedImageId));
|
dispatch(imageSelected(newSelectedImageId as string));
|
||||||
} else {
|
} else {
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelected(null));
|
||||||
}
|
}
|
||||||
@ -63,16 +79,15 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
dispatch(nodeEditorReset());
|
dispatch(nodeEditorReset());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preemptively remove from gallery
|
|
||||||
dispatch(imageRemoved(image_name));
|
|
||||||
|
|
||||||
// Delete from server
|
// 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
|
// Wait for successful deletion, then trigger boards to re-fetch
|
||||||
const wasImageDeleted = await condition(
|
const wasImageDeleted = await condition(
|
||||||
(action): action is ReturnType<typeof imageDeleted.fulfilled> =>
|
(action) =>
|
||||||
imageDeleted.fulfilled.match(action) &&
|
imagesApi.endpoints.deleteImage.matchFulfilled(action) &&
|
||||||
action.meta.requestId === requestId,
|
action.meta.requestId === requestId,
|
||||||
30000
|
30000
|
||||||
);
|
);
|
||||||
@ -91,7 +106,7 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
*/
|
*/
|
||||||
export const addImageDeletedPendingListener = () => {
|
export const addImageDeletedPendingListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeleted.pending,
|
matcher: imagesApi.endpoints.deleteImage.matchPending,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
//
|
//
|
||||||
},
|
},
|
||||||
@ -103,9 +118,12 @@ export const addImageDeletedPendingListener = () => {
|
|||||||
*/
|
*/
|
||||||
export const addImageDeletedFulfilledListener = () => {
|
export const addImageDeletedFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeleted.fulfilled,
|
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
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 = () => {
|
export const addImageDeletedRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeleted.rejected,
|
matcher: imagesApi.endpoints.deleteImage.matchRejected,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ data: { image: action.meta.arg } },
|
{ data: { image: action.meta.arg.originalArgs } },
|
||||||
'Unable to delete image'
|
'Unable to delete image'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -10,12 +10,9 @@ import {
|
|||||||
imageSelected,
|
imageSelected,
|
||||||
imagesAddedToBatch,
|
imagesAddedToBatch,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
fieldValueChanged,
|
|
||||||
imageCollectionFieldValueChanged,
|
|
||||||
} from 'features/nodes/store/nodesSlice';
|
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '../';
|
import { startAppListening } from '../';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'dnd' });
|
const moduleLog = log.child({ namespace: 'dnd' });
|
||||||
@ -137,23 +134,23 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set multiple nodes images (multiple images handler)
|
// // set multiple nodes images (multiple images handler)
|
||||||
if (
|
// if (
|
||||||
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
// overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES'
|
// activeData.payloadType === 'IMAGE_NAMES'
|
||||||
) {
|
// ) {
|
||||||
const { fieldName, nodeId } = overData.context;
|
// const { fieldName, nodeId } = overData.context;
|
||||||
dispatch(
|
// dispatch(
|
||||||
imageCollectionFieldValueChanged({
|
// imageCollectionFieldValueChanged({
|
||||||
nodeId,
|
// nodeId,
|
||||||
fieldName,
|
// fieldName,
|
||||||
value: activeData.payload.image_names.map((image_name) => ({
|
// value: activeData.payload.image_names.map((image_name) => ({
|
||||||
image_name,
|
// image_name,
|
||||||
})),
|
// })),
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// add image to board
|
// add image to board
|
||||||
if (
|
if (
|
||||||
@ -162,97 +159,95 @@ export const addImageDroppedListener = () => {
|
|||||||
activeData.payload.imageDTO &&
|
activeData.payload.imageDTO &&
|
||||||
overData.context.boardId
|
overData.context.boardId
|
||||||
) {
|
) {
|
||||||
const { image_name } = activeData.payload.imageDTO;
|
const { imageDTO } = activeData.payload;
|
||||||
const { boardId } = overData.context;
|
const { boardId } = overData.context;
|
||||||
|
|
||||||
|
// if the board is "No Board", this is a remove action
|
||||||
|
if (boardId === 'no_board') {
|
||||||
dispatch(
|
dispatch(
|
||||||
boardImagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
image_name,
|
imageDTO,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle adding image to batch
|
||||||
|
if (boardId === 'batch') {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, add the image to the board
|
||||||
|
dispatch(
|
||||||
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
|
imageDTO,
|
||||||
board_id: boardId,
|
board_id: boardId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove image from board
|
// // add gallery selection to board
|
||||||
if (
|
// if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
// overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
// activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
activeData.payload.imageDTO &&
|
// overData.context.boardId
|
||||||
overData.context.boardId === null
|
// ) {
|
||||||
) {
|
// console.log('adding gallery selection to board');
|
||||||
const { image_name, board_id } = activeData.payload.imageDTO;
|
// const board_id = overData.context.boardId;
|
||||||
if (board_id) {
|
// dispatch(
|
||||||
dispatch(
|
// boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||||
boardImagesApi.endpoints.removeImageFromBoard.initiate({
|
// board_id,
|
||||||
image_name,
|
// image_names: activeData.payload.image_names,
|
||||||
board_id,
|
// })
|
||||||
})
|
// );
|
||||||
);
|
// return;
|
||||||
}
|
// }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add gallery selection to board
|
// // remove gallery selection from board
|
||||||
if (
|
// if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
// overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
// activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
overData.context.boardId
|
// overData.context.boardId === null
|
||||||
) {
|
// ) {
|
||||||
console.log('adding gallery selection to board');
|
// console.log('removing gallery selection to board');
|
||||||
const board_id = overData.context.boardId;
|
// dispatch(
|
||||||
dispatch(
|
// boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||||
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
// image_names: activeData.payload.image_names,
|
||||||
board_id,
|
// })
|
||||||
image_names: activeData.payload.image_names,
|
// );
|
||||||
})
|
// return;
|
||||||
);
|
// }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove gallery selection from board
|
// // add batch selection to board
|
||||||
if (
|
// if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
// overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
// activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
overData.context.boardId === null
|
// overData.context.boardId
|
||||||
) {
|
// ) {
|
||||||
console.log('removing gallery selection to board');
|
// const board_id = overData.context.boardId;
|
||||||
dispatch(
|
// dispatch(
|
||||||
boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
// boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||||
image_names: activeData.payload.image_names,
|
// board_id,
|
||||||
})
|
// image_names: activeData.payload.image_names,
|
||||||
);
|
// })
|
||||||
return;
|
// );
|
||||||
}
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
// add batch selection to board
|
// // remove batch selection from board
|
||||||
if (
|
// if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
// overData.actionType === 'MOVE_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
// activeData.payloadType === 'IMAGE_NAMES' &&
|
||||||
overData.context.boardId
|
// overData.context.boardId === null
|
||||||
) {
|
// ) {
|
||||||
const board_id = overData.context.boardId;
|
// dispatch(
|
||||||
dispatch(
|
// boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||||
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
// image_names: activeData.payload.image_names,
|
||||||
board_id,
|
// })
|
||||||
image_names: activeData.payload.image_names,
|
// );
|
||||||
})
|
// return;
|
||||||
);
|
// }
|
||||||
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 { log } from 'app/logging/useLogger';
|
||||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'boards' });
|
const moduleLog = log.child({ namespace: 'boards' });
|
||||||
|
|
||||||
export const addImageRemovedFromBoardFulfilledListener = () => {
|
export const addImageRemovedFromBoardFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchFulfilled,
|
matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export const addImageRemovedFromBoardFulfilledListener = () => {
|
|||||||
|
|
||||||
export const addImageRemovedFromBoardRejectedListener = () => {
|
export const addImageRemovedFromBoardRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchRejected,
|
matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
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 { log } from 'app/logging/useLogger';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
export const addImageUpdatedFulfilledListener = () => {
|
export const addImageUpdatedFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageUpdated.fulfilled,
|
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ oldImage: action.meta.arg, updatedImage: action.payload },
|
{
|
||||||
|
data: {
|
||||||
|
oldImage: action.meta.arg.originalArgs,
|
||||||
|
updatedImage: action.payload,
|
||||||
|
},
|
||||||
|
},
|
||||||
'Image updated'
|
'Image updated'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -18,9 +23,12 @@ export const addImageUpdatedFulfilledListener = () => {
|
|||||||
|
|
||||||
export const addImageUpdatedRejectedListener = () => {
|
export const addImageUpdatedRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageUpdated.rejected,
|
matcher: imagesApi.endpoints.updateImage.matchRejected,
|
||||||
effect: (action, { dispatch }) => {
|
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 { log } from 'app/logging/useLogger';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import {
|
import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice';
|
||||||
imageUpserted,
|
|
||||||
imagesAddedToBatch,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
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 { startAppListening } from '..';
|
||||||
|
import {
|
||||||
|
SYSTEM_BOARDS,
|
||||||
|
imagesApi,
|
||||||
|
} from '../../../../../services/api/endpoints/images';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
|
const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
|
||||||
|
title: 'Image Uploaded',
|
||||||
|
status: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
export const addImageUploadedFulfilledListener = () => {
|
export const addImageUploadedFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageUploaded.fulfilled,
|
matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
|
||||||
effect: (action, { dispatch, getState }) => {
|
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) {
|
const { postUploadAction } = action.meta.arg.originalArgs;
|
||||||
// No further actions needed for intermediate images
|
|
||||||
|
if (
|
||||||
|
// No further actions needed for intermediate images,
|
||||||
|
action.payload.is_intermediate &&
|
||||||
|
// unless they have an explicit post-upload action
|
||||||
|
!postUploadAction
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(imageUpserted(image));
|
// default action - just upload and alert user
|
||||||
|
if (postUploadAction?.type === 'TOAST') {
|
||||||
const { postUploadAction } = action.meta.arg;
|
const { toastOptions } = postUploadAction;
|
||||||
|
if (SYSTEM_BOARDS.includes(selectedBoardId)) {
|
||||||
if (postUploadAction?.type === 'TOAST_CANVAS_SAVED_TO_GALLERY') {
|
dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
|
||||||
|
} else {
|
||||||
|
// Add this image to the board
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
|
board_id: selectedBoardId,
|
||||||
|
imageDTO,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postUploadAction?.type === 'TOAST_CANVAS_MERGED') {
|
// Attempt to get the board's name for the toast
|
||||||
dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
|
const { data } = boardsApi.endpoints.listAllBoards.select()(state);
|
||||||
|
|
||||||
|
// 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}`;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
...DEFAULT_UPLOADED_TOAST,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,30 +90,49 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
controlNetImageChanged({
|
controlNetImageChanged({
|
||||||
controlNetId,
|
controlNetId,
|
||||||
controlImage: image.image_name,
|
controlImage: imageDTO.image_name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
...DEFAULT_UPLOADED_TOAST,
|
||||||
|
description: 'Set as control image',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
|
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
|
||||||
dispatch(initialImageChanged(image));
|
dispatch(initialImageChanged(imageDTO));
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
...DEFAULT_UPLOADED_TOAST,
|
||||||
|
description: 'Set as initial image',
|
||||||
|
})
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
|
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
|
||||||
const { nodeId, fieldName } = postUploadAction;
|
const { nodeId, fieldName } = postUploadAction;
|
||||||
dispatch(fieldValueChanged({ nodeId, fieldName, value: image }));
|
dispatch(fieldValueChanged({ nodeId, fieldName, value: imageDTO }));
|
||||||
return;
|
dispatch(
|
||||||
}
|
addToast({
|
||||||
|
...DEFAULT_UPLOADED_TOAST,
|
||||||
if (postUploadAction?.type === 'TOAST_UPLOADED') {
|
description: `Set as node field ${fieldName}`,
|
||||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
})
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'ADD_TO_BATCH') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -84,10 +141,10 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
|
|
||||||
export const addImageUploadedRejectedListener = () => {
|
export const addImageUploadedRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageUploaded.rejected,
|
matcher: imagesApi.endpoints.uploadImage.matchRejected,
|
||||||
effect: (action, { dispatch }) => {
|
effect: (action, { dispatch }) => {
|
||||||
const { formData, ...rest } = action.meta.arg;
|
const { file, postUploadAction, ...rest } = action.meta.arg.originalArgs;
|
||||||
const sanitizedData = { arg: { ...rest, formData: { file: '<Blob>' } } };
|
const sanitizedData = { arg: { ...rest, file: '<Blob>' } };
|
||||||
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
|
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast({
|
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 { makeToast } from 'app/components/Toaster';
|
||||||
import { selectImagesById } from 'features/gallery/store/gallerySlice';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { isImageDTO } from 'services/api/guards';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
export const addInitialImageSelectedListener = () => {
|
export const addInitialImageSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -20,26 +18,8 @@ export const addInitialImageSelectedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isImageDTO(action.payload)) {
|
|
||||||
dispatch(initialImageChanged(action.payload));
|
dispatch(initialImageChanged(action.payload));
|
||||||
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
|
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(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 { log } from 'app/logging/useLogger';
|
||||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
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 { 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 { isImageOutput } from 'services/api/guards';
|
||||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
|
||||||
import { sessionCanceled } from 'services/api/thunks/session';
|
import { sessionCanceled } from 'services/api/thunks/session';
|
||||||
import {
|
import {
|
||||||
appSocketInvocationComplete,
|
appSocketInvocationComplete,
|
||||||
@ -22,7 +30,6 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
{ data: action.payload },
|
{ data: action.payload },
|
||||||
`Invocation complete (${action.payload.data.node.type})`
|
`Invocation complete (${action.payload.data.node.type})`
|
||||||
);
|
);
|
||||||
|
|
||||||
const session_id = action.payload.data.graph_execution_state_id;
|
const session_id = action.payload.data.graph_execution_state_id;
|
||||||
|
|
||||||
const { cancelType, isCancelScheduled, boardIdToAddTo } =
|
const { cancelType, isCancelScheduled, boardIdToAddTo } =
|
||||||
@ -39,35 +46,72 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
// This complete event has an associated image output
|
// This complete event has an associated image output
|
||||||
if (isImageOutput(result) && !nodeDenylist.includes(node.type)) {
|
if (isImageOutput(result) && !nodeDenylist.includes(node.type)) {
|
||||||
const { image_name } = result.image;
|
const { image_name } = result.image;
|
||||||
|
const { canvas, gallery } = getState();
|
||||||
|
|
||||||
// Get its metadata
|
const imageDTO = await dispatch(
|
||||||
dispatch(
|
imagesApi.endpoints.getImageDTO.initiate(image_name)
|
||||||
imageDTOReceived({
|
).unwrap();
|
||||||
image_name,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const [{ payload: imageDTO }] = await take(
|
// Add canvas images to the staging area
|
||||||
imageDTOReceived.fulfilled.match
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle canvas image
|
|
||||||
if (
|
if (
|
||||||
graph_execution_state_id ===
|
graph_execution_state_id === canvas.layerState.stagingArea.sessionId
|
||||||
getState().canvas.layerState.stagingArea.sessionId
|
|
||||||
) {
|
) {
|
||||||
dispatch(addImageToStagingArea(imageDTO));
|
dispatch(addImageToStagingArea(imageDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boardIdToAddTo && !imageDTO.is_intermediate) {
|
if (!imageDTO.is_intermediate) {
|
||||||
|
// update the cache for 'All Images'
|
||||||
dispatch(
|
dispatch(
|
||||||
boardImagesApi.endpoints.addImageToBoard.initiate({
|
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,
|
board_id: boardIdToAddTo,
|
||||||
image_name,
|
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));
|
dispatch(progressImageSet(null));
|
||||||
}
|
}
|
||||||
// pass along the socket event as an application action
|
// pass along the socket event as an application action
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
|
||||||
import { startAppListening } from '..';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { imageUpdated } from 'services/api/thunks/image';
|
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
||||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'canvas' });
|
const moduleLog = log.child({ namespace: 'canvas' });
|
||||||
|
|
||||||
@ -11,41 +10,27 @@ export const addStagingAreaImageSavedListener = () => {
|
|||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: stagingAreaImageSaved,
|
actionCreator: stagingAreaImageSaved,
|
||||||
effect: async (action, { dispatch, getState, take }) => {
|
effect: async (action, { dispatch, getState, take }) => {
|
||||||
const { imageName } = action.payload;
|
const { imageDTO } = action.payload;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imageUpdated({
|
imagesApi.endpoints.updateImage.initiate({
|
||||||
image_name: imageName,
|
imageDTO,
|
||||||
is_intermediate: false,
|
changes: { is_intermediate: false },
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
.unwrap()
|
||||||
const [imageUpdatedAction] = await take(
|
.then((image) => {
|
||||||
(action) =>
|
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
||||||
(imageUpdated.fulfilled.match(action) ||
|
})
|
||||||
imageUpdated.rejected.match(action)) &&
|
.catch((error) => {
|
||||||
action.meta.arg.image_name === imageName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (imageUpdated.rejected.match(imageUpdatedAction)) {
|
|
||||||
moduleLog.error(
|
|
||||||
{ data: { arg: imageUpdatedAction.meta.arg } },
|
|
||||||
'Image saving failed'
|
|
||||||
);
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addToast({
|
addToast({
|
||||||
title: 'Image Saving Failed',
|
title: 'Image Saving Failed',
|
||||||
description: imageUpdatedAction.error.message,
|
description: error.message,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
|
|
||||||
dispatch(imageUpserted(imageUpdatedAction.payload));
|
|
||||||
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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 { log } from 'app/logging/useLogger';
|
||||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
import { userInvoked } from 'app/store/actions';
|
||||||
import { imageUpdated, imageUploaded } from 'services/api/thunks/image';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import {
|
import {
|
||||||
canvasSessionIdChanged,
|
canvasSessionIdChanged,
|
||||||
stagingAreaInitialized,
|
stagingAreaInitialized,
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} 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 { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import { buildCanvasGraph } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
|
||||||
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
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' });
|
const moduleLog = log.child({ namespace: 'invoke' });
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
||||||
// upload the image, saving the request id
|
// upload the image, saving the request id
|
||||||
const { requestId: initImageUploadedRequestId } = dispatch(
|
const { requestId: initImageUploadedRequestId } = dispatch(
|
||||||
imageUploaded({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([baseBlob], 'canvasInitImage.png', {
|
file: new File([baseBlob], 'canvasInitImage.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
}),
|
}),
|
||||||
@ -85,19 +85,20 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
|
|
||||||
// Wait for the image to be uploaded, matching by request id
|
// Wait for the image to be uploaded, matching by request id
|
||||||
const [{ payload }] = await take(
|
const [{ payload }] = await take(
|
||||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
// TODO: figure out how to narrow this action's type
|
||||||
imageUploaded.fulfilled.match(action) &&
|
(action) =>
|
||||||
|
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
|
||||||
action.meta.requestId === initImageUploadedRequestId
|
action.meta.requestId === initImageUploadedRequestId
|
||||||
);
|
);
|
||||||
|
|
||||||
canvasInitImage = payload;
|
canvasInitImage = payload as ImageDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For inpaint/outpaint, we also need to upload the mask layer
|
// For inpaint/outpaint, we also need to upload the mask layer
|
||||||
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
||||||
// upload the image, saving the request id
|
// upload the image, saving the request id
|
||||||
const { requestId: maskImageUploadedRequestId } = dispatch(
|
const { requestId: maskImageUploadedRequestId } = dispatch(
|
||||||
imageUploaded({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
}),
|
}),
|
||||||
@ -108,12 +109,13 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
|
|
||||||
// Wait for the image to be uploaded, matching by request id
|
// Wait for the image to be uploaded, matching by request id
|
||||||
const [{ payload }] = await take(
|
const [{ payload }] = await take(
|
||||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
// TODO: figure out how to narrow this action's type
|
||||||
imageUploaded.fulfilled.match(action) &&
|
(action) =>
|
||||||
|
imagesApi.endpoints.uploadImage.matchFulfilled(action) &&
|
||||||
action.meta.requestId === maskImageUploadedRequestId
|
action.meta.requestId === maskImageUploadedRequestId
|
||||||
);
|
);
|
||||||
|
|
||||||
canvasMaskImage = payload;
|
canvasMaskImage = payload as ImageDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = buildCanvasGraph(
|
const graph = buildCanvasGraph(
|
||||||
@ -144,9 +146,9 @@ export const addUserInvokedCanvasListener = () => {
|
|||||||
// Associate the init image with the session, now that we have the session ID
|
// Associate the init image with the session, now that we have the session ID
|
||||||
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
|
if (['img2img', 'inpaint'].includes(generationMode) && canvasInitImage) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imageUpdated({
|
imagesApi.endpoints.updateImage.initiate({
|
||||||
image_name: canvasInitImage.image_name,
|
imageDTO: canvasInitImage,
|
||||||
session_id: sessionId,
|
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
|
// Associate the mask image with the session, now that we have the session ID
|
||||||
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
|
if (['inpaint'].includes(generationMode) && canvasMaskImage) {
|
||||||
dispatch(
|
dispatch(
|
||||||
imageUpdated({
|
imagesApi.endpoints.updateImage.initiate({
|
||||||
image_name: canvasMaskImage.image_name,
|
imageDTO: canvasMaskImage,
|
||||||
session_id: sessionId,
|
changes: { session_id: sessionId },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,15 @@ import {
|
|||||||
TypesafeDroppableData,
|
TypesafeDroppableData,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
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 ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
||||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
import { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import IAIDraggable from './IAIDraggable';
|
import IAIDraggable from './IAIDraggable';
|
||||||
import IAIDroppable from './IAIDroppable';
|
import IAIDroppable from './IAIDroppable';
|
||||||
@ -46,6 +48,7 @@ type IAIDndImageProps = {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
thumbnail?: boolean;
|
thumbnail?: boolean;
|
||||||
noContentFallback?: ReactElement;
|
noContentFallback?: ReactElement;
|
||||||
|
useThumbailFallback?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||||
@ -71,6 +74,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
resetTooltip = 'Reset',
|
resetTooltip = 'Reset',
|
||||||
resetIcon = <FaUndo />,
|
resetIcon = <FaUndo />,
|
||||||
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
noContentFallback = <IAINoContentFallback icon={FaImage} />,
|
||||||
|
useThumbailFallback,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
@ -126,9 +130,14 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
<Image
|
<Image
|
||||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||||
fallbackStrategy="beforeLoadOrError"
|
fallbackStrategy="beforeLoadOrError"
|
||||||
// If we fall back to thumbnail, it feels much snappier than the skeleton...
|
fallbackSrc={
|
||||||
fallbackSrc={imageDTO.thumbnail_url}
|
useThumbailFallback ? imageDTO.thumbnail_url : undefined
|
||||||
// fallback={<IAILoadingImageFallback image={imageDTO} />}
|
}
|
||||||
|
fallback={
|
||||||
|
useThumbailFallback ? undefined : (
|
||||||
|
<IAILoadingImageFallback image={imageDTO} />
|
||||||
|
)
|
||||||
|
}
|
||||||
width={imageDTO.width}
|
width={imageDTO.width}
|
||||||
height={imageDTO.height}
|
height={imageDTO.height}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Flex, Text, useColorMode } from '@chakra-ui/react';
|
import { Flex, Text, useColorMode } from '@chakra-ui/react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { memo, useRef } from 'react';
|
import { ReactNode, memo, useRef } from 'react';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOver: boolean;
|
isOver: boolean;
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IAIDropOverlay = (props: Props) => {
|
export const IAIDropOverlay = (props: Props) => {
|
||||||
@ -57,16 +57,16 @@ export const IAIDropOverlay = (props: Props) => {
|
|||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0.5,
|
||||||
insetInlineStart: 0,
|
insetInlineStart: 0.5,
|
||||||
w: 'full',
|
insetInlineEnd: 0.5,
|
||||||
h: 'full',
|
bottom: 0.5,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
borderWidth: 3,
|
borderWidth: 2,
|
||||||
borderColor: isOver
|
borderColor: isOver
|
||||||
? mode('base.50', 'base.200')(colorMode)
|
? mode('base.50', 'base.50')(colorMode)
|
||||||
: mode('base.100', 'base.500')(colorMode),
|
: mode('base.200', 'base.300')(colorMode),
|
||||||
borderRadius: 'base',
|
borderRadius: 'lg',
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
transitionProperty: 'common',
|
transitionProperty: 'common',
|
||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
@ -78,10 +78,10 @@ export const IAIDropOverlay = (props: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
fontSize: '2xl',
|
fontSize: '2xl',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
transform: isOver ? 'scale(1.02)' : 'scale(1)',
|
transform: isOver ? 'scale(1.1)' : 'scale(1)',
|
||||||
color: isOver
|
color: isOver
|
||||||
? mode('base.50', 'base.50')(colorMode)
|
? mode('base.50', 'base.50')(colorMode)
|
||||||
: mode('base.100', 'base.200')(colorMode),
|
: mode('base.200', 'base.300')(colorMode),
|
||||||
transitionProperty: 'common',
|
transitionProperty: 'common',
|
||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
}}
|
}}
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
useDroppable,
|
useDroppable,
|
||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { memo, useRef } from 'react';
|
import { ReactNode, memo, useRef } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import IAIDropOverlay from './IAIDropOverlay';
|
import IAIDropOverlay from './IAIDropOverlay';
|
||||||
|
|
||||||
type IAIDroppableProps = {
|
type IAIDroppableProps = {
|
||||||
dropLabel?: string;
|
dropLabel?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
data?: TypesafeDroppableData;
|
data?: TypesafeDroppableData;
|
||||||
};
|
};
|
||||||
|
@ -68,6 +68,7 @@ export const IAINoContentFallback = (props: IAINoImageFallbackProps) => {
|
|||||||
flexDir: 'column',
|
flexDir: 'column',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
opacity: 0.7,
|
||||||
color: 'base.700',
|
color: 'base.700',
|
||||||
_dark: {
|
_dark: {
|
||||||
color: 'base.500',
|
color: 'base.500',
|
||||||
|
@ -32,17 +32,46 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
|||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
opacity: 0.4,
|
position: 'absolute',
|
||||||
width: '100%',
|
top: 0,
|
||||||
height: '100%',
|
insetInlineStart: 0,
|
||||||
flexDirection: 'column',
|
w: 'full',
|
||||||
rowGap: 4,
|
h: 'full',
|
||||||
|
bg: 'base.700',
|
||||||
|
_dark: { bg: 'base.900' },
|
||||||
|
opacity: 0.7,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
bg: 'base.900',
|
transitionProperty: 'common',
|
||||||
boxShadow: `inset 0 0 20rem 1rem var(--invokeai-colors-${
|
transitionDuration: '0.1s',
|
||||||
isDragAccept ? 'accent' : 'error'
|
}}
|
||||||
}-500)`,
|
/>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
width: 'full',
|
||||||
|
height: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 ? (
|
{isDragAccept ? (
|
||||||
@ -54,6 +83,7 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,35 +1,43 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
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 { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
memo,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[systemSelector, activeTabNameSelector],
|
[activeTabNameSelector],
|
||||||
(system, activeTabName) => {
|
(activeTabName) => {
|
||||||
const { isConnected, isUploading } = system;
|
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 {
|
return {
|
||||||
isUploaderDisabled,
|
postUploadAction,
|
||||||
activeTabName,
|
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
@ -38,12 +46,13 @@ type ImageUploaderProps = {
|
|||||||
|
|
||||||
const ImageUploader = (props: ImageUploaderProps) => {
|
const ImageUploader = (props: ImageUploaderProps) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const dispatch = useAppDispatch();
|
const { postUploadAction } = useAppSelector(selector);
|
||||||
const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
|
const isBusy = useAppSelector(selectIsBusy);
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||||
const { setOpenUploaderFunction } = useImageUploader();
|
|
||||||
|
const [uploadImage] = useUploadImageMutation();
|
||||||
|
|
||||||
const fileRejectionCallback = useCallback(
|
const fileRejectionCallback = useCallback(
|
||||||
(rejection: FileRejection) => {
|
(rejection: FileRejection) => {
|
||||||
@ -60,16 +69,14 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
|
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
dispatch(
|
uploadImage({
|
||||||
imageUploaded({
|
|
||||||
file,
|
file,
|
||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction: { type: 'TOAST_UPLOADED' },
|
postUploadAction,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[postUploadAction, uploadImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
@ -101,13 +108,12 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
isDragReject,
|
isDragReject,
|
||||||
isDragActive,
|
isDragActive,
|
||||||
inputRef,
|
inputRef,
|
||||||
open,
|
|
||||||
} = useDropzone({
|
} = useDropzone({
|
||||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||||
noClick: true,
|
noClick: true,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragOver: () => setIsHandlingUpload(true),
|
onDragOver: () => setIsHandlingUpload(true),
|
||||||
disabled: isUploaderDisabled,
|
disabled: isBusy,
|
||||||
multiple: false,
|
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
|
// Add the paste event listener
|
||||||
document.addEventListener('paste', handlePaste);
|
document.addEventListener('paste', handlePaste);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('paste', handlePaste);
|
document.removeEventListener('paste', handlePaste);
|
||||||
setOpenUploaderFunction(() => {
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}, [inputRef, open, setOpenUploaderFunction]);
|
}, [inputRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -150,13 +150,30 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{children}
|
{children}
|
||||||
|
<AnimatePresence>
|
||||||
{isDragActive && isHandlingUpload && (
|
{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
|
<ImageUploadOverlay
|
||||||
isDragAccept={isDragAccept}
|
isDragAccept={isDragAccept}
|
||||||
isDragReject={isDragReject}
|
isDragReject={isDragReject}
|
||||||
setIsHandlingUpload={setIsHandlingUpload}
|
setIsHandlingUpload={setIsHandlingUpload}
|
||||||
/>
|
/>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</Box>
|
</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 { useCallback } from 'react';
|
||||||
import { useDropzone } from 'react-dropzone';
|
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 = {
|
type UseImageUploadButtonArgs = {
|
||||||
postUploadAction?: PostUploadAction;
|
postUploadAction?: PostUploadAction;
|
||||||
@ -12,7 +12,7 @@ type UseImageUploadButtonArgs = {
|
|||||||
* Provides image uploader functionality to any component.
|
* Provides image uploader functionality to any component.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
* const { getUploadButtonProps, getUploadInputProps, openUploader } = useImageUploadButton({
|
||||||
* postUploadAction: {
|
* postUploadAction: {
|
||||||
* type: 'SET_CONTROLNET_IMAGE',
|
* type: 'SET_CONTROLNET_IMAGE',
|
||||||
* controlNetId: '12345',
|
* controlNetId: '12345',
|
||||||
@ -20,6 +20,9 @@ type UseImageUploadButtonArgs = {
|
|||||||
* isDisabled: getIsUploadDisabled(),
|
* isDisabled: getIsUploadDisabled(),
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
|
* // open the uploaded directly
|
||||||
|
* const handleSomething = () => { openUploader() }
|
||||||
|
*
|
||||||
* // in the render function
|
* // in the render function
|
||||||
* <Button {...getUploadButtonProps()} /> // will open the file dialog on click
|
* <Button {...getUploadButtonProps()} /> // will open the file dialog on click
|
||||||
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
|
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
|
||||||
@ -28,24 +31,23 @@ export const useImageUploadButton = ({
|
|||||||
postUploadAction,
|
postUploadAction,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: UseImageUploadButtonArgs) => {
|
}: UseImageUploadButtonArgs) => {
|
||||||
const dispatch = useAppDispatch();
|
const [uploadImage] = useUploadImageMutation();
|
||||||
const onDropAccepted = useCallback(
|
const onDropAccepted = useCallback(
|
||||||
(files: File[]) => {
|
(files: File[]) => {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
uploadImage({
|
||||||
imageUploaded({
|
|
||||||
file,
|
file,
|
||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction,
|
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
||||||
})
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, postUploadAction]
|
[postUploadAction, uploadImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
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,
|
FaSave,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { stagingAreaImageSaved } from '../store/actions';
|
import { stagingAreaImageSaved } from '../store/actions';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector],
|
[canvasSelector],
|
||||||
@ -123,6 +125,10 @@ const IAICanvasStagingAreaToolbar = () => {
|
|||||||
[dispatch, sessionId]
|
[dispatch, sessionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: imageDTO } = useGetImageDTOQuery(
|
||||||
|
currentStagingAreaImage?.imageName ?? skipToken
|
||||||
|
);
|
||||||
|
|
||||||
if (!currentStagingAreaImage) return null;
|
if (!currentStagingAreaImage) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -173,14 +179,19 @@ const IAICanvasStagingAreaToolbar = () => {
|
|||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||||
|
isDisabled={!imageDTO || !imageDTO.is_intermediate}
|
||||||
icon={<FaSave />}
|
icon={<FaSave />}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
stagingAreaImageSaved({
|
stagingAreaImageSaved({
|
||||||
imageName: currentStagingAreaImage.imageName,
|
imageDTO,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -2,7 +2,6 @@ import { Box, ButtonGroup, Flex } from '@chakra-ui/react';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
|
||||||
import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick';
|
import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick';
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
@ -25,6 +24,7 @@ import { systemSelector } from 'features/system/store/systemSelectors';
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import {
|
import {
|
||||||
canvasCopiedToClipboard,
|
canvasCopiedToClipboard,
|
||||||
canvasDownloadedAsImage,
|
canvasDownloadedAsImage,
|
||||||
@ -82,7 +82,9 @@ const IAICanvasToolbar = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||||
|
|
||||||
const { openUploader } = useImageUploader();
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
|
postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' },
|
||||||
|
});
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['v'],
|
['v'],
|
||||||
@ -288,9 +290,10 @@ const IAICanvasToolbar = () => {
|
|||||||
aria-label={`${t('common.upload')}`}
|
aria-label={`${t('common.upload')}`}
|
||||||
tooltip={`${t('common.upload')}`}
|
tooltip={`${t('common.upload')}`}
|
||||||
icon={<FaUpload />}
|
icon={<FaUpload />}
|
||||||
onClick={openUploader}
|
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
|
<input {...getUploadInputProps()} />
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||||
|
|
||||||
@ -12,6 +13,6 @@ export const canvasDownloadedAsImage = createAction(
|
|||||||
|
|
||||||
export const canvasMerged = createAction('canvas/canvasMerged');
|
export const canvasMerged = createAction('canvas/canvasMerged');
|
||||||
|
|
||||||
export const stagingAreaImageSaved = createAction<{ imageName: string }>(
|
export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>(
|
||||||
'canvas/stagingAreaImageSaved'
|
'canvas/stagingAreaImageSaved'
|
||||||
);
|
);
|
||||||
|
@ -11,8 +11,8 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
|
||||||
import { controlNetImageChanged } from '../store/controlNetSlice';
|
import { controlNetImageChanged } from '../store/controlNetSlice';
|
||||||
|
import { PostUploadAction } from 'services/api/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlNetId: string;
|
controlNetId: string;
|
||||||
|
@ -2,7 +2,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { ControlNetModelParam } from 'features/parameters/types/parameterSchemas';
|
import { ControlNetModelParam } from 'features/parameters/types/parameterSchemas';
|
||||||
import { cloneDeep, forEach } from 'lodash-es';
|
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 { isAnySessionRejected } from 'services/api/thunks/session';
|
||||||
import { appSocketInvocationError } from 'services/events/actions';
|
import { appSocketInvocationError } from 'services/events/actions';
|
||||||
import { controlNetImageProcessed } from './actions';
|
import { controlNetImageProcessed } from './actions';
|
||||||
@ -300,10 +300,20 @@ export const controlNetSlice = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
builder.addCase(appSocketInvocationError, (state, action) => {
|
||||||
|
state.pendingControlImages = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addMatcher(isAnySessionRejected, (state, action) => {
|
||||||
|
state.pendingControlImages = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addMatcher(
|
||||||
|
imagesApi.endpoints.deleteImage.matchFulfilled,
|
||||||
|
(state, action) => {
|
||||||
// Preemptively remove the image from all controlnets
|
// Preemptively remove the image from all controlnets
|
||||||
// TODO: doesn't the imageusage stuff do this for us?
|
// TODO: doesn't the imageusage stuff do this for us?
|
||||||
const { image_name } = action.meta.arg;
|
const { image_name } = action.meta.arg.originalArgs;
|
||||||
forEach(state.controlNets, (c) => {
|
forEach(state.controlNets, (c) => {
|
||||||
if (c.controlImage === image_name) {
|
if (c.controlImage === image_name) {
|
||||||
c.controlImage = null;
|
c.controlImage = null;
|
||||||
@ -313,15 +323,8 @@ export const controlNetSlice = createSlice({
|
|||||||
c.processedControlImage = null;
|
c.processedControlImage = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
builder.addCase(appSocketInvocationError, (state, action) => {
|
|
||||||
state.pendingControlImages = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addMatcher(isAnySessionRejected, (state, action) => {
|
|
||||||
state.pendingControlImages = [];
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
IMAGE_CATEGORIES,
|
||||||
|
INITIAL_IMAGE_LIMIT,
|
||||||
|
boardIdSelected,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { FaImages } from 'react-icons/fa';
|
import { FaImages } from 'react-icons/fa';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import {
|
||||||
|
ListImagesArgs,
|
||||||
|
useListImagesQuery,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
import GenericBoard from './GenericBoard';
|
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 AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const handleAllImagesBoardClick = () => {
|
const handleClick = () => {
|
||||||
dispatch(boardIdSelected('all'));
|
dispatch(boardIdSelected('images'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = {
|
const { total } = useListImagesQuery(baseQueryArg, {
|
||||||
id: 'all-images-board',
|
selectFromResult: ({ data }) => ({ total: data?.total ?? 0 }),
|
||||||
actionType: 'MOVE_BOARD',
|
});
|
||||||
context: { boardId: null },
|
|
||||||
};
|
// 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 (
|
return (
|
||||||
<GenericBoard
|
<GenericBoard
|
||||||
droppableData={droppableData}
|
onClick={handleClick}
|
||||||
onClick={handleAllImagesBoardClick}
|
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
icon={FaImages}
|
icon={FaImages}
|
||||||
label="All Images"
|
label="All Images"
|
||||||
|
badgeCount={total}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { CloseIcon } from '@chakra-ui/icons';
|
|
||||||
import {
|
import {
|
||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
GridItem,
|
GridItem,
|
||||||
IconButton,
|
useDisclosure,
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputRightElement,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
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 { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { memo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
|
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
||||||
import AddBoardButton from './AddBoardButton';
|
import AddBoardButton from './AddBoardButton';
|
||||||
|
import AllAssetsBoard from './AllAssetsBoard';
|
||||||
import AllImagesBoard from './AllImagesBoard';
|
import AllImagesBoard from './AllImagesBoard';
|
||||||
import BatchBoard from './BatchBoard';
|
import BatchBoard from './BatchBoard';
|
||||||
|
import BoardsSearch from './BoardsSearch';
|
||||||
import GalleryBoard from './GalleryBoard';
|
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(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -39,31 +39,19 @@ type Props = {
|
|||||||
|
|
||||||
const BoardsList = (props: Props) => {
|
const BoardsList = (props: Props) => {
|
||||||
const { isOpen } = props;
|
const { isOpen } = props;
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { selectedBoardId, searchText } = useAppSelector(selector);
|
const { selectedBoardId, searchText } = useAppSelector(selector);
|
||||||
|
|
||||||
const { data: boards } = useListAllBoardsQuery();
|
const { data: boards } = useListAllBoardsQuery();
|
||||||
|
|
||||||
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
|
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
|
||||||
|
|
||||||
const filteredBoards = searchText
|
const filteredBoards = searchText
|
||||||
? boards?.filter((board) =>
|
? boards?.filter((board) =>
|
||||||
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
||||||
)
|
)
|
||||||
: boards;
|
: boards;
|
||||||
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
const [searchMode, setSearchMode] = useState(false);
|
const [searchMode, setSearchMode] = useState(false);
|
||||||
|
|
||||||
const handleBoardSearch = (searchTerm: string) => {
|
|
||||||
setSearchMode(searchTerm.length > 0);
|
|
||||||
dispatch(setBoardSearchText(searchTerm));
|
|
||||||
};
|
|
||||||
const clearBoardSearch = () => {
|
|
||||||
setSearchMode(false);
|
|
||||||
dispatch(setBoardSearchText(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Collapse in={isOpen} animateOpacity>
|
<Collapse in={isOpen} animateOpacity>
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle={'first'}
|
layerStyle={'first'}
|
||||||
@ -76,26 +64,7 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
<InputGroup>
|
<BoardsSearch setSearchMode={setSearchMode} />
|
||||||
<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 />
|
<AddBoardButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
@ -121,7 +90,13 @@ const BoardsList = (props: Props) => {
|
|||||||
{!searchMode && (
|
{!searchMode && (
|
||||||
<>
|
<>
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
<AllImagesBoard isSelected={selectedBoardId === 'all'} />
|
<AllImagesBoard isSelected={selectedBoardId === 'images'} />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
|
<AllAssetsBoard isSelected={selectedBoardId === 'assets'} />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
|
<NoBoardBoard isSelected={selectedBoardId === 'no_board'} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
{isBatchEnabled && (
|
{isBatchEnabled && (
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
@ -136,6 +111,7 @@ const BoardsList = (props: Props) => {
|
|||||||
<GalleryBoard
|
<GalleryBoard
|
||||||
board={board}
|
board={board}
|
||||||
isSelected={selectedBoardId === board.board_id}
|
isSelected={selectedBoardId === board.board_id}
|
||||||
|
setBoardToDelete={setBoardToDelete}
|
||||||
/>
|
/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
@ -143,6 +119,11 @@ const BoardsList = (props: Props) => {
|
|||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Collapse>
|
</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,35 +8,32 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
|
Text,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
} from '@chakra-ui/react';
|
} 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 { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
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 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 { mode } from 'theme/util/mode';
|
||||||
import { DeleteBoardImagesContext } from '../../../../../app/contexts/DeleteBoardImagesContext';
|
|
||||||
|
|
||||||
interface GalleryBoardProps {
|
interface GalleryBoardProps {
|
||||||
board: BoardDTO;
|
board: BoardDTO;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
setBoardToDelete: (board?: BoardDTO) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
const GalleryBoard = memo(
|
||||||
|
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||||
@ -44,11 +41,7 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
const { board_name, board_id } = board;
|
const { board_name, board_id } = board;
|
||||||
|
|
||||||
const { onClickDeleteBoardImages } = useContext(DeleteBoardImagesContext);
|
|
||||||
|
|
||||||
const handleSelectBoard = useCallback(() => {
|
const handleSelectBoard = useCallback(() => {
|
||||||
dispatch(boardIdSelected(board_id));
|
dispatch(boardIdSelected(board_id));
|
||||||
}, [board_id, dispatch]);
|
}, [board_id, dispatch]);
|
||||||
@ -56,24 +49,13 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||||
useUpdateBoardMutation();
|
useUpdateBoardMutation();
|
||||||
|
|
||||||
const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
|
|
||||||
useDeleteBoardMutation();
|
|
||||||
|
|
||||||
const handleUpdateBoardName = (newBoardName: string) => {
|
const handleUpdateBoardName = (newBoardName: string) => {
|
||||||
updateBoard({ board_id, changes: { board_name: newBoardName } });
|
updateBoard({ board_id, changes: { board_name: newBoardName } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteBoard = useCallback(() => {
|
const handleDeleteBoard = useCallback(() => {
|
||||||
deleteBoard(board_id);
|
setBoardToDelete(board);
|
||||||
}, [board_id, deleteBoard]);
|
}, [board, setBoardToDelete]);
|
||||||
|
|
||||||
const handleAddBoardToBatch = useCallback(() => {
|
|
||||||
// dispatch(boardAddedToBatch({ board_id }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteBoardAndImages = useCallback(() => {
|
|
||||||
onClickDeleteBoardImages(board);
|
|
||||||
}, [board, onClickDeleteBoardImages]);
|
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = useMemo(
|
const droppableData: MoveBoardDropData = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -88,24 +70,24 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
|
menuButtonProps={{
|
||||||
|
bg: 'transparent',
|
||||||
|
_hover: { bg: 'transparent' },
|
||||||
|
}}
|
||||||
renderMenu={() => (
|
renderMenu={() => (
|
||||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
<MenuList
|
||||||
|
sx={{ visibility: 'visible !important' }}
|
||||||
|
motionProps={menuListMotionProps}
|
||||||
|
>
|
||||||
{board.image_count > 0 && (
|
{board.image_count > 0 && (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
{/* <MenuItem
|
||||||
isDisabled={!board.image_count}
|
isDisabled={!board.image_count}
|
||||||
icon={<FaImages />}
|
icon={<FaImages />}
|
||||||
onClickCapture={handleAddBoardToBatch}
|
onClickCapture={handleAddBoardToBatch}
|
||||||
>
|
>
|
||||||
Add Board to Batch
|
Add Board to Batch
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
<MenuItem
|
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
|
||||||
icon={<FaTrash />}
|
|
||||||
onClickCapture={handleDeleteBoardAndImages}
|
|
||||||
>
|
|
||||||
Delete Board and Images
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -147,17 +129,19 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{board.cover_image_name && coverImage?.image_url && (
|
{board.cover_image_name && coverImage?.thumbnail_url && (
|
||||||
<Image src={coverImage?.image_url} draggable={false} />
|
<Image src={coverImage?.thumbnail_url} draggable={false} />
|
||||||
)}
|
)}
|
||||||
{!(board.cover_image_name && coverImage?.image_url) && (
|
{!(board.cover_image_name && coverImage?.thumbnail_url) && (
|
||||||
<IAINoContentFallback
|
<IAINoContentFallback
|
||||||
boxSize={8}
|
boxSize={8}
|
||||||
icon={FaFolder}
|
icon={FaUser}
|
||||||
sx={{
|
sx={{
|
||||||
border: '2px solid var(--invokeai-colors-base-200)',
|
borderWidth: '2px',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: 'base.200',
|
||||||
_dark: {
|
_dark: {
|
||||||
border: '2px solid var(--invokeai-colors-base-800)',
|
borderColor: 'base.800',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -172,7 +156,10 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
>
|
>
|
||||||
<Badge variant="solid">{board.image_count}</Badge>
|
<Badge variant="solid">{board.image_count}</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<IAIDroppable data={droppableData} />
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
dropLabel={<Text fontSize="md">Move</Text>}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
@ -189,6 +176,7 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
onSubmit={(nextValue) => {
|
onSubmit={(nextValue) => {
|
||||||
handleUpdateBoardName(nextValue);
|
handleUpdateBoardName(nextValue);
|
||||||
}}
|
}}
|
||||||
|
sx={{ maxW: 'full' }}
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
sx={{
|
sx={{
|
||||||
@ -199,6 +187,8 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
fontSize: 'xs',
|
fontSize: 'xs',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
p: 0,
|
p: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
}}
|
}}
|
||||||
noOfLines={1}
|
noOfLines={1}
|
||||||
/>
|
/>
|
||||||
@ -218,7 +208,8 @@ const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
GalleryBoard.displayName = 'HoverableBoard';
|
GalleryBoard.displayName = 'HoverableBoard';
|
||||||
|
|
||||||
|
@ -2,18 +2,34 @@ import { As, Badge, Flex } from '@chakra-ui/react';
|
|||||||
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
|
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type GenericBoardProps = {
|
type GenericBoardProps = {
|
||||||
droppableData: TypesafeDroppableData;
|
droppableData?: TypesafeDroppableData;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
icon: As;
|
icon: As;
|
||||||
label: string;
|
label: string;
|
||||||
|
dropLabel?: ReactNode;
|
||||||
badgeCount?: number;
|
badgeCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatBadgeCount = (count: number) =>
|
||||||
|
Intl.NumberFormat('en-US', {
|
||||||
|
notation: 'compact',
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
}).format(count);
|
||||||
|
|
||||||
const GenericBoard = (props: GenericBoardProps) => {
|
const GenericBoard = (props: GenericBoardProps) => {
|
||||||
const { droppableData, onClick, isSelected, icon, label, badgeCount } = props;
|
const {
|
||||||
|
droppableData,
|
||||||
|
onClick,
|
||||||
|
isSelected,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
badgeCount,
|
||||||
|
dropLabel,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -59,10 +75,10 @@ const GenericBoard = (props: GenericBoardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{badgeCount !== undefined && (
|
{badgeCount !== undefined && (
|
||||||
<Badge variant="solid">{badgeCount}</Badge>
|
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<IAIDroppable data={droppableData} />
|
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
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 { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
|
||||||
import NextPrevImageButtons from '../NextPrevImageButtons';
|
import NextPrevImageButtons from '../NextPrevImageButtons';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { FaImage } from 'react-icons/fa';
|
||||||
|
|
||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
[stateSelector, selectLastSelectedImage],
|
[stateSelector, selectLastSelectedImage],
|
||||||
@ -168,7 +170,11 @@ const CurrentImagePreview = () => {
|
|||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
fitContainer
|
fitContainer
|
||||||
|
useThumbailFallback
|
||||||
dropLabel="Set as Current Image"
|
dropLabel="Set as Current Image"
|
||||||
|
noContentFallback={
|
||||||
|
<IAINoContentFallback icon={FaImage} label="No image selected" />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shouldShowImageDetails && imageDTO && (
|
{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 { 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 { 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 { ImageDTO } from 'services/api/types';
|
||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
|
||||||
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -16,23 +11,23 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||||
const selector = useMemo(
|
// const selector = useMemo(
|
||||||
() =>
|
// () =>
|
||||||
createSelector(
|
// createSelector(
|
||||||
[stateSelector],
|
// [stateSelector],
|
||||||
({ gallery }) => {
|
// ({ gallery }) => {
|
||||||
const selectionCount = gallery.selection.length;
|
// const selectionCount = gallery.selection.length;
|
||||||
|
|
||||||
return { selectionCount };
|
// return { selectionCount };
|
||||||
},
|
// },
|
||||||
defaultSelectorOptions
|
// defaultSelectorOptions
|
||||||
),
|
// ),
|
||||||
[]
|
// []
|
||||||
);
|
// );
|
||||||
|
|
||||||
const { selectionCount } = useAppSelector(selector);
|
// const { selectionCount } = useAppSelector(selector);
|
||||||
|
|
||||||
const handleContextMenu = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -48,13 +43,9 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
<MenuList
|
<MenuList
|
||||||
sx={{ visibility: 'visible !important' }}
|
sx={{ visibility: 'visible !important' }}
|
||||||
motionProps={menuListMotionProps}
|
motionProps={menuListMotionProps}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={skipEvent}
|
||||||
>
|
>
|
||||||
{selectionCount === 1 ? (
|
|
||||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||||
) : (
|
|
||||||
<MultipleSelectionMenuItems />
|
|
||||||
)}
|
|
||||||
</MenuList>
|
</MenuList>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,10 @@ import {
|
|||||||
FaShare,
|
FaShare,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
import {
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
useGetImageMetadataQuery,
|
||||||
|
useRemoveImageFromBoardMutation,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
@ -128,15 +130,8 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
if (!imageDTO.board_id) {
|
if (!imageDTO.board_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeFromBoard({
|
removeFromBoard({ imageDTO });
|
||||||
board_id: imageDTO.board_id,
|
}, [imageDTO, removeFromBoard]);
|
||||||
image_name: imageDTO.image_name,
|
|
||||||
});
|
|
||||||
}, [imageDTO.board_id, imageDTO.image_name, removeFromBoard]);
|
|
||||||
|
|
||||||
const handleOpenInNewTab = useCallback(() => {
|
|
||||||
window.open(imageDTO.image_url, '_blank');
|
|
||||||
}, [imageDTO.image_url]);
|
|
||||||
|
|
||||||
const handleAddToBatch = useCallback(() => {
|
const handleAddToBatch = useCallback(() => {
|
||||||
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
||||||
@ -149,10 +144,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link href={imageDTO.image_url} target="_blank">
|
<Link href={imageDTO.image_url} target="_blank">
|
||||||
<MenuItem
|
<MenuItem icon={<FaExternalLinkAlt />}>
|
||||||
icon={<FaExternalLinkAlt />}
|
|
||||||
onClickCapture={handleOpenInNewTab}
|
|
||||||
>
|
|
||||||
{t('common.openInNewTab')}
|
{t('common.openInNewTab')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
@ -161,6 +153,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
{t('parameters.copyImage')}
|
{t('parameters.copyImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<Link download={true} href={imageDTO.image_url} target="_blank">
|
||||||
|
<MenuItem icon={<FaDownload />} w="100%">
|
||||||
|
{t('parameters.downloadImage')}
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<FaQuoteRight />}
|
icon={<FaQuoteRight />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
@ -219,11 +216,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
Remove from Board
|
Remove from Board
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<Link download={true} href={imageDTO.image_url} target="_blank">
|
|
||||||
<MenuItem icon={<FaDownload />} w="100%">
|
|
||||||
{t('parameters.downloadImage')}
|
|
||||||
</MenuItem>
|
|
||||||
</Link>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
|
@ -1,113 +1,34 @@
|
|||||||
import {
|
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react';
|
||||||
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 { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { memo, useRef } from 'react';
|
||||||
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
|
import GalleryPinButton from './GalleryPinButton';
|
||||||
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const {
|
const { selectedBoardId } = state.gallery;
|
||||||
selectedBoardId,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
galleryView,
|
|
||||||
shouldAutoSwitch,
|
|
||||||
} = state.gallery;
|
|
||||||
const { shouldPinGallery } = state.ui;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedBoardId,
|
selectedBoardId,
|
||||||
shouldPinGallery,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
shouldAutoSwitch,
|
|
||||||
galleryView,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
const galleryGridRef = useRef<HTMLDivElement>(null);
|
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { selectedBoardId } = useAppSelector(selector);
|
||||||
const { colorMode } = useColorMode();
|
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
||||||
|
useDisclosure();
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
@ -127,95 +48,12 @@ const ImageGalleryContent = () => {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonGroup isAttached>
|
<GallerySettingsPopover />
|
||||||
<IAIIconButton
|
<GalleryBoardName
|
||||||
tooltip={t('gallery.images')}
|
isOpen={isBoardListOpen}
|
||||||
aria-label={t('gallery.images')}
|
onToggle={onToggleBoardList}
|
||||||
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 />}
|
|
||||||
/>
|
/>
|
||||||
|
<GalleryPinButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<BoardsList isOpen={isBoardListOpen} />
|
<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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
||||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
import {
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
imageRangeEndSelected,
|
|
||||||
imageSelected,
|
|
||||||
imageSelectionToggled,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
||||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
@ -84,7 +81,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
}, [imageDTO, selection, selectionCount]);
|
}, [imageDTO, selection, selectionCount]);
|
||||||
|
|
||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return <Spinner />;
|
return <IAIFillSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,58 +1,26 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, Spinner } from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
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 { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaImage } from 'react-icons/fa';
|
import { FaExclamationCircle, 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 { VirtuosoGrid } from 'react-virtuoso';
|
import { VirtuosoGrid } from 'react-virtuoso';
|
||||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
import {
|
||||||
import { useListBoardImagesQuery } from '../../../../services/api/endpoints/boardImages';
|
useLazyListImagesQuery,
|
||||||
|
useListImagesQuery,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
|
import GalleryImage from './GalleryImage';
|
||||||
import ImageGridItemContainer from './ImageGridItemContainer';
|
import ImageGridItemContainer from './ImageGridItemContainer';
|
||||||
import ImageGridListContainer from './ImageGridListContainer';
|
import ImageGridListContainer from './ImageGridListContainer';
|
||||||
|
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
|
|
||||||
const selector = createSelector(
|
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
|
||||||
[stateSelector, selectFilteredImages],
|
|
||||||
(state, filteredImages) => {
|
|
||||||
const {
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
selectedBoardId,
|
|
||||||
galleryView,
|
|
||||||
total,
|
|
||||||
isLoading,
|
|
||||||
} = state.gallery;
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageNames: filteredImages.map((i) => i.image_name),
|
|
||||||
total,
|
|
||||||
selectedBoardId,
|
|
||||||
galleryView,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const GalleryImageGrid = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
const emptyGalleryRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
|
||||||
defer: true,
|
defer: true,
|
||||||
options: {
|
options: {
|
||||||
scrollbars: {
|
scrollbars: {
|
||||||
@ -63,62 +31,40 @@ const GalleryImageGrid = () => {
|
|||||||
},
|
},
|
||||||
overflow: { x: 'hidden' },
|
overflow: { x: 'hidden' },
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const [didInitialFetch, setDidInitialFetch] = useState(false);
|
const GalleryImageGrid = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||||
const {
|
const [initialize, osInstance] = useOverlayScrollbars(
|
||||||
galleryImageMinimumWidth,
|
overlayScrollbarsConfig
|
||||||
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(() => {
|
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||||
if (selectedBoardId === 'all') {
|
|
||||||
return imageNamesAll; // already sorted by images/uploads in gallery selector
|
const { currentData, isFetching, isSuccess, isError } =
|
||||||
} else {
|
useListImagesQuery(queryArgs);
|
||||||
const categories =
|
|
||||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
const [listImages] = useLazyListImagesQuery();
|
||||||
const imageList = (imagesForBoard?.items || []).filter((img) =>
|
|
||||||
categories.includes(img.image_category)
|
|
||||||
);
|
|
||||||
return imageList.map((img) => img.image_name);
|
|
||||||
}
|
|
||||||
}, [selectedBoardId, galleryView, imagesForBoard, imageNamesAll]);
|
|
||||||
|
|
||||||
const areMoreAvailable = useMemo(() => {
|
const areMoreAvailable = useMemo(() => {
|
||||||
return selectedBoardId === 'all' ? totalAll > imageNamesAll.length : false;
|
if (!currentData) {
|
||||||
}, [selectedBoardId, imageNamesAll.length, totalAll]);
|
return false;
|
||||||
|
}
|
||||||
const isLoading = useMemo(() => {
|
return currentData.ids.length < currentData.total;
|
||||||
return selectedBoardId === 'all' ? isLoadingAll : isLoadingImagesForBoard;
|
}, [currentData]);
|
||||||
}, [selectedBoardId, isLoadingAll, isLoadingImagesForBoard]);
|
|
||||||
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
dispatch(
|
listImages({
|
||||||
receivedPageOfImages({
|
...queryArgs,
|
||||||
categories:
|
offset: currentData?.ids.length ?? 0,
|
||||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
|
|
||||||
is_intermediate: false,
|
|
||||||
offset: imageNames.length,
|
|
||||||
limit: IMAGE_LIMIT,
|
limit: IMAGE_LIMIT,
|
||||||
})
|
});
|
||||||
);
|
}, [listImages, queryArgs, currentData?.ids.length]);
|
||||||
}, [dispatch, imageNames.length, galleryView]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set up gallery scroler
|
// Initialize the gallery's custom scrollbar
|
||||||
const { current: root } = rootRef;
|
const { current: root } = rootRef;
|
||||||
if (scroller && root) {
|
if (scroller && root) {
|
||||||
initialize({
|
initialize({
|
||||||
@ -131,47 +77,17 @@ const GalleryImageGrid = () => {
|
|||||||
return () => osInstance()?.destroy();
|
return () => osInstance()?.destroy();
|
||||||
}, [scroller, initialize, osInstance]);
|
}, [scroller, initialize, osInstance]);
|
||||||
|
|
||||||
const handleEndReached = useMemo(() => {
|
if (!currentData) {
|
||||||
if (areMoreAvailable) {
|
|
||||||
return handleLoadMoreImages;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [areMoreAvailable, handleLoadMoreImages]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!didInitialFetch) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// // rough, conservative calculation of how many images fit in the gallery
|
|
||||||
// // TODO: this gets an incorrect value on first load...
|
|
||||||
// const galleryHeight = rootRef.current?.clientHeight ?? 0;
|
|
||||||
// const galleryWidth = rootRef.current?.clientHeight ?? 0;
|
|
||||||
|
|
||||||
// const rows = galleryHeight / galleryImageMinimumWidth;
|
|
||||||
// const columns = galleryWidth / galleryImageMinimumWidth;
|
|
||||||
|
|
||||||
// const imagesToLoad = Math.ceil(rows * columns);
|
|
||||||
|
|
||||||
// setDidInitialFetch(true);
|
|
||||||
|
|
||||||
// // load up that many images
|
|
||||||
// dispatch(
|
|
||||||
// receivedPageOfImages({
|
|
||||||
// offset: 0,
|
|
||||||
// limit: 10,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }, [
|
|
||||||
// didInitialFetch,
|
|
||||||
// dispatch,
|
|
||||||
// galleryImageMinimumWidth,
|
|
||||||
// galleryView,
|
|
||||||
// selectedBoardId,
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
if (!isLoading && imageNames.length === 0) {
|
|
||||||
return (
|
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
|
<IAINoContentFallback
|
||||||
label={t('gallery.noImagesInGallery')}
|
label={t('gallery.noImagesInGallery')}
|
||||||
icon={FaImage}
|
icon={FaImage}
|
||||||
@ -180,27 +96,28 @@ const GalleryImageGrid = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status !== 'rejected') {
|
if (isSuccess && currentData) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||||
<VirtuosoGrid
|
<VirtuosoGrid
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
data={imageNames}
|
data={currentData.ids}
|
||||||
|
endReached={handleLoadMoreImages}
|
||||||
components={{
|
components={{
|
||||||
Item: ImageGridItemContainer,
|
Item: ImageGridItemContainer,
|
||||||
List: ImageGridListContainer,
|
List: ImageGridListContainer,
|
||||||
}}
|
}}
|
||||||
scrollerRef={setScroller}
|
scrollerRef={setScroller}
|
||||||
itemContent={(index, imageName) => (
|
itemContent={(index, imageName) => (
|
||||||
<GalleryImage key={imageName} imageName={imageName} />
|
<GalleryImage key={imageName} imageName={imageName as string} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
onClick={handleLoadMoreImages}
|
onClick={handleLoadMoreImages}
|
||||||
isDisabled={!areMoreAvailable}
|
isDisabled={!areMoreAvailable}
|
||||||
isLoading={status === 'pending'}
|
isLoading={isFetching}
|
||||||
loadingText="Loading"
|
loadingText="Loading"
|
||||||
flexShrink={0}
|
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);
|
export default memo(GalleryImageGrid);
|
||||||
|
@ -11,11 +11,9 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
const { metadata } = props;
|
const { metadata } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
recallBothPrompts,
|
|
||||||
recallPositivePrompt,
|
recallPositivePrompt,
|
||||||
recallNegativePrompt,
|
recallNegativePrompt,
|
||||||
recallSeed,
|
recallSeed,
|
||||||
recallInitialImage,
|
|
||||||
recallCfgScale,
|
recallCfgScale,
|
||||||
recallModel,
|
recallModel,
|
||||||
recallScheduler,
|
recallScheduler,
|
||||||
@ -23,7 +21,6 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
recallWidth,
|
recallWidth,
|
||||||
recallHeight,
|
recallHeight,
|
||||||
recallStrength,
|
recallStrength,
|
||||||
recallAllParameters,
|
|
||||||
} = useRecallParameters();
|
} = useRecallParameters();
|
||||||
|
|
||||||
const handleRecallPositivePrompt = useCallback(() => {
|
const handleRecallPositivePrompt = useCallback(() => {
|
||||||
|
@ -2,61 +2,76 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
|
IMAGE_LIMIT,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
selectImagesById,
|
selectImagesById,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
import {
|
||||||
import { selectFilteredImages } from '../store/gallerySelectors';
|
ListImagesArgs,
|
||||||
|
imagesAdapter,
|
||||||
|
imagesApi,
|
||||||
|
useLazyListImagesQuery,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
|
import { selectListImagesBaseQueryArgs } from '../store/gallerySelectors';
|
||||||
|
|
||||||
export const nextPrevImageButtonsSelector = createSelector(
|
export const nextPrevImageButtonsSelector = createSelector(
|
||||||
[stateSelector, selectFilteredImages],
|
[stateSelector, selectListImagesBaseQueryArgs],
|
||||||
(state, filteredImages) => {
|
(state, baseQueryArgs) => {
|
||||||
const { total, isFetching } = state.gallery;
|
const { data, status } =
|
||||||
|
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||||
|
|
||||||
const lastSelectedImage =
|
const lastSelectedImage =
|
||||||
state.gallery.selection[state.gallery.selection.length - 1];
|
state.gallery.selection[state.gallery.selection.length - 1];
|
||||||
|
|
||||||
if (!lastSelectedImage || filteredImages.length === 0) {
|
const isFetching = status === 'pending';
|
||||||
|
|
||||||
|
if (!data || !lastSelectedImage || data.total === 0) {
|
||||||
return {
|
return {
|
||||||
|
isFetching,
|
||||||
|
queryArgs: baseQueryArgs,
|
||||||
isOnFirstImage: true,
|
isOnFirstImage: true,
|
||||||
isOnLastImage: 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
|
(i) => i.image_name === lastSelectedImage
|
||||||
);
|
);
|
||||||
const nextImageIndex = clamp(
|
const nextImageIndex = clamp(currentImageIndex + 1, 0, images.length - 1);
|
||||||
currentImageIndex + 1,
|
|
||||||
0,
|
|
||||||
filteredImages.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const prevImageIndex = clamp(
|
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||||
currentImageIndex - 1,
|
|
||||||
0,
|
|
||||||
filteredImages.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextImageId = filteredImages[nextImageIndex].image_name;
|
const nextImageId = images[nextImageIndex].image_name;
|
||||||
const prevImageId = filteredImages[prevImageIndex].image_name;
|
const prevImageId = images[prevImageIndex].image_name;
|
||||||
|
|
||||||
const nextImage = selectImagesById(state, nextImageId);
|
const nextImage = selectors.selectById(data, nextImageId);
|
||||||
const prevImage = selectImagesById(state, prevImageId);
|
const prevImage = selectors.selectById(data, prevImageId);
|
||||||
|
|
||||||
const imagesLength = filteredImages.length;
|
const imagesLength = images.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
isOnLastImage:
|
isOnLastImage:
|
||||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||||
areMoreImagesAvailable: total > imagesLength,
|
areMoreImagesAvailable: data?.total ?? 0 > imagesLength,
|
||||||
isFetching,
|
isFetching: status === 'pending',
|
||||||
nextImage,
|
nextImage,
|
||||||
prevImage,
|
prevImage,
|
||||||
nextImageId,
|
nextImageId,
|
||||||
prevImageId,
|
prevImageId,
|
||||||
|
queryArgs,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -76,6 +91,7 @@ export const useNextPrevImage = () => {
|
|||||||
prevImageId,
|
prevImageId,
|
||||||
areMoreImagesAvailable,
|
areMoreImagesAvailable,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
queryArgs,
|
||||||
} = useAppSelector(nextPrevImageButtonsSelector);
|
} = useAppSelector(nextPrevImageButtonsSelector);
|
||||||
|
|
||||||
const handlePrevImage = useCallback(() => {
|
const handlePrevImage = useCallback(() => {
|
||||||
@ -86,13 +102,11 @@ export const useNextPrevImage = () => {
|
|||||||
nextImageId && dispatch(imageSelected(nextImageId));
|
nextImageId && dispatch(imageSelected(nextImageId));
|
||||||
}, [dispatch, nextImageId]);
|
}, [dispatch, nextImageId]);
|
||||||
|
|
||||||
|
const [listImages] = useLazyListImagesQuery();
|
||||||
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
dispatch(
|
listImages(queryArgs);
|
||||||
receivedPageOfImages({
|
}, [listImages, queryArgs]);
|
||||||
is_intermediate: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handlePrevImage,
|
handlePrevImage,
|
||||||
|
@ -1,136 +1,38 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { clamp, keyBy } from 'lodash-es';
|
import { ListImagesArgs } from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { INITIAL_IMAGE_LIMIT } from './gallerySlice';
|
||||||
import {
|
import {
|
||||||
ASSETS_CATEGORIES,
|
getBoardIdQueryParamForBoard,
|
||||||
BoardId,
|
getCategoriesQueryParamForBoard,
|
||||||
IMAGE_CATEGORIES,
|
} from './util';
|
||||||
imagesAdapter,
|
|
||||||
initialGalleryState,
|
|
||||||
} from './gallerySlice';
|
|
||||||
|
|
||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
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(
|
export const selectLastSelectedImage = createSelector(
|
||||||
(state: RootState) => state,
|
(state: RootState) => state,
|
||||||
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectSelectedImages = createSelector(
|
export const selectListImagesBaseQueryArgs = createSelector(
|
||||||
(state: RootState) => state,
|
[(state: RootState) => state],
|
||||||
(state) =>
|
(state) => {
|
||||||
imagesAdapter
|
const { selectedBoardId } = state.gallery;
|
||||||
.getSelectors()
|
|
||||||
.selectAll(state.gallery)
|
|
||||||
.filter((i) => state.gallery.selection.includes(i.image_name)),
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectNextImageToSelectLocal = createSelector(
|
const categories = getCategoriesQueryParamForBoard(selectedBoardId);
|
||||||
[
|
const board_id = getBoardIdQueryParamForBoard(selectedBoardId);
|
||||||
(state: typeof initialGalleryState) => state,
|
|
||||||
(state: typeof initialGalleryState, image_name: string) => image_name,
|
|
||||||
],
|
|
||||||
(state, image_name) => {
|
|
||||||
const filteredImages = selectFilteredImagesLocal(state);
|
|
||||||
const ids = filteredImages.map((i) => i.image_name);
|
|
||||||
|
|
||||||
const deletedImageIndex = ids.findIndex(
|
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||||
(result) => result.toString() === image_name
|
categories,
|
||||||
);
|
board_id,
|
||||||
|
offset: 0,
|
||||||
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
|
is_intermediate: false,
|
||||||
|
};
|
||||||
|
|
||||||
const filteredIds = ids.filter((id) => id.toString() !== image_name);
|
return listImagesBaseQueryArgs;
|
||||||
|
|
||||||
const newSelectedImageIndex = clamp(
|
|
||||||
deletedImageIndex,
|
|
||||||
0,
|
|
||||||
filteredIds.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
|
||||||
|
|
||||||
return newSelectedImageId;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectNextImageToSelect = createSelector(
|
|
||||||
[
|
|
||||||
(state: RootState) => state,
|
|
||||||
(state: RootState, image_name: string) => image_name,
|
|
||||||
],
|
|
||||||
(state, image_name) => {
|
|
||||||
return selectNextImageToSelectLocal(state.gallery, image_name);
|
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
@ -1,20 +1,8 @@
|
|||||||
import type { PayloadAction, Update } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { dateComparator } from 'common/util/dateComparator';
|
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import {
|
import { ImageCategory } from 'services/api/types';
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
||||||
export const ASSETS_CATEGORIES: ImageCategory[] = [
|
export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||||
@ -26,113 +14,74 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
|||||||
export const INITIAL_IMAGE_LIMIT = 100;
|
export const INITIAL_IMAGE_LIMIT = 100;
|
||||||
export const IMAGE_LIMIT = 20;
|
export const IMAGE_LIMIT = 20;
|
||||||
|
|
||||||
export type GalleryView = 'images' | 'assets';
|
// export type GalleryView = 'images' | 'assets';
|
||||||
export type BoardId =
|
export type BoardId =
|
||||||
| 'all'
|
| 'images'
|
||||||
| 'none'
|
| 'assets'
|
||||||
|
| 'no_board'
|
||||||
| 'batch'
|
| 'batch'
|
||||||
| (string & Record<never, never>);
|
| (string & Record<never, never>);
|
||||||
|
|
||||||
type AdditionaGalleryState = {
|
type GalleryState = {
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
total: number;
|
|
||||||
isLoading: boolean;
|
|
||||||
isFetching: boolean;
|
|
||||||
selection: string[];
|
selection: string[];
|
||||||
shouldAutoSwitch: boolean;
|
shouldAutoSwitch: boolean;
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
galleryView: GalleryView;
|
|
||||||
selectedBoardId: BoardId;
|
selectedBoardId: BoardId;
|
||||||
isInitialized: boolean;
|
|
||||||
batchImageNames: string[];
|
batchImageNames: string[];
|
||||||
isBatchEnabled: boolean;
|
isBatchEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialGalleryState =
|
export const initialGalleryState: GalleryState = {
|
||||||
imagesAdapter.getInitialState<AdditionaGalleryState>({
|
|
||||||
offset: 0,
|
|
||||||
limit: 0,
|
|
||||||
total: 0,
|
|
||||||
isLoading: true,
|
|
||||||
isFetching: true,
|
|
||||||
selection: [],
|
selection: [],
|
||||||
shouldAutoSwitch: true,
|
shouldAutoSwitch: true,
|
||||||
galleryImageMinimumWidth: 96,
|
galleryImageMinimumWidth: 96,
|
||||||
galleryView: 'images',
|
selectedBoardId: 'images',
|
||||||
selectedBoardId: 'all',
|
|
||||||
isInitialized: false,
|
|
||||||
batchImageNames: [],
|
batchImageNames: [],
|
||||||
isBatchEnabled: false,
|
isBatchEnabled: false,
|
||||||
});
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
reducers: {
|
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[]>) => {
|
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
||||||
imagesAdapter.removeMany(state, action.payload);
|
// TODO: port all instances of this to use RTK Query cache
|
||||||
state.batchImageNames = state.batchImageNames.filter(
|
// imagesAdapter.removeMany(state, action.payload);
|
||||||
(name) => !action.payload.includes(name)
|
// state.batchImageNames = state.batchImageNames.filter(
|
||||||
);
|
// (name) => !action.payload.includes(name)
|
||||||
|
// );
|
||||||
},
|
},
|
||||||
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
||||||
const rangeEndImageName = action.payload;
|
// const rangeEndImageName = action.payload;
|
||||||
const lastSelectedImage = state.selection[state.selection.length - 1];
|
// const lastSelectedImage = state.selection[state.selection.length - 1];
|
||||||
|
// const filteredImages = selectFilteredImagesLocal(state);
|
||||||
const filteredImages = selectFilteredImagesLocal(state);
|
// const lastClickedIndex = filteredImages.findIndex(
|
||||||
|
// (n) => n.image_name === lastSelectedImage
|
||||||
const lastClickedIndex = filteredImages.findIndex(
|
// );
|
||||||
(n) => n.image_name === lastSelectedImage
|
// const currentClickedIndex = filteredImages.findIndex(
|
||||||
);
|
// (n) => n.image_name === rangeEndImageName
|
||||||
|
// );
|
||||||
const currentClickedIndex = filteredImages.findIndex(
|
// if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||||
(n) => n.image_name === rangeEndImageName
|
// // We have a valid range!
|
||||||
);
|
// const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||||
|
// const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
// const imagesToSelect = filteredImages
|
||||||
// We have a valid range!
|
// .slice(start, end + 1)
|
||||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
// .map((i) => i.image_name);
|
||||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
// state.selection = uniq(state.selection.concat(imagesToSelect));
|
||||||
|
// }
|
||||||
const imagesToSelect = filteredImages
|
|
||||||
.slice(start, end + 1)
|
|
||||||
.map((i) => i.image_name);
|
|
||||||
|
|
||||||
state.selection = uniq(state.selection.concat(imagesToSelect));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
||||||
if (
|
// if (
|
||||||
state.selection.includes(action.payload) &&
|
// state.selection.includes(action.payload) &&
|
||||||
state.selection.length > 1
|
// state.selection.length > 1
|
||||||
) {
|
// ) {
|
||||||
state.selection = state.selection.filter(
|
// state.selection = state.selection.filter(
|
||||||
(imageName) => imageName !== action.payload
|
// (imageName) => imageName !== action.payload
|
||||||
);
|
// );
|
||||||
} else {
|
// } else {
|
||||||
state.selection = uniq(state.selection.concat(action.payload));
|
// state.selection = uniq(state.selection.concat(action.payload));
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
imageSelected: (state, action: PayloadAction<string | null>) => {
|
imageSelected: (state, action: PayloadAction<string | null>) => {
|
||||||
state.selection = action.payload ? [action.payload] : [];
|
state.selection = action.payload ? [action.payload] : [];
|
||||||
@ -143,15 +92,9 @@ export const gallerySlice = createSlice({
|
|||||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||||
state.galleryImageMinimumWidth = action.payload;
|
state.galleryImageMinimumWidth = action.payload;
|
||||||
},
|
},
|
||||||
setGalleryView: (state, action: PayloadAction<GalleryView>) => {
|
|
||||||
state.galleryView = action.payload;
|
|
||||||
},
|
|
||||||
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
boardIdSelected: (state, action: PayloadAction<BoardId>) => {
|
||||||
state.selectedBoardId = action.payload;
|
state.selectedBoardId = action.payload;
|
||||||
},
|
},
|
||||||
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.isLoading = action.payload;
|
|
||||||
},
|
|
||||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isBatchEnabled = action.payload;
|
state.isBatchEnabled = action.payload;
|
||||||
},
|
},
|
||||||
@ -182,47 +125,11 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
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(
|
builder.addMatcher(
|
||||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||||
(state, action) => {
|
(state, action) => {
|
||||||
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
||||||
state.selectedBoardId = 'all';
|
state.selectedBoardId = 'images';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -230,26 +137,13 @@ export const gallerySlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
selectAll: selectImagesAll,
|
|
||||||
selectById: selectImagesById,
|
|
||||||
selectEntities: selectImagesEntities,
|
|
||||||
selectIds: selectImagesIds,
|
|
||||||
selectTotal: selectImagesTotal,
|
|
||||||
} = imagesAdapter.getSelectors<RootState>((state) => state.gallery);
|
|
||||||
|
|
||||||
export const {
|
|
||||||
imageUpserted,
|
|
||||||
imageUpdatedOne,
|
|
||||||
imageRemoved,
|
|
||||||
imagesRemoved,
|
imagesRemoved,
|
||||||
imageRangeEndSelected,
|
imageRangeEndSelected,
|
||||||
imageSelectionToggled,
|
imageSelectionToggled,
|
||||||
imageSelected,
|
imageSelected,
|
||||||
shouldAutoSwitchChanged,
|
shouldAutoSwitchChanged,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
setGalleryView,
|
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
isLoadingChanged,
|
|
||||||
isBatchEnabledChanged,
|
isBatchEnabledChanged,
|
||||||
imagesAddedToBatch,
|
imagesAddedToBatch,
|
||||||
imagesRemovedFromBatch,
|
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 { memo } from 'react';
|
||||||
import { ImageUsage } from '../store/imageDeletionSlice';
|
import { ImageUsage } from '../store/imageDeletionSlice';
|
||||||
import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||||
|
type Props = {
|
||||||
const ImageUsageMessage = (props: { imageUsage?: ImageUsage }) => {
|
imageUsage?: ImageUsage;
|
||||||
const { imageUsage } = props;
|
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) {
|
if (!imageUsage) {
|
||||||
return null;
|
return null;
|
||||||
@ -16,16 +24,14 @@ const ImageUsageMessage = (props: { imageUsage?: ImageUsage }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text>This image is currently in use in the following features:</Text>
|
<Text>{topMessage}</Text>
|
||||||
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
<UnorderedList sx={{ paddingInlineStart: 6 }}>
|
||||||
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
|
||||||
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
|
||||||
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
|
||||||
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
<Text>
|
<Text>{bottomMessage}</Text>
|
||||||
If you delete this image, those features will immediately be reset.
|
|
||||||
</Text>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -51,17 +51,8 @@ export type ImageUsage = {
|
|||||||
isControlNetImage: boolean;
|
isControlNetImage: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectImageUsage = createSelector(
|
export const getImageUsage = (state: RootState, image_name: string) => {
|
||||||
[(state: RootState) => state],
|
const { generation, canvas, nodes, controlNet } = state;
|
||||||
({ imageDeletion, generation, canvas, nodes, controlNet }) => {
|
|
||||||
const { imageToDelete } = imageDeletion;
|
|
||||||
|
|
||||||
if (!imageToDelete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { image_name } = imageToDelete;
|
|
||||||
|
|
||||||
const isInitialImage = generation.initialImage?.imageName === image_name;
|
const isInitialImage = generation.initialImage?.imageName === image_name;
|
||||||
|
|
||||||
const isCanvasImage = canvas.layerState.objects.some(
|
const isCanvasImage = canvas.layerState.objects.some(
|
||||||
@ -89,6 +80,22 @@ export const selectImageUsage = createSelector(
|
|||||||
isControlNetImage,
|
isControlNetImage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return imageUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectImageUsage = createSelector(
|
||||||
|
[(state: RootState) => state],
|
||||||
|
(state) => {
|
||||||
|
const { imageToDelete } = state.imageDeletion;
|
||||||
|
|
||||||
|
if (!imageToDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { image_name } = imageToDelete;
|
||||||
|
|
||||||
|
const imageUsage = getImageUsage(state, image_name);
|
||||||
|
|
||||||
return imageUsage;
|
return imageUsage;
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
|
@ -15,8 +15,8 @@ import {
|
|||||||
} from 'app/components/ImageDnd/typesafeDnd';
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
|
||||||
import { FieldComponentProps } from './types';
|
import { FieldComponentProps } from './types';
|
||||||
|
import { PostUploadAction } from 'services/api/types';
|
||||||
|
|
||||||
const ImageInputFieldComponent = (
|
const ImageInputFieldComponent = (
|
||||||
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
||||||
|
@ -29,6 +29,7 @@ export const addControlNetToLinearGraph = (
|
|||||||
const controlNetIterateNode: CollectInvocation = {
|
const controlNetIterateNode: CollectInvocation = {
|
||||||
id: CONTROL_NET_COLLECT,
|
id: CONTROL_NET_COLLECT,
|
||||||
type: 'collect',
|
type: 'collect',
|
||||||
|
is_intermediate: true,
|
||||||
};
|
};
|
||||||
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
|
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
|
||||||
graph.edges.push({
|
graph.edges.push({
|
||||||
@ -55,6 +56,7 @@ export const addControlNetToLinearGraph = (
|
|||||||
const controlNetNode: ControlNetInvocation = {
|
const controlNetNode: ControlNetInvocation = {
|
||||||
id: `control_net_${controlNetId}`,
|
id: `control_net_${controlNetId}`,
|
||||||
type: 'controlnet',
|
type: 'controlnet',
|
||||||
|
is_intermediate: true,
|
||||||
begin_step_percent: beginStepPct,
|
begin_step_percent: beginStepPct,
|
||||||
end_step_percent: endStepPct,
|
end_step_percent: endStepPct,
|
||||||
control_mode: controlMode,
|
control_mode: controlMode,
|
||||||
|
@ -43,6 +43,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const dynamicPromptNode: DynamicPromptInvocation = {
|
const dynamicPromptNode: DynamicPromptInvocation = {
|
||||||
id: DYNAMIC_PROMPT,
|
id: DYNAMIC_PROMPT,
|
||||||
type: 'dynamic_prompt',
|
type: 'dynamic_prompt',
|
||||||
|
is_intermediate: true,
|
||||||
max_prompts: combinatorial ? maxPrompts : iterations,
|
max_prompts: combinatorial ? maxPrompts : iterations,
|
||||||
combinatorial,
|
combinatorial,
|
||||||
prompt: positivePrompt,
|
prompt: positivePrompt,
|
||||||
@ -51,6 +52,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const iterateNode: IterateInvocation = {
|
const iterateNode: IterateInvocation = {
|
||||||
id: ITERATE,
|
id: ITERATE,
|
||||||
type: 'iterate',
|
type: 'iterate',
|
||||||
|
is_intermediate: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
graph.nodes[DYNAMIC_PROMPT] = dynamicPromptNode;
|
graph.nodes[DYNAMIC_PROMPT] = dynamicPromptNode;
|
||||||
@ -99,6 +101,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const randomIntNode: RandomIntInvocation = {
|
const randomIntNode: RandomIntInvocation = {
|
||||||
id: RANDOM_INT,
|
id: RANDOM_INT,
|
||||||
type: 'rand_int',
|
type: 'rand_int',
|
||||||
|
is_intermediate: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
graph.nodes[RANDOM_INT] = randomIntNode;
|
graph.nodes[RANDOM_INT] = randomIntNode;
|
||||||
@ -133,6 +136,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const rangeOfSizeNode: RangeOfSizeInvocation = {
|
const rangeOfSizeNode: RangeOfSizeInvocation = {
|
||||||
id: RANGE_OF_SIZE,
|
id: RANGE_OF_SIZE,
|
||||||
type: 'range_of_size',
|
type: 'range_of_size',
|
||||||
|
is_intermediate: true,
|
||||||
size: iterations,
|
size: iterations,
|
||||||
step: 1,
|
step: 1,
|
||||||
};
|
};
|
||||||
@ -140,6 +144,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const iterateNode: IterateInvocation = {
|
const iterateNode: IterateInvocation = {
|
||||||
id: ITERATE,
|
id: ITERATE,
|
||||||
type: 'iterate',
|
type: 'iterate',
|
||||||
|
is_intermediate: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
graph.nodes[ITERATE] = iterateNode;
|
graph.nodes[ITERATE] = iterateNode;
|
||||||
@ -186,6 +191,7 @@ export const addDynamicPromptsToGraph = (
|
|||||||
const randomIntNode: RandomIntInvocation = {
|
const randomIntNode: RandomIntInvocation = {
|
||||||
id: RANDOM_INT,
|
id: RANDOM_INT,
|
||||||
type: 'rand_int',
|
type: 'rand_int',
|
||||||
|
is_intermediate: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
graph.nodes[RANDOM_INT] = randomIntNode;
|
graph.nodes[RANDOM_INT] = randomIntNode;
|
||||||
|
@ -60,6 +60,7 @@ export const addLoRAsToGraph = (
|
|||||||
const loraLoaderNode: LoraLoaderInvocation = {
|
const loraLoaderNode: LoraLoaderInvocation = {
|
||||||
type: 'lora_loader',
|
type: 'lora_loader',
|
||||||
id: currentLoraNodeId,
|
id: currentLoraNodeId,
|
||||||
|
is_intermediate: true,
|
||||||
lora: { model_name, base_model },
|
lora: { model_name, base_model },
|
||||||
weight,
|
weight,
|
||||||
};
|
};
|
||||||
|
@ -28,6 +28,7 @@ export const addVAEToGraph = (
|
|||||||
graph.nodes[VAE_LOADER] = {
|
graph.nodes[VAE_LOADER] = {
|
||||||
type: 'vae_loader',
|
type: 'vae_loader',
|
||||||
id: VAE_LOADER,
|
id: VAE_LOADER,
|
||||||
|
is_intermediate: true,
|
||||||
vae_model: vae,
|
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 { log } from 'app/logging/useLogger';
|
||||||
import { forEach } from 'lodash-es';
|
import { RootState } from 'app/store/store';
|
||||||
import { buildCanvasInpaintGraph } from './buildCanvasInpaintGraph';
|
|
||||||
import { NonNullableGraph } from 'features/nodes/types/types';
|
import { NonNullableGraph } from 'features/nodes/types/types';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { buildCanvasImageToImageGraph } from './buildCanvasImageToImageGraph';
|
import { buildCanvasImageToImageGraph } from './buildCanvasImageToImageGraph';
|
||||||
|
import { buildCanvasInpaintGraph } from './buildCanvasInpaintGraph';
|
||||||
import { buildCanvasTextToImageGraph } from './buildCanvasTextToImageGraph';
|
import { buildCanvasTextToImageGraph } from './buildCanvasTextToImageGraph';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'nodes' });
|
const moduleLog = log.child({ namespace: 'nodes' });
|
||||||
@ -31,9 +30,5 @@ export const buildCanvasGraph = (
|
|||||||
graph = buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage);
|
graph = buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach(graph.nodes, (node) => {
|
|
||||||
graph.nodes[node.id].is_intermediate = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
};
|
};
|
||||||
|
@ -50,6 +50,8 @@ export const buildCanvasImageToImageGraph = (
|
|||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
|
||||||
|
const { shouldAutoSave } = state.canvas;
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
moduleLog.error('No model found in state');
|
moduleLog.error('No model found in state');
|
||||||
throw new Error('No model found in state');
|
throw new Error('No model found in state');
|
||||||
@ -75,35 +77,42 @@ export const buildCanvasImageToImageGraph = (
|
|||||||
[POSITIVE_CONDITIONING]: {
|
[POSITIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: POSITIVE_CONDITIONING,
|
id: POSITIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: positivePrompt,
|
prompt: positivePrompt,
|
||||||
},
|
},
|
||||||
[NEGATIVE_CONDITIONING]: {
|
[NEGATIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: NEGATIVE_CONDITIONING,
|
id: NEGATIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: negativePrompt,
|
prompt: negativePrompt,
|
||||||
},
|
},
|
||||||
[NOISE]: {
|
[NOISE]: {
|
||||||
type: 'noise',
|
type: 'noise',
|
||||||
id: NOISE,
|
id: NOISE,
|
||||||
|
is_intermediate: true,
|
||||||
use_cpu,
|
use_cpu,
|
||||||
},
|
},
|
||||||
[MAIN_MODEL_LOADER]: {
|
[MAIN_MODEL_LOADER]: {
|
||||||
type: 'main_model_loader',
|
type: 'main_model_loader',
|
||||||
id: MAIN_MODEL_LOADER,
|
id: MAIN_MODEL_LOADER,
|
||||||
|
is_intermediate: true,
|
||||||
model,
|
model,
|
||||||
},
|
},
|
||||||
[CLIP_SKIP]: {
|
[CLIP_SKIP]: {
|
||||||
type: 'clip_skip',
|
type: 'clip_skip',
|
||||||
id: CLIP_SKIP,
|
id: CLIP_SKIP,
|
||||||
|
is_intermediate: true,
|
||||||
skipped_layers: clipSkip,
|
skipped_layers: clipSkip,
|
||||||
},
|
},
|
||||||
[LATENTS_TO_IMAGE]: {
|
[LATENTS_TO_IMAGE]: {
|
||||||
|
is_intermediate: !shouldAutoSave,
|
||||||
type: 'l2i',
|
type: 'l2i',
|
||||||
id: LATENTS_TO_IMAGE,
|
id: LATENTS_TO_IMAGE,
|
||||||
},
|
},
|
||||||
[LATENTS_TO_LATENTS]: {
|
[LATENTS_TO_LATENTS]: {
|
||||||
type: 'l2l',
|
type: 'l2l',
|
||||||
id: LATENTS_TO_LATENTS,
|
id: LATENTS_TO_LATENTS,
|
||||||
|
is_intermediate: true,
|
||||||
cfg_scale,
|
cfg_scale,
|
||||||
scheduler,
|
scheduler,
|
||||||
steps,
|
steps,
|
||||||
@ -112,6 +121,7 @@ export const buildCanvasImageToImageGraph = (
|
|||||||
[IMAGE_TO_LATENTS]: {
|
[IMAGE_TO_LATENTS]: {
|
||||||
type: 'i2l',
|
type: 'i2l',
|
||||||
id: IMAGE_TO_LATENTS,
|
id: IMAGE_TO_LATENTS,
|
||||||
|
is_intermediate: true,
|
||||||
// must be set manually later, bc `fit` parameter may require a resize node inserted
|
// must be set manually later, bc `fit` parameter may require a resize node inserted
|
||||||
// image: {
|
// image: {
|
||||||
// image_name: initialImage.image_name,
|
// image_name: initialImage.image_name,
|
||||||
|
@ -61,12 +61,17 @@ export const buildCanvasInpaintGraph = (
|
|||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
|
||||||
// We may need to set the inpaint width and height to scale the image
|
// 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 = {
|
const graph: NonNullableGraph = {
|
||||||
id: INPAINT_GRAPH,
|
id: INPAINT_GRAPH,
|
||||||
nodes: {
|
nodes: {
|
||||||
[INPAINT]: {
|
[INPAINT]: {
|
||||||
|
is_intermediate: !shouldAutoSave,
|
||||||
type: 'inpaint',
|
type: 'inpaint',
|
||||||
id: INPAINT,
|
id: INPAINT,
|
||||||
steps,
|
steps,
|
||||||
@ -100,26 +105,31 @@ export const buildCanvasInpaintGraph = (
|
|||||||
[POSITIVE_CONDITIONING]: {
|
[POSITIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: POSITIVE_CONDITIONING,
|
id: POSITIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: positivePrompt,
|
prompt: positivePrompt,
|
||||||
},
|
},
|
||||||
[NEGATIVE_CONDITIONING]: {
|
[NEGATIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: NEGATIVE_CONDITIONING,
|
id: NEGATIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: negativePrompt,
|
prompt: negativePrompt,
|
||||||
},
|
},
|
||||||
[MAIN_MODEL_LOADER]: {
|
[MAIN_MODEL_LOADER]: {
|
||||||
type: 'main_model_loader',
|
type: 'main_model_loader',
|
||||||
id: MAIN_MODEL_LOADER,
|
id: MAIN_MODEL_LOADER,
|
||||||
|
is_intermediate: true,
|
||||||
model,
|
model,
|
||||||
},
|
},
|
||||||
[CLIP_SKIP]: {
|
[CLIP_SKIP]: {
|
||||||
type: 'clip_skip',
|
type: 'clip_skip',
|
||||||
id: CLIP_SKIP,
|
id: CLIP_SKIP,
|
||||||
|
is_intermediate: true,
|
||||||
skipped_layers: clipSkip,
|
skipped_layers: clipSkip,
|
||||||
},
|
},
|
||||||
[RANGE_OF_SIZE]: {
|
[RANGE_OF_SIZE]: {
|
||||||
type: 'range_of_size',
|
type: 'range_of_size',
|
||||||
id: RANGE_OF_SIZE,
|
id: RANGE_OF_SIZE,
|
||||||
|
is_intermediate: true,
|
||||||
// seed - must be connected manually
|
// seed - must be connected manually
|
||||||
// start: 0,
|
// start: 0,
|
||||||
size: iterations,
|
size: iterations,
|
||||||
@ -128,6 +138,7 @@ export const buildCanvasInpaintGraph = (
|
|||||||
[ITERATE]: {
|
[ITERATE]: {
|
||||||
type: 'iterate',
|
type: 'iterate',
|
||||||
id: ITERATE,
|
id: ITERATE,
|
||||||
|
is_intermediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
edges: [
|
edges: [
|
||||||
|
@ -41,6 +41,8 @@ export const buildCanvasTextToImageGraph = (
|
|||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
|
||||||
|
const { shouldAutoSave } = state.canvas;
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
moduleLog.error('No model found in state');
|
moduleLog.error('No model found in state');
|
||||||
throw new Error('No model found in state');
|
throw new Error('No model found in state');
|
||||||
@ -66,16 +68,19 @@ export const buildCanvasTextToImageGraph = (
|
|||||||
[POSITIVE_CONDITIONING]: {
|
[POSITIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: POSITIVE_CONDITIONING,
|
id: POSITIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: positivePrompt,
|
prompt: positivePrompt,
|
||||||
},
|
},
|
||||||
[NEGATIVE_CONDITIONING]: {
|
[NEGATIVE_CONDITIONING]: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: NEGATIVE_CONDITIONING,
|
id: NEGATIVE_CONDITIONING,
|
||||||
|
is_intermediate: true,
|
||||||
prompt: negativePrompt,
|
prompt: negativePrompt,
|
||||||
},
|
},
|
||||||
[NOISE]: {
|
[NOISE]: {
|
||||||
type: 'noise',
|
type: 'noise',
|
||||||
id: NOISE,
|
id: NOISE,
|
||||||
|
is_intermediate: true,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
use_cpu,
|
use_cpu,
|
||||||
@ -83,6 +88,7 @@ export const buildCanvasTextToImageGraph = (
|
|||||||
[TEXT_TO_LATENTS]: {
|
[TEXT_TO_LATENTS]: {
|
||||||
type: 't2l',
|
type: 't2l',
|
||||||
id: TEXT_TO_LATENTS,
|
id: TEXT_TO_LATENTS,
|
||||||
|
is_intermediate: true,
|
||||||
cfg_scale,
|
cfg_scale,
|
||||||
scheduler,
|
scheduler,
|
||||||
steps,
|
steps,
|
||||||
@ -90,16 +96,19 @@ export const buildCanvasTextToImageGraph = (
|
|||||||
[MAIN_MODEL_LOADER]: {
|
[MAIN_MODEL_LOADER]: {
|
||||||
type: 'main_model_loader',
|
type: 'main_model_loader',
|
||||||
id: MAIN_MODEL_LOADER,
|
id: MAIN_MODEL_LOADER,
|
||||||
|
is_intermediate: true,
|
||||||
model,
|
model,
|
||||||
},
|
},
|
||||||
[CLIP_SKIP]: {
|
[CLIP_SKIP]: {
|
||||||
type: 'clip_skip',
|
type: 'clip_skip',
|
||||||
id: CLIP_SKIP,
|
id: CLIP_SKIP,
|
||||||
|
is_intermediate: true,
|
||||||
skipped_layers: clipSkip,
|
skipped_layers: clipSkip,
|
||||||
},
|
},
|
||||||
[LATENTS_TO_IMAGE]: {
|
[LATENTS_TO_IMAGE]: {
|
||||||
type: 'l2i',
|
type: 'l2i',
|
||||||
id: LATENTS_TO_IMAGE,
|
id: LATENTS_TO_IMAGE,
|
||||||
|
is_intermediate: !shouldAutoSave,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
edges: [
|
edges: [
|
||||||
|
@ -5,12 +5,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FaUndo, FaUpload } from 'react-icons/fa';
|
import { FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
import { PostUploadAction } from 'services/api/thunks/image';
|
|
||||||
import InitialImage from './InitialImage';
|
import InitialImage from './InitialImage';
|
||||||
|
import { PostUploadAction } from 'services/api/types';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -30,7 +29,6 @@ const postUploadAction: PostUploadAction = {
|
|||||||
const InitialImageDisplay = () => {
|
const InitialImageDisplay = () => {
|
||||||
const { isResetButtonDisabled } = useAppSelector(selector);
|
const { isResetButtonDisabled } = useAppSelector(selector);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { openUploader } = useImageUploader();
|
|
||||||
|
|
||||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
@ -40,10 +38,6 @@ const InitialImageDisplay = () => {
|
|||||||
dispatch(clearInitialImage());
|
dispatch(clearInitialImage());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleUpload = useCallback(() => {
|
|
||||||
openUploader();
|
|
||||||
}, [openUploader]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle={'first'}
|
layerStyle={'first'}
|
||||||
@ -85,7 +79,6 @@ const InitialImageDisplay = () => {
|
|||||||
tooltip={'Upload Initial Image'}
|
tooltip={'Upload Initial Image'}
|
||||||
aria-label={'Upload Initial Image'}
|
aria-label={'Upload Initial Image'}
|
||||||
icon={<FaUpload />}
|
icon={<FaUpload />}
|
||||||
onClick={handleUpload}
|
|
||||||
{...getUploadButtonProps()}
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -244,22 +244,7 @@ export const useRecallParameters = () => {
|
|||||||
[dispatch, parameterSetToast, parameterNotSetToast]
|
[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
|
* Sets image as initial image with toast
|
||||||
*/
|
*/
|
||||||
const sendToImageToImage = useCallback(
|
const sendToImageToImage = useCallback(
|
||||||
@ -330,7 +315,6 @@ export const useRecallParameters = () => {
|
|||||||
recallPositivePrompt,
|
recallPositivePrompt,
|
||||||
recallNegativePrompt,
|
recallNegativePrompt,
|
||||||
recallSeed,
|
recallSeed,
|
||||||
recallInitialImage,
|
|
||||||
recallCfgScale,
|
recallCfgScale,
|
||||||
recallModel,
|
recallModel,
|
||||||
recallScheduler,
|
recallScheduler,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { ImageDTO, MainModelField } from 'services/api/types';
|
import { ImageDTO, MainModelField } from 'services/api/types';
|
||||||
|
|
||||||
export const initialImageSelected = createAction<ImageDTO | string | undefined>(
|
export const initialImageSelected = createAction<ImageDTO | undefined>(
|
||||||
'generation/initialImageSelected'
|
'generation/initialImageSelected'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import { userInvoked } from 'app/store/actions';
|
|||||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { LogLevelName } from 'roarr';
|
import { LogLevelName } from 'roarr';
|
||||||
import { imageUploaded } from 'services/api/thunks/image';
|
|
||||||
import {
|
import {
|
||||||
isAnySessionRejected,
|
isAnySessionRejected,
|
||||||
sessionCanceled,
|
sessionCanceled,
|
||||||
@ -360,27 +359,6 @@ export const systemSlice = createSlice({
|
|||||||
state.wasSchemaParsed = true;
|
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 ***
|
// *** Matchers - must be after all cases ***
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,14 +17,14 @@ type ModelListProps = {
|
|||||||
setSelectedModelId: (name: string | undefined) => void;
|
setSelectedModelId: (name: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelFormat = 'all' | 'checkpoint' | 'diffusers';
|
type ModelFormat = 'images' | 'checkpoint' | 'diffusers';
|
||||||
|
|
||||||
const ModelList = (props: ModelListProps) => {
|
const ModelList = (props: ModelListProps) => {
|
||||||
const { selectedModelId, setSelectedModelId } = props;
|
const { selectedModelId, setSelectedModelId } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [nameFilter, setNameFilter] = useState<string>('');
|
const [nameFilter, setNameFilter] = useState<string>('');
|
||||||
const [modelFormatFilter, setModelFormatFilter] =
|
const [modelFormatFilter, setModelFormatFilter] =
|
||||||
useState<ModelFormat>('all');
|
useState<ModelFormat>('images');
|
||||||
|
|
||||||
const { filteredDiffusersModels } = useGetMainModelsQuery(undefined, {
|
const { filteredDiffusersModels } = useGetMainModelsQuery(undefined, {
|
||||||
selectFromResult: ({ data }) => ({
|
selectFromResult: ({ data }) => ({
|
||||||
@ -47,8 +47,8 @@ const ModelList = (props: ModelListProps) => {
|
|||||||
<Flex flexDirection="column" gap={4} paddingInlineEnd={4}>
|
<Flex flexDirection="column" gap={4} paddingInlineEnd={4}>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
onClick={() => setModelFormatFilter('all')}
|
onClick={() => setModelFormatFilter('images')}
|
||||||
isChecked={modelFormatFilter === 'all'}
|
isChecked={modelFormatFilter === 'images'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{t('modelManager.allModels')}
|
{t('modelManager.allModels')}
|
||||||
@ -75,7 +75,7 @@ const ModelList = (props: ModelListProps) => {
|
|||||||
labelPos="side"
|
labelPos="side"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{['all', 'diffusers'].includes(modelFormatFilter) &&
|
{['images', 'diffusers'].includes(modelFormatFilter) &&
|
||||||
filteredDiffusersModels.length > 0 && (
|
filteredDiffusersModels.length > 0 && (
|
||||||
<StyledModelContainer>
|
<StyledModelContainer>
|
||||||
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||||
@ -93,7 +93,7 @@ const ModelList = (props: ModelListProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</StyledModelContainer>
|
</StyledModelContainer>
|
||||||
)}
|
)}
|
||||||
{['all', 'checkpoint'].includes(modelFormatFilter) &&
|
{['images', 'checkpoint'].includes(modelFormatFilter) &&
|
||||||
filteredCheckpointModels.length > 0 && (
|
filteredCheckpointModels.length > 0 && (
|
||||||
<StyledModelContainer>
|
<StyledModelContainer>
|
||||||
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
<Flex sx={{ gap: 2, flexDir: 'column' }}>
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
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 { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
|
|
||||||
export default function UnifiedCanvasFileUploader() {
|
export default function UnifiedCanvasFileUploader() {
|
||||||
const isStaging = useAppSelector(isStagingSelector);
|
const isStaging = useAppSelector(isStagingSelector);
|
||||||
const { openUploader } = useImageUploader();
|
|
||||||
|
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||||
|
postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' },
|
||||||
|
});
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('common.upload')}
|
aria-label={t('common.upload')}
|
||||||
tooltip={t('common.upload')}
|
tooltip={t('common.upload')}
|
||||||
icon={<FaUpload />}
|
icon={<FaUpload />}
|
||||||
onClick={openUploader}
|
|
||||||
isDisabled={isStaging}
|
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 { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||||
import { paths } from '../schema';
|
import { paths } from '../schema';
|
||||||
|
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||||
|
|
||||||
type ListBoardImagesArg =
|
type ListBoardImagesArg =
|
||||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
||||||
@ -45,39 +46,7 @@ export const boardImagesApi = api.injectEndpoints({
|
|||||||
return tags;
|
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 {
|
export const { useListBoardImagesQuery } = boardImagesApi;
|
||||||
useAddImageToBoardMutation,
|
|
||||||
useRemoveImageFromBoardMutation,
|
|
||||||
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 { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||||
import { paths } from '../schema';
|
import { paths } from '../schema';
|
||||||
|
import { getListImagesUrl, imagesAdapter, imagesApi } from './images';
|
||||||
|
|
||||||
type ListBoardsArg = NonNullable<
|
type ListBoardsArg = NonNullable<
|
||||||
paths['/api/v1/boards/']['get']['parameters']['query']
|
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'];
|
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({
|
export const boardsApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
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
|
* Boards Mutations
|
||||||
*/
|
*/
|
||||||
@ -82,11 +106,92 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
{ type: 'Board', id: arg.board_id },
|
{ 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' }),
|
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) => ({
|
query: (board_id) => ({
|
||||||
url: `boards/${board_id}`,
|
url: `boards/${board_id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@ -94,8 +199,63 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
invalidatesTags: (result, error, arg) => [
|
invalidatesTags: (result, error, arg) => [
|
||||||
{ type: 'Board', id: 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,
|
useUpdateBoardMutation,
|
||||||
useDeleteBoardMutation,
|
useDeleteBoardMutation,
|
||||||
useDeleteBoardAndImagesMutation,
|
useDeleteBoardAndImagesMutation,
|
||||||
|
useListAllImageNamesForBoardQuery,
|
||||||
} = boardsApi;
|
} = 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 { ApiFullTagDescription, api } from '..';
|
||||||
import { components } from '../schema';
|
import { components, paths } from '../schema';
|
||||||
import { ImageDTO } from '../types';
|
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.
|
* 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']>;
|
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({
|
export const imagesApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
/**
|
/**
|
||||||
* Image Queries
|
* 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>({
|
getImageDTO: build.query<ImageDTO, string>({
|
||||||
query: (image_name) => ({ url: `images/${image_name}` }),
|
query: (image_name) => ({ url: `images/${image_name}` }),
|
||||||
providesTags: (result, error, arg) => {
|
providesTags: (result, error, arg) => {
|
||||||
@ -40,7 +151,480 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
clearIntermediates: build.mutation({
|
clearIntermediates: build.mutation({
|
||||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
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';
|
} from '@reduxjs/toolkit/query/react';
|
||||||
import { $authToken, $baseUrl } from 'services/api/client';
|
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<
|
export type ApiFullTagDescription = FullTagDescription<
|
||||||
(typeof tagTypes)[number]
|
(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"];
|
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/": {
|
"/api/v1/board_images/": {
|
||||||
/**
|
/**
|
||||||
* Create Board Image
|
* Create Board Image
|
||||||
@ -240,13 +247,6 @@ export type paths = {
|
|||||||
*/
|
*/
|
||||||
delete: operations["remove_board_image"];
|
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": {
|
"/api/v1/app/version": {
|
||||||
/** Get Version */
|
/** Get Version */
|
||||||
get: operations["app_version"];
|
get: operations["app_version"];
|
||||||
@ -1037,6 +1037,24 @@ export type components = {
|
|||||||
*/
|
*/
|
||||||
mask?: components["schemas"]["ImageField"];
|
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
|
* DivideInvocation
|
||||||
* @description Divides two numbers
|
* @description Divides two numbers
|
||||||
@ -1261,7 +1279,7 @@ export type components = {
|
|||||||
* @description The nodes in this graph
|
* @description The nodes in this graph
|
||||||
*/
|
*/
|
||||||
nodes?: {
|
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
|
* Edges
|
||||||
@ -1304,7 +1322,7 @@ export type components = {
|
|||||||
* @description The results of node executions
|
* @description The results of node executions
|
||||||
*/
|
*/
|
||||||
results: {
|
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
|
* Errors
|
||||||
@ -5306,11 +5324,11 @@ export type components = {
|
|||||||
image?: components["schemas"]["ImageField"];
|
image?: components["schemas"]["ImageField"];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* StableDiffusionXLModelFormat
|
* StableDiffusion1ModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||||
/**
|
/**
|
||||||
* StableDiffusion2ModelFormat
|
* StableDiffusion2ModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
@ -5318,11 +5336,11 @@ export type components = {
|
|||||||
*/
|
*/
|
||||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||||
/**
|
/**
|
||||||
* StableDiffusion1ModelFormat
|
* StableDiffusionXLModelFormat
|
||||||
* @description An enumeration.
|
* @description An enumeration.
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
parameters: never;
|
parameters: never;
|
||||||
@ -5433,7 +5451,7 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
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: {
|
responses: {
|
||||||
@ -5470,7 +5488,7 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
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: {
|
responses: {
|
||||||
@ -5956,13 +5974,13 @@ export type operations = {
|
|||||||
list_image_dtos: {
|
list_image_dtos: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
/** @description The origin of images to list */
|
/** @description The origin of images to list. */
|
||||||
image_origin?: components["schemas"]["ResourceOrigin"];
|
image_origin?: components["schemas"]["ResourceOrigin"];
|
||||||
/** @description The categories of image to include */
|
/** @description The categories of image to include. */
|
||||||
categories?: (components["schemas"]["ImageCategory"])[];
|
categories?: (components["schemas"]["ImageCategory"])[];
|
||||||
/** @description Whether to list intermediate images */
|
/** @description Whether to list intermediate images. */
|
||||||
is_intermediate?: boolean;
|
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;
|
board_id?: string;
|
||||||
/** @description The page offset */
|
/** @description The page offset */
|
||||||
offset?: number;
|
offset?: number;
|
||||||
@ -6328,7 +6346,7 @@ export type operations = {
|
|||||||
/** @description Successful Response */
|
/** @description Successful Response */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": unknown;
|
"application/json": components["schemas"]["DeleteBoardResult"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @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
|
* Create Board Image
|
||||||
* @description Creates a 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 */
|
/** Get Version */
|
||||||
app_version: {
|
app_version: {
|
||||||
responses: {
|
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 { O } from 'ts-toolbelt';
|
||||||
import { components } from './schema';
|
import { components } from './schema';
|
||||||
|
|
||||||
@ -186,3 +187,41 @@ export type CollectInvocationOutput =
|
|||||||
export type LatentsOutput = components['schemas']['LatentsOutput'];
|
export type LatentsOutput = components['schemas']['LatentsOutput'];
|
||||||
export type GraphInvocationOutput =
|
export type GraphInvocationOutput =
|
||||||
components['schemas']['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