mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into release/invokeai-3-0-beta
This commit is contained in:
commit
8439e30798
@ -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
|
||||||
|
@ -84,6 +84,17 @@ async def delete_image(
|
|||||||
# TODO: Does this need any exception handling at all?
|
# TODO: Does this need any exception handling at all?
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@images_router.post("/clear-intermediates", operation_id="clear_intermediates")
|
||||||
|
async def clear_intermediates() -> int:
|
||||||
|
"""Clears first 100 intermediates"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True)
|
||||||
|
return count_deleted
|
||||||
|
except Exception as e:
|
||||||
|
# TODO: Does this need any exception handling at all?
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@images_router.patch(
|
@images_router.patch(
|
||||||
"/{image_name}",
|
"/{image_name}",
|
||||||
@ -234,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"),
|
||||||
|
@ -22,7 +22,7 @@ from ...backend.stable_diffusion.diffusers_pipeline import (
|
|||||||
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
|
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
|
||||||
PostprocessingSettings
|
PostprocessingSettings
|
||||||
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
|
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
|
||||||
from ...backend.util.devices import torch_dtype
|
from ...backend.util.devices import choose_torch_device, torch_dtype
|
||||||
from ..models.image import ImageCategory, ImageField, ResourceOrigin
|
from ..models.image import ImageCategory, ImageField, ResourceOrigin
|
||||||
from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
|
from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
|
||||||
InvocationConfig, InvocationContext)
|
InvocationConfig, InvocationContext)
|
||||||
@ -38,7 +38,6 @@ from diffusers.models.attention_processor import (
|
|||||||
XFormersAttnProcessor,
|
XFormersAttnProcessor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LatentsField(BaseModel):
|
class LatentsField(BaseModel):
|
||||||
"""A latents field used for passing latents between invocations"""
|
"""A latents field used for passing latents between invocations"""
|
||||||
|
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -141,7 +141,7 @@ class EventServiceBase:
|
|||||||
model_type=model_type,
|
model_type=model_type,
|
||||||
submodel=submodel,
|
submodel=submodel,
|
||||||
hash=model_info.hash,
|
hash=model_info.hash,
|
||||||
location=model_info.location,
|
location=str(model_info.location),
|
||||||
precision=str(model_info.precision),
|
precision=str(model_info.precision),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
@ -97,8 +100,8 @@ class ImageRecordStorageBase(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_many(
|
def get_many(
|
||||||
self,
|
self,
|
||||||
offset: int = 0,
|
offset: Optional[int] = None,
|
||||||
limit: int = 10,
|
limit: Optional[int] = None,
|
||||||
image_origin: Optional[ResourceOrigin] = None,
|
image_origin: Optional[ResourceOrigin] = None,
|
||||||
categories: Optional[list[ImageCategory]] = None,
|
categories: Optional[list[ImageCategory]] = None,
|
||||||
is_intermediate: Optional[bool] = None,
|
is_intermediate: Optional[bool] = None,
|
||||||
@ -322,8 +325,8 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
|||||||
|
|
||||||
def get_many(
|
def get_many(
|
||||||
self,
|
self,
|
||||||
offset: int = 0,
|
offset: Optional[int] = None,
|
||||||
limit: int = 10,
|
limit: Optional[int] = None,
|
||||||
image_origin: Optional[ResourceOrigin] = None,
|
image_origin: Optional[ResourceOrigin] = None,
|
||||||
categories: Optional[list[ImageCategory]] = None,
|
categories: Optional[list[ImageCategory]] = None,
|
||||||
is_intermediate: Optional[bool] = None,
|
is_intermediate: Optional[bool] = None,
|
||||||
@ -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
|
||||||
@ -392,8 +399,12 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
|||||||
images_query += query_conditions + query_pagination + ";"
|
images_query += query_conditions + query_pagination + ";"
|
||||||
# Add all the parameters
|
# Add all the parameters
|
||||||
images_params = query_params.copy()
|
images_params = query_params.copy()
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
images_params.append(limit)
|
images_params.append(limit)
|
||||||
|
if offset is not None:
|
||||||
images_params.append(offset)
|
images_params.append(offset)
|
||||||
|
|
||||||
# Build the list of images, deserializing each row
|
# Build the list of images, deserializing each row
|
||||||
self._cursor.execute(images_query, images_params)
|
self._cursor.execute(images_query, images_params)
|
||||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||||
|
@ -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)
|
||||||
@ -109,6 +108,13 @@ class ImageServiceABC(ABC):
|
|||||||
"""Deletes an image."""
|
"""Deletes an image."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_many(self, is_intermediate: bool) -> int:
|
||||||
|
"""Deletes many images."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def delete_images_on_board(self, board_id: str):
|
def delete_images_on_board(self, board_id: str):
|
||||||
"""Deletes all images on a board."""
|
"""Deletes all images on a board."""
|
||||||
@ -378,16 +384,39 @@ 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:
|
||||||
|
self._services.logger.error(f"Failed to delete image records")
|
||||||
|
raise
|
||||||
|
except ImageFileDeleteException:
|
||||||
|
self._services.logger.error(f"Failed to delete image files")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self._services.logger.error("Problem deleting image records and files")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def delete_many(self, is_intermediate: bool):
|
||||||
|
try:
|
||||||
|
# only clears 100 at a time
|
||||||
|
images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,)
|
||||||
|
count = len(images.items)
|
||||||
|
image_name_list = list(
|
||||||
|
map(
|
||||||
|
lambda r: r.image_name,
|
||||||
|
images.items,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for image_name in image_name_list:
|
||||||
|
self._services.image_files.delete(image_name)
|
||||||
|
self._services.image_records.delete_many(image_name_list)
|
||||||
|
return count
|
||||||
except ImageRecordDeleteException:
|
except ImageRecordDeleteException:
|
||||||
self._services.logger.error(f"Failed to delete image records")
|
self._services.logger.error(f"Failed to delete image records")
|
||||||
raise
|
raise
|
||||||
|
@ -21,6 +21,7 @@ import re
|
|||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from safetensors.torch import load_file
|
from safetensors.torch import load_file
|
||||||
@ -63,6 +64,7 @@ from diffusers.pipelines.stable_diffusion.safety_checker import (
|
|||||||
StableDiffusionSafetyChecker,
|
StableDiffusionSafetyChecker,
|
||||||
)
|
)
|
||||||
from diffusers.utils import is_safetensors_available
|
from diffusers.utils import is_safetensors_available
|
||||||
|
import transformers
|
||||||
from transformers import (
|
from transformers import (
|
||||||
AutoFeatureExtractor,
|
AutoFeatureExtractor,
|
||||||
BertTokenizerFast,
|
BertTokenizerFast,
|
||||||
@ -841,6 +843,15 @@ def convert_ldm_clip_checkpoint(checkpoint):
|
|||||||
key
|
key
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# transformers 4.31.0 and higher - this key no longer in state dict
|
||||||
|
if version.parse(transformers.__version__) >= version.parse("4.31.0"):
|
||||||
|
position_ids = text_model_dict.pop("text_model.embeddings.position_ids", None)
|
||||||
|
text_model.load_state_dict(text_model_dict)
|
||||||
|
if position_ids is not None:
|
||||||
|
text_model.text_model.embeddings.position_ids.copy_(position_ids)
|
||||||
|
|
||||||
|
# transformers 4.30.2 and lower - position_ids is part of state_dict
|
||||||
|
else:
|
||||||
text_model.load_state_dict(text_model_dict)
|
text_model.load_state_dict(text_model_dict)
|
||||||
|
|
||||||
return text_model
|
return text_model
|
||||||
@ -947,6 +958,15 @@ def convert_open_clip_checkpoint(checkpoint):
|
|||||||
|
|
||||||
text_model_dict[new_key] = checkpoint[key]
|
text_model_dict[new_key] = checkpoint[key]
|
||||||
|
|
||||||
|
# transformers 4.31.0 and higher - this key no longer in state dict
|
||||||
|
if version.parse(transformers.__version__) >= version.parse("4.31.0"):
|
||||||
|
position_ids = text_model_dict.pop("text_model.embeddings.position_ids", None)
|
||||||
|
text_model.load_state_dict(text_model_dict)
|
||||||
|
if position_ids is not None:
|
||||||
|
text_model.text_model.embeddings.position_ids.copy_(position_ids)
|
||||||
|
|
||||||
|
# transformers 4.30.2 and lower - position_ids is part of state_dict
|
||||||
|
else:
|
||||||
text_model.load_state_dict(text_model_dict)
|
text_model.load_state_dict(text_model_dict)
|
||||||
|
|
||||||
return text_model
|
return text_model
|
||||||
|
@ -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
|
||||||
|
@ -1,33 +1,46 @@
|
|||||||
import { Flex, Image } from '@chakra-ui/react';
|
import { Flex, Image } from '@chakra-ui/react';
|
||||||
import { NodeProps } from 'reactflow';
|
import { RootState } from 'app/store/store';
|
||||||
import { InvocationValue } from '../types/types';
|
|
||||||
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { NodeProps, OnResize } from 'reactflow';
|
||||||
|
import { setProgressNodeSize } from '../store/nodesSlice';
|
||||||
import IAINodeHeader from './IAINode/IAINodeHeader';
|
import IAINodeHeader from './IAINode/IAINodeHeader';
|
||||||
import IAINodeResizer from './IAINode/IAINodeResizer';
|
import IAINodeResizer from './IAINode/IAINodeResizer';
|
||||||
import NodeWrapper from './NodeWrapper';
|
import NodeWrapper from './NodeWrapper';
|
||||||
|
|
||||||
const ProgressImageNode = (props: NodeProps<InvocationValue>) => {
|
const ProgressImageNode = (props: NodeProps) => {
|
||||||
const progressImage = useAppSelector((state) => state.system.progressImage);
|
const progressImage = useSelector(
|
||||||
|
(state: RootState) => state.system.progressImage
|
||||||
|
);
|
||||||
|
const progressNodeSize = useSelector(
|
||||||
|
(state: RootState) => state.nodes.progressNodeSize
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
const { selected } = props;
|
const { selected } = props;
|
||||||
|
|
||||||
|
const handleResize: OnResize = (_, newSize) => {
|
||||||
|
dispatch(setProgressNodeSize(newSize));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeWrapper selected={selected}>
|
<NodeWrapper selected={selected}>
|
||||||
<IAINodeHeader
|
<IAINodeHeader
|
||||||
title="Progress Image"
|
title="Progress Image"
|
||||||
description="Displays the progress image in the Node Editor"
|
description="Displays the progress image in the Node Editor"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
className="nopan"
|
|
||||||
sx={{
|
sx={{
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
flexShrink: 0,
|
||||||
borderBottomRadius: 'md',
|
borderBottomRadius: 'md',
|
||||||
p: 2,
|
|
||||||
bg: 'base.200',
|
bg: 'base.200',
|
||||||
_dark: { bg: 'base.800' },
|
_dark: { bg: 'base.800' },
|
||||||
|
width: progressNodeSize.width - 2,
|
||||||
|
height: progressNodeSize.height - 2,
|
||||||
|
minW: 250,
|
||||||
|
minH: 250,
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{progressImage ? (
|
{progressImage ? (
|
||||||
@ -42,22 +55,17 @@ const ProgressImageNode = (props: NodeProps<InvocationValue>) => {
|
|||||||
) : (
|
) : (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
minW: 250,
|
||||||
h: 'full',
|
minH: 250,
|
||||||
minW: 32,
|
width: progressNodeSize.width - 2,
|
||||||
minH: 32,
|
height: progressNodeSize.height - 2,
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IAINoContentFallback />
|
<IAINoContentFallback />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<IAINodeResizer
|
<IAINodeResizer onResize={handleResize} />
|
||||||
maxHeight={progressImage?.height ?? 512}
|
|
||||||
maxWidth={progressImage?.width ?? 512}
|
|
||||||
/>
|
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -35,6 +35,7 @@ export type NodesState = {
|
|||||||
shouldShowFieldTypeLegend: boolean;
|
shouldShowFieldTypeLegend: boolean;
|
||||||
shouldShowMinimapPanel: boolean;
|
shouldShowMinimapPanel: boolean;
|
||||||
editorInstance: ReactFlowInstance | undefined;
|
editorInstance: ReactFlowInstance | undefined;
|
||||||
|
progressNodeSize: { width: number; height: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialNodesState: NodesState = {
|
export const initialNodesState: NodesState = {
|
||||||
@ -47,6 +48,7 @@ export const initialNodesState: NodesState = {
|
|||||||
shouldShowFieldTypeLegend: false,
|
shouldShowFieldTypeLegend: false,
|
||||||
shouldShowMinimapPanel: true,
|
shouldShowMinimapPanel: true,
|
||||||
editorInstance: undefined,
|
editorInstance: undefined,
|
||||||
|
progressNodeSize: { width: 512, height: 512 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodesSlice = createSlice({
|
const nodesSlice = createSlice({
|
||||||
@ -157,6 +159,12 @@ const nodesSlice = createSlice({
|
|||||||
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
|
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
|
||||||
state.edges = action.payload;
|
state.edges = action.payload;
|
||||||
},
|
},
|
||||||
|
setProgressNodeSize: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ width: number; height: number }>
|
||||||
|
) => {
|
||||||
|
state.progressNodeSize = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
||||||
@ -182,6 +190,7 @@ export const {
|
|||||||
setEditorInstance,
|
setEditorInstance,
|
||||||
loadFileNodes,
|
loadFileNodes,
|
||||||
loadFileEdges,
|
loadFileEdges,
|
||||||
|
setProgressNodeSize,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -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'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { StyledFlex } from './SettingsModal';
|
||||||
|
import { Heading, Text } from '@chakra-ui/react';
|
||||||
|
import IAIButton from '../../../../common/components/IAIButton';
|
||||||
|
import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images';
|
||||||
|
import { addToast } from '../../store/systemSlice';
|
||||||
|
import { resetCanvas } from '../../../canvas/store/canvasSlice';
|
||||||
|
|
||||||
|
export default function SettingsClearIntermediates() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [isDisabled, setIsDisabled] = useState(false);
|
||||||
|
|
||||||
|
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
|
||||||
|
useClearIntermediatesMutation();
|
||||||
|
|
||||||
|
const handleClickClearIntermediates = useCallback(() => {
|
||||||
|
clearIntermediates({})
|
||||||
|
.unwrap()
|
||||||
|
.then((response) => {
|
||||||
|
dispatch(resetCanvas());
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title:
|
||||||
|
response === 0
|
||||||
|
? `No intermediates to clear`
|
||||||
|
: `Successfully cleared ${response} intermediates`,
|
||||||
|
status: 'info',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (response < 100) {
|
||||||
|
setIsDisabled(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [clearIntermediates, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledFlex>
|
||||||
|
<Heading size="sm">Clear Intermediates</Heading>
|
||||||
|
<IAIButton
|
||||||
|
colorScheme="error"
|
||||||
|
onClick={handleClickClearIntermediates}
|
||||||
|
isLoading={isLoadingClearIntermediates}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
{isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'}
|
||||||
|
</IAIButton>
|
||||||
|
<Text>
|
||||||
|
Will permanently delete first 100 intermediates found on disk and in
|
||||||
|
database
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="bold">This will also clear your canvas state.</Text>
|
||||||
|
<Text>
|
||||||
|
Intermediate images are byproducts of generation, different from the
|
||||||
|
result images in the gallery. Purging intermediates will free disk
|
||||||
|
space. Your gallery images will not be deleted.
|
||||||
|
</Text>
|
||||||
|
</StyledFlex>
|
||||||
|
);
|
||||||
|
}
|
@ -11,7 +11,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector, current } from '@reduxjs/toolkit';
|
||||||
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
|
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
|
||||||
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
|
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
SystemState,
|
SystemState,
|
||||||
consoleLogLevelChanged,
|
consoleLogLevelChanged,
|
||||||
setEnableImageDebugging,
|
setEnableImageDebugging,
|
||||||
|
setIsNodesEnabled,
|
||||||
setShouldConfirmOnDelete,
|
setShouldConfirmOnDelete,
|
||||||
setShouldDisplayGuides,
|
setShouldDisplayGuides,
|
||||||
shouldAntialiasProgressImageChanged,
|
shouldAntialiasProgressImageChanged,
|
||||||
@ -48,6 +49,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LogLevelName } from 'roarr';
|
import { LogLevelName } from 'roarr';
|
||||||
import SettingsSchedulers from './SettingsSchedulers';
|
import SettingsSchedulers from './SettingsSchedulers';
|
||||||
|
import SettingsClearIntermediates from './SettingsClearIntermediates';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[systemSelector, uiSelector],
|
[systemSelector, uiSelector],
|
||||||
@ -59,6 +61,7 @@ const selector = createSelector(
|
|||||||
consoleLogLevel,
|
consoleLogLevel,
|
||||||
shouldLogToConsole,
|
shouldLogToConsole,
|
||||||
shouldAntialiasProgressImage,
|
shouldAntialiasProgressImage,
|
||||||
|
isNodesEnabled,
|
||||||
} = system;
|
} = system;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -79,6 +82,7 @@ const selector = createSelector(
|
|||||||
shouldLogToConsole,
|
shouldLogToConsole,
|
||||||
shouldAntialiasProgressImage,
|
shouldAntialiasProgressImage,
|
||||||
shouldShowAdvancedOptions,
|
shouldShowAdvancedOptions,
|
||||||
|
isNodesEnabled,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -91,6 +95,8 @@ type ConfigOptions = {
|
|||||||
shouldShowResetWebUiText: boolean;
|
shouldShowResetWebUiText: boolean;
|
||||||
shouldShowBetaLayout: boolean;
|
shouldShowBetaLayout: boolean;
|
||||||
shouldShowAdvancedOptionsSettings: boolean;
|
shouldShowAdvancedOptionsSettings: boolean;
|
||||||
|
shouldShowClearIntermediates: boolean;
|
||||||
|
shouldShowNodesToggle: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsModalProps = {
|
type SettingsModalProps = {
|
||||||
@ -109,6 +115,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
|
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
|
||||||
const shouldShowAdvancedOptionsSettings =
|
const shouldShowAdvancedOptionsSettings =
|
||||||
config?.shouldShowAdvancedOptionsSettings ?? true;
|
config?.shouldShowAdvancedOptionsSettings ?? true;
|
||||||
|
const shouldShowClearIntermediates =
|
||||||
|
config?.shouldShowClearIntermediates ?? true;
|
||||||
|
const shouldShowNodesToggle = config?.shouldShowNodesToggle ?? true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldShowDeveloperSettings) {
|
if (!shouldShowDeveloperSettings) {
|
||||||
@ -139,6 +148,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
shouldLogToConsole,
|
shouldLogToConsole,
|
||||||
shouldAntialiasProgressImage,
|
shouldAntialiasProgressImage,
|
||||||
shouldShowAdvancedOptions,
|
shouldShowAdvancedOptions,
|
||||||
|
isNodesEnabled,
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const handleClickResetWebUI = useCallback(() => {
|
const handleClickResetWebUI = useCallback(() => {
|
||||||
@ -169,6 +179,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleToggleNodes = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setIsNodesEnabled(e.target.checked));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
@ -253,6 +270,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{shouldShowNodesToggle && (
|
||||||
|
<IAISwitch
|
||||||
|
label="Enable Nodes Editor (Experimental)"
|
||||||
|
isChecked={isNodesEnabled}
|
||||||
|
onChange={handleToggleNodes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledFlex>
|
</StyledFlex>
|
||||||
|
|
||||||
{shouldShowDeveloperSettings && (
|
{shouldShowDeveloperSettings && (
|
||||||
@ -280,6 +304,8 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
</StyledFlex>
|
</StyledFlex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShowClearIntermediates && <SettingsClearIntermediates />}
|
||||||
|
|
||||||
<StyledFlex>
|
<StyledFlex>
|
||||||
<Heading size="sm">{t('settings.resetWebUI')}</Heading>
|
<Heading size="sm">{t('settings.resetWebUI')}</Heading>
|
||||||
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
|
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
|
||||||
@ -328,7 +354,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
|
|
||||||
export default SettingsModal;
|
export default SettingsModal;
|
||||||
|
|
||||||
const StyledFlex = (props: PropsWithChildren) => {
|
export const StyledFlex = (props: PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -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,
|
||||||
@ -86,6 +85,7 @@ export interface SystemState {
|
|||||||
language: keyof typeof LANGUAGES;
|
language: keyof typeof LANGUAGES;
|
||||||
isUploading: boolean;
|
isUploading: boolean;
|
||||||
boardIdToAddTo?: string;
|
boardIdToAddTo?: string;
|
||||||
|
isNodesEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialSystemState: SystemState = {
|
export const initialSystemState: SystemState = {
|
||||||
@ -118,6 +118,7 @@ export const initialSystemState: SystemState = {
|
|||||||
isPersisted: false,
|
isPersisted: false,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
|
isNodesEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemSlice = createSlice({
|
export const systemSlice = createSlice({
|
||||||
@ -193,6 +194,9 @@ export const systemSlice = createSlice({
|
|||||||
progressImageSet(state, action: PayloadAction<ProgressImage | null>) {
|
progressImageSet(state, action: PayloadAction<ProgressImage | null>) {
|
||||||
state.progressImage = action.payload;
|
state.progressImage = action.payload;
|
||||||
},
|
},
|
||||||
|
setIsNodesEnabled(state, action: PayloadAction<boolean>) {
|
||||||
|
state.isNodesEnabled = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
/**
|
/**
|
||||||
@ -360,27 +364,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 ***
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -422,6 +405,7 @@ export const {
|
|||||||
shouldAntialiasProgressImageChanged,
|
shouldAntialiasProgressImageChanged,
|
||||||
languageChanged,
|
languageChanged,
|
||||||
progressImageSet,
|
progressImageSet,
|
||||||
|
setIsNodesEnabled,
|
||||||
} = systemSlice.actions;
|
} = systemSlice.actions;
|
||||||
|
|
||||||
export default systemSlice.reducer;
|
export default systemSlice.reducer;
|
||||||
|
@ -37,6 +37,7 @@ import NodesTab from './tabs/Nodes/NodesTab';
|
|||||||
import ResizeHandle from './tabs/ResizeHandle';
|
import ResizeHandle from './tabs/ResizeHandle';
|
||||||
import TextToImageTab from './tabs/TextToImage/TextToImageTab';
|
import TextToImageTab from './tabs/TextToImage/TextToImageTab';
|
||||||
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
|
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
|
||||||
|
import { systemSelector } from '../../system/store/systemSelectors';
|
||||||
|
|
||||||
export interface InvokeTabInfo {
|
export interface InvokeTabInfo {
|
||||||
id: InvokeTabName;
|
id: InvokeTabName;
|
||||||
@ -84,11 +85,20 @@ const tabs: InvokeTabInfo[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const enabledTabsSelector = createSelector(
|
const enabledTabsSelector = createSelector(
|
||||||
configSelector,
|
[configSelector, systemSelector],
|
||||||
(config) => {
|
(config, system) => {
|
||||||
const { disabledTabs } = config;
|
const { disabledTabs } = config;
|
||||||
|
const { isNodesEnabled } = system;
|
||||||
|
|
||||||
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
|
const enabledTabs = tabs.filter((tab) => {
|
||||||
|
if (tab.id === 'nodes') {
|
||||||
|
return isNodesEnabled && !disabledTabs.includes(tab.id);
|
||||||
|
} else {
|
||||||
|
return !disabledTabs.includes(tab.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return enabledTabs;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||||
|
@ -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()} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user