mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
20 Commits
ryan/upsca
...
feat/many-
Author | SHA1 | Date | |
---|---|---|---|
704cfd8ff5 | |||
2990fa23fe | |||
58cb5fefd0 | |||
2ef5919475 | |||
7f07528b08 | |||
a2f944a657 | |||
0317cc158a | |||
8648332b4f | |||
c2aee42fa3 | |||
a77f6b0c18 | |||
8771e32ed2 | |||
5e1ed63076 | |||
cad358dc9a | |||
8501ca0843 | |||
560a59123a | |||
62b700b908 | |||
9aedf84ac2 | |||
a08179bf34 | |||
0b9aaf1b0b | |||
da98f281ee |
@ -1,9 +1,9 @@
|
||||
from fastapi import Body, HTTPException, Path, Query
|
||||
from fastapi import Body, HTTPException, Path
|
||||
from fastapi.routing import APIRouter
|
||||
from invokeai.app.services.board_record_storage import BoardRecord, BoardChanges
|
||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||
from invokeai.app.services.models.board_record import BoardDTO
|
||||
from invokeai.app.services.models.image_record import ImageDTO
|
||||
|
||||
from invokeai.app.models.image import (AddManyImagesToBoardResult,
|
||||
GetAllBoardImagesForBoardResult,
|
||||
RemoveManyImagesFromBoardResult)
|
||||
|
||||
from ..dependencies import ApiDependencies
|
||||
|
||||
@ -11,7 +11,7 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
|
||||
|
||||
|
||||
@board_images_router.post(
|
||||
"/",
|
||||
"/{board_id}",
|
||||
operation_id="create_board_image",
|
||||
responses={
|
||||
201: {"description": "The image was added to a board successfully"},
|
||||
@ -19,16 +19,19 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
|
||||
status_code=201,
|
||||
)
|
||||
async def create_board_image(
|
||||
board_id: str = Body(description="The id of the board to add to"),
|
||||
board_id: str = Path(description="The id of the board to add to"),
|
||||
image_name: str = Body(description="The name of the image to add"),
|
||||
):
|
||||
"""Creates a board_image"""
|
||||
try:
|
||||
result = ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
|
||||
result = ApiDependencies.invoker.services.board_images.add_image_to_board(
|
||||
board_id=board_id, image_name=image_name
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Failed to add to board")
|
||||
|
||||
|
||||
|
||||
@board_images_router.delete(
|
||||
"/",
|
||||
operation_id="remove_board_image",
|
||||
@ -38,32 +41,78 @@ async def create_board_image(
|
||||
status_code=201,
|
||||
)
|
||||
async def remove_board_image(
|
||||
board_id: str = Body(description="The id of the board"),
|
||||
image_name: str = Body(description="The name of the image to remove"),
|
||||
image_name: str = Body(
|
||||
description="The name of the image to remove from its board"
|
||||
),
|
||||
):
|
||||
"""Deletes a board_image"""
|
||||
try:
|
||||
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(board_id=board_id, image_name=image_name)
|
||||
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(
|
||||
image_name=image_name
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Failed to update board")
|
||||
|
||||
|
||||
|
||||
@board_images_router.get(
|
||||
"/{board_id}",
|
||||
operation_id="list_board_images",
|
||||
response_model=OffsetPaginatedResults[ImageDTO],
|
||||
operation_id="get_all_board_images_for_board",
|
||||
response_model=GetAllBoardImagesForBoardResult,
|
||||
)
|
||||
async def list_board_images(
|
||||
async def get_all_board_images_for_board(
|
||||
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"""
|
||||
) -> GetAllBoardImagesForBoardResult:
|
||||
"""Gets all image names for a board"""
|
||||
|
||||
results = ApiDependencies.invoker.services.board_images.get_images_for_board(
|
||||
board_id,
|
||||
result = (
|
||||
ApiDependencies.invoker.services.board_images.get_all_board_images_for_board(
|
||||
board_id,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@board_images_router.patch(
|
||||
"/{board_id}/images",
|
||||
operation_id="create_multiple_board_images",
|
||||
responses={
|
||||
201: {"description": "The images were added to the board successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
)
|
||||
async def create_multiple_board_images(
|
||||
board_id: str = Path(description="The id of the board"),
|
||||
image_names: list[str] = Body(
|
||||
description="The names of the images to add to the board"
|
||||
),
|
||||
) -> AddManyImagesToBoardResult:
|
||||
"""Add many images to a board"""
|
||||
|
||||
results = ApiDependencies.invoker.services.board_images.add_many_images_to_board(
|
||||
board_id, image_names
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@board_images_router.post(
|
||||
"/images",
|
||||
operation_id="delete_multiple_board_images",
|
||||
responses={
|
||||
201: {"description": "The images were removed from their boards successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
)
|
||||
async def delete_multiple_board_images(
|
||||
image_names: list[str] = Body(
|
||||
description="The names of the images to remove from their boards, if they have one"
|
||||
),
|
||||
) -> RemoveManyImagesFromBoardResult:
|
||||
"""Remove many images from their boards, if they have one"""
|
||||
|
||||
results = (
|
||||
ApiDependencies.invoker.services.board_images.remove_many_images_from_board(
|
||||
image_names
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
@ -1,11 +1,13 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from fastapi import Body, HTTPException, Path, Query
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from invokeai.app.models.image import DeleteManyImagesResult
|
||||
from invokeai.app.services.board_record_storage import BoardChanges
|
||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||
from invokeai.app.services.models.board_record import BoardDTO
|
||||
|
||||
|
||||
from ..dependencies import ApiDependencies
|
||||
|
||||
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
|
||||
@ -69,25 +71,26 @@ async def update_board(
|
||||
raise HTTPException(status_code=500, detail="Failed to update board")
|
||||
|
||||
|
||||
@boards_router.delete("/{board_id}", operation_id="delete_board")
|
||||
@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteManyImagesResult)
|
||||
async def delete_board(
|
||||
board_id: str = Path(description="The id of board to delete"),
|
||||
include_images: Optional[bool] = Query(
|
||||
description="Permanently delete all images on the board", default=False
|
||||
),
|
||||
) -> None:
|
||||
) -> DeleteManyImagesResult:
|
||||
"""Deletes a board"""
|
||||
try:
|
||||
if include_images is True:
|
||||
ApiDependencies.invoker.services.images.delete_images_on_board(
|
||||
result = ApiDependencies.invoker.services.images.delete_images_on_board(
|
||||
board_id=board_id
|
||||
)
|
||||
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
|
||||
else:
|
||||
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
|
||||
result = DeleteManyImagesResult(deleted_images=[])
|
||||
return result
|
||||
except Exception as e:
|
||||
# TODO: Does this need any exception handling at all?
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail="Failed to delete images on board")
|
||||
|
||||
|
||||
@boards_router.get(
|
||||
|
@ -1,20 +1,19 @@
|
||||
import io
|
||||
from typing import Optional
|
||||
from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from fastapi import (Body, HTTPException, Path, Query, Request, Response,
|
||||
UploadFile)
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from PIL import Image
|
||||
from invokeai.app.models.image import (
|
||||
ImageCategory,
|
||||
ResourceOrigin,
|
||||
)
|
||||
|
||||
from invokeai.app.models.image import (DeleteManyImagesResult, ImageCategory,
|
||||
ResourceOrigin)
|
||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||
from invokeai.app.services.models.image_record import (
|
||||
ImageDTO,
|
||||
ImageRecordChanges,
|
||||
ImageUrlsDTO,
|
||||
)
|
||||
from invokeai.app.services.item_storage import PaginatedResults
|
||||
from invokeai.app.services.models.image_record import (GetImagesByNamesResult,
|
||||
ImageDTO,
|
||||
ImageRecordChanges,
|
||||
ImageUrlsDTO)
|
||||
|
||||
from ..dependencies import ApiDependencies
|
||||
|
||||
@ -22,7 +21,7 @@ images_router = APIRouter(prefix="/v1/images", tags=["images"])
|
||||
|
||||
|
||||
@images_router.post(
|
||||
"/",
|
||||
"/upload",
|
||||
operation_id="upload_image",
|
||||
responses={
|
||||
201: {"description": "The image was uploaded successfully"},
|
||||
@ -103,14 +102,14 @@ async def update_image(
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/{image_name}/metadata",
|
||||
operation_id="get_image_metadata",
|
||||
"/{image_name}",
|
||||
operation_id="get_image",
|
||||
response_model=ImageDTO,
|
||||
)
|
||||
async def get_image_metadata(
|
||||
async def get_image_dto(
|
||||
image_name: str = Path(description="The name of image to get"),
|
||||
) -> ImageDTO:
|
||||
"""Gets an image's metadata"""
|
||||
"""Gets an image's DTO"""
|
||||
|
||||
try:
|
||||
return ApiDependencies.invoker.services.images.get_dto(image_name)
|
||||
@ -119,8 +118,8 @@ async def get_image_metadata(
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/{image_name}",
|
||||
operation_id="get_image_full",
|
||||
"/{image_name}/full_size",
|
||||
operation_id="get_image_full_size",
|
||||
response_class=Response,
|
||||
responses={
|
||||
200: {
|
||||
@ -130,7 +129,7 @@ async def get_image_metadata(
|
||||
404: {"description": "Image not found"},
|
||||
},
|
||||
)
|
||||
async def get_image_full(
|
||||
async def get_image_full_size(
|
||||
image_name: str = Path(description="The name of full-resolution image file to get"),
|
||||
) -> FileResponse:
|
||||
"""Gets a full-resolution image file"""
|
||||
@ -208,10 +207,10 @@ async def get_image_urls(
|
||||
|
||||
@images_router.get(
|
||||
"/",
|
||||
operation_id="list_images_with_metadata",
|
||||
operation_id="get_many_images",
|
||||
response_model=OffsetPaginatedResults[ImageDTO],
|
||||
)
|
||||
async def list_images_with_metadata(
|
||||
async def get_many_images(
|
||||
image_origin: Optional[ResourceOrigin] = Query(
|
||||
default=None, description="The origin of images to list"
|
||||
),
|
||||
@ -222,7 +221,8 @@ async def list_images_with_metadata(
|
||||
default=None, description="Whether to list intermediate images"
|
||||
),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None, description="The board id to filter by"
|
||||
default=None,
|
||||
description="The board id to filter by, provide 'none' for images without a board",
|
||||
),
|
||||
offset: int = Query(default=0, description="The page offset"),
|
||||
limit: int = Query(default=10, description="The number of images per page"),
|
||||
@ -239,3 +239,36 @@ async def list_images_with_metadata(
|
||||
)
|
||||
|
||||
return image_dtos
|
||||
|
||||
|
||||
@images_router.post(
|
||||
"/",
|
||||
operation_id="get_images_by_names",
|
||||
response_model=GetImagesByNamesResult,
|
||||
)
|
||||
async def get_images_by_names(
|
||||
image_names: list[str] = Body(description="The names of the images to get"),
|
||||
) -> GetImagesByNamesResult:
|
||||
"""Gets a list of images"""
|
||||
|
||||
result = ApiDependencies.invoker.services.images.get_images_by_names(
|
||||
image_names
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@images_router.post(
|
||||
"/delete",
|
||||
operation_id="delete_many_images",
|
||||
response_model=DeleteManyImagesResult,
|
||||
)
|
||||
async def delete_many_images(
|
||||
image_names: list[str] = Body(description="The names of the images to delete"),
|
||||
) -> DeleteManyImagesResult:
|
||||
"""Deletes many images"""
|
||||
|
||||
try:
|
||||
return ApiDependencies.invoker.services.images.delete_many(image_names)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Failed to delete images")
|
||||
|
@ -1,5 +1,6 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from invokeai.app.util.metaenum import MetaEnum
|
||||
@ -88,3 +89,41 @@ class ProgressImage(BaseModel):
|
||||
width: int = Field(description="The effective width of the image in pixels")
|
||||
height: int = Field(description="The effective height of the image in pixels")
|
||||
dataURL: str = Field(description="The image data as a b64 data URL")
|
||||
|
||||
|
||||
class DeleteManyImagesResult(BaseModel):
|
||||
"""The result of a delete many image operation."""
|
||||
|
||||
deleted_images: list[str] = Field(
|
||||
description="The names of the images that were successfully deleted"
|
||||
)
|
||||
|
||||
|
||||
class AddManyImagesToBoardResult(BaseModel):
|
||||
"""The result of an add many images to board operation."""
|
||||
|
||||
board_id: str = Field(description="The id of the board the images were added to")
|
||||
added_images: list[str] = Field(
|
||||
description="The names of the images that were successfully added"
|
||||
)
|
||||
total: int = Field(description="The total number of images on the board")
|
||||
|
||||
|
||||
class RemoveManyImagesFromBoardResult(BaseModel):
|
||||
"""The result of a remove many images from their boards operation."""
|
||||
|
||||
removed_images: list[str] = Field(
|
||||
description="The names of the images that were successfully removed from their boards"
|
||||
)
|
||||
|
||||
|
||||
class GetAllBoardImagesForBoardResult(BaseModel):
|
||||
"""The result of a get all image names for board operation."""
|
||||
|
||||
board_id: str = Field(
|
||||
description="The id of the board with which the images are associated"
|
||||
)
|
||||
image_names: list[str] = Field(
|
||||
description="The names of the images that are associated with the board"
|
||||
)
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import sqlite3
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, cast
|
||||
|
||||
from invokeai.app.models.image import GetAllBoardImagesForBoardResult
|
||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||
from invokeai.app.services.models.image_record import (
|
||||
ImageRecord,
|
||||
deserialize_image_record,
|
||||
)
|
||||
ImageRecord, deserialize_image_record)
|
||||
|
||||
|
||||
class BoardImageRecordStorageBase(ABC):
|
||||
@ -25,18 +24,17 @@ class BoardImageRecordStorageBase(ABC):
|
||||
@abstractmethod
|
||||
def remove_image_from_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_name: str,
|
||||
) -> None:
|
||||
"""Removes an image from a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_images_for_board(
|
||||
def get_all_board_images_for_board(
|
||||
self,
|
||||
board_id: str,
|
||||
) -> OffsetPaginatedResults[ImageRecord]:
|
||||
"""Gets images for a board."""
|
||||
) -> GetAllBoardImagesForBoardResult:
|
||||
"""Gets all image names for a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@ -154,7 +152,6 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
||||
|
||||
def remove_image_from_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_name: str,
|
||||
) -> None:
|
||||
try:
|
||||
@ -162,9 +159,9 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
DELETE FROM board_images
|
||||
WHERE board_id = ? AND image_name = ?;
|
||||
WHERE image_name = ?;
|
||||
""",
|
||||
(board_id, image_name),
|
||||
(image_name,),
|
||||
)
|
||||
self._conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
@ -173,42 +170,32 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def get_images_for_board(
|
||||
def get_all_board_images_for_board(
|
||||
self,
|
||||
board_id: str,
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
) -> OffsetPaginatedResults[ImageRecord]:
|
||||
# TODO: this isn't paginated yet?
|
||||
) -> GetAllBoardImagesForBoardResult:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT images.*
|
||||
SELECT image_name
|
||||
FROM board_images
|
||||
INNER JOIN images ON board_images.image_name = images.image_name
|
||||
WHERE board_images.board_id = ?
|
||||
ORDER BY board_images.updated_at DESC;
|
||||
WHERE board_id = ?
|
||||
ORDER BY updated_at DESC;
|
||||
""",
|
||||
(board_id,),
|
||||
)
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
images = list(map(lambda r: deserialize_image_record(dict(r)), result))
|
||||
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT COUNT(*) FROM images WHERE 1=1;
|
||||
"""
|
||||
)
|
||||
count = cast(int, self._cursor.fetchone()[0])
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
image_names = list(map(lambda r: r[0], result))
|
||||
|
||||
except sqlite3.Error as e:
|
||||
self._conn.rollback()
|
||||
raise e
|
||||
finally:
|
||||
self._lock.release()
|
||||
return OffsetPaginatedResults(
|
||||
items=images, offset=offset, limit=limit, total=count
|
||||
return GetAllBoardImagesForBoardResult(
|
||||
board_id=board_id, image_names=image_names
|
||||
)
|
||||
|
||||
def get_board_for_image(
|
||||
|
@ -1,18 +1,19 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from logging import Logger
|
||||
from typing import List, Union, Optional
|
||||
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
|
||||
from invokeai.app.services.board_record_storage import (
|
||||
BoardRecord,
|
||||
BoardRecordStorageBase,
|
||||
)
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from invokeai.app.services.image_record_storage import (
|
||||
ImageRecordStorageBase,
|
||||
OffsetPaginatedResults,
|
||||
)
|
||||
from invokeai.app.models.image import (AddManyImagesToBoardResult,
|
||||
GetAllBoardImagesForBoardResult,
|
||||
RemoveManyImagesFromBoardResult)
|
||||
from invokeai.app.services.board_image_record_storage import \
|
||||
BoardImageRecordStorageBase
|
||||
from invokeai.app.services.board_record_storage import (BoardRecord,
|
||||
BoardRecordStorageBase)
|
||||
from invokeai.app.services.image_record_storage import (ImageRecordStorageBase,
|
||||
OffsetPaginatedResults)
|
||||
from invokeai.app.services.models.board_record import BoardDTO
|
||||
from invokeai.app.services.models.image_record import ImageDTO, image_record_to_dto
|
||||
from invokeai.app.services.models.image_record import (ImageDTO,
|
||||
image_record_to_dto)
|
||||
from invokeai.app.services.urls import UrlServiceBase
|
||||
|
||||
|
||||
@ -25,24 +26,40 @@ class BoardImagesServiceABC(ABC):
|
||||
board_id: str,
|
||||
image_name: str,
|
||||
) -> None:
|
||||
"""Adds an image to a board."""
|
||||
"""Adds an image to a board. If the image is on a different board, it is removed from that board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_many_images_to_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_names: list[str],
|
||||
) -> AddManyImagesToBoardResult:
|
||||
"""Adds many images to a board. If an image is on a different board, it is removed from that board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_image_from_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_name: str,
|
||||
) -> None:
|
||||
"""Removes an image from a board."""
|
||||
"""Removes an image from its board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_images_for_board(
|
||||
def remove_many_images_from_board(
|
||||
self,
|
||||
image_names: list[str],
|
||||
) -> RemoveManyImagesFromBoardResult:
|
||||
"""Removes many images from their board, if they had one."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_all_board_images_for_board(
|
||||
self,
|
||||
board_id: str,
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
"""Gets images for a board."""
|
||||
) -> GetAllBoardImagesForBoardResult:
|
||||
"""Gets all image names for a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@ -91,37 +108,59 @@ class BoardImagesService(BoardImagesServiceABC):
|
||||
) -> None:
|
||||
self._services.board_image_records.add_image_to_board(board_id, image_name)
|
||||
|
||||
def add_many_images_to_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_names: list[str],
|
||||
) -> AddManyImagesToBoardResult:
|
||||
added_images: list[str] = []
|
||||
|
||||
for image_name in image_names:
|
||||
try:
|
||||
self._services.board_image_records.add_image_to_board(
|
||||
board_id, image_name
|
||||
)
|
||||
added_images.append(image_name)
|
||||
except Exception as e:
|
||||
self._services.logger.exception(e)
|
||||
|
||||
total = self._services.board_image_records.get_image_count_for_board(board_id)
|
||||
|
||||
return AddManyImagesToBoardResult(
|
||||
board_id=board_id, added_images=added_images, total=total
|
||||
)
|
||||
|
||||
def remove_image_from_board(
|
||||
self,
|
||||
board_id: str,
|
||||
image_name: str,
|
||||
) -> None:
|
||||
self._services.board_image_records.remove_image_from_board(board_id, image_name)
|
||||
self._services.board_image_records.remove_image_from_board(image_name)
|
||||
|
||||
def get_images_for_board(
|
||||
def remove_many_images_from_board(
|
||||
self,
|
||||
image_names: list[str],
|
||||
) -> RemoveManyImagesFromBoardResult:
|
||||
removed_images: list[str] = []
|
||||
|
||||
for image_name in image_names:
|
||||
try:
|
||||
self._services.board_image_records.remove_image_from_board(image_name)
|
||||
removed_images.append(image_name)
|
||||
except Exception as e:
|
||||
self._services.logger.exception(e)
|
||||
|
||||
return RemoveManyImagesFromBoardResult(
|
||||
removed_images=removed_images,
|
||||
)
|
||||
|
||||
def get_all_board_images_for_board(
|
||||
self,
|
||||
board_id: str,
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
image_records = self._services.board_image_records.get_images_for_board(
|
||||
) -> GetAllBoardImagesForBoardResult:
|
||||
result = self._services.board_image_records.get_all_board_images_for_board(
|
||||
board_id
|
||||
)
|
||||
image_dtos = list(
|
||||
map(
|
||||
lambda r: image_record_to_dto(
|
||||
r,
|
||||
self._services.urls.get_image_url(r.image_name),
|
||||
self._services.urls.get_image_url(r.image_name, True),
|
||||
board_id,
|
||||
),
|
||||
image_records.items,
|
||||
)
|
||||
)
|
||||
return OffsetPaginatedResults[ImageDTO](
|
||||
items=image_dtos,
|
||||
offset=image_records.offset,
|
||||
limit=image_records.limit,
|
||||
total=image_records.total,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_board_for_image(
|
||||
self,
|
||||
@ -136,7 +175,7 @@ def board_record_to_dto(
|
||||
) -> BoardDTO:
|
||||
"""Converts a board record to a board DTO."""
|
||||
return BoardDTO(
|
||||
**board_record.dict(exclude={'cover_image_name'}),
|
||||
**board_record.dict(exclude={"cover_image_name"}),
|
||||
cover_image_name=cover_image_name,
|
||||
image_count=image_count,
|
||||
)
|
||||
|
@ -80,6 +80,11 @@ class ImageRecordStorageBase(ABC):
|
||||
"""Gets a page of image records."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_by_names(self, image_names: list[str]) -> list[ImageRecord]:
|
||||
"""Gets a list of image records by name."""
|
||||
pass
|
||||
|
||||
# TODO: The database has a nullable `deleted_at` column, currently unused.
|
||||
# Should we implement soft deletes? Would need coordination with ImageFileStorage.
|
||||
@abstractmethod
|
||||
@ -329,11 +334,15 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
query_params.append(is_intermediate)
|
||||
|
||||
if board_id is not None:
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
|
||||
query_params.append(board_id)
|
||||
if board_id == "none":
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
else:
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
query_params.append(board_id)
|
||||
|
||||
query_pagination = """--sql
|
||||
ORDER BY images.created_at DESC LIMIT ? OFFSET ?
|
||||
@ -365,6 +374,30 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
items=images, offset=offset, limit=limit, total=count
|
||||
)
|
||||
|
||||
def get_by_names(self, image_names: list[str]) -> list[ImageRecord]:
|
||||
try:
|
||||
placeholders = ",".join("?" for _ in image_names)
|
||||
|
||||
self._lock.acquire()
|
||||
|
||||
# Construct the SQLite query with the placeholders
|
||||
query = f"""--sql
|
||||
SELECT * FROM images
|
||||
WHERE image_name IN ({placeholders})
|
||||
"""
|
||||
|
||||
# Execute the query with the list of IDs as parameters
|
||||
self._cursor.execute(query, image_names)
|
||||
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
images = list(map(lambda r: deserialize_image_record(dict(r)), result))
|
||||
return images
|
||||
except sqlite3.Error as e:
|
||||
self._conn.rollback()
|
||||
raise e
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def delete(self, image_name: str) -> None:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
@ -465,9 +498,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def get_most_recent_image_for_board(
|
||||
self, board_id: str
|
||||
) -> Optional[ImageRecord]:
|
||||
def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
self._cursor.execute(
|
||||
|
@ -1,37 +1,27 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from logging import Logger
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from PIL.Image import Image as PILImageType
|
||||
|
||||
from invokeai.app.models.image import (
|
||||
ImageCategory,
|
||||
ResourceOrigin,
|
||||
InvalidImageCategoryException,
|
||||
InvalidOriginException,
|
||||
)
|
||||
from invokeai.app.models.image import (DeleteManyImagesResult, ImageCategory,
|
||||
InvalidImageCategoryException,
|
||||
InvalidOriginException, ResourceOrigin)
|
||||
from invokeai.app.models.metadata import ImageMetadata
|
||||
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
|
||||
from invokeai.app.services.image_record_storage import (
|
||||
ImageRecordDeleteException,
|
||||
ImageRecordNotFoundException,
|
||||
ImageRecordSaveException,
|
||||
ImageRecordStorageBase,
|
||||
OffsetPaginatedResults,
|
||||
)
|
||||
from invokeai.app.services.models.image_record import (
|
||||
ImageRecord,
|
||||
ImageDTO,
|
||||
ImageRecordChanges,
|
||||
image_record_to_dto,
|
||||
)
|
||||
from invokeai.app.services.board_image_record_storage import \
|
||||
BoardImageRecordStorageBase
|
||||
from invokeai.app.services.image_file_storage import (
|
||||
ImageFileDeleteException,
|
||||
ImageFileNotFoundException,
|
||||
ImageFileSaveException,
|
||||
ImageFileStorageBase,
|
||||
)
|
||||
from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults
|
||||
ImageFileDeleteException, ImageFileNotFoundException,
|
||||
ImageFileSaveException, ImageFileStorageBase)
|
||||
from invokeai.app.services.image_record_storage import (
|
||||
ImageRecordDeleteException, ImageRecordNotFoundException,
|
||||
ImageRecordSaveException, ImageRecordStorageBase, OffsetPaginatedResults)
|
||||
from invokeai.app.services.item_storage import ItemStorageABC
|
||||
from invokeai.app.services.metadata import MetadataServiceBase
|
||||
from invokeai.app.services.models.image_record import (GetImagesByNamesResult,
|
||||
ImageDTO, ImageRecord,
|
||||
ImageRecordChanges,
|
||||
image_record_to_dto)
|
||||
from invokeai.app.services.resource_name import NameServiceBase
|
||||
from invokeai.app.services.urls import UrlServiceBase
|
||||
|
||||
@ -107,13 +97,23 @@ class ImageServiceABC(ABC):
|
||||
"""Gets a paginated list of image DTOs."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_images_by_names(self, image_names: list[str]) -> GetImagesByNamesResult:
|
||||
"""Gets image DTOs by list of names."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, image_name: str):
|
||||
"""Deletes an image."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_images_on_board(self, board_id: str):
|
||||
def delete_many(self, image_names: list[str]) -> DeleteManyImagesResult:
|
||||
"""Deletes many images."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult:
|
||||
"""Deletes all images on a board."""
|
||||
pass
|
||||
|
||||
@ -332,6 +332,28 @@ class ImageService(ImageServiceABC):
|
||||
self._services.logger.error("Problem getting paginated image DTOs")
|
||||
raise e
|
||||
|
||||
def get_images_by_names(self, image_names: list[str]) -> GetImagesByNamesResult:
|
||||
try:
|
||||
image_records = self._services.image_records.get_by_names(image_names)
|
||||
|
||||
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),
|
||||
self._services.board_image_records.get_board_for_image(
|
||||
r.image_name
|
||||
),
|
||||
),
|
||||
image_records,
|
||||
)
|
||||
)
|
||||
return GetImagesByNamesResult(image_dtos=image_dtos)
|
||||
except Exception as e:
|
||||
self._services.logger.error("Problem getting image DTOs from names")
|
||||
raise e
|
||||
|
||||
def delete(self, image_name: str):
|
||||
try:
|
||||
self._services.image_files.delete(image_name)
|
||||
@ -346,18 +368,36 @@ class ImageService(ImageServiceABC):
|
||||
self._services.logger.error("Problem deleting image record and file")
|
||||
raise e
|
||||
|
||||
def delete_images_on_board(self, board_id: str):
|
||||
def delete_many(self, image_names: list[str]) -> DeleteManyImagesResult:
|
||||
deleted_images: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
self._services.image_files.delete(image_name)
|
||||
self._services.image_records.delete(image_name)
|
||||
deleted_images.append(image_name)
|
||||
except ImageRecordDeleteException:
|
||||
self._services.logger.error(f"Failed to delete image record")
|
||||
deleted_images.append(image_name)
|
||||
except ImageFileDeleteException:
|
||||
self._services.logger.error(f"Failed to delete image file")
|
||||
deleted_images.append(image_name)
|
||||
except Exception as e:
|
||||
self._services.logger.error("Problem deleting image record and file")
|
||||
deleted_images.append(image_name)
|
||||
return DeleteManyImagesResult(deleted_images=deleted_images)
|
||||
|
||||
def delete_images_on_board(self, board_id: str) -> DeleteManyImagesResult:
|
||||
try:
|
||||
images = self._services.board_image_records.get_images_for_board(board_id)
|
||||
image_name_list = list(
|
||||
map(
|
||||
lambda r: r.image_name,
|
||||
images.items,
|
||||
board_images = (
|
||||
self._services.board_image_records.get_all_board_images_for_board(
|
||||
board_id
|
||||
)
|
||||
)
|
||||
image_name_list = board_images.image_names
|
||||
for image_name in image_name_list:
|
||||
self._services.image_files.delete(image_name)
|
||||
self._services.image_records.delete_many(image_name_list)
|
||||
return DeleteManyImagesResult(deleted_images=board_images.image_names)
|
||||
except ImageRecordDeleteException:
|
||||
self._services.logger.error(f"Failed to delete image records")
|
||||
raise
|
||||
|
@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Extra, Field, StrictBool, StrictStr
|
||||
|
||||
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
||||
from invokeai.app.models.metadata import ImageMetadata
|
||||
from invokeai.app.util.misc import get_iso_timestamp
|
||||
@ -95,8 +97,19 @@ class ImageDTO(ImageRecord, ImageUrlsDTO):
|
||||
pass
|
||||
|
||||
|
||||
class GetImagesByNamesResult(BaseModel):
|
||||
"""The result of a get all image names for board operation."""
|
||||
|
||||
image_dtos: list[ImageDTO] = Field(
|
||||
description="The names of the images that are associated with the board"
|
||||
)
|
||||
|
||||
|
||||
def image_record_to_dto(
|
||||
image_record: ImageRecord, image_url: str, thumbnail_url: str, board_id: Optional[str]
|
||||
image_record: ImageRecord,
|
||||
image_url: str,
|
||||
thumbnail_url: str,
|
||||
board_id: Optional[str],
|
||||
) -> ImageDTO:
|
||||
"""Converts an image record to an image DTO."""
|
||||
return ImageDTO(
|
||||
|
@ -22,4 +22,4 @@ class LocalUrlService(UrlServiceBase):
|
||||
if thumbnail:
|
||||
return f"{self._base_url}/images/{image_basename}/thumbnail"
|
||||
|
||||
return f"{self._base_url}/images/{image_basename}"
|
||||
return f"{self._base_url}/images/{image_basename}/full_size"
|
||||
|
@ -128,13 +128,13 @@
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"axios": "^1.4.0",
|
||||
"babel-plugin-transform-imports": "^2.0.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
@ -151,6 +151,8 @@
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"terser": "^5.18.1",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-css-injected-by-js": "^3.1.1",
|
||||
"vite-plugin-dts": "^2.3.0",
|
||||
|
@ -118,7 +118,7 @@
|
||||
"pinGallery": "Pin Gallery",
|
||||
"allImagesLoaded": "All Images Loaded",
|
||||
"loadMore": "Load More",
|
||||
"noImagesInGallery": "No Images In Gallery",
|
||||
"noImagesInGallery": "No Images to Display",
|
||||
"deleteImage": "Delete Image",
|
||||
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
|
||||
"deleteImagePermanent": "Deleted images cannot be restored.",
|
||||
|
@ -82,7 +82,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.dragData.payloadType === 'BATCH_SELECTION') {
|
||||
if (props.dragData.payloadType === 'IMAGE_NAMES') {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@ -95,26 +95,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
||||
...STYLES,
|
||||
}}
|
||||
>
|
||||
<Heading>{batchSelectionCount}</Heading>
|
||||
<Heading size="sm">Images</Heading>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.dragData.payloadType === 'GALLERY_SELECTION') {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
cursor: 'none',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDir: 'column',
|
||||
...STYLES,
|
||||
}}
|
||||
>
|
||||
<Heading>{gallerySelectionCount}</Heading>
|
||||
<Heading>{props.dragData.payload.image_names.length}</Heading>
|
||||
<Heading size="sm">Images</Heading>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -6,18 +6,18 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||
import DragPreview from './DragPreview';
|
||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragStartEvent,
|
||||
TypesafeDraggableData,
|
||||
} from './typesafeDnd';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imageDropped } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped';
|
||||
|
||||
type ImageDndContextProps = PropsWithChildren;
|
||||
|
||||
@ -42,18 +42,18 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
||||
if (!activeData || !overData) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageDropped({ overData, activeData }));
|
||||
dispatch(dndDropped({ overData, activeData }));
|
||||
setActiveDragData(null);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { delay: 150, tolerance: 5 },
|
||||
activationConstraint: { distance: 10 },
|
||||
});
|
||||
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { delay: 150, tolerance: 5 },
|
||||
activationConstraint: { distance: 10 },
|
||||
});
|
||||
|
||||
// TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos
|
||||
|
@ -77,18 +77,14 @@ export type ImageDraggableData = BaseDragData & {
|
||||
payload: { imageDTO: ImageDTO };
|
||||
};
|
||||
|
||||
export type GallerySelectionDraggableData = BaseDragData & {
|
||||
payloadType: 'GALLERY_SELECTION';
|
||||
};
|
||||
|
||||
export type BatchSelectionDraggableData = BaseDragData & {
|
||||
payloadType: 'BATCH_SELECTION';
|
||||
export type ImageNamesDraggableData = BaseDragData & {
|
||||
payloadType: 'IMAGE_NAMES';
|
||||
payload: { image_names: string[] };
|
||||
};
|
||||
|
||||
export type TypesafeDraggableData =
|
||||
| ImageDraggableData
|
||||
| GallerySelectionDraggableData
|
||||
| BatchSelectionDraggableData;
|
||||
| ImageNamesDraggableData;
|
||||
|
||||
interface UseDroppableTypesafeArguments
|
||||
extends Omit<UseDroppableArguments, 'data'> {
|
||||
@ -159,13 +155,11 @@ export const isValidDrop = (
|
||||
case 'SET_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_MULTI_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||
case 'ADD_TO_BATCH':
|
||||
return payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION';
|
||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||
case 'MOVE_BOARD':
|
||||
return (
|
||||
payloadType === 'IMAGE_DTO' || 'GALLERY_SELECTION' || 'BATCH_SELECTION'
|
||||
);
|
||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||
import { useAddBoardImageMutation } from 'services/api/endpoints/boardImages';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useAddImageToBoardMutation } from 'services/api/endpoints/boardImages';
|
||||
|
||||
export type ImageUsage = {
|
||||
isInitialImage: boolean;
|
||||
@ -41,7 +41,7 @@ export const AddImageToBoardContextProvider = (props: Props) => {
|
||||
const [imageToMove, setImageToMove] = useState<ImageDTO>();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const [addImageToBoard, result] = useAddImageToBoardMutation();
|
||||
const [addImageToBoard, result] = useAddBoardImageMutation();
|
||||
|
||||
// Clean up after deleting or dismissing the modal
|
||||
const closeAndClearImageToDelete = useCallback(() => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { initialBatchState } from 'features/batch/store/batchSlice';
|
||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||
import { initialControlNetState } from 'features/controlNet/store/controlNetSlice';
|
||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||
@ -17,6 +18,7 @@ const initialStates: {
|
||||
} = {
|
||||
canvas: initialCanvasState,
|
||||
gallery: initialGalleryState,
|
||||
batch: initialBatchState,
|
||||
generation: initialGenerationState,
|
||||
lightbox: initialLightboxState,
|
||||
nodes: initialNodesState,
|
||||
|
@ -1,4 +1,8 @@
|
||||
/**
|
||||
* This is a list of actions that should be excluded in the Redux DevTools.
|
||||
*/
|
||||
export const actionsDenylist = [
|
||||
// very spammy canvas actions
|
||||
'canvas/setCursorPosition',
|
||||
'canvas/setStageCoordinates',
|
||||
'canvas/setStageScale',
|
||||
@ -7,7 +11,11 @@ export const actionsDenylist = [
|
||||
'canvas/setBoundingBoxDimensions',
|
||||
'canvas/setIsDrawing',
|
||||
'canvas/addPointToCurrentLine',
|
||||
// bazillions during generation
|
||||
'socket/socketGeneratorProgress',
|
||||
'socket/appSocketGeneratorProgress',
|
||||
// every time user presses shift
|
||||
'hotkeys/shiftKeyPressed',
|
||||
// this happens after every state change
|
||||
'@@REMEMBER_PERSISTED',
|
||||
];
|
||||
|
@ -7,6 +7,8 @@ import {
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
import type { AppDispatch, RootState } from '../../store';
|
||||
import { addBoardApiListeners } from './listeners/addBoardApiListeners';
|
||||
import { addAddBoardToBatchListener } from './listeners/addBoardToBatch';
|
||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||
import { addAppStartedListener } from './listeners/appStarted';
|
||||
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
|
||||
@ -18,9 +20,9 @@ import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGaller
|
||||
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
|
||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||
import {
|
||||
addImageAddedToBoardFulfilledListener,
|
||||
addImageAddedToBoardRejectedListener,
|
||||
} from './listeners/imageAddedToBoard';
|
||||
addImageDTOReceivedFulfilledListener,
|
||||
addImageDTOReceivedRejectedListener,
|
||||
} from './listeners/imageDTOReceived';
|
||||
import {
|
||||
addImageDeletedFulfilledListener,
|
||||
addImageDeletedPendingListener,
|
||||
@ -28,14 +30,6 @@ import {
|
||||
addRequestedImageDeletionListener,
|
||||
} from './listeners/imageDeleted';
|
||||
import { addImageDroppedListener } from './listeners/imageDropped';
|
||||
import {
|
||||
addImageMetadataReceivedFulfilledListener,
|
||||
addImageMetadataReceivedRejectedListener,
|
||||
} from './listeners/imageMetadataReceived';
|
||||
import {
|
||||
addImageRemovedFromBoardFulfilledListener,
|
||||
addImageRemovedFromBoardRejectedListener,
|
||||
} from './listeners/imageRemovedFromBoard';
|
||||
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
|
||||
import {
|
||||
addImageUpdatedFulfilledListener,
|
||||
@ -45,18 +39,11 @@ import {
|
||||
addImageUploadedFulfilledListener,
|
||||
addImageUploadedRejectedListener,
|
||||
} from './listeners/imageUploaded';
|
||||
import {
|
||||
addImageUrlsReceivedFulfilledListener,
|
||||
addImageUrlsReceivedRejectedListener,
|
||||
} from './listeners/imageUrlsReceived';
|
||||
import { addImagesLoadedListener } from './listeners/imagesLoaded';
|
||||
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
|
||||
import { addModelSelectedListener } from './listeners/modelSelected';
|
||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||
import {
|
||||
addReceivedPageOfImagesFulfilledListener,
|
||||
addReceivedPageOfImagesRejectedListener,
|
||||
} from './listeners/receivedPageOfImages';
|
||||
import { addSelectionAddedToBatchListener } from './listeners/selectionAddedToBatch';
|
||||
import { addReceivedPageOfImagesListener } from './listeners/receivedPageOfImages';
|
||||
import {
|
||||
addSessionCanceledFulfilledListener,
|
||||
addSessionCanceledPendingListener,
|
||||
@ -132,12 +119,8 @@ addRequestedBoardImageDeletionListener();
|
||||
addImageToDeleteSelectedListener();
|
||||
|
||||
// Image metadata
|
||||
addImageMetadataReceivedFulfilledListener();
|
||||
addImageMetadataReceivedRejectedListener();
|
||||
|
||||
// Image URLs
|
||||
addImageUrlsReceivedFulfilledListener();
|
||||
addImageUrlsReceivedRejectedListener();
|
||||
addImageDTOReceivedFulfilledListener();
|
||||
addImageDTOReceivedRejectedListener();
|
||||
|
||||
// User Invoked
|
||||
addUserInvokedCanvasListener();
|
||||
@ -193,8 +176,8 @@ addSessionCanceledFulfilledListener();
|
||||
addSessionCanceledRejectedListener();
|
||||
|
||||
// Fetching images
|
||||
addReceivedPageOfImagesFulfilledListener();
|
||||
addReceivedPageOfImagesRejectedListener();
|
||||
addReceivedPageOfImagesListener();
|
||||
addImagesLoadedListener();
|
||||
|
||||
// ControlNet
|
||||
addControlNetImageProcessedListener();
|
||||
@ -204,17 +187,15 @@ addControlNetAutoProcessListener();
|
||||
// addUpdateImageUrlsOnConnectListener();
|
||||
|
||||
// Boards
|
||||
addImageAddedToBoardFulfilledListener();
|
||||
addImageAddedToBoardRejectedListener();
|
||||
addImageRemovedFromBoardFulfilledListener();
|
||||
addImageRemovedFromBoardRejectedListener();
|
||||
addBoardApiListeners();
|
||||
addBoardIdSelectedListener();
|
||||
|
||||
// Node schemas
|
||||
addReceivedOpenAPISchemaListener();
|
||||
|
||||
// Batches
|
||||
addSelectionAddedToBatchListener();
|
||||
// addSelectionAddedToBatchListener();
|
||||
addAddBoardToBatchListener();
|
||||
|
||||
// DND
|
||||
addImageDroppedListener();
|
||||
|
@ -0,0 +1,105 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addBoardApiListeners = () => {
|
||||
// add image to board - fulfilled
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addBoardImage.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Image added to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// add image to board - rejected
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addBoardImage.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Problem adding image to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// remove image from board - fulfilled
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.deleteBoardImage.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug({ data: { image_name } }, 'Image removed from board');
|
||||
},
|
||||
});
|
||||
|
||||
// remove image from board - rejected
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.deleteBoardImage.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const image_name = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { image_name } },
|
||||
'Problem removing image from board'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// many images added to board - fulfilled
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addManyBoardImages.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_names } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_names } },
|
||||
'Images added to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// many images added to board - rejected
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addManyBoardImages.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_names } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_names } },
|
||||
'Problem adding many images to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// remove many images from board - fulfilled
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.deleteManyBoardImages.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { image_names } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug({ data: { image_names } }, 'Images removed from board');
|
||||
},
|
||||
});
|
||||
|
||||
// remove many images from board - rejected
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.deleteManyBoardImages.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const image_names = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { image_names } },
|
||||
'Problem removing many images from board'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imagesAddedToBatch } from 'features/batch/store/batchSlice';
|
||||
import { boardImageNamesReceived } from 'services/api/thunks/boardImages';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'batch' });
|
||||
|
||||
export const boardAddedToBatch = createAction<{ board_id: string }>(
|
||||
'batch/boardAddedToBatch'
|
||||
);
|
||||
|
||||
export const addAddBoardToBatchListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: boardAddedToBatch,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const { board_id } = action.payload;
|
||||
|
||||
const { requestId } = dispatch(boardImageNamesReceived({ board_id }));
|
||||
|
||||
const [{ payload }] = await take(
|
||||
(
|
||||
action
|
||||
): action is ReturnType<typeof boardImageNamesReceived.fulfilled> =>
|
||||
action.meta.requestId === requestId
|
||||
);
|
||||
|
||||
dispatch(imagesAddedToBatch(payload.image_names));
|
||||
},
|
||||
});
|
||||
};
|
@ -1,9 +1,4 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
INITIAL_IMAGE_LIMIT,
|
||||
isLoadingChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const appStarted = createAction('app/appStarted');
|
||||
@ -15,29 +10,27 @@ export const addAppStartedListener = () => {
|
||||
action,
|
||||
{ getState, dispatch, unsubscribe, cancelActiveListeners }
|
||||
) => {
|
||||
cancelActiveListeners();
|
||||
unsubscribe();
|
||||
// fill up the gallery tab with images
|
||||
await dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: ['general'],
|
||||
is_intermediate: false,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
// fill up the assets tab with images
|
||||
await dispatch(
|
||||
receivedPageOfImages({
|
||||
categories: ['control', 'mask', 'user', 'other'],
|
||||
is_intermediate: false,
|
||||
offset: 0,
|
||||
limit: INITIAL_IMAGE_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(isLoadingChanged(false));
|
||||
// cancelActiveListeners();
|
||||
// unsubscribe();
|
||||
// // fill up the gallery tab with images
|
||||
// await dispatch(
|
||||
// receivedPageOfImages({
|
||||
// categories: ['general'],
|
||||
// is_intermediate: false,
|
||||
// offset: 0,
|
||||
// // limit: INITIAL_IMAGE_LIMIT,
|
||||
// })
|
||||
// );
|
||||
// // fill up the assets tab with images
|
||||
// await dispatch(
|
||||
// receivedPageOfImages({
|
||||
// categories: ['control', 'mask', 'user', 'other'],
|
||||
// is_intermediate: false,
|
||||
// offset: 0,
|
||||
// // limit: INITIAL_IMAGE_LIMIT,
|
||||
// })
|
||||
// );
|
||||
// dispatch(isLoadingChanged(false));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,15 +1,6 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { startAppListening } from '..';
|
||||
import {
|
||||
imageSelected,
|
||||
selectImagesAll,
|
||||
boardIdSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
IMAGES_PER_PAGE,
|
||||
receivedPageOfImages,
|
||||
} from 'services/api/thunks/image';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
@ -17,49 +8,40 @@ export const addBoardIdSelectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: boardIdSelected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const board_id = action.payload;
|
||||
|
||||
// we need to check if we need to fetch more images
|
||||
|
||||
const state = getState();
|
||||
const allImages = selectImagesAll(state);
|
||||
|
||||
if (!board_id) {
|
||||
// a board was unselected
|
||||
dispatch(imageSelected(allImages[0]?.image_name));
|
||||
return;
|
||||
}
|
||||
|
||||
const { categories } = state.gallery;
|
||||
|
||||
const filteredImages = allImages.filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
|
||||
// get the board from the cache
|
||||
const { data: boards } =
|
||||
boardsApi.endpoints.listAllBoards.select()(state);
|
||||
const board = boards?.find((b) => b.board_id === board_id);
|
||||
|
||||
if (!board) {
|
||||
// can't find the board in cache...
|
||||
dispatch(imageSelected(allImages[0]?.image_name));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(imageSelected(board.cover_image_name ?? null));
|
||||
|
||||
// if we haven't loaded one full page of images from this board, load more
|
||||
if (
|
||||
filteredImages.length < board.image_count &&
|
||||
filteredImages.length < IMAGES_PER_PAGE
|
||||
) {
|
||||
dispatch(
|
||||
receivedPageOfImages({ categories, board_id, is_intermediate: false })
|
||||
);
|
||||
}
|
||||
// const board_id = action.payload;
|
||||
// // we need to check if we need to fetch more images
|
||||
// const state = getState();
|
||||
// const allImages = selectImagesAll(state);
|
||||
// if (!board_id) {
|
||||
// // a board was unselected
|
||||
// dispatch(imageSelected(allImages[0]?.image_name));
|
||||
// return;
|
||||
// }
|
||||
// const { categories } = state.gallery;
|
||||
// const filteredImages = allImages.filter((i) => {
|
||||
// const isInCategory = categories.includes(i.image_category);
|
||||
// const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
||||
// return isInCategory && isInSelectedBoard;
|
||||
// });
|
||||
// // get the board from the cache
|
||||
// const { data: boards } =
|
||||
// boardsApi.endpoints.listAllBoards.select()(state);
|
||||
// const board = boards?.find((b) => b.board_id === board_id);
|
||||
// if (!board) {
|
||||
// // can't find the board in cache...
|
||||
// dispatch(imageSelected(allImages[0]?.image_name));
|
||||
// return;
|
||||
// }
|
||||
// dispatch(imageSelected(board.cover_image_name ?? null));
|
||||
// // if we haven't loaded one full page of images from this board, load more
|
||||
// if (
|
||||
// filteredImages.length < board.image_count &&
|
||||
// filteredImages.length < IMAGES_PER_PAGE
|
||||
// ) {
|
||||
// dispatch(
|
||||
// receivedPageOfImages({ categories, board_id, is_intermediate: false })
|
||||
// );
|
||||
// }
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -68,43 +50,36 @@ export const addBoardIdSelected_changeSelectedImage_listener = () => {
|
||||
startAppListening({
|
||||
actionCreator: boardIdSelected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const board_id = action.payload;
|
||||
|
||||
const state = getState();
|
||||
|
||||
// we need to check if we need to fetch more images
|
||||
|
||||
if (!board_id) {
|
||||
// a board was unselected - we don't need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
const { categories } = state.gallery;
|
||||
|
||||
const filteredImages = selectImagesAll(state).filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
|
||||
// get the board from the cache
|
||||
const { data: boards } =
|
||||
boardsApi.endpoints.listAllBoards.select()(state);
|
||||
const board = boards?.find((b) => b.board_id === board_id);
|
||||
if (!board) {
|
||||
// can't find the board in cache...
|
||||
return;
|
||||
}
|
||||
|
||||
// if we haven't loaded one full page of images from this board, load more
|
||||
if (
|
||||
filteredImages.length < board.image_count &&
|
||||
filteredImages.length < IMAGES_PER_PAGE
|
||||
) {
|
||||
dispatch(
|
||||
receivedPageOfImages({ categories, board_id, is_intermediate: false })
|
||||
);
|
||||
}
|
||||
// const board_id = action.payload;
|
||||
// const state = getState();
|
||||
// // we need to check if we need to fetch more images
|
||||
// if (!board_id) {
|
||||
// // a board was unselected - we don't need to do anything
|
||||
// return;
|
||||
// }
|
||||
// const { categories } = state.gallery;
|
||||
// const filteredImages = selectImagesAll(state).filter((i) => {
|
||||
// const isInCategory = categories.includes(i.image_category);
|
||||
// const isInSelectedBoard = board_id ? i.board_id === board_id : true;
|
||||
// return isInCategory && isInSelectedBoard;
|
||||
// });
|
||||
// // get the board from the cache
|
||||
// const { data: boards } =
|
||||
// boardsApi.endpoints.listAllBoards.select()(state);
|
||||
// const board = boards?.find((b) => b.board_id === board_id);
|
||||
// if (!board) {
|
||||
// // can't find the board in cache...
|
||||
// return;
|
||||
// }
|
||||
// // if we haven't loaded one full page of images from this board, load more
|
||||
// if (
|
||||
// filteredImages.length < board.image_count &&
|
||||
// filteredImages.length < IMAGES_PER_PAGE
|
||||
// ) {
|
||||
// dispatch(
|
||||
// receivedPageOfImages({ categories, board_id, is_intermediate: false })
|
||||
// );
|
||||
// }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,21 +1,19 @@
|
||||
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 { requestedBoardImagesDeletion as requestedBoardAndImagesDeletion } from 'features/gallery/store/actions';
|
||||
import {
|
||||
imageSelected,
|
||||
selectImagesById,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { LIST_TAG, api } from 'services/api';
|
||||
import { startAppListening } from '..';
|
||||
import { boardsApi } from '../../../../../services/api/endpoints/boards';
|
||||
|
||||
export const addRequestedBoardImageDeletionListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: requestedBoardImagesDeletion,
|
||||
actionCreator: requestedBoardAndImagesDeletion,
|
||||
effect: async (action, { dispatch, getState, condition }) => {
|
||||
const { board, imagesUsage } = action.payload;
|
||||
|
||||
@ -51,20 +49,12 @@ export const addRequestedBoardImageDeletionListener = () => {
|
||||
dispatch(nodeEditorReset());
|
||||
}
|
||||
|
||||
// Preemptively remove from gallery
|
||||
const images = selectImagesAll(state).reduce((acc: string[], img) => {
|
||||
if (img.board_id === board_id) {
|
||||
acc.push(img.image_name);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
dispatch(imagesRemoved(images));
|
||||
|
||||
// Delete from server
|
||||
dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id));
|
||||
const result =
|
||||
boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state);
|
||||
const { isSuccess } = result;
|
||||
|
||||
const { isSuccess, data } = result;
|
||||
|
||||
// Wait for successful deletion, then trigger boards to re-fetch
|
||||
const wasBoardDeleted = await condition(() => !!isSuccess, 30000);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUploaded } from 'services/api/thunks/image';
|
||||
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imageUploaded } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
@ -49,7 +49,11 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
|
||||
);
|
||||
|
||||
dispatch(imageUpserted(uploadedImageDTO));
|
||||
imagesApi.util.upsertQueryData(
|
||||
'getImageDTO',
|
||||
uploadedImageDTO.image_name,
|
||||
uploadedImageDTO
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/api/thunks/image';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||
import { Graph } from 'services/api/types';
|
||||
import { sessionCreated } from 'services/api/thunks/session';
|
||||
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
import { isImageOutput } from 'services/api/guards';
|
||||
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { sessionReadyToInvoke } from 'features/system/store/actions';
|
||||
import { isImageOutput } from 'services/api/guards';
|
||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
||||
import { sessionCreated } from 'services/api/thunks/session';
|
||||
import { Graph } from 'services/api/types';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'controlNet' });
|
||||
|
||||
@ -63,10 +63,8 @@ export const addControlNetImageProcessedListener = () => {
|
||||
|
||||
// Wait for the ImageDTO to be received
|
||||
const [imageMetadataReceivedAction] = await take(
|
||||
(
|
||||
action
|
||||
): action is ReturnType<typeof imageMetadataReceived.fulfilled> =>
|
||||
imageMetadataReceived.fulfilled.match(action) &&
|
||||
(action): action is ReturnType<typeof imageDTOReceived.fulfilled> =>
|
||||
imageDTOReceived.fulfilled.match(action) &&
|
||||
action.payload.image_name === image_name
|
||||
);
|
||||
const processedControlImage = imageMetadataReceivedAction.payload;
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/api/thunks/image';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addImageAddedToBoardFulfilledListener = () => {
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addImageToBoard.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Image added to board'
|
||||
);
|
||||
|
||||
dispatch(
|
||||
imageMetadataReceived({
|
||||
image_name,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageAddedToBoardRejectedListener = () => {
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.addImageToBoard.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Problem adding image to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -1,13 +1,13 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived, imageUpdated } from 'services/api/thunks/image';
|
||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
export const addImageMetadataReceivedFulfilledListener = () => {
|
||||
export const addImageDTOReceivedFulfilledListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageMetadataReceived.fulfilled,
|
||||
actionCreator: imageDTOReceived.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const image = action.payload;
|
||||
|
||||
@ -33,14 +33,14 @@ export const addImageMetadataReceivedFulfilledListener = () => {
|
||||
}
|
||||
|
||||
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
||||
dispatch(imageUpserted(image));
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageMetadataReceivedRejectedListener = () => {
|
||||
export const addImageDTOReceivedRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageMetadataReceived.rejected,
|
||||
actionCreator: imageDTOReceived.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
moduleLog.debug(
|
||||
{ data: { image: action.meta.arg } },
|
@ -1,11 +1,8 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||
import {
|
||||
imageRemoved,
|
||||
imageSelected,
|
||||
selectFilteredImages,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { selectFilteredImages } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
imageDeletionConfirmed,
|
||||
isModalOpenChanged,
|
||||
@ -80,9 +77,6 @@ export const addRequestedImageDeletionListener = () => {
|
||||
dispatch(nodeEditorReset());
|
||||
}
|
||||
|
||||
// Preemptively remove from gallery
|
||||
dispatch(imageRemoved(image_name));
|
||||
|
||||
// Delete from server
|
||||
const { requestId } = dispatch(imageDeleted({ image_name }));
|
||||
|
||||
@ -91,7 +85,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
(action): action is ReturnType<typeof imageDeleted.fulfilled> =>
|
||||
imageDeleted.fulfilled.match(action) &&
|
||||
action.meta.requestId === requestId,
|
||||
30000
|
||||
30_000
|
||||
);
|
||||
|
||||
if (wasImageDeleted) {
|
||||
|
@ -21,57 +21,66 @@ import { startAppListening } from '../';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'dnd' });
|
||||
|
||||
export const imageDropped = createAction<{
|
||||
export const dndDropped = createAction<{
|
||||
overData: TypesafeDroppableData;
|
||||
activeData: TypesafeDraggableData;
|
||||
}>('dnd/imageDropped');
|
||||
}>('dnd/dndDropped');
|
||||
|
||||
export const addImageDroppedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageDropped,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
actionCreator: dndDropped,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const { activeData, overData } = action.payload;
|
||||
const { actionType } = overData;
|
||||
const state = getState();
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { activeData, overData } },
|
||||
'Image or selection dropped'
|
||||
);
|
||||
|
||||
// set current image
|
||||
if (
|
||||
actionType === 'SET_CURRENT_IMAGE' &&
|
||||
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(imageSelected(activeData.payload.imageDTO.image_name));
|
||||
return;
|
||||
}
|
||||
|
||||
// set initial image
|
||||
if (
|
||||
actionType === 'SET_INITIAL_IMAGE' &&
|
||||
overData.actionType === 'SET_INITIAL_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(initialImageChanged(activeData.payload.imageDTO));
|
||||
return;
|
||||
}
|
||||
|
||||
// add image to batch
|
||||
if (
|
||||
actionType === 'ADD_TO_BATCH' &&
|
||||
overData.actionType === 'ADD_TO_BATCH' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(imageAddedToBatch(activeData.payload.imageDTO.image_name));
|
||||
return;
|
||||
}
|
||||
|
||||
// add multiple images to batch
|
||||
if (
|
||||
actionType === 'ADD_TO_BATCH' &&
|
||||
activeData.payloadType === 'GALLERY_SELECTION'
|
||||
overData.actionType === 'ADD_TO_BATCH' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES'
|
||||
) {
|
||||
dispatch(imagesAddedToBatch(state.gallery.selection));
|
||||
dispatch(imagesAddedToBatch(activeData.payload.image_names));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// set control image
|
||||
if (
|
||||
actionType === 'SET_CONTROLNET_IMAGE' &&
|
||||
overData.actionType === 'SET_CONTROLNET_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
@ -82,20 +91,22 @@ export const addImageDroppedListener = () => {
|
||||
controlNetId,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set canvas image
|
||||
if (
|
||||
actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
||||
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(setInitialCanvasImage(activeData.payload.imageDTO));
|
||||
return;
|
||||
}
|
||||
|
||||
// set nodes image
|
||||
if (
|
||||
actionType === 'SET_NODES_IMAGE' &&
|
||||
overData.actionType === 'SET_NODES_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
@ -107,11 +118,12 @@ export const addImageDroppedListener = () => {
|
||||
value: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set multiple nodes images (single image handler)
|
||||
if (
|
||||
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
@ -123,43 +135,30 @@ export const addImageDroppedListener = () => {
|
||||
value: [activeData.payload.imageDTO],
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set multiple nodes images (multiple images handler)
|
||||
if (
|
||||
actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||
activeData.payloadType === 'GALLERY_SELECTION'
|
||||
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES'
|
||||
) {
|
||||
const { fieldName, nodeId } = overData.context;
|
||||
dispatch(
|
||||
imageCollectionFieldValueChanged({
|
||||
nodeId,
|
||||
fieldName,
|
||||
value: state.gallery.selection.map((image_name) => ({
|
||||
value: activeData.payload.image_names.map((image_name) => ({
|
||||
image_name,
|
||||
})),
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove image from board
|
||||
// TODO: remove board_id from `removeImageFromBoard()` endpoint
|
||||
// TODO: handle multiple images
|
||||
// if (
|
||||
// actionType === 'MOVE_BOARD' &&
|
||||
// activeData.payloadType === 'IMAGE_DTO' &&
|
||||
// activeData.payload.imageDTO &&
|
||||
// overData.boardId !== null
|
||||
// ) {
|
||||
// const { image_name } = activeData.payload.imageDTO;
|
||||
// dispatch(
|
||||
// boardImagesApi.endpoints.removeImageFromBoard.initiate({ image_name })
|
||||
// );
|
||||
// }
|
||||
|
||||
// add image to board
|
||||
if (
|
||||
actionType === 'MOVE_BOARD' &&
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO &&
|
||||
overData.context.boardId
|
||||
@ -167,22 +166,89 @@ export const addImageDroppedListener = () => {
|
||||
const { image_name } = activeData.payload.imageDTO;
|
||||
const { boardId } = overData.context;
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.addImageToBoard.initiate({
|
||||
boardImagesApi.endpoints.addBoardImage.initiate({
|
||||
image_name,
|
||||
board_id: boardId,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// add multiple images to board
|
||||
// TODO: add endpoint
|
||||
// if (
|
||||
// actionType === 'ADD_TO_BATCH' &&
|
||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
||||
// activeData.payload.imageDTONames
|
||||
// ) {
|
||||
// dispatch(boardImagesApi.endpoints.addImagesToBoard.intiate({}));
|
||||
// }
|
||||
// remove image from board
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO &&
|
||||
overData.context.boardId === null
|
||||
) {
|
||||
const { image_name } = activeData.payload.imageDTO;
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.deleteBoardImage.initiate({ image_name })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// add gallery selection to board
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||
overData.context.boardId
|
||||
) {
|
||||
console.log('adding gallery selection to board');
|
||||
const board_id = overData.context.boardId;
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||
board_id,
|
||||
image_names: activeData.payload.image_names,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove gallery selection from board
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||
overData.context.boardId === null
|
||||
) {
|
||||
console.log('removing gallery selection to board');
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||
image_names: activeData.payload.image_names,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// add batch selection to board
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||
overData.context.boardId
|
||||
) {
|
||||
const board_id = overData.context.boardId;
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.addManyBoardImages.initiate({
|
||||
board_id,
|
||||
image_names: activeData.payload.image_names,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove batch selection from board
|
||||
if (
|
||||
overData.actionType === 'MOVE_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_NAMES' &&
|
||||
overData.context.boardId === null
|
||||
) {
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
||||
image_names: activeData.payload.image_names,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/api/thunks/image';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'boards' });
|
||||
|
||||
export const addImageRemovedFromBoardFulfilledListener = () => {
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchFulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Image added to board'
|
||||
);
|
||||
|
||||
dispatch(
|
||||
imageMetadataReceived({
|
||||
image_name,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageRemovedFromBoardRejectedListener = () => {
|
||||
startAppListening({
|
||||
matcher: boardImagesApi.endpoints.removeImageFromBoard.matchRejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
|
||||
moduleLog.debug(
|
||||
{ data: { board_id, image_name } },
|
||||
'Problem adding image to board'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -1,13 +1,13 @@
|
||||
import { startAppListening } from '..';
|
||||
import { imageUploaded } from 'services/api/thunks/image';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
||||
import { imageAddedToBatch } from 'features/batch/store/batchSlice';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { imageAddedToBatch } from 'features/batch/store/batchSlice';
|
||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imageUploaded } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -24,7 +24,8 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(imageUpserted(image));
|
||||
// update RTK query cache
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image);
|
||||
|
||||
const { postUploadAction } = action.meta.arg;
|
||||
|
||||
@ -84,8 +85,8 @@ export const addImageUploadedRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageUploaded.rejected,
|
||||
effect: (action, { dispatch }) => {
|
||||
const { formData, ...rest } = action.meta.arg;
|
||||
const sanitizedData = { arg: { ...rest, formData: { file: '<Blob>' } } };
|
||||
const { file, ...rest } = action.meta.arg;
|
||||
const sanitizedData = { arg: { ...rest, file: '<Blob>' } };
|
||||
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
|
||||
dispatch(
|
||||
addToast({
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageUrlsReceived } from 'services/api/thunks/image';
|
||||
import { imageUpdatedOne } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
export const addImageUrlsReceivedFulfilledListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageUrlsReceived.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const image = action.payload;
|
||||
moduleLog.debug({ data: { image } }, 'Image URLs received');
|
||||
|
||||
const { image_name, image_url, thumbnail_url } = image;
|
||||
|
||||
dispatch(
|
||||
imageUpdatedOne({
|
||||
id: image_name,
|
||||
changes: { image_url, thumbnail_url },
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageUrlsReceivedRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageUrlsReceived.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
moduleLog.debug(
|
||||
{ data: { image: action.meta.arg } },
|
||||
'Problem getting image URLs'
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesLoaded } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addImagesLoadedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imagesLoaded.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { items } = action.payload;
|
||||
moduleLog.debug(
|
||||
{ data: { payload: action.payload } },
|
||||
`Loaded ${items.length} images`
|
||||
);
|
||||
|
||||
items.forEach((image) => {
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: imagesLoaded.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
if (action.payload) {
|
||||
moduleLog.debug(
|
||||
{ data: { error: serializeError(action.payload) } },
|
||||
'Problem loading images'
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { receivedListOfImages } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addReceivedListOfImagesListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedListOfImages.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { image_dtos } = action.payload;
|
||||
moduleLog.debug(
|
||||
{ data: { payload: action.payload } },
|
||||
`Received ${image_dtos.length} images`
|
||||
);
|
||||
|
||||
image_dtos.forEach((image) => {
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: receivedListOfImages.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
if (action.payload) {
|
||||
moduleLog.debug(
|
||||
{ data: { error: serializeError(action.payload) } },
|
||||
'Problem receiving images'
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -1,12 +1,12 @@
|
||||
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';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addReceivedPageOfImagesFulfilledListener = () => {
|
||||
export const addReceivedPageOfImagesListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedPageOfImages.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
@ -16,6 +16,8 @@ export const addReceivedPageOfImagesFulfilledListener = () => {
|
||||
`Received ${items.length} images`
|
||||
);
|
||||
|
||||
// inject the received images into the RTK Query cache so consumers of the useGetImageDTOQuery
|
||||
// hook can get their data from the cache instead of fetching the data again
|
||||
items.forEach((image) => {
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
|
||||
@ -23,9 +25,7 @@ export const addReceivedPageOfImagesFulfilledListener = () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addReceivedPageOfImagesRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedPageOfImages.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
|
@ -1,19 +1,38 @@
|
||||
import { startAppListening } from '..';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import {
|
||||
imagesAddedToBatch,
|
||||
selectionAddedToBatch,
|
||||
} from 'features/batch/store/batchSlice';
|
||||
import { imagesAddedToBatch } from 'features/batch/store/batchSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { receivedListOfImages } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'batch' });
|
||||
|
||||
export const selectionAddedToBatch = createAction<{ images_names: string[] }>(
|
||||
'batch/selectionAddedToBatch'
|
||||
);
|
||||
|
||||
export const addSelectionAddedToBatchListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: selectionAddedToBatch,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { selection } = getState().gallery;
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const { requestId } = dispatch(
|
||||
receivedListOfImages(action.payload.images_names)
|
||||
);
|
||||
|
||||
dispatch(imagesAddedToBatch(selection));
|
||||
const [{ payload }] = await take(
|
||||
(action): action is ReturnType<typeof receivedListOfImages.fulfilled> =>
|
||||
action.meta.requestId === requestId
|
||||
);
|
||||
|
||||
moduleLog.debug({ data: { payload } }, 'receivedListOfImages');
|
||||
|
||||
dispatch(imagesAddedToBatch(payload.image_dtos));
|
||||
|
||||
payload.image_dtos.forEach((image) => {
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData('getImageDTO', image.image_name, image)
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import { startAppListening } from '../..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { isImageOutput } from 'services/api/guards';
|
||||
import { imageDTOReceived } from 'services/api/thunks/image';
|
||||
import { sessionCanceled } from 'services/api/thunks/session';
|
||||
import {
|
||||
appSocketInvocationComplete,
|
||||
socketInvocationComplete,
|
||||
} from 'services/events/actions';
|
||||
import { imageMetadataReceived } from 'services/api/thunks/image';
|
||||
import { sessionCanceled } from 'services/api/thunks/session';
|
||||
import { isImageOutput } from 'services/api/guards';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'socketio' });
|
||||
const nodeDenylist = ['dataURL_image'];
|
||||
@ -41,14 +42,16 @@ export const addInvocationCompleteEventListener = () => {
|
||||
const { image_name } = result.image;
|
||||
|
||||
// Get its metadata
|
||||
dispatch(
|
||||
imageMetadataReceived({
|
||||
const { requestId } = dispatch(
|
||||
imageDTOReceived({
|
||||
image_name,
|
||||
})
|
||||
);
|
||||
|
||||
const [{ payload: imageDTO }] = await take(
|
||||
imageMetadataReceived.fulfilled.match
|
||||
(action): action is ReturnType<typeof imageDTOReceived.fulfilled> =>
|
||||
imageDTOReceived.fulfilled.match(action) &&
|
||||
action.meta.requestId === requestId
|
||||
);
|
||||
|
||||
// Handle canvas image
|
||||
@ -59,13 +62,33 @@ export const addInvocationCompleteEventListener = () => {
|
||||
dispatch(addImageToStagingArea(imageDTO));
|
||||
}
|
||||
|
||||
// Update the RTK Query cache
|
||||
dispatch(
|
||||
imagesApi.util.upsertQueryData(
|
||||
'getImageDTO',
|
||||
imageDTO.image_name,
|
||||
imageDTO
|
||||
)
|
||||
);
|
||||
|
||||
if (boardIdToAddTo && !imageDTO.is_intermediate) {
|
||||
dispatch(
|
||||
boardImagesApi.endpoints.addImageToBoard.initiate({
|
||||
boardImagesApi.endpoints.addBoardImage.initiate({
|
||||
board_id: boardIdToAddTo,
|
||||
image_name,
|
||||
})
|
||||
);
|
||||
|
||||
// Set the board_id on the image in the RTK Query cache
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'getImageDTO',
|
||||
imageDTO.image_name,
|
||||
(draft) => {
|
||||
Object.assign(draft, { board_id: boardIdToAddTo });
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(progressImageSet(null));
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUpdated } from 'services/api/thunks/image';
|
||||
import { imageUpserted } from 'features/gallery/store/gallerySlice';
|
||||
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imageUpdated } from 'services/api/thunks/image';
|
||||
import { startAppListening } from '..';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvas' });
|
||||
|
||||
@ -43,7 +43,10 @@ export const addStagingAreaImageSavedListener = () => {
|
||||
}
|
||||
|
||||
if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
|
||||
dispatch(imageUpserted(imageUpdatedAction.payload));
|
||||
// update cache
|
||||
imagesApi.util.updateQueryData('getImageDTO', imageName, (draft) => {
|
||||
Object.assign(draft, { is_intermediate: false });
|
||||
});
|
||||
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
||||
}
|
||||
},
|
||||
|
@ -96,10 +96,26 @@ export const store = configureStore({
|
||||
.concat(dynamicMiddlewares)
|
||||
.prepend(listenerMiddleware.middleware),
|
||||
devTools: {
|
||||
actionsDenylist,
|
||||
actionSanitizer,
|
||||
stateSanitizer,
|
||||
trace: true,
|
||||
predicate: (state, action) => {
|
||||
// TODO: hook up to the log level param in system slice
|
||||
// manually type state, cannot type the arg
|
||||
// const typedState = state as ReturnType<typeof rootReducer>;
|
||||
|
||||
// if (action.type.startsWith('api/')) {
|
||||
// // don't log api actions, with manual cache updates they are extremely noisy
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (actionsDenylist.includes(action.type)) {
|
||||
// don't log other noisy actions
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -6,30 +6,24 @@ import {
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { useCombinedRefs } from '@dnd-kit/utilities';
|
||||
import {
|
||||
TypesafeDraggableData,
|
||||
TypesafeDroppableData,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import {
|
||||
IAILoadingImageFallback,
|
||||
IAINoContentFallback,
|
||||
} from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { MouseEvent, ReactElement, SyntheticEvent } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import IAIDropOverlay from './IAIDropOverlay';
|
||||
import { PostUploadAction } from 'services/api/thunks/image';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||
import { PostUploadAction } from 'services/api/thunks/image';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import {
|
||||
TypesafeDraggableData,
|
||||
TypesafeDroppableData,
|
||||
isValidDrop,
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import IAIDraggable from './IAIDraggable';
|
||||
import IAIDroppable from './IAIDroppable';
|
||||
|
||||
type IAIDndImageProps = {
|
||||
imageDTO: ImageDTO | undefined;
|
||||
@ -83,28 +77,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const dndId = useRef(uuidv4());
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef: setDraggableRef,
|
||||
isDragging,
|
||||
active,
|
||||
} = useDraggable({
|
||||
id: dndId.current,
|
||||
disabled: isDragDisabled || !imageDTO,
|
||||
data: draggableData,
|
||||
});
|
||||
|
||||
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
||||
id: dndId.current,
|
||||
disabled: isDropDisabled,
|
||||
data: droppableData,
|
||||
});
|
||||
|
||||
const setDndRef = useCombinedRefs(setDroppableRef, setDraggableRef);
|
||||
|
||||
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
|
||||
postUploadAction,
|
||||
isDisabled: isUploadDisabled,
|
||||
@ -139,9 +111,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
userSelect: 'none',
|
||||
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setDndRef}
|
||||
>
|
||||
{imageDTO && (
|
||||
<Flex
|
||||
@ -154,7 +123,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
onClick={onClick}
|
||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallback={<IAILoadingImageFallback image={imageDTO} />}
|
||||
@ -171,30 +139,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
}}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
|
||||
{onClickReset && withResetIcon && (
|
||||
<IAIIconButton
|
||||
onClick={onClickReset}
|
||||
aria-label={resetTooltip}
|
||||
tooltip={resetTooltip}
|
||||
icon={resetIcon}
|
||||
size="sm"
|
||||
variant="link"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
insetInlineEnd: 1,
|
||||
p: 0,
|
||||
minW: 0,
|
||||
svg: {
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
fill: 'base.100',
|
||||
_hover: { fill: 'base.50' },
|
||||
filter: resetIconShadow,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{!imageDTO && !isUploadDisabled && (
|
||||
@ -225,11 +169,42 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
</>
|
||||
)}
|
||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||
<AnimatePresence>
|
||||
{isValidDrop(droppableData, active) && !isDragging && (
|
||||
<IAIDropOverlay isOver={isOver} label={dropLabel} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<IAIDroppable
|
||||
data={droppableData}
|
||||
disabled={isDropDisabled}
|
||||
dropLabel={dropLabel}
|
||||
/>
|
||||
{imageDTO && (
|
||||
<IAIDraggable
|
||||
data={draggableData}
|
||||
disabled={isDragDisabled || !imageDTO}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
{onClickReset && withResetIcon && imageDTO && (
|
||||
<IAIIconButton
|
||||
onClick={onClickReset}
|
||||
aria-label={resetTooltip}
|
||||
tooltip={resetTooltip}
|
||||
icon={resetIcon}
|
||||
size="sm"
|
||||
variant="link"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
insetInlineEnd: 1,
|
||||
p: 0,
|
||||
minW: 0,
|
||||
svg: {
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
fill: 'base.100',
|
||||
_hover: { fill: 'base.50' },
|
||||
filter: resetIconShadow,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
40
invokeai/frontend/web/src/common/components/IAIDraggable.tsx
Normal file
40
invokeai/frontend/web/src/common/components/IAIDraggable.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import {
|
||||
TypesafeDraggableData,
|
||||
useDraggable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { MouseEvent, memo, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
type IAIDraggableProps = {
|
||||
disabled?: boolean;
|
||||
data?: TypesafeDraggableData;
|
||||
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
const IAIDraggable = (props: IAIDraggableProps) => {
|
||||
const { data, disabled, onClick } = props;
|
||||
const dndId = useRef(uuidv4());
|
||||
|
||||
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||
id: dndId.current,
|
||||
disabled,
|
||||
data,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
ref={setNodeRef}
|
||||
position="absolute"
|
||||
w="full"
|
||||
h="full"
|
||||
top={0}
|
||||
insetInlineStart={0}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAIDraggable);
|
47
invokeai/frontend/web/src/common/components/IAIDroppable.tsx
Normal file
47
invokeai/frontend/web/src/common/components/IAIDroppable.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import {
|
||||
TypesafeDroppableData,
|
||||
isValidDrop,
|
||||
useDroppable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { memo, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import IAIDropOverlay from './IAIDropOverlay';
|
||||
|
||||
type IAIDroppableProps = {
|
||||
dropLabel?: string;
|
||||
disabled?: boolean;
|
||||
data?: TypesafeDroppableData;
|
||||
};
|
||||
|
||||
const IAIDroppable = (props: IAIDroppableProps) => {
|
||||
const { dropLabel, data, disabled } = props;
|
||||
const dndId = useRef(uuidv4());
|
||||
|
||||
const { isOver, setNodeRef, active } = useDroppable({
|
||||
id: dndId.current,
|
||||
disabled,
|
||||
data,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineStart={0}
|
||||
w="full"
|
||||
h="full"
|
||||
pointerEvents="none"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{isValidDrop(data, active) && (
|
||||
<IAIDropOverlay isOver={isOver} label={dropLabel} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IAIDroppable);
|
@ -0,0 +1,42 @@
|
||||
import { Box, Flex, Icon } from '@chakra-ui/react';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
const IAIErrorLoadingImageFallback = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: 'full',
|
||||
width: 'full',
|
||||
'::before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
pt: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
height: 'full',
|
||||
width: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
bg: 'base.100',
|
||||
color: 'base.500',
|
||||
_dark: {
|
||||
color: 'base.700',
|
||||
bg: 'base.850',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon as={FaExclamation} boxSize={16} opacity={0.7} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIErrorLoadingImageFallback;
|
@ -0,0 +1,30 @@
|
||||
import { Box, Skeleton } from '@chakra-ui/react';
|
||||
|
||||
const IAIFillSkeleton = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: 'full',
|
||||
width: 'full',
|
||||
'::before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
pt: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
height: 'full',
|
||||
width: 'full',
|
||||
}}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIFillSkeleton;
|
@ -1,18 +1,20 @@
|
||||
import { Box, Icon, Skeleton } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback';
|
||||
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
||||
import {
|
||||
batchImageRangeEndSelected,
|
||||
batchImageSelected,
|
||||
batchImageSelectionToggled,
|
||||
imageRemovedFromBatch,
|
||||
} from 'features/batch/store/batchSlice';
|
||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu';
|
||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||
import { FaExclamationCircle } from 'react-icons/fa';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const makeSelector = (image_name: string) =>
|
||||
@ -20,6 +22,7 @@ const makeSelector = (image_name: string) =>
|
||||
[stateSelector],
|
||||
(state) => ({
|
||||
selectionCount: state.batch.selection.length,
|
||||
selection: state.batch.selection,
|
||||
isSelected: state.batch.selection.includes(image_name),
|
||||
}),
|
||||
defaultSelectorOptions
|
||||
@ -30,43 +33,41 @@ type BatchImageProps = {
|
||||
};
|
||||
|
||||
const BatchImage = (props: BatchImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { imageName } = props;
|
||||
const {
|
||||
currentData: imageDTO,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useGetImageDTOQuery(props.imageName);
|
||||
const dispatch = useAppDispatch();
|
||||
} = useGetImageDTOQuery(imageName);
|
||||
const selector = useMemo(() => makeSelector(imageName), [imageName]);
|
||||
|
||||
const selector = useMemo(
|
||||
() => makeSelector(props.imageName),
|
||||
[props.imageName]
|
||||
);
|
||||
|
||||
const { isSelected, selectionCount } = useAppSelector(selector);
|
||||
const { isSelected, selectionCount, selection } = useAppSelector(selector);
|
||||
|
||||
const handleClickRemove = useCallback(() => {
|
||||
dispatch(imageRemovedFromBatch(props.imageName));
|
||||
}, [dispatch, props.imageName]);
|
||||
dispatch(imageRemovedFromBatch(imageName));
|
||||
}, [dispatch, imageName]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (e.shiftKey) {
|
||||
dispatch(batchImageRangeEndSelected(props.imageName));
|
||||
dispatch(batchImageRangeEndSelected(imageName));
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
dispatch(batchImageSelectionToggled(props.imageName));
|
||||
dispatch(batchImageSelectionToggled(imageName));
|
||||
} else {
|
||||
dispatch(batchImageSelected(props.imageName));
|
||||
dispatch(batchImageSelected(imageName));
|
||||
}
|
||||
},
|
||||
[dispatch, props.imageName]
|
||||
[dispatch, imageName]
|
||||
);
|
||||
|
||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||
if (selectionCount > 1) {
|
||||
return {
|
||||
id: 'batch',
|
||||
payloadType: 'BATCH_SELECTION',
|
||||
payloadType: 'IMAGE_NAMES',
|
||||
payload: { image_names: selection },
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,38 +78,49 @@ const BatchImage = (props: BatchImageProps) => {
|
||||
payload: { imageDTO },
|
||||
};
|
||||
}
|
||||
}, [imageDTO, selectionCount]);
|
||||
}, [imageDTO, selection, selectionCount]);
|
||||
|
||||
if (isError) {
|
||||
return <Icon as={FaExclamationCircle} />;
|
||||
if (isLoading) {
|
||||
return <IAIFillSkeleton />;
|
||||
}
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Skeleton>
|
||||
<Box w="full" h="full" aspectRatio="1/1" />
|
||||
</Skeleton>
|
||||
);
|
||||
if (isError || !imageDTO) {
|
||||
return <IAIErrorLoadingImageFallback />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative', aspectRatio: '1/1' }}>
|
||||
<IAIDndImage
|
||||
imageDTO={imageDTO}
|
||||
draggableData={draggableData}
|
||||
isDropDisabled={true}
|
||||
isUploadDisabled={true}
|
||||
imageSx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
onClickReset={handleClickRemove}
|
||||
resetTooltip="Remove from batch"
|
||||
withResetIcon
|
||||
thumbnail
|
||||
/>
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||
<ImageContextMenu imageDTO={imageDTO}>
|
||||
{(ref) => (
|
||||
<Box
|
||||
position="relative"
|
||||
key={imageName}
|
||||
userSelect="none"
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
>
|
||||
<IAIDndImage
|
||||
onClick={handleClick}
|
||||
imageDTO={imageDTO}
|
||||
draggableData={draggableData}
|
||||
isSelected={isSelected}
|
||||
minSize={0}
|
||||
onClickReset={handleClickRemove}
|
||||
isDropDisabled={true}
|
||||
imageSx={{ w: 'full', h: 'full' }}
|
||||
isUploadDisabled={true}
|
||||
resetTooltip="Remove from batch"
|
||||
withResetIcon
|
||||
thumbnail
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ImageContextMenu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { AddToBatchDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import BatchImageGrid from './BatchImageGrid';
|
||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
||||
import {
|
||||
AddToBatchDropData,
|
||||
isValidDrop,
|
||||
useDroppable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
|
||||
const droppableData: AddToBatchDropData = {
|
||||
id: 'batch',
|
||||
@ -13,17 +9,10 @@ const droppableData: AddToBatchDropData = {
|
||||
};
|
||||
|
||||
const BatchImageContainer = () => {
|
||||
const { isOver, setNodeRef, active } = useDroppable({
|
||||
id: 'batch-manager',
|
||||
data: droppableData,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box ref={setNodeRef} position="relative" w="full" h="full">
|
||||
<Box position="relative" w="full" h="full">
|
||||
<BatchImageGrid />
|
||||
{isValidDrop(droppableData, active) && (
|
||||
<IAIDropOverlay isOver={isOver} label="Add to Batch" />
|
||||
)}
|
||||
<IAIDroppable data={droppableData} dropLabel="Add to Batch" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { imageDeleted } from 'services/api/thunks/image';
|
||||
|
||||
@ -26,10 +26,10 @@ const batch = createSlice({
|
||||
state.isEnabled = action.payload;
|
||||
},
|
||||
imageAddedToBatch: (state, action: PayloadAction<string>) => {
|
||||
state.imageNames = uniq(state.imageNames.concat(action.payload));
|
||||
state.imageNames.push(action.payload);
|
||||
},
|
||||
imagesAddedToBatch: (state, action: PayloadAction<string[]>) => {
|
||||
state.imageNames = uniq(state.imageNames.concat(action.payload));
|
||||
state.imageNames = state.imageNames.concat(action.payload);
|
||||
},
|
||||
imageRemovedFromBatch: (state, action: PayloadAction<string>) => {
|
||||
state.imageNames = state.imageNames.filter(
|
||||
@ -50,10 +50,13 @@ const batch = createSlice({
|
||||
batchImageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
||||
const rangeEndImageName = action.payload;
|
||||
const lastSelectedImage = state.selection[state.selection.length - 1];
|
||||
const lastClickedIndex = state.imageNames.findIndex(
|
||||
|
||||
const { imageNames } = state;
|
||||
|
||||
const lastClickedIndex = imageNames.findIndex(
|
||||
(n) => n === lastSelectedImage
|
||||
);
|
||||
const currentClickedIndex = state.imageNames.findIndex(
|
||||
const currentClickedIndex = imageNames.findIndex(
|
||||
(n) => n === rangeEndImageName
|
||||
);
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
@ -61,7 +64,8 @@ const batch = createSlice({
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
|
||||
const imagesToSelect = state.imageNames.slice(start, end + 1);
|
||||
const imagesToSelect = imageNames.slice(start, end + 1);
|
||||
|
||||
state.selection = uniq(state.selection.concat(imagesToSelect));
|
||||
}
|
||||
},
|
||||
@ -136,7 +140,3 @@ export const {
|
||||
} = batch.actions;
|
||||
|
||||
export default batch.reducer;
|
||||
|
||||
export const selectionAddedToBatch = createAction(
|
||||
'batch/selectionAddedToBatch'
|
||||
);
|
||||
|
@ -0,0 +1,87 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
|
||||
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 BatchImage from 'features/batch/components/BatchImage';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import ItemContainer from './ItemContainer';
|
||||
import ListContainer from './ListContainer';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
return {
|
||||
imageNames: state.batch.imageNames,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const BatchGrid = () => {
|
||||
const { t } = useTranslation();
|
||||
const rootRef = useRef(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
options: {
|
||||
scrollbars: {
|
||||
visibility: 'auto',
|
||||
autoHide: 'leave',
|
||||
autoHideDelay: 1300,
|
||||
theme: 'os-theme-dark',
|
||||
},
|
||||
overflow: { x: 'hidden' },
|
||||
},
|
||||
});
|
||||
|
||||
const { imageNames } = useAppSelector(selector);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
if (imageNames.length) {
|
||||
return (
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={imageNames}
|
||||
components={{
|
||||
Item: ItemContainer,
|
||||
List: ListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, imageName) => (
|
||||
<BatchImage key={imageName} imageName={imageName} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BatchGrid);
|
@ -0,0 +1,102 @@
|
||||
import { Box, Spinner } from '@chakra-ui/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation, FaImage } from 'react-icons/fa';
|
||||
|
||||
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 BatchImage from 'features/batch/components/BatchImage';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { useGetAllBoardImagesForBoardQuery } from 'services/api/endpoints/boardImages';
|
||||
import ItemContainer from './ItemContainer';
|
||||
import ListContainer from './ListContainer';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
return {
|
||||
imageNames: state.batch.imageNames,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type BoardGridProps = {
|
||||
board_id: string;
|
||||
};
|
||||
|
||||
const BoardGrid = (props: BoardGridProps) => {
|
||||
const { board_id } = props;
|
||||
const { data, isLoading, isError, isSuccess } =
|
||||
useGetAllBoardImagesForBoardQuery({
|
||||
board_id,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const rootRef = useRef(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
options: {
|
||||
scrollbars: {
|
||||
visibility: 'auto',
|
||||
autoHide: 'leave',
|
||||
autoHideDelay: 1300,
|
||||
theme: 'os-theme-dark',
|
||||
},
|
||||
overflow: { x: 'hidden' },
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <FaExclamation />;
|
||||
}
|
||||
|
||||
if (isSuccess && data.image_names) {
|
||||
return (
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={data.image_names}
|
||||
components={{
|
||||
Item: ItemContainer,
|
||||
List: ListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, imageName) => (
|
||||
<BatchImage key={imageName} imageName={imageName} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardGrid);
|
@ -1,23 +1,14 @@
|
||||
import { Flex, useColorMode } from '@chakra-ui/react';
|
||||
import { FaImages } from 'react-icons/fa';
|
||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { FaImages } from 'react-icons/fa';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import {
|
||||
MoveBoardDropData,
|
||||
isValidDrop,
|
||||
useDroppable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import GenericBoard from './GenericBoard';
|
||||
|
||||
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const handleAllImagesBoardClick = () => {
|
||||
dispatch(boardIdSelected());
|
||||
dispatch(boardIdSelected('all'));
|
||||
};
|
||||
|
||||
const droppableData: MoveBoardDropData = {
|
||||
@ -26,67 +17,14 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
context: { boardId: null },
|
||||
};
|
||||
|
||||
const { isOver, setNodeRef, active } = useDroppable({
|
||||
id: `board_droppable_all_images`,
|
||||
data: droppableData,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
ref={setNodeRef}
|
||||
onClick={handleAllImagesBoardClick}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={FaImages}
|
||||
sx={{
|
||||
border: '2px solid var(--invokeai-colors-base-200)',
|
||||
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
||||
}}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{isValidDrop(droppableData, active) && (
|
||||
<IAIDropOverlay isOver={isOver} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
color: isSelected
|
||||
? mode('base.900', 'base.50')(colorMode)
|
||||
: mode('base.700', 'base.200')(colorMode),
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
}}
|
||||
>
|
||||
All Images
|
||||
</Flex>
|
||||
</Flex>
|
||||
<GenericBoard
|
||||
droppableData={droppableData}
|
||||
onClick={handleAllImagesBoardClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaImages}
|
||||
label="All Images"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { AddToBatchDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback } from 'react';
|
||||
import { FaLayerGroup } from 'react-icons/fa';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import GenericBoard from './GenericBoard';
|
||||
|
||||
const selector = createSelector(stateSelector, (state) => {
|
||||
return {
|
||||
count: state.batch.imageNames.length,
|
||||
};
|
||||
});
|
||||
|
||||
const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { count } = useAppSelector(selector);
|
||||
|
||||
const handleBatchBoardClick = useCallback(() => {
|
||||
dispatch(boardIdSelected('batch'));
|
||||
}, [dispatch]);
|
||||
|
||||
const droppableData: AddToBatchDropData = {
|
||||
id: 'batch-board',
|
||||
actionType: 'ADD_TO_BATCH',
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
droppableData={droppableData}
|
||||
onClick={handleBatchBoardClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaLayerGroup}
|
||||
label="Batch"
|
||||
badgeCount={count}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BatchBoard;
|
@ -1,3 +1,4 @@
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Collapse,
|
||||
Flex,
|
||||
@ -9,17 +10,17 @@ import {
|
||||
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, useState } from 'react';
|
||||
import HoverableBoard from './HoverableBoard';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import AddBoardButton from './AddBoardButton';
|
||||
import AllImagesBoard from './AllImagesBoard';
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import BatchBoard from './BatchBoard';
|
||||
import GalleryBoard from './GalleryBoard';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
@ -115,14 +116,19 @@ const BoardsList = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
{!searchMode && (
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<AllImagesBoard isSelected={!selectedBoardId} />
|
||||
</GridItem>
|
||||
<>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<AllImagesBoard isSelected={selectedBoardId === 'all'} />
|
||||
</GridItem>
|
||||
<GridItem sx={{ p: 1.5 }}>
|
||||
<BatchBoard isSelected={selectedBoardId === 'batch'} />
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board) => (
|
||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||
<HoverableBoard
|
||||
<GalleryBoard
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
/>
|
||||
|
@ -12,35 +12,31 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { memo, useCallback, useContext } from 'react';
|
||||
import { FaFolder, FaTrash } from 'react-icons/fa';
|
||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
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 { AnimatePresence } from 'framer-motion';
|
||||
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
||||
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
|
||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { boardAddedToBatch } from 'app/store/middleware/listenerMiddleware/listeners/addBoardToBatch';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import {
|
||||
MoveBoardDropData,
|
||||
isValidDrop,
|
||||
useDroppable,
|
||||
} from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
|
||||
|
||||
interface HoverableBoardProps {
|
||||
interface GalleryBoardProps {
|
||||
board: BoardDTO;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
const GalleryBoard = memo(({ board, isSelected }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||
@ -71,21 +67,23 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
deleteBoard(board_id);
|
||||
}, [board_id, deleteBoard]);
|
||||
|
||||
const handleAddBoardToBatch = useCallback(() => {
|
||||
dispatch(boardAddedToBatch({ board_id }));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
const handleDeleteBoardAndImages = useCallback(() => {
|
||||
console.log({ board });
|
||||
onClickDeleteBoardImages(board);
|
||||
}, [board, onClickDeleteBoardImages]);
|
||||
|
||||
const droppableData: MoveBoardDropData = {
|
||||
id: board_id,
|
||||
actionType: 'MOVE_BOARD',
|
||||
context: { boardId: board_id },
|
||||
};
|
||||
|
||||
const { isOver, setNodeRef, active } = useDroppable({
|
||||
id: `board_droppable_${board_id}`,
|
||||
data: droppableData,
|
||||
});
|
||||
const droppableData: MoveBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: board_id,
|
||||
actionType: 'MOVE_BOARD',
|
||||
context: { boardId: board_id },
|
||||
}),
|
||||
[board_id]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||
@ -94,16 +92,25 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
renderMenu={() => (
|
||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
||||
{board.image_count > 0 && (
|
||||
<MenuItem
|
||||
sx={{ color: 'error.300' }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoardAndImages}
|
||||
>
|
||||
Delete Board and Images
|
||||
</MenuItem>
|
||||
<>
|
||||
<MenuItem
|
||||
isDisabled={!board.image_count}
|
||||
icon={<FaImages />}
|
||||
onClickCapture={handleAddBoardToBatch}
|
||||
>
|
||||
Add Board to Batch
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoardAndImages}
|
||||
>
|
||||
Delete Board and Images
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
sx={{ color: mode('error.700', 'error.300')(colorMode) }}
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoard}
|
||||
>
|
||||
@ -127,7 +134,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
ref={setNodeRef}
|
||||
onClick={handleSelectBoard}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
@ -167,11 +173,7 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
>
|
||||
<Badge variant="solid">{board.image_count}</Badge>
|
||||
</Flex>
|
||||
<AnimatePresence>
|
||||
{isValidDrop(droppableData, active) && (
|
||||
<IAIDropOverlay isOver={isOver} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<IAIDroppable data={droppableData} />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
@ -219,6 +221,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
HoverableBoard.displayName = 'HoverableBoard';
|
||||
GalleryBoard.displayName = 'HoverableBoard';
|
||||
|
||||
export default HoverableBoard;
|
||||
export default GalleryBoard;
|
@ -0,0 +1,83 @@
|
||||
import { As, Badge, Flex } from '@chakra-ui/react';
|
||||
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
|
||||
type GenericBoardProps = {
|
||||
droppableData: TypesafeDroppableData;
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
icon: As;
|
||||
label: string;
|
||||
badgeCount?: number;
|
||||
};
|
||||
|
||||
const GenericBoard = (props: GenericBoardProps) => {
|
||||
const { droppableData, onClick, isSelected, icon, label, badgeCount } = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={icon}
|
||||
sx={{
|
||||
border: '2px solid var(--invokeai-colors-base-200)',
|
||||
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{badgeCount !== undefined && (
|
||||
<Badge variant="solid">{badgeCount}</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<IAIDroppable data={droppableData} />
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
color: isSelected ? 'base.900' : 'base.700',
|
||||
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericBoard;
|
@ -8,7 +8,7 @@ import {
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { Box, Spinner } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
@ -7,9 +7,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import {
|
||||
imageRangeEndSelected,
|
||||
imageSelected,
|
||||
@ -20,50 +18,38 @@ import ImageContextMenu from './ImageContextMenu';
|
||||
export const makeSelector = (image_name: string) =>
|
||||
createSelector(
|
||||
[stateSelector],
|
||||
({ gallery }) => {
|
||||
const isSelected = gallery.selection.includes(image_name);
|
||||
const selectionCount = gallery.selection.length;
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
selectionCount,
|
||||
};
|
||||
},
|
||||
({ gallery }) => ({
|
||||
isSelected: gallery.selection.includes(image_name),
|
||||
selectionCount: gallery.selection.length,
|
||||
selection: gallery.selection,
|
||||
}),
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
interface HoverableImageProps {
|
||||
imageDTO: ImageDTO;
|
||||
imageName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gallery image component with delete/use all/use seed buttons on hover.
|
||||
*/
|
||||
const GalleryImage = (props: HoverableImageProps) => {
|
||||
const { imageDTO } = props;
|
||||
const { image_url, thumbnail_url, image_name } = imageDTO;
|
||||
|
||||
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
|
||||
|
||||
const { isSelected, selectionCount } = useAppSelector(localSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { imageName } = props;
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
||||
const localSelector = useMemo(() => makeSelector(imageName), [imageName]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { isSelected, selectionCount, selection } =
|
||||
useAppSelector(localSelector);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
// multiselect disabled for now
|
||||
// if (e.shiftKey) {
|
||||
// dispatch(imageRangeEndSelected(props.imageDTO.image_name));
|
||||
// } else if (e.ctrlKey || e.metaKey) {
|
||||
// dispatch(imageSelectionToggled(props.imageDTO.image_name));
|
||||
// } else {
|
||||
// dispatch(imageSelected(props.imageDTO.image_name));
|
||||
// }
|
||||
dispatch(imageSelected(props.imageDTO.image_name));
|
||||
if (e.shiftKey) {
|
||||
dispatch(imageRangeEndSelected(imageName));
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
dispatch(imageSelectionToggled(imageName));
|
||||
} else {
|
||||
dispatch(imageSelected(imageName));
|
||||
}
|
||||
},
|
||||
[dispatch, props.imageDTO.image_name]
|
||||
[dispatch, imageName]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@ -81,7 +67,8 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
if (selectionCount > 1) {
|
||||
return {
|
||||
id: 'gallery-image',
|
||||
payloadType: 'GALLERY_SELECTION',
|
||||
payloadType: 'IMAGE_NAMES',
|
||||
payload: { image_names: selection },
|
||||
};
|
||||
}
|
||||
|
||||
@ -92,15 +79,19 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
payload: { imageDTO },
|
||||
};
|
||||
}
|
||||
}, [imageDTO, selectionCount]);
|
||||
}, [imageDTO, selection, selectionCount]);
|
||||
|
||||
if (!imageDTO) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||
<ImageContextMenu image={imageDTO}>
|
||||
<ImageContextMenu imageDTO={imageDTO}>
|
||||
{(ref) => (
|
||||
<Box
|
||||
position="relative"
|
||||
key={image_name}
|
||||
key={imageName}
|
||||
userSelect="none"
|
||||
ref={ref}
|
||||
sx={{
|
||||
@ -117,13 +108,13 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
isSelected={isSelected}
|
||||
minSize={0}
|
||||
onClickReset={handleDelete}
|
||||
resetIcon={<FaTrash />}
|
||||
resetTooltip="Delete image"
|
||||
imageSx={{ w: 'full', h: 'full' }}
|
||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||
isDropDisabled={true}
|
||||
isUploadDisabled={true}
|
||||
thumbnail={true}
|
||||
// resetIcon={<FaTrash />}
|
||||
// resetTooltip="Delete image"
|
||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
@ -7,8 +7,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import {
|
||||
imageAddedToBatch,
|
||||
imageRemovedFromBatch,
|
||||
imagesAddedToBatch,
|
||||
selectionAddedToBatch,
|
||||
} from 'features/batch/store/batchSlice';
|
||||
import {
|
||||
resizeAndScaleCanvas,
|
||||
@ -21,34 +22,46 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
|
||||
import {
|
||||
FaExpand,
|
||||
FaFolder,
|
||||
FaLayerGroup,
|
||||
FaShare,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
||||
import {
|
||||
useAddManyBoardImagesMutation,
|
||||
useDeleteBoardImageMutation,
|
||||
useDeleteManyBoardImagesMutation,
|
||||
} from 'services/api/endpoints/boardImages';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
|
||||
|
||||
type Props = {
|
||||
image: ImageDTO;
|
||||
imageDTO: ImageDTO;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
};
|
||||
|
||||
const ImageContextMenu = ({ image, children }: Props) => {
|
||||
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
[stateSelector],
|
||||
({ gallery, batch }) => {
|
||||
const selectionCount = gallery.selection.length;
|
||||
const isInBatch = batch.imageNames.includes(image.image_name);
|
||||
const isBatch = gallery.selectedBoardId === 'batch';
|
||||
|
||||
return { selectionCount, isInBatch };
|
||||
const selection = isBatch ? batch.selection : gallery.selection;
|
||||
const isInBatch = batch.imageNames.includes(imageDTO.image_name);
|
||||
|
||||
return { selection, isInBatch };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[image.image_name]
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const { selectionCount, isInBatch } = useAppSelector(selector);
|
||||
const { selection, isInBatch } = useAppSelector(selector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -60,37 +73,39 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!image) {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageToDeleteSelected(image));
|
||||
}, [dispatch, image]);
|
||||
dispatch(imageToDeleteSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||
useRecallParameters();
|
||||
|
||||
const [removeFromBoard] = useRemoveImageFromBoardMutation();
|
||||
const [deleteBoardImage] = useDeleteBoardImageMutation();
|
||||
const [deleteManyBoardImages] = useDeleteManyBoardImagesMutation();
|
||||
const [addManyBoardImages] = useAddManyBoardImagesMutation();
|
||||
|
||||
// Recall parameters handlers
|
||||
const handleRecallPrompt = useCallback(() => {
|
||||
recallBothPrompts(
|
||||
image.metadata?.positive_conditioning,
|
||||
image.metadata?.negative_conditioning
|
||||
imageDTO.metadata?.positive_conditioning,
|
||||
imageDTO.metadata?.negative_conditioning
|
||||
);
|
||||
}, [
|
||||
image.metadata?.negative_conditioning,
|
||||
image.metadata?.positive_conditioning,
|
||||
imageDTO.metadata?.negative_conditioning,
|
||||
imageDTO.metadata?.positive_conditioning,
|
||||
recallBothPrompts,
|
||||
]);
|
||||
|
||||
const handleRecallSeed = useCallback(() => {
|
||||
recallSeed(image.metadata?.seed);
|
||||
}, [image, recallSeed]);
|
||||
recallSeed(imageDTO.metadata?.seed);
|
||||
}, [imageDTO, recallSeed]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(image));
|
||||
}, [dispatch, image]);
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
// const handleRecallInitialImage = useCallback(() => {
|
||||
// recallInitialImage(image.metadata.invokeai?.node?.image);
|
||||
@ -98,7 +113,7 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
|
||||
const handleSendToCanvas = () => {
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setInitialCanvasImage(image));
|
||||
dispatch(setInitialCanvasImage(imageDTO));
|
||||
dispatch(resizeAndScaleCanvas());
|
||||
dispatch(setActiveTab('unifiedCanvas'));
|
||||
|
||||
@ -111,8 +126,8 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
};
|
||||
|
||||
const handleUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(image);
|
||||
}, [image, recallAllParameters]);
|
||||
recallAllParameters(imageDTO);
|
||||
}, [imageDTO, recallAllParameters]);
|
||||
|
||||
const handleLightBox = () => {
|
||||
// dispatch(setCurrentImage(image));
|
||||
@ -120,34 +135,50 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
};
|
||||
|
||||
const handleAddToBoard = useCallback(() => {
|
||||
onClickAddToBoard(image);
|
||||
}, [image, onClickAddToBoard]);
|
||||
onClickAddToBoard(imageDTO);
|
||||
}, [imageDTO, onClickAddToBoard]);
|
||||
|
||||
const handleRemoveFromBoard = useCallback(() => {
|
||||
if (!image.board_id) {
|
||||
if (!imageDTO.board_id) {
|
||||
return;
|
||||
}
|
||||
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
|
||||
}, [image.board_id, image.image_name, removeFromBoard]);
|
||||
deleteBoardImage({ image_name: imageDTO.image_name });
|
||||
}, [deleteBoardImage, imageDTO.board_id, imageDTO.image_name]);
|
||||
|
||||
const handleOpenInNewTab = () => {
|
||||
window.open(image.image_url, '_blank');
|
||||
};
|
||||
const handleAddSelectionToBoard = useCallback(() => {
|
||||
// addManyBoardImages({ board_id, image_names: selection });
|
||||
}, []);
|
||||
|
||||
const handleRemoveSelectionFromBoard = useCallback(() => {
|
||||
deleteManyBoardImages({ image_names: selection });
|
||||
}, [deleteManyBoardImages, selection]);
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
window.open(imageDTO.image_url, '_blank');
|
||||
}, [imageDTO.image_url]);
|
||||
|
||||
const handleAddSelectionToBatch = useCallback(() => {
|
||||
dispatch(selectionAddedToBatch());
|
||||
}, [dispatch]);
|
||||
dispatch(imagesAddedToBatch(selection));
|
||||
}, [dispatch, selection]);
|
||||
|
||||
const handleAddToBatch = useCallback(() => {
|
||||
dispatch(imagesAddedToBatch([image.image_name]));
|
||||
}, [dispatch, image.image_name]);
|
||||
dispatch(imageAddedToBatch(imageDTO.image_name));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleRemoveFromBatch = useCallback(() => {
|
||||
dispatch(imageRemovedFromBatch(imageDTO.image_name));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
renderMenu={() => (
|
||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
||||
{selectionCount === 1 ? (
|
||||
{selection.length === 1 ? (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<ExternalLinkIcon />}
|
||||
@ -164,7 +195,7 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
onClickCapture={handleRecallPrompt}
|
||||
isDisabled={
|
||||
image?.metadata?.positive_conditioning === undefined
|
||||
imageDTO?.metadata?.positive_conditioning === undefined
|
||||
}
|
||||
>
|
||||
{t('parameters.usePrompt')}
|
||||
@ -173,24 +204,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
<MenuItem
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
onClickCapture={handleRecallSeed}
|
||||
isDisabled={image?.metadata?.seed === undefined}
|
||||
isDisabled={imageDTO?.metadata?.seed === undefined}
|
||||
>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
onClickCapture={handleRecallInitialImage}
|
||||
isDisabled={image?.metadata?.type !== 'img2img'}
|
||||
>
|
||||
{t('parameters.useInitImg')}
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
onClickCapture={handleUseAllParameters}
|
||||
isDisabled={
|
||||
// what should these be
|
||||
!['t2l', 'l2l', 'inpaint'].includes(
|
||||
String(image?.metadata?.type)
|
||||
String(imageDTO?.metadata?.type)
|
||||
)
|
||||
}
|
||||
>
|
||||
@ -212,17 +236,18 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
{t('parameters.sendToUnifiedCanvas')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* <MenuItem
|
||||
icon={<FaFolder />}
|
||||
isDisabled={isInBatch}
|
||||
onClickCapture={handleAddToBatch}
|
||||
<MenuItem
|
||||
icon={<FaLayerGroup />}
|
||||
onClickCapture={
|
||||
isInBatch ? handleRemoveFromBatch : handleAddToBatch
|
||||
}
|
||||
>
|
||||
Add to Batch
|
||||
</MenuItem> */}
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
||||
{image.board_id ? 'Change Board' : 'Add to Board'}
|
||||
{isInBatch ? 'Remove from Batch' : 'Add to Batch'}
|
||||
</MenuItem>
|
||||
{image.board_id && (
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
||||
{imageDTO.board_id ? 'Change Board' : 'Add to Board'}
|
||||
</MenuItem>
|
||||
{imageDTO.board_id && (
|
||||
<MenuItem
|
||||
icon={<FaFolder />}
|
||||
onClickCapture={handleRemoveFromBoard}
|
||||
@ -241,18 +266,23 @@ const ImageContextMenu = ({ image, children }: Props) => {
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
isDisabled={true}
|
||||
icon={<FaFolder />}
|
||||
onClickCapture={handleAddToBoard}
|
||||
onClickCapture={handleAddSelectionToBoard}
|
||||
>
|
||||
Move Selection to Board
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
icon={<FaFolderPlus />}
|
||||
<MenuItem
|
||||
icon={<FaFolder />}
|
||||
onClickCapture={handleRemoveSelectionFromBoard}
|
||||
>
|
||||
Reset Board for Selection
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<FaLayerGroup />}
|
||||
onClickCapture={handleAddSelectionToBatch}
|
||||
>
|
||||
Add Selection to Batch
|
||||
</MenuItem> */}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
||||
|
||||
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
||||
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';
|
||||
@ -29,14 +29,10 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import {
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
imageCategoriesChanged,
|
||||
shouldAutoSwitchChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import BatchGrid from './BatchGrid';
|
||||
import BoardsList from './Boards/BoardsList';
|
||||
import ImageGalleryGrid from './ImageGalleryGrid';
|
||||
|
||||
@ -66,6 +62,7 @@ const ImageGalleryContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
@ -83,6 +80,16 @@ const ImageGalleryContent = () => {
|
||||
}),
|
||||
});
|
||||
|
||||
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) => {
|
||||
@ -95,12 +102,10 @@ const ImageGalleryContent = () => {
|
||||
};
|
||||
|
||||
const handleClickImagesCategory = useCallback(() => {
|
||||
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
|
||||
dispatch(setGalleryView('images'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickAssetsCategory = useCallback(() => {
|
||||
dispatch(imageCategoriesChanged(ASSETS_CATEGORIES));
|
||||
dispatch(setGalleryView('assets'));
|
||||
}, [dispatch]);
|
||||
|
||||
@ -163,7 +168,7 @@ const ImageGalleryContent = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedBoard ? selectedBoard.board_name : 'All Images'}
|
||||
{boardTitle}
|
||||
</Text>
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
@ -216,8 +221,8 @@ const ImageGalleryContent = () => {
|
||||
<BoardsList isOpen={isBoardListOpen} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex direction="column" gap={2} h="full" w="full">
|
||||
<ImageGalleryGrid />
|
||||
<Flex ref={galleryGridRef} direction="column" gap={2} h="full" w="full">
|
||||
{selectedBoardId === 'batch' ? <BatchGrid /> : <ImageGalleryGrid />}
|
||||
</Flex>
|
||||
</VStack>
|
||||
);
|
||||
|
@ -1,74 +1,38 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Grid,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
forwardRef,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { IMAGE_LIMIT } from 'features/gallery/store/gallerySlice';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
|
||||
import {
|
||||
PropsWithChildren,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
import GalleryImage from './GalleryImage';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState, stateSelector } from 'app/store/store';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { selectFilteredImages } from 'features/gallery/store/gallerySlice';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useLoadMoreImages } from '../hooks/useLoadMoreImages';
|
||||
import ItemContainer from './ItemContainer';
|
||||
import ListContainer from './ListContainer';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector, selectFilteredImages],
|
||||
(state, filteredImages) => {
|
||||
const {
|
||||
categories,
|
||||
total: allImagesTotal,
|
||||
isLoading,
|
||||
isFetching,
|
||||
selectedBoardId,
|
||||
} = state.gallery;
|
||||
|
||||
let images = filteredImages as (ImageDTO | 'loading')[];
|
||||
|
||||
if (!isLoading && isFetching) {
|
||||
// loading, not not the initial load
|
||||
images = images.concat(Array(IMAGE_LIMIT).fill('loading'));
|
||||
}
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { galleryImageMinimumWidth } = state.gallery;
|
||||
|
||||
return {
|
||||
images,
|
||||
allImagesTotal,
|
||||
isLoading,
|
||||
isFetching,
|
||||
categories,
|
||||
selectedBoardId,
|
||||
galleryImageMinimumWidth,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const ImageGalleryGrid = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const rootRef = useRef(null);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const emptyGalleryRef = useRef<HTMLDivElement>(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
@ -83,46 +47,27 @@ const ImageGalleryGrid = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { galleryImageMinimumWidth } = useAppSelector(selector);
|
||||
|
||||
const {
|
||||
images,
|
||||
isLoading,
|
||||
isFetching,
|
||||
allImagesTotal,
|
||||
categories,
|
||||
imageNames,
|
||||
galleryView,
|
||||
loadMoreImages,
|
||||
selectedBoardId,
|
||||
} = useAppSelector(selector);
|
||||
|
||||
const { selectedBoard } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => ({
|
||||
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
|
||||
}),
|
||||
});
|
||||
|
||||
const filteredImagesTotal = useMemo(
|
||||
() => selectedBoard?.image_count ?? allImagesTotal,
|
||||
[allImagesTotal, selectedBoard?.image_count]
|
||||
);
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
return images.length < filteredImagesTotal;
|
||||
}, [images.length, filteredImagesTotal]);
|
||||
status,
|
||||
areMoreAvailable,
|
||||
} = useLoadMoreImages();
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
dispatch(
|
||||
receivedPageOfImages({
|
||||
categories,
|
||||
board_id: selectedBoardId,
|
||||
is_intermediate: false,
|
||||
})
|
||||
);
|
||||
}, [categories, dispatch, selectedBoardId]);
|
||||
loadMoreImages({});
|
||||
}, [loadMoreImages]);
|
||||
|
||||
const handleEndReached = useMemo(() => {
|
||||
if (areMoreAvailable && !isLoading) {
|
||||
if (areMoreAvailable && status !== 'pending') {
|
||||
return handleLoadMoreImages;
|
||||
}
|
||||
return undefined;
|
||||
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
||||
}, [areMoreAvailable, handleLoadMoreImages, status]);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
@ -137,53 +82,68 @@ const ImageGalleryGrid = () => {
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
if (isLoading) {
|
||||
useEffect(() => {
|
||||
// TODO: this doesn't actually prevent 2 intial image loads...
|
||||
if (status !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// rough, conservative calculation of how many images fit in the gallery
|
||||
// TODO: this gets an incorrect value on first load...
|
||||
const galleryHeight = rootRef.current?.clientHeight ?? 0;
|
||||
const galleryWidth = rootRef.current?.clientHeight ?? 0;
|
||||
|
||||
const rows = galleryHeight / galleryImageMinimumWidth;
|
||||
const columns = galleryWidth / galleryImageMinimumWidth;
|
||||
|
||||
const imagesToLoad = Math.ceil(rows * columns);
|
||||
|
||||
// load up that many images
|
||||
loadMoreImages({
|
||||
offset: 0,
|
||||
limit: imagesToLoad,
|
||||
});
|
||||
}, [
|
||||
galleryImageMinimumWidth,
|
||||
galleryView,
|
||||
loadMoreImages,
|
||||
selectedBoardId,
|
||||
status,
|
||||
]);
|
||||
|
||||
if (status === 'fulfilled' && imageNames.length === 0) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
size="xl"
|
||||
sx={{ color: 'base.300', _dark: { color: 'base.700' } }}
|
||||
<Box ref={emptyGalleryRef} sx={{ w: 'full', h: 'full' }}>
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (images.length) {
|
||||
if (status !== 'rejected') {
|
||||
return (
|
||||
<>
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<VirtuosoGrid
|
||||
style={{ height: '100%' }}
|
||||
data={images}
|
||||
data={imageNames}
|
||||
endReached={handleEndReached}
|
||||
components={{
|
||||
Item: ItemContainer,
|
||||
List: ListContainer,
|
||||
}}
|
||||
scrollerRef={setScroller}
|
||||
itemContent={(index, item) =>
|
||||
typeof item === 'string' ? (
|
||||
<Skeleton sx={{ w: 'full', h: 'full', aspectRatio: '1/1' }} />
|
||||
) : (
|
||||
<GalleryImage
|
||||
key={`${item.image_name}-${item.thumbnail_url}`}
|
||||
imageDTO={item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
itemContent={(index, imageName) => (
|
||||
<GalleryImage key={imageName} imageName={imageName} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<IAIButton
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreAvailable}
|
||||
isLoading={isFetching}
|
||||
isLoading={status === 'pending'}
|
||||
loadingText="Loading"
|
||||
flexShrink={0}
|
||||
>
|
||||
@ -194,40 +154,6 @@ const ImageGalleryGrid = () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IAINoContentFallback
|
||||
label={t('gallery.noImagesInGallery')}
|
||||
icon={FaImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
<Box className="item-container" ref={ref} p={1.5}>
|
||||
{props.children}
|
||||
</Box>
|
||||
));
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
const galleryImageMinimumWidth = useAppSelector(
|
||||
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
className="list-container"
|
||||
ref={ref}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(ImageGalleryGrid);
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { Box, FlexProps, forwardRef } from '@chakra-ui/react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
<Box className="item-container" ref={ref} p={1.5}>
|
||||
{props.children}
|
||||
</Box>
|
||||
));
|
||||
|
||||
export default ItemContainer;
|
@ -0,0 +1,26 @@
|
||||
import { FlexProps, Grid, forwardRef } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
const galleryImageMinimumWidth = useAppSelector(
|
||||
(state: RootState) => state.gallery.galleryImageMinimumWidth
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
className="list-container"
|
||||
ref={ref}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr));`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListContainer;
|
@ -4,7 +4,6 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
imageSelected,
|
||||
selectFilteredImages,
|
||||
selectImagesById,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
@ -13,6 +12,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { receivedPageOfImages } from 'services/api/thunks/image';
|
||||
import { selectFilteredImages } from '../store/gallerySelectors';
|
||||
|
||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
||||
height: '100%',
|
||||
|
@ -0,0 +1,64 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useCallback } from 'react';
|
||||
import { ImagesLoadedArg, imagesLoaded } from 'services/api/thunks/image';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { selectedBoardId, galleryView } = state.gallery;
|
||||
|
||||
const imageNames =
|
||||
state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView]
|
||||
.imageNames ?? [];
|
||||
|
||||
const total =
|
||||
state.gallery.imageNamesByIdAndView[selectedBoardId]?.[galleryView]
|
||||
.total ?? 0;
|
||||
|
||||
const status =
|
||||
state.gallery.statusByIdAndView[selectedBoardId]?.[galleryView] ??
|
||||
undefined;
|
||||
|
||||
return {
|
||||
imageNames,
|
||||
status,
|
||||
total,
|
||||
selectedBoardId,
|
||||
galleryView,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const useLoadMoreImages = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { selectedBoardId, imageNames, galleryView, total, status } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const loadMoreImages = useCallback(
|
||||
(arg: Partial<ImagesLoadedArg>) => {
|
||||
dispatch(
|
||||
imagesLoaded({
|
||||
board_id: selectedBoardId,
|
||||
offset: imageNames.length,
|
||||
view: galleryView,
|
||||
...arg,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, galleryView, imageNames.length, selectedBoardId]
|
||||
);
|
||||
|
||||
return {
|
||||
loadMoreImages,
|
||||
selectedBoardId,
|
||||
imageNames,
|
||||
galleryView,
|
||||
areMoreAvailable: imageNames.length < total,
|
||||
total,
|
||||
status,
|
||||
};
|
||||
};
|
@ -15,4 +15,6 @@ export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [
|
||||
'galleryView',
|
||||
'total',
|
||||
'isInitialized',
|
||||
'imageNamesByIdAndView',
|
||||
'statusByIdAndView',
|
||||
];
|
||||
|
@ -1,3 +1,61 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { keyBy } from 'lodash-es';
|
||||
import { galleryImagesAdapter, initialGalleryState } from './gallerySlice';
|
||||
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
export const selectFilteredImagesLocal = createSelector(
|
||||
(state: typeof initialGalleryState) => state,
|
||||
(galleryState) => {
|
||||
const allImages = galleryImagesAdapter
|
||||
.getSelectors()
|
||||
.selectAll(galleryState);
|
||||
const { categories, selectedBoardId } = galleryState;
|
||||
|
||||
const filteredImages = allImages.filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = selectedBoardId
|
||||
? i.board_id === selectedBoardId
|
||||
: true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
|
||||
return filteredImages;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFilteredImages = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => {
|
||||
return selectFilteredImagesLocal(state.gallery);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const selectFilteredImagesAsObject = createSelector(
|
||||
selectFilteredImages,
|
||||
(filteredImages) => keyBy(filteredImages, 'image_name')
|
||||
);
|
||||
|
||||
export const selectFilteredImagesIds = createSelector(
|
||||
selectFilteredImages,
|
||||
(filteredImages) => filteredImages.map((i) => i.image_name)
|
||||
);
|
||||
|
||||
export const selectLastSelectedImage = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const selectSelectedImages = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) =>
|
||||
galleryImagesAdapter
|
||||
.getSelectors()
|
||||
.selectAll(state.gallery)
|
||||
.filter((i) => state.gallery.selection.includes(i.image_name)),
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
@ -1,21 +1,14 @@
|
||||
import type { PayloadAction, Update } from '@reduxjs/toolkit';
|
||||
import {
|
||||
createEntityAdapter,
|
||||
createSelector,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import { keyBy, uniq } from 'lodash-es';
|
||||
import { filter, forEach, uniq } from 'lodash-es';
|
||||
import { boardImagesApi } from 'services/api/endpoints/boardImages';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import {
|
||||
imageUrlsReceived,
|
||||
receivedPageOfImages,
|
||||
} from 'services/api/thunks/image';
|
||||
import { imageDeleted, imagesLoaded } from 'services/api/thunks/image';
|
||||
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
export const galleryImagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
|
||||
});
|
||||
@ -31,23 +24,99 @@ export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||
export const INITIAL_IMAGE_LIMIT = 100;
|
||||
export const IMAGE_LIMIT = 20;
|
||||
|
||||
type AdditionaGalleryState = {
|
||||
type RequestState = 'pending' | 'fulfilled' | 'rejected';
|
||||
type GalleryView = 'images' | 'assets';
|
||||
|
||||
// dirty hack to get autocompletion while still accepting any string
|
||||
type BoardPath =
|
||||
| 'all.images'
|
||||
| 'all.assets'
|
||||
| 'none.images'
|
||||
| 'none.assets'
|
||||
| 'batch.images'
|
||||
| 'batch.assets'
|
||||
| `${string}.${GalleryView}`;
|
||||
|
||||
const systemBoards = [
|
||||
'all.images',
|
||||
'all.assets',
|
||||
'none.images',
|
||||
'none.assets',
|
||||
'batch.images',
|
||||
'batch.assets',
|
||||
];
|
||||
|
||||
type Boards = Record<
|
||||
BoardPath,
|
||||
{
|
||||
path: BoardPath;
|
||||
id: 'all' | 'none' | 'batch' | (string & Record<never, never>);
|
||||
view: GalleryView;
|
||||
imageNames: string[];
|
||||
total: number;
|
||||
status: RequestState | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
type AdditionalGalleryState = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
categories: ImageCategory[];
|
||||
selectedBoardId?: string;
|
||||
selection: string[];
|
||||
shouldAutoSwitch: boolean;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryView: 'images' | 'assets';
|
||||
isInitialized: boolean;
|
||||
galleryView: GalleryView;
|
||||
selectedBoardId: 'all' | 'none' | 'batch' | (string & Record<never, never>);
|
||||
boards: Boards;
|
||||
};
|
||||
|
||||
const initialBoardState = { imageNames: [], total: 0, status: undefined };
|
||||
|
||||
const initialBoards: Boards = {
|
||||
'all.images': {
|
||||
path: 'all.images',
|
||||
id: 'all',
|
||||
view: 'images',
|
||||
...initialBoardState,
|
||||
},
|
||||
'all.assets': {
|
||||
path: 'all.assets',
|
||||
id: 'all',
|
||||
view: 'assets',
|
||||
...initialBoardState,
|
||||
},
|
||||
'none.images': {
|
||||
path: 'none.images',
|
||||
id: 'none',
|
||||
view: 'images',
|
||||
...initialBoardState,
|
||||
},
|
||||
'none.assets': {
|
||||
path: 'none.assets',
|
||||
id: 'none',
|
||||
view: 'assets',
|
||||
...initialBoardState,
|
||||
},
|
||||
'batch.images': {
|
||||
path: 'batch.images',
|
||||
id: 'batch',
|
||||
view: 'images',
|
||||
...initialBoardState,
|
||||
},
|
||||
'batch.assets': {
|
||||
path: 'batch.assets',
|
||||
id: 'batch',
|
||||
view: 'assets',
|
||||
...initialBoardState,
|
||||
},
|
||||
};
|
||||
|
||||
export const initialGalleryState =
|
||||
imagesAdapter.getInitialState<AdditionaGalleryState>({
|
||||
galleryImagesAdapter.getInitialState<AdditionalGalleryState>({
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
total: 0,
|
||||
@ -59,57 +128,45 @@ export const initialGalleryState =
|
||||
galleryImageMinimumWidth: 96,
|
||||
galleryView: 'images',
|
||||
isInitialized: false,
|
||||
selectedBoardId: 'all',
|
||||
boards: initialBoards,
|
||||
});
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState: initialGalleryState,
|
||||
reducers: {
|
||||
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||
imagesAdapter.upsertOne(state, action.payload);
|
||||
if (
|
||||
state.shouldAutoSwitch &&
|
||||
action.payload.image_category === 'general'
|
||||
) {
|
||||
state.selection = [action.payload.image_name];
|
||||
state.galleryView = 'images';
|
||||
state.categories = IMAGE_CATEGORIES;
|
||||
}
|
||||
},
|
||||
imageUpdatedOne: (state, action: PayloadAction<Update<ImageDTO>>) => {
|
||||
imagesAdapter.updateOne(state, action.payload);
|
||||
},
|
||||
imageRemoved: (state, action: PayloadAction<string>) => {
|
||||
imagesAdapter.removeOne(state, action.payload);
|
||||
galleryImagesAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
imagesRemoved: (state, action: PayloadAction<string[]>) => {
|
||||
imagesAdapter.removeMany(state, action.payload);
|
||||
},
|
||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
||||
state.categories = action.payload;
|
||||
galleryImagesAdapter.removeMany(state, action.payload);
|
||||
},
|
||||
imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
||||
const rangeEndImageName = action.payload;
|
||||
const lastSelectedImage = state.selection[state.selection.length - 1];
|
||||
|
||||
const filteredImages = selectFilteredImagesLocal(state);
|
||||
// get image names for the current board and view
|
||||
const imageNames =
|
||||
state.boards[`${state.selectedBoardId}.${state.galleryView}`]
|
||||
.imageNames;
|
||||
|
||||
const lastClickedIndex = filteredImages.findIndex(
|
||||
(n) => n.image_name === lastSelectedImage
|
||||
// get the index of the last selected image
|
||||
const lastClickedIndex = imageNames.findIndex(
|
||||
(n) => n === lastSelectedImage
|
||||
);
|
||||
|
||||
const currentClickedIndex = filteredImages.findIndex(
|
||||
(n) => n.image_name === rangeEndImageName
|
||||
// get the index of the just-clicked image
|
||||
const currentClickedIndex = imageNames.findIndex(
|
||||
(n) => n === rangeEndImageName
|
||||
);
|
||||
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
// We have a valid range!
|
||||
// We have a valid range, selected it!
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
|
||||
const imagesToSelect = filteredImages
|
||||
.slice(start, end + 1)
|
||||
.map((i) => i.image_name);
|
||||
const imagesToSelect = imageNames.slice(start, end + 1);
|
||||
|
||||
state.selection = uniq(state.selection.concat(imagesToSelect));
|
||||
}
|
||||
@ -122,9 +179,10 @@ export const gallerySlice = createSlice({
|
||||
state.selection = state.selection.filter(
|
||||
(imageName) => imageName !== action.payload
|
||||
);
|
||||
} else {
|
||||
state.selection = uniq(state.selection.concat(action.payload));
|
||||
return;
|
||||
}
|
||||
|
||||
state.selection = uniq(state.selection.concat(action.payload));
|
||||
},
|
||||
imageSelected: (state, action: PayloadAction<string | null>) => {
|
||||
state.selection = action.payload
|
||||
@ -137,59 +195,210 @@ export const gallerySlice = createSlice({
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
setGalleryView: (state, action: PayloadAction<'images' | 'assets'>) => {
|
||||
setGalleryView: (state, action: PayloadAction<GalleryView>) => {
|
||||
state.galleryView = action.payload;
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
boardIdSelected: (state, action: PayloadAction<BoardPath>) => {
|
||||
const boardId = action.payload;
|
||||
|
||||
if (state.selectedBoardId === boardId) {
|
||||
// selected same board, no-op
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedBoardId = boardId;
|
||||
|
||||
// handle selecting an unitialized board
|
||||
const boardImagesId: BoardPath = `${boardId}.images`;
|
||||
const boardAssetsId: BoardPath = `${boardId}.assets`;
|
||||
|
||||
if (!state.boards[boardImagesId]) {
|
||||
state.boards[boardImagesId] = {
|
||||
path: boardImagesId,
|
||||
id: boardId,
|
||||
view: 'images',
|
||||
...initialBoardState,
|
||||
};
|
||||
}
|
||||
|
||||
if (!state.boards[boardAssetsId]) {
|
||||
state.boards[boardAssetsId] = {
|
||||
path: boardAssetsId,
|
||||
id: boardId,
|
||||
view: 'assets',
|
||||
...initialBoardState,
|
||||
};
|
||||
}
|
||||
|
||||
// set the first image as selected
|
||||
const firstImageName =
|
||||
state.boards[`${boardId}.${state.galleryView}`].imageNames[0];
|
||||
|
||||
state.selection = firstImageName ? [firstImageName] : [];
|
||||
},
|
||||
isLoadingChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedPageOfImages.pending, (state) => {
|
||||
state.isFetching = true;
|
||||
/**
|
||||
* Image deleted
|
||||
*/
|
||||
builder.addCase(imageDeleted.pending, (state, action) => {
|
||||
// optimistic update, but no undo :/
|
||||
const { image_name } = action.meta.arg;
|
||||
// remove image from all boards
|
||||
forEach(state.boards, (board) => {
|
||||
board.imageNames = board.imageNames.filter((n) => n !== image_name);
|
||||
});
|
||||
// and selection
|
||||
state.selection = state.selection.filter((n) => n !== image_name);
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.rejected, (state) => {
|
||||
state.isFetching = false;
|
||||
/**
|
||||
* Images loaded into gallery - PENDING
|
||||
*/
|
||||
builder.addCase(imagesLoaded.pending, (state, action) => {
|
||||
const { board_id, view } = action.meta.arg;
|
||||
state.boards[`${board_id}.${view}`].status = 'pending';
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
|
||||
state.isFetching = false;
|
||||
const { board_id, categories, image_origin, is_intermediate } =
|
||||
action.meta.arg;
|
||||
/**
|
||||
* Images loaded into gallery - FULFILLED
|
||||
*/
|
||||
builder.addCase(imagesLoaded.fulfilled, (state, action) => {
|
||||
const { items, total } = action.payload;
|
||||
const { board_id, view } = action.meta.arg;
|
||||
const board = state.boards[`${board_id}.${view}`];
|
||||
|
||||
const { items, offset, limit, total } = action.payload;
|
||||
board.status = 'fulfilled';
|
||||
|
||||
imagesAdapter.upsertMany(state, items);
|
||||
board.imageNames = uniq(
|
||||
board.imageNames.concat(items.map((i) => i.image_name))
|
||||
);
|
||||
|
||||
board.total = total;
|
||||
|
||||
if (state.selection.length === 0 && items.length) {
|
||||
state.selection = [items[0].image_name];
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Images loaded into gallery - REJECTED
|
||||
*/
|
||||
builder.addCase(imagesLoaded.rejected, (state, action) => {
|
||||
const { board_id, view } = action.meta.arg;
|
||||
state.boards[`${board_id}.${view}`].status = 'rejected';
|
||||
});
|
||||
/**
|
||||
* Image added to board
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardImagesApi.endpoints.addBoardImage.matchFulfilled,
|
||||
(state, action) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
// update user board stores
|
||||
const userBoards = selectUserBoards(state);
|
||||
userBoards.forEach((board) => {
|
||||
// only update the current view
|
||||
if (board.view !== state.galleryView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!categories?.includes('general') || board_id) {
|
||||
// need to skip updating the total images count if the images recieved were for a specific board
|
||||
// TODO: this doesn't work when on the Asset tab/category...
|
||||
return;
|
||||
if (board_id === board.id) {
|
||||
// add image to the board
|
||||
board.imageNames = uniq(board.imageNames.concat(image_name));
|
||||
} else {
|
||||
// remove image from other boards
|
||||
board.imageNames = board.imageNames.filter((n) => n !== image_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.offset = offset;
|
||||
state.total = total;
|
||||
});
|
||||
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||
const { image_name, image_url, thumbnail_url } = action.payload;
|
||||
|
||||
imagesAdapter.updateOne(state, {
|
||||
id: image_name,
|
||||
changes: { image_url, thumbnail_url },
|
||||
});
|
||||
});
|
||||
);
|
||||
/**
|
||||
* Many images added to board
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardImagesApi.endpoints.addManyBoardImages.matchFulfilled,
|
||||
(state, action) => {
|
||||
const { board_id, image_names } = action.meta.arg.originalArgs;
|
||||
// update local board stores
|
||||
forEach(state.boards, (board, board_id) => {
|
||||
// only update the current view
|
||||
if (board_id === board.id) {
|
||||
// add images to the board
|
||||
board.imageNames = uniq(board.imageNames.concat(image_names));
|
||||
} else {
|
||||
// remove images from other boards
|
||||
board.imageNames = board.imageNames.filter((n) =>
|
||||
image_names.includes(n)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Board deleted (not images)
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||
(state, action) => {
|
||||
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
||||
state.selectedBoardId = undefined;
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
if (deletedBoardId === state.selectedBoardId) {
|
||||
state.selectedBoardId = 'all';
|
||||
}
|
||||
// remove board from local store
|
||||
delete state.boards[`${deletedBoardId}.images`];
|
||||
delete state.boards[`${deletedBoardId}.assets`];
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Board deleted (with images)
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled,
|
||||
(state, action) => {
|
||||
const { deleted_images } = action.payload;
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
// remove images from all boards
|
||||
forEach(state.boards, (board) => {
|
||||
// remove images from all boards
|
||||
board.imageNames = board.imageNames.filter((n) =>
|
||||
deleted_images.includes(n)
|
||||
);
|
||||
});
|
||||
|
||||
delete state.boards[`${deletedBoardId}.images`];
|
||||
delete state.boards[`${deletedBoardId}.assets`];
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Image removed from board; i.e. Board reset for image
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardImagesApi.endpoints.deleteBoardImage.matchFulfilled,
|
||||
(state, action) => {
|
||||
const { image_name } = action.meta.arg.originalArgs;
|
||||
// remove from all user boards (skip all, none, batch)
|
||||
const userBoards = selectUserBoards(state);
|
||||
userBoards.forEach((board) => {
|
||||
board.imageNames = board.imageNames.filter((n) => n !== image_name);
|
||||
});
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Many images removed from board; i.e. Board reset for many images
|
||||
*/
|
||||
builder.addMatcher(
|
||||
boardImagesApi.endpoints.deleteManyBoardImages.matchFulfilled,
|
||||
(state, action) => {
|
||||
const { image_names } = action.meta.arg.originalArgs;
|
||||
// remove images from all boards
|
||||
forEach(state.imageNamesByIdAndView, (board) => {
|
||||
// only update the current view
|
||||
const view = board[state.galleryView];
|
||||
view.imageNames = view.imageNames.filter((n) =>
|
||||
image_names.includes(n)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
@ -201,14 +410,10 @@ export const {
|
||||
selectEntities: selectImagesEntities,
|
||||
selectIds: selectImagesIds,
|
||||
selectTotal: selectImagesTotal,
|
||||
} = imagesAdapter.getSelectors<RootState>((state) => state.gallery);
|
||||
} = galleryImagesAdapter.getSelectors<RootState>((state) => state.gallery);
|
||||
|
||||
export const {
|
||||
imageUpserted,
|
||||
imageUpdatedOne,
|
||||
imageRemoved,
|
||||
imagesRemoved,
|
||||
imageCategoriesChanged,
|
||||
imageRangeEndSelected,
|
||||
imageSelectionToggled,
|
||||
imageSelected,
|
||||
@ -221,44 +426,12 @@ export const {
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
||||
export const selectFilteredImagesLocal = createSelector(
|
||||
(state: typeof initialGalleryState) => state,
|
||||
(galleryState) => {
|
||||
const allImages = imagesAdapter.getSelectors().selectAll(galleryState);
|
||||
const { categories, selectedBoardId } = galleryState;
|
||||
const selectUserBoards = (state: typeof initialGalleryState) =>
|
||||
filter(state.boards, (board, path) => !systemBoards.includes(path));
|
||||
|
||||
const filteredImages = allImages.filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = selectedBoardId
|
||||
? i.board_id === selectedBoardId
|
||||
: true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
const selectCurrentBoard = (state: typeof initialGalleryState) =>
|
||||
state.boards[`${state.selectedBoardId}.${state.galleryView}`];
|
||||
|
||||
return filteredImages;
|
||||
}
|
||||
);
|
||||
const isImagesView = (board: BoardPath) => board.split('.')[1] === 'images';
|
||||
|
||||
export const selectFilteredImages = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => {
|
||||
return selectFilteredImagesLocal(state.gallery);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const selectFilteredImagesAsObject = createSelector(
|
||||
selectFilteredImages,
|
||||
(filteredImages) => keyBy(filteredImages, 'image_name')
|
||||
);
|
||||
|
||||
export const selectFilteredImagesIds = createSelector(
|
||||
selectFilteredImages,
|
||||
(filteredImages) => filteredImages.map((i) => i.image_name)
|
||||
);
|
||||
|
||||
export const selectLastSelectedImage = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => state.gallery.selection[state.gallery.selection.length - 1],
|
||||
defaultSelectorOptions
|
||||
);
|
||||
const isAssetsView = (board: BoardPath) => board.split('.')[1] === 'assets';
|
||||
|
@ -121,8 +121,9 @@ const nodesSlice = createSlice({
|
||||
) => {
|
||||
state.invocationTemplates = action.payload;
|
||||
},
|
||||
nodeEditorReset: () => {
|
||||
return { ...initialNodesState };
|
||||
nodeEditorReset: (state) => {
|
||||
state.nodes = [];
|
||||
state.edges = [];
|
||||
},
|
||||
setEditorInstance: (state, action) => {
|
||||
state.editorInstance = action.payload;
|
||||
|
@ -1,22 +1,22 @@
|
||||
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import BatchImageContainer from 'features/batch/components/BatchImageContainer';
|
||||
import {
|
||||
asInitialImageToggled,
|
||||
batchReset,
|
||||
} from 'features/batch/store/batchSlice';
|
||||
import BatchImageContainer from 'features/batch/components/BatchImageContainer';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FaLayerGroup, FaUndo, FaUpload } from 'react-icons/fa';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { PostUploadAction } from 'services/api/thunks/image';
|
||||
import InitialImage from './InitialImage';
|
||||
|
||||
@ -114,7 +114,7 @@ const InitialImageDisplay = () => {
|
||||
Initial Image
|
||||
</Text>
|
||||
<Spacer />
|
||||
{/* <IAIButton
|
||||
<IAIButton
|
||||
tooltip={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
|
||||
aria-label={useBatchAsInitialImage ? 'Disable Batch' : 'Enable Batch'}
|
||||
leftIcon={<FaLayerGroup />}
|
||||
@ -122,7 +122,7 @@ const InitialImageDisplay = () => {
|
||||
onClick={handleClickUseBatch}
|
||||
>
|
||||
{useBatchAsInitialImage ? 'Batch' : 'Single'}
|
||||
</IAIButton> */}
|
||||
</IAIButton>
|
||||
<IAIIconButton
|
||||
tooltip={
|
||||
useBatchAsInitialImage ? 'Upload to Batch' : 'Upload Initial Image'
|
||||
@ -146,8 +146,7 @@ const InitialImageDisplay = () => {
|
||||
isDisabled={isResetButtonDisabled}
|
||||
/>
|
||||
</Flex>
|
||||
<InitialImage />
|
||||
{/* {useBatchAsInitialImage ? <BatchImageContainer /> : <InitialImage />} */}
|
||||
{useBatchAsInitialImage ? <BatchImageContainer /> : <InitialImage />}
|
||||
<input {...getUploadInputProps()} />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ import { isEqual } from 'lodash-es';
|
||||
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
|
||||
import { FaCube, FaFont, FaImage, FaLayerGroup } from 'react-icons/fa';
|
||||
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
|
||||
@ -32,6 +32,7 @@ import {
|
||||
activeTabIndexSelector,
|
||||
activeTabNameSelector,
|
||||
} from '../store/uiSelectors';
|
||||
import BatchTab from './tabs/Batch/BatchTab';
|
||||
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
|
||||
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
|
||||
import NodesTab from './tabs/Nodes/NodesTab';
|
||||
@ -78,11 +79,12 @@ const tabs: InvokeTabInfo[] = [
|
||||
icon: <Icon as={FaCube} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
|
||||
content: <ModelManagerTab />,
|
||||
},
|
||||
// {
|
||||
// id: 'batch',
|
||||
// icon: <Icon as={FaLayerGroup} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
|
||||
// content: <BatchTab />,
|
||||
// },
|
||||
{
|
||||
id: 'batch',
|
||||
translationKey: 'modelManager.modelManager',
|
||||
icon: <Icon as={FaLayerGroup} sx={{ boxSize: 6, pointerEvents: 'none' }} />,
|
||||
content: <BatchTab />,
|
||||
},
|
||||
];
|
||||
|
||||
const enabledTabsSelector = createSelector(
|
||||
|
@ -1,44 +1,57 @@
|
||||
import { OffsetPaginatedResults_ImageDTO_ } from 'services/api/types';
|
||||
import { api } from '..';
|
||||
import { paths } from '../schema';
|
||||
import { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks';
|
||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||
import { components, paths } from '../schema';
|
||||
import { imagesApi } from './images';
|
||||
|
||||
type ListBoardImagesArg =
|
||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['path'] &
|
||||
paths['/api/v1/board_images/{board_id}']['get']['parameters']['query'];
|
||||
|
||||
type AddImageToBoardArg =
|
||||
paths['/api/v1/board_images/']['post']['requestBody']['content']['application/json'];
|
||||
paths['/api/v1/board_images/{board_id}']['post']['requestBody']['content']['application/json'];
|
||||
|
||||
type AddManyImagesToBoardArg =
|
||||
paths['/api/v1/board_images/{board_id}/images']['patch']['requestBody']['content']['application/json'];
|
||||
|
||||
type RemoveImageFromBoardArg =
|
||||
paths['/api/v1/board_images/']['delete']['requestBody']['content']['application/json'];
|
||||
|
||||
type RemoveManyBoardImagesArg =
|
||||
paths['/api/v1/board_images/images']['post']['requestBody']['content']['application/json'];
|
||||
|
||||
type GetAllBoardImagesForBoardResult =
|
||||
components['schemas']['GetAllBoardImagesForBoardResult'];
|
||||
|
||||
export const boardImagesApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
/**
|
||||
* Board Images Queries
|
||||
*/
|
||||
|
||||
listBoardImages: build.query<
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
ListBoardImagesArg
|
||||
getAllBoardImagesForBoard: build.query<
|
||||
GetAllBoardImagesForBoardResult,
|
||||
{ board_id: string }
|
||||
>({
|
||||
query: ({ board_id, offset, limit }) => ({
|
||||
query: ({ board_id }) => ({
|
||||
url: `board_images/${board_id}`,
|
||||
method: 'DELETE',
|
||||
body: { offset, limit },
|
||||
method: 'GET',
|
||||
}),
|
||||
providesTags: (result, error, arg) => [
|
||||
{
|
||||
type: 'Board',
|
||||
id: arg.board_id,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Board Images Mutations
|
||||
*/
|
||||
|
||||
addImageToBoard: build.mutation<void, AddImageToBoardArg>({
|
||||
addBoardImage: build.mutation<
|
||||
void,
|
||||
{ board_id: string; image_name: string }
|
||||
>({
|
||||
query: ({ board_id, image_name }) => ({
|
||||
url: `board_images/`,
|
||||
url: `board_images/${board_id}`,
|
||||
method: 'POST',
|
||||
body: { board_id, image_name },
|
||||
body: image_name,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
@ -60,19 +73,55 @@ export const boardImagesApi = api.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
|
||||
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
|
||||
query: ({ board_id, image_name }) => ({
|
||||
url: `board_images/`,
|
||||
method: 'DELETE',
|
||||
body: { board_id, image_name },
|
||||
addManyBoardImages: build.mutation<
|
||||
string[],
|
||||
{ board_id: string; image_names: string[] }
|
||||
>({
|
||||
query: ({ board_id, image_names }) => ({
|
||||
url: `board_images/${board_id}/images`,
|
||||
method: 'PATCH',
|
||||
body: image_names,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
],
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ type: 'Board', id: arg.board_id },
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
];
|
||||
return tags;
|
||||
},
|
||||
async onQueryStarted(
|
||||
{ image_name, ...patch },
|
||||
{ image_names, board_id },
|
||||
{ dispatch, queryFulfilled }
|
||||
) {
|
||||
const patches: PatchCollection[] = [];
|
||||
|
||||
image_names.forEach((n) => {
|
||||
const patchResult = dispatch(
|
||||
imagesApi.util.updateQueryData('getImageDTO', n, (draft) => {
|
||||
Object.assign(draft, { board_id });
|
||||
})
|
||||
);
|
||||
patches.push(patchResult);
|
||||
});
|
||||
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
patches.forEach((p) => p.undo());
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteBoardImage: build.mutation<void, { image_name: string }>({
|
||||
query: ({ image_name }) => ({
|
||||
url: `board_images/`,
|
||||
method: 'DELETE',
|
||||
body: image_name,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
],
|
||||
async onQueryStarted({ image_name }, { dispatch, queryFulfilled }) {
|
||||
const patchResult = dispatch(
|
||||
imagesApi.util.updateQueryData('getImageDTO', image_name, (draft) => {
|
||||
Object.assign(draft, { board_id: null });
|
||||
@ -85,11 +134,42 @@ export const boardImagesApi = api.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteManyBoardImages: build.mutation<void, { image_names: string[] }>({
|
||||
query: ({ image_names }) => ({
|
||||
url: `board_images/images`,
|
||||
method: 'POST',
|
||||
body: image_names,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: LIST_TAG },
|
||||
],
|
||||
async onQueryStarted({ image_names }, { dispatch, queryFulfilled }) {
|
||||
const patches: PatchCollection[] = [];
|
||||
|
||||
image_names.forEach((n) => {
|
||||
const patchResult = dispatch(
|
||||
imagesApi.util.updateQueryData('getImageDTO', n, (draft) => {
|
||||
Object.assign(draft, { board_id: null });
|
||||
})
|
||||
);
|
||||
patches.push(patchResult);
|
||||
});
|
||||
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
patches.forEach((p) => p.undo());
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useAddImageToBoardMutation,
|
||||
useRemoveImageFromBoardMutation,
|
||||
useListBoardImagesQuery,
|
||||
useGetAllBoardImagesForBoardQuery,
|
||||
useAddBoardImageMutation,
|
||||
useAddManyBoardImagesMutation,
|
||||
useDeleteBoardImageMutation,
|
||||
useDeleteManyBoardImagesMutation,
|
||||
} = boardImagesApi;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BoardDTO, OffsetPaginatedResults_BoardDTO_ } from 'services/api/types';
|
||||
import { ApiFullTagDescription, LIST_TAG, api } from '..';
|
||||
import { paths } from '../schema';
|
||||
import { components, paths } from '../schema';
|
||||
|
||||
type ListBoardsArg = NonNullable<
|
||||
paths['/api/v1/boards/']['get']['parameters']['query']
|
||||
@ -20,7 +20,7 @@ export const boardsApi = api.injectEndpoints({
|
||||
query: (arg) => ({ url: 'boards/', params: arg }),
|
||||
providesTags: (result, error, arg) => {
|
||||
// any list of boards
|
||||
const tags: ApiFullTagDescription[] = [{ id: 'Board', type: LIST_TAG }];
|
||||
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
||||
|
||||
if (result) {
|
||||
// and individual tags for each board
|
||||
@ -43,7 +43,7 @@ export const boardsApi = api.injectEndpoints({
|
||||
}),
|
||||
providesTags: (result, error, arg) => {
|
||||
// any list of boards
|
||||
const tags: ApiFullTagDescription[] = [{ id: 'Board', type: LIST_TAG }];
|
||||
const tags: ApiFullTagDescription[] = [{ type: 'Board', id: LIST_TAG }];
|
||||
|
||||
if (result) {
|
||||
// and individual tags for each board
|
||||
@ -69,7 +69,7 @@ export const boardsApi = api.injectEndpoints({
|
||||
method: 'POST',
|
||||
params: { board_name },
|
||||
}),
|
||||
invalidatesTags: [{ id: 'Board', type: LIST_TAG }],
|
||||
invalidatesTags: [{ type: 'Board', id: LIST_TAG }],
|
||||
}),
|
||||
|
||||
updateBoard: build.mutation<BoardDTO, UpdateBoardArg>({
|
||||
@ -86,9 +86,19 @@ export const boardsApi = api.injectEndpoints({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
|
||||
}),
|
||||
deleteBoardAndImages: build.mutation<void, string>({
|
||||
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE', params: { include_images: true } }),
|
||||
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }, { type: 'Image', id: LIST_TAG }],
|
||||
deleteBoardAndImages: build.mutation<
|
||||
components['schemas']['DeleteManyImagesResult'],
|
||||
string
|
||||
>({
|
||||
query: (board_id) => ({
|
||||
url: `boards/${board_id}`,
|
||||
method: 'DELETE',
|
||||
params: { include_images: true },
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [
|
||||
{ type: 'Board', id: arg },
|
||||
{ type: 'Image', id: LIST_TAG },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@ -99,5 +109,5 @@ export const {
|
||||
useCreateBoardMutation,
|
||||
useUpdateBoardMutation,
|
||||
useDeleteBoardMutation,
|
||||
useDeleteBoardAndImagesMutation
|
||||
useDeleteBoardAndImagesMutation,
|
||||
} = boardsApi;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ApiFullTagDescription, api } from '..';
|
||||
import { api } from '..';
|
||||
import { ImageDTO } from '../types';
|
||||
|
||||
export const imagesApi = api.injectEndpoints({
|
||||
@ -7,14 +7,8 @@ export const imagesApi = api.injectEndpoints({
|
||||
* Image Queries
|
||||
*/
|
||||
getImageDTO: build.query<ImageDTO, string>({
|
||||
query: (image_name) => ({ url: `images/${image_name}/metadata` }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }];
|
||||
if (result?.board_id) {
|
||||
tags.push({ type: 'Board', id: result.board_id });
|
||||
}
|
||||
return tags;
|
||||
},
|
||||
query: (image_name) => ({ url: `images/${image_name}` }),
|
||||
providesTags: (result, error, arg) => [{ type: 'Image', id: arg }],
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
}),
|
||||
|
@ -73,7 +73,7 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => ({ url: 'models/', params: { model_type: 'main' } }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ id: 'MainModel', type: LIST_TAG },
|
||||
{ type: 'MainModel', id: LIST_TAG },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
@ -105,7 +105,7 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => ({ url: 'models/', params: { model_type: 'lora' } }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ id: 'LoRAModel', type: LIST_TAG },
|
||||
{ type: 'LoRAModel', id: LIST_TAG },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
@ -140,7 +140,7 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => ({ url: 'models/', params: { model_type: 'controlnet' } }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ id: 'ControlNetModel', type: LIST_TAG },
|
||||
{ type: 'ControlNetModel', id: LIST_TAG },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
@ -172,7 +172,7 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => ({ url: 'models/', params: { model_type: 'vae' } }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ id: 'VaeModel', type: LIST_TAG },
|
||||
{ type: 'VaeModel', id: LIST_TAG },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
@ -207,7 +207,7 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => ({ url: 'models/', params: { model_type: 'embedding' } }),
|
||||
providesTags: (result, error, arg) => {
|
||||
const tags: ApiFullTagDescription[] = [
|
||||
{ id: 'TextualInversionModel', type: LIST_TAG },
|
||||
{ type: 'TextualInversionModel', id: LIST_TAG },
|
||||
];
|
||||
|
||||
if (result) {
|
||||
|
384
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
384
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
@ -107,12 +107,7 @@ export type paths = {
|
||||
*/
|
||||
put: operations["merge_models"];
|
||||
};
|
||||
"/api/v1/images/": {
|
||||
/**
|
||||
* List Images With Metadata
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
get: operations["list_images_with_metadata"];
|
||||
"/api/v1/images/upload": {
|
||||
/**
|
||||
* Upload Image
|
||||
* @description Uploads an image
|
||||
@ -121,10 +116,10 @@ export type paths = {
|
||||
};
|
||||
"/api/v1/images/{image_name}": {
|
||||
/**
|
||||
* Get Image Full
|
||||
* @description Gets a full-resolution image file
|
||||
* Get Image Dto
|
||||
* @description Gets an image's DTO
|
||||
*/
|
||||
get: operations["get_image_full"];
|
||||
get: operations["get_image"];
|
||||
/**
|
||||
* Delete Image
|
||||
* @description Deletes an image
|
||||
@ -136,12 +131,12 @@ export type paths = {
|
||||
*/
|
||||
patch: operations["update_image"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/metadata": {
|
||||
"/api/v1/images/{image_name}/full_size": {
|
||||
/**
|
||||
* Get Image Metadata
|
||||
* @description Gets an image's metadata
|
||||
* Get Image Full Size
|
||||
* @description Gets a full-resolution image file
|
||||
*/
|
||||
get: operations["get_image_metadata"];
|
||||
get: operations["get_image_full_size"];
|
||||
};
|
||||
"/api/v1/images/{image_name}/thumbnail": {
|
||||
/**
|
||||
@ -157,6 +152,25 @@ export type paths = {
|
||||
*/
|
||||
get: operations["get_image_urls"];
|
||||
};
|
||||
"/api/v1/images/": {
|
||||
/**
|
||||
* Get Many Images
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
get: operations["get_many_images"];
|
||||
/**
|
||||
* Get Images By Names
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
post: operations["get_images_by_names"];
|
||||
};
|
||||
"/api/v1/images/delete": {
|
||||
/**
|
||||
* Delete Many Images
|
||||
* @description Deletes many images
|
||||
*/
|
||||
post: operations["delete_many_images"];
|
||||
};
|
||||
"/api/v1/boards/": {
|
||||
/**
|
||||
* List Boards
|
||||
@ -186,24 +200,38 @@ export type paths = {
|
||||
*/
|
||||
patch: operations["update_board"];
|
||||
};
|
||||
"/api/v1/board_images/": {
|
||||
"/api/v1/board_images/{board_id}": {
|
||||
/**
|
||||
* Get All Board Images For Board
|
||||
* @description Gets all image names for a board
|
||||
*/
|
||||
get: operations["get_all_board_images_for_board"];
|
||||
/**
|
||||
* Create Board Image
|
||||
* @description Creates a board_image
|
||||
*/
|
||||
post: operations["create_board_image"];
|
||||
};
|
||||
"/api/v1/board_images/": {
|
||||
/**
|
||||
* Remove Board Image
|
||||
* @description Deletes a board_image
|
||||
*/
|
||||
delete: operations["remove_board_image"];
|
||||
};
|
||||
"/api/v1/board_images/{board_id}": {
|
||||
"/api/v1/board_images/{board_id}/images": {
|
||||
/**
|
||||
* List Board Images
|
||||
* @description Gets a list of images for a board
|
||||
* Create Multiple Board Images
|
||||
* @description Add many images to a board
|
||||
*/
|
||||
get: operations["list_board_images"];
|
||||
patch: operations["create_multiple_board_images"];
|
||||
};
|
||||
"/api/v1/board_images/images": {
|
||||
/**
|
||||
* Delete Multiple Board Images
|
||||
* @description Remove many images from their boards, if they have one
|
||||
*/
|
||||
post: operations["delete_multiple_board_images"];
|
||||
};
|
||||
"/api/v1/app/version": {
|
||||
/** Get Version */
|
||||
@ -318,19 +346,6 @@ export type components = {
|
||||
*/
|
||||
image_count: number;
|
||||
};
|
||||
/** Body_create_board_image */
|
||||
Body_create_board_image: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board to add to
|
||||
*/
|
||||
board_id: string;
|
||||
/**
|
||||
* Image Name
|
||||
* @description The name of the image to add
|
||||
*/
|
||||
image_name: string;
|
||||
};
|
||||
/** Body_import_model */
|
||||
Body_import_model: {
|
||||
/**
|
||||
@ -373,19 +388,6 @@ export type components = {
|
||||
*/
|
||||
force?: boolean;
|
||||
};
|
||||
/** Body_remove_board_image */
|
||||
Body_remove_board_image: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board
|
||||
*/
|
||||
board_id: string;
|
||||
/**
|
||||
* Image Name
|
||||
* @description The name of the image to remove
|
||||
*/
|
||||
image_name: string;
|
||||
};
|
||||
/** Body_upload_image */
|
||||
Body_upload_image: {
|
||||
/**
|
||||
@ -869,6 +871,17 @@ export type components = {
|
||||
*/
|
||||
mask?: components["schemas"]["ImageField"];
|
||||
};
|
||||
/**
|
||||
* DeleteManyImagesResult
|
||||
* @description The result of a delete many image operation.
|
||||
*/
|
||||
DeleteManyImagesResult: {
|
||||
/**
|
||||
* Deleted Images
|
||||
* @description The names of the images that were successfully deleted
|
||||
*/
|
||||
deleted_images: (string)[];
|
||||
};
|
||||
/**
|
||||
* DivideInvocation
|
||||
* @description Divides two numbers
|
||||
@ -1046,6 +1059,33 @@ export type components = {
|
||||
*/
|
||||
param?: number;
|
||||
};
|
||||
/**
|
||||
* GetAllBoardImagesForBoardResult
|
||||
* @description The result of a get all image names for board operation.
|
||||
*/
|
||||
GetAllBoardImagesForBoardResult: {
|
||||
/**
|
||||
* Board Id
|
||||
* @description The id of the board with which the images are associated
|
||||
*/
|
||||
board_id: string;
|
||||
/**
|
||||
* Image Names
|
||||
* @description The names of the images that are associated with the board
|
||||
*/
|
||||
image_names: (string)[];
|
||||
};
|
||||
/**
|
||||
* GetImagesByNamesResult
|
||||
* @description The result of a get all image names for board operation.
|
||||
*/
|
||||
GetImagesByNamesResult: {
|
||||
/**
|
||||
* Image Dtos
|
||||
* @description The names of the images that are associated with the board
|
||||
*/
|
||||
image_dtos: (components["schemas"]["ImageDTO"])[];
|
||||
};
|
||||
/** Graph */
|
||||
Graph: {
|
||||
/**
|
||||
@ -1058,7 +1098,7 @@ export type components = {
|
||||
* @description The nodes in this graph
|
||||
*/
|
||||
nodes?: {
|
||||
[key: string]: (components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
|
||||
[key: string]: (components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"]) | undefined;
|
||||
};
|
||||
/**
|
||||
* Edges
|
||||
@ -1101,7 +1141,7 @@ export type components = {
|
||||
* @description The results of node executions
|
||||
*/
|
||||
results: {
|
||||
[key: string]: (components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
|
||||
[key: string]: (components["schemas"]["ImageOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["PromptOutput"] | components["schemas"]["PromptCollectionOutput"] | components["schemas"]["CompelOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["IntOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IntCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]) | undefined;
|
||||
};
|
||||
/**
|
||||
* Errors
|
||||
@ -4425,18 +4465,18 @@ export type components = {
|
||||
*/
|
||||
image?: components["schemas"]["ImageField"];
|
||||
};
|
||||
/**
|
||||
* StableDiffusion2ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusion1ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
|
||||
/**
|
||||
* StableDiffusion2ModelFormat
|
||||
* @description An enumeration.
|
||||
* @enum {string}
|
||||
*/
|
||||
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -4547,7 +4587,7 @@ export type operations = {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -4584,7 +4624,7 @@ export type operations = {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||
"application/json": components["schemas"]["LoadImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ParamIntInvocation"] | components["schemas"]["ParamFloatInvocation"] | components["schemas"]["TextToLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["UpscaleInvocation"] | components["schemas"]["RestoreFaceInvocation"] | components["schemas"]["InpaintInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LatentsToLatentsInvocation"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -4976,42 +5016,6 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Images With Metadata
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
list_images_with_metadata: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The origin of images to list */
|
||||
image_origin?: components["schemas"]["ResourceOrigin"];
|
||||
/** @description The categories of image to include */
|
||||
categories?: (components["schemas"]["ImageCategory"])[];
|
||||
/** @description Whether to list intermediate images */
|
||||
is_intermediate?: boolean;
|
||||
/** @description The board id to filter by */
|
||||
board_id?: string;
|
||||
/** @description The page offset */
|
||||
offset?: number;
|
||||
/** @description The number of images per page */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Upload Image
|
||||
* @description Uploads an image
|
||||
@ -5050,25 +5054,23 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Image Full
|
||||
* @description Gets a full-resolution image file
|
||||
* Get Image Dto
|
||||
* @description Gets an image's DTO
|
||||
*/
|
||||
get_image_full: {
|
||||
get_image: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description The name of full-resolution image file to get */
|
||||
/** @description The name of image to get */
|
||||
image_name: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Return the full-resolution image */
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"image/png": unknown;
|
||||
"application/json": components["schemas"]["ImageDTO"];
|
||||
};
|
||||
};
|
||||
/** @description Image not found */
|
||||
404: never;
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
@ -5135,23 +5137,25 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Image Metadata
|
||||
* @description Gets an image's metadata
|
||||
* Get Image Full Size
|
||||
* @description Gets a full-resolution image file
|
||||
*/
|
||||
get_image_metadata: {
|
||||
get_image_full_size: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description The name of image to get */
|
||||
/** @description The name of full-resolution image file to get */
|
||||
image_name: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
/** @description Return the full-resolution image */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ImageDTO"];
|
||||
"image/png": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Image not found */
|
||||
404: never;
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
@ -5214,6 +5218,92 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Many Images
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
get_many_images: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The origin of images to list */
|
||||
image_origin?: components["schemas"]["ResourceOrigin"];
|
||||
/** @description The categories of image to include */
|
||||
categories?: (components["schemas"]["ImageCategory"])[];
|
||||
/** @description Whether to list intermediate images */
|
||||
is_intermediate?: boolean;
|
||||
/** @description The board id to filter by, provide 'none' for images without a board */
|
||||
board_id?: string;
|
||||
/** @description The page offset */
|
||||
offset?: number;
|
||||
/** @description The number of images per page */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Images By Names
|
||||
* @description Gets a list of images
|
||||
*/
|
||||
get_images_by_names: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": (string)[];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetImagesByNamesResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Delete Many Images
|
||||
* @description Deletes many images
|
||||
*/
|
||||
delete_many_images: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": (string)[];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeleteManyImagesResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Boards
|
||||
* @description Gets a list of boards
|
||||
@ -5315,7 +5405,7 @@ export type operations = {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
"application/json": components["schemas"]["DeleteManyImagesResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -5357,14 +5447,46 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get All Board Images For Board
|
||||
* @description Gets all image names for a board
|
||||
*/
|
||||
get_all_board_images_for_board: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description The id of the board */
|
||||
board_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetAllBoardImagesForBoardResult"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Create Board Image
|
||||
* @description Creates a board_image
|
||||
*/
|
||||
create_board_image: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description The id of the board to add to */
|
||||
board_id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_create_board_image"];
|
||||
"application/json": string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -5389,7 +5511,7 @@ export type operations = {
|
||||
remove_board_image: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_remove_board_image"];
|
||||
"application/json": string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -5408,27 +5530,51 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Board Images
|
||||
* @description Gets a list of images for a board
|
||||
* Create Multiple Board Images
|
||||
* @description Add many images to a board
|
||||
*/
|
||||
list_board_images: {
|
||||
create_multiple_board_images: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The page offset */
|
||||
offset?: number;
|
||||
/** @description The number of boards per page */
|
||||
limit?: number;
|
||||
};
|
||||
path: {
|
||||
/** @description The id of the board */
|
||||
board_id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": (string)[];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
/** @description The images were added to the board successfully */
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Delete Multiple Board Images
|
||||
* @description Remove many images from their boards, if they have one
|
||||
*/
|
||||
delete_multiple_board_images: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": (string)[];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The images were removed from their boards successfully */
|
||||
201: {
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
41
invokeai/frontend/web/src/services/api/thunks/boardImages.ts
Normal file
41
invokeai/frontend/web/src/services/api/thunks/boardImages.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { $client } from '../client';
|
||||
import { components } from '../schema';
|
||||
|
||||
type Arg = { board_id: string };
|
||||
|
||||
type GetAllBoardImagesForBoardResult =
|
||||
components['schemas']['GetAllBoardImagesForBoardResult'];
|
||||
|
||||
type GetImageUrlsThunkConfig = {
|
||||
rejectValue: {
|
||||
arg: Arg;
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Thunk to get image URLs
|
||||
*/
|
||||
export const boardImageNamesReceived = createAppAsyncThunk<
|
||||
GetAllBoardImagesForBoardResult,
|
||||
Arg,
|
||||
GetImageUrlsThunkConfig
|
||||
>('thunkApi/boardImageNamesReceived', async (arg, { rejectWithValue }) => {
|
||||
const { get } = $client.get();
|
||||
const { data, error, response } = await get(
|
||||
'/api/v1/board_images/{board_id}',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
board_id: arg.board_id,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
@ -1,9 +1,10 @@
|
||||
import queryString from 'query-string';
|
||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { selectImagesAll } from 'features/gallery/store/gallerySlice';
|
||||
import { size } from 'lodash-es';
|
||||
import { paths } from 'services/api/schema';
|
||||
import queryString from 'query-string';
|
||||
import { $client } from 'services/api/client';
|
||||
import { paths } from 'services/api/schema';
|
||||
import { ImageCategory, OffsetPaginatedResults_ImageDTO_ } from '../types';
|
||||
|
||||
type GetImageUrlsArg =
|
||||
paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path'];
|
||||
@ -24,7 +25,7 @@ export const imageUrlsReceived = createAppAsyncThunk<
|
||||
GetImageUrlsResponse,
|
||||
GetImageUrlsArg,
|
||||
GetImageUrlsThunkConfig
|
||||
>('api/imageUrlsReceived', async (arg, { rejectWithValue }) => {
|
||||
>('thunkApi/imageUrlsReceived', async (arg, { rejectWithValue }) => {
|
||||
const { image_name } = arg;
|
||||
const { get } = $client.get();
|
||||
const { data, error, response } = await get(
|
||||
@ -45,34 +46,31 @@ export const imageUrlsReceived = createAppAsyncThunk<
|
||||
return data;
|
||||
});
|
||||
|
||||
type GetImageMetadataArg =
|
||||
paths['/api/v1/images/{image_name}/metadata']['get']['parameters']['path'];
|
||||
type GetImageDTOArg =
|
||||
paths['/api/v1/images/{image_name}']['get']['parameters']['path'];
|
||||
|
||||
type GetImageMetadataResponse =
|
||||
paths['/api/v1/images/{image_name}/metadata']['get']['responses']['200']['content']['application/json'];
|
||||
type GetImageDTOResponse =
|
||||
paths['/api/v1/images/{image_name}']['get']['responses']['200']['content']['application/json'];
|
||||
|
||||
type GetImageMetadataThunkConfig = {
|
||||
type GetImageDTOThunkConfig = {
|
||||
rejectValue: {
|
||||
arg: GetImageMetadataArg;
|
||||
arg: GetImageDTOArg;
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export const imageMetadataReceived = createAppAsyncThunk<
|
||||
GetImageMetadataResponse,
|
||||
GetImageMetadataArg,
|
||||
GetImageMetadataThunkConfig
|
||||
>('api/imageMetadataReceived', async (arg, { rejectWithValue }) => {
|
||||
export const imageDTOReceived = createAppAsyncThunk<
|
||||
GetImageDTOResponse,
|
||||
GetImageDTOArg,
|
||||
GetImageDTOThunkConfig
|
||||
>('thunkApi/imageDTOReceived', async (arg, { rejectWithValue }) => {
|
||||
const { image_name } = arg;
|
||||
const { get } = $client.get();
|
||||
const { data, error, response } = await get(
|
||||
'/api/v1/images/{image_name}/metadata',
|
||||
{
|
||||
params: {
|
||||
path: { image_name },
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data, error, response } = await get('/api/v1/images/{image_name}', {
|
||||
params: {
|
||||
path: { image_name },
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
@ -127,13 +125,13 @@ export type PostUploadAction =
|
||||
| AddToBatchAction;
|
||||
|
||||
type UploadImageArg =
|
||||
paths['/api/v1/images/']['post']['parameters']['query'] & {
|
||||
paths['/api/v1/images/upload']['post']['parameters']['query'] & {
|
||||
file: File;
|
||||
postUploadAction?: PostUploadAction;
|
||||
};
|
||||
|
||||
type UploadImageResponse =
|
||||
paths['/api/v1/images/']['post']['responses']['201']['content']['application/json'];
|
||||
paths['/api/v1/images/upload']['post']['responses']['201']['content']['application/json'];
|
||||
|
||||
type UploadImageThunkConfig = {
|
||||
rejectValue: {
|
||||
@ -148,7 +146,7 @@ export const imageUploaded = createAppAsyncThunk<
|
||||
UploadImageResponse,
|
||||
UploadImageArg,
|
||||
UploadImageThunkConfig
|
||||
>('api/imageUploaded', async (arg, { rejectWithValue }) => {
|
||||
>('thunkApi/imageUploaded', async (arg, { rejectWithValue }) => {
|
||||
const {
|
||||
postUploadAction,
|
||||
file,
|
||||
@ -157,7 +155,7 @@ export const imageUploaded = createAppAsyncThunk<
|
||||
session_id,
|
||||
} = arg;
|
||||
const { post } = $client.get();
|
||||
const { data, error, response } = await post('/api/v1/images/', {
|
||||
const { data, error, response } = await post('/api/v1/images/upload', {
|
||||
params: {
|
||||
query: {
|
||||
image_category,
|
||||
@ -199,7 +197,7 @@ export const imageDeleted = createAppAsyncThunk<
|
||||
DeleteImageResponse,
|
||||
DeleteImageArg,
|
||||
DeleteImageThunkConfig
|
||||
>('api/imageDeleted', async (arg, { rejectWithValue }) => {
|
||||
>('thunkApi/imageDeleted', async (arg, { rejectWithValue }) => {
|
||||
const { image_name } = arg;
|
||||
const { del } = $client.get();
|
||||
const { data, error, response } = await del('/api/v1/images/{image_name}', {
|
||||
@ -235,7 +233,7 @@ export const imageUpdated = createAppAsyncThunk<
|
||||
UpdateImageResponse,
|
||||
UpdateImageArg,
|
||||
UpdateImageThunkConfig
|
||||
>('api/imageUpdated', async (arg, { rejectWithValue }) => {
|
||||
>('thunkApi/imageUpdated', async (arg, { rejectWithValue }) => {
|
||||
const { image_name, image_category, is_intermediate, session_id } = arg;
|
||||
const { patch } = $client.get();
|
||||
const { data, error, response } = await patch('/api/v1/images/{image_name}', {
|
||||
@ -277,6 +275,7 @@ type ListImagesThunkConfig = {
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* `ImagesService.listImagesWithMetadata()` thunk
|
||||
*/
|
||||
@ -284,46 +283,156 @@ export const receivedPageOfImages = createAppAsyncThunk<
|
||||
ListImagesResponse,
|
||||
ListImagesArg,
|
||||
ListImagesThunkConfig
|
||||
>('api/receivedPageOfImages', async (arg, { getState, rejectWithValue }) => {
|
||||
const { get } = $client.get();
|
||||
>(
|
||||
'thunkApi/receivedPageOfImages',
|
||||
async (arg, { getState, rejectWithValue }) => {
|
||||
const { get } = $client.get();
|
||||
|
||||
const state = getState();
|
||||
const { categories, selectedBoardId } = state.gallery;
|
||||
const state = getState();
|
||||
const { categories, selectedBoardId } = state.gallery;
|
||||
|
||||
const images = selectImagesAll(state).filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = selectedBoardId
|
||||
? i.board_id === selectedBoardId
|
||||
: true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
const images = selectImagesAll(state).filter((i) => {
|
||||
const isInCategory = categories.includes(i.image_category);
|
||||
const isInSelectedBoard = selectedBoardId
|
||||
? i.board_id === selectedBoardId
|
||||
: true;
|
||||
return isInCategory && isInSelectedBoard;
|
||||
});
|
||||
|
||||
let query: ListImagesArg = {};
|
||||
let query: ListImagesArg = {};
|
||||
|
||||
if (size(arg)) {
|
||||
query = {
|
||||
...DEFAULT_IMAGES_LISTED_ARG,
|
||||
offset: images.length,
|
||||
...arg,
|
||||
};
|
||||
} else {
|
||||
query = {
|
||||
...DEFAULT_IMAGES_LISTED_ARG,
|
||||
categories,
|
||||
offset: images.length,
|
||||
};
|
||||
if (size(arg)) {
|
||||
query = {
|
||||
...DEFAULT_IMAGES_LISTED_ARG,
|
||||
offset: images.length,
|
||||
...arg,
|
||||
};
|
||||
} else {
|
||||
query = {
|
||||
...DEFAULT_IMAGES_LISTED_ARG,
|
||||
categories,
|
||||
offset: images.length,
|
||||
};
|
||||
}
|
||||
|
||||
const { data, error, response } = await get('/api/v1/images/', {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
||||
|
||||
const { data, error, response } = await get('/api/v1/images/', {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
|
||||
});
|
||||
export type ImagesLoadedArg = {
|
||||
board_id: 'all' | 'none' | (string & Record<never, never>);
|
||||
view: 'images' | 'assets';
|
||||
offset: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
type ImagesLoadedThunkConfig = {
|
||||
rejectValue: {
|
||||
arg: ImagesLoadedArg;
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const getCategories = (view: 'images' | 'assets'): ImageCategory[] => {
|
||||
if (view === 'images') {
|
||||
return ['general'];
|
||||
}
|
||||
return ['control', 'mask', 'user', 'other'];
|
||||
};
|
||||
|
||||
return data;
|
||||
});
|
||||
const getBoardId = (
|
||||
board_id: 'all' | 'none' | (string & Record<never, never>)
|
||||
) => {
|
||||
if (board_id === 'all') {
|
||||
return undefined;
|
||||
}
|
||||
if (board_id === 'none') {
|
||||
return 'none';
|
||||
}
|
||||
return board_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* `ImagesService.listImagesWithMetadata()` thunk
|
||||
*/
|
||||
export const imagesLoaded = createAppAsyncThunk<
|
||||
OffsetPaginatedResults_ImageDTO_,
|
||||
ImagesLoadedArg,
|
||||
ImagesLoadedThunkConfig
|
||||
>(
|
||||
'thunkApi/imagesLoaded',
|
||||
async (arg, { getState, rejectWithValue, requestId }) => {
|
||||
const { get } = $client.get();
|
||||
|
||||
// TODO: do not make request if request in progress
|
||||
|
||||
const query = {
|
||||
categories: getCategories(arg.view),
|
||||
board_id: getBoardId(arg.board_id),
|
||||
offset: arg.offset,
|
||||
limit: arg.limit ?? IMAGES_PER_PAGE,
|
||||
};
|
||||
|
||||
const { data, error, response } = await get('/api/v1/images/', {
|
||||
params: {
|
||||
query,
|
||||
},
|
||||
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
||||
|
||||
type GetImagesByNamesArg = NonNullable<
|
||||
paths['/api/v1/images/']['post']['requestBody']['content']['application/json']
|
||||
>;
|
||||
|
||||
type GetImagesByNamesResponse =
|
||||
paths['/api/v1/images/']['post']['responses']['200']['content']['application/json'];
|
||||
|
||||
type GetImagesByNamesThunkConfig = {
|
||||
rejectValue: {
|
||||
arg: GetImagesByNamesArg;
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* `ImagesService.GetImagesByNamesWithMetadata()` thunk
|
||||
*/
|
||||
export const receivedListOfImages = createAppAsyncThunk<
|
||||
GetImagesByNamesResponse,
|
||||
GetImagesByNamesArg,
|
||||
GetImagesByNamesThunkConfig
|
||||
>(
|
||||
'thunkApi/receivedListOfImages',
|
||||
async (arg, { getState, rejectWithValue }) => {
|
||||
const { post } = $client.get();
|
||||
|
||||
const { data, error, response } = await post('/api/v1/images/', {
|
||||
body: arg,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return rejectWithValue({ arg, error });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
||||
|
@ -5,8 +5,13 @@ const subtext = defineStyle((props) => ({
|
||||
color: mode('colors.base.500', 'colors.base.400')(props),
|
||||
}));
|
||||
|
||||
const destructive = defineStyle((props) => ({
|
||||
color: mode('colors.error.600', 'colors.error.300')(props),
|
||||
}));
|
||||
|
||||
export const textTheme = defineStyleConfig({
|
||||
variants: {
|
||||
subtext,
|
||||
destructive,
|
||||
},
|
||||
});
|
||||
|
@ -1175,14 +1175,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
|
||||
integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@eslint-community/regexpp@^4.4.0":
|
||||
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.0":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884"
|
||||
integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
|
||||
@ -1982,7 +1982,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
|
||||
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9":
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.11", "@types/json-schema@^7.0.6":
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
|
||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||
@ -2086,49 +2086,53 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b"
|
||||
integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.0.tgz#2f4bea6a3718bed2ba52905358d0f45cd3620d31"
|
||||
integrity sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==
|
||||
"@typescript-eslint/eslint-plugin@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.0.0.tgz#19ff4f1cab8d6f8c2c1825150f7a840bc5d9bdc4"
|
||||
integrity sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/type-utils" "5.60.0"
|
||||
"@typescript-eslint/utils" "5.60.0"
|
||||
"@eslint-community/regexpp" "^4.5.0"
|
||||
"@typescript-eslint/scope-manager" "6.0.0"
|
||||
"@typescript-eslint/type-utils" "6.0.0"
|
||||
"@typescript-eslint/utils" "6.0.0"
|
||||
"@typescript-eslint/visitor-keys" "6.0.0"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.4"
|
||||
natural-compare "^1.4.0"
|
||||
natural-compare-lite "^1.4.0"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
semver "^7.5.0"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/parser@^5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.0.tgz#08f4daf5fc6548784513524f4f2f359cebb4068a"
|
||||
integrity sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==
|
||||
"@typescript-eslint/parser@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.0.0.tgz#46b2600fd1f67e62fc00a28093a75f41bf7effc4"
|
||||
integrity sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
"@typescript-eslint/scope-manager" "6.0.0"
|
||||
"@typescript-eslint/types" "6.0.0"
|
||||
"@typescript-eslint/typescript-estree" "6.0.0"
|
||||
"@typescript-eslint/visitor-keys" "6.0.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c"
|
||||
integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==
|
||||
"@typescript-eslint/scope-manager@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.0.0.tgz#8ede47a37cb2b7ed82d329000437abd1113b5e11"
|
||||
integrity sha512-o4q0KHlgCZTqjuaZ25nw5W57NeykZT9LiMEG4do/ovwvOcPnDO1BI5BQdCsUkjxFyrCL0cSzLjvIMfR9uo7cWg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
"@typescript-eslint/types" "6.0.0"
|
||||
"@typescript-eslint/visitor-keys" "6.0.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz#69b09087eb12d7513d5b07747e7d47f5533aa228"
|
||||
integrity sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==
|
||||
"@typescript-eslint/type-utils@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.0.0.tgz#0478d8a94f05e51da2877cc0500f1b3c27ac7e18"
|
||||
integrity sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
"@typescript-eslint/utils" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "6.0.0"
|
||||
"@typescript-eslint/utils" "6.0.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/types@4.33.0":
|
||||
version "4.33.0"
|
||||
@ -2140,18 +2144,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558"
|
||||
integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.60.0", "@typescript-eslint/typescript-estree@^5.55.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
|
||||
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
|
||||
"@typescript-eslint/types@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.0.0.tgz#19795f515f8decbec749c448b0b5fc76d82445a1"
|
||||
integrity sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.0.0.tgz#1e09aab7320e404fb9f83027ea568ac24e372f81"
|
||||
integrity sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
"@typescript-eslint/types" "6.0.0"
|
||||
"@typescript-eslint/visitor-keys" "6.0.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
semver "^7.5.0"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@^4.33.0":
|
||||
version "4.33.0"
|
||||
@ -2166,19 +2175,32 @@
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.60.0":
|
||||
"@typescript-eslint/typescript-estree@^5.55.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c"
|
||||
integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
|
||||
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
eslint-scope "^5.1.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.0.0.tgz#27a16d0d8f2719274a39417b9782f7daa3802db0"
|
||||
integrity sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.3.0"
|
||||
"@types/json-schema" "^7.0.11"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "6.0.0"
|
||||
"@typescript-eslint/types" "6.0.0"
|
||||
"@typescript-eslint/typescript-estree" "6.0.0"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.5.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.33.0":
|
||||
version "4.33.0"
|
||||
@ -2196,6 +2218,14 @@
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.0.0.tgz#0b49026049fbd096d2c00c5e784866bc69532a31"
|
||||
integrity sha512-cvJ63l8c0yXdeT5POHpL0Q1cZoRcmRKFCtSjNGJxPkcP571EfZMcNbzWAc7oK3D1dRzm/V5EwtkANTZxqvuuUA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.0.0"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
"@vitejs/plugin-react-swc@^3.3.2":
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz#34a82c1728066f48a86dfecb2f15df60f89207fb"
|
||||
@ -3426,7 +3456,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
|
||||
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
|
||||
|
||||
eslint@^8.43.0:
|
||||
eslint@^8.44.0:
|
||||
version "8.44.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
|
||||
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
|
||||
@ -4069,7 +4099,7 @@ ieee754@^1.1.13:
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ignore@^5.2.0:
|
||||
ignore@^5.2.0, ignore@^5.2.4:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
@ -5795,6 +5825,13 @@ semver@^7.3.5, semver@^7.3.7:
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.5.0:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@~7.3.0:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
@ -6235,6 +6272,11 @@ tree-kill@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
ts-api-utils@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d"
|
||||
integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==
|
||||
|
||||
ts-easing@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
|
||||
@ -6338,6 +6380,11 @@ typed-array-length@^1.0.4:
|
||||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typescript-eslint@^0.0.1-alpha.0:
|
||||
version "0.0.1-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz#285d68a4e96588295cd436278801bcb6a6b916c1"
|
||||
integrity sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg==
|
||||
|
||||
typescript@^3.9.10, typescript@^3.9.7:
|
||||
version "3.9.10"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
|
||||
@ -6348,6 +6395,11 @@ typescript@^4.0.0, typescript@^4.9.5:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
typescript@^5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
|
||||
typescript@~5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
|
||||
|
Reference in New Issue
Block a user