mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-17 08:07:59 -05:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab4dec4aa6 | ||
|
|
3ae6714188 | ||
|
|
0b461fa8da | ||
|
|
26a7e342c3 | ||
|
|
55e5f8be1b | ||
|
|
5a65121247 | ||
|
|
c2fe280c38 | ||
|
|
e7dee83dad | ||
|
|
3bf08b6d88 | ||
|
|
f821bc30a0 | ||
|
|
bc6f493931 | ||
|
|
fa332d1a56 | ||
|
|
4452ddf6c6 | ||
|
|
5dec79fde5 | ||
|
|
df833d7563 | ||
|
|
c2556f99bc | ||
|
|
391b883a87 | ||
|
|
9a06ffe3f5 | ||
|
|
548273643e | ||
|
|
d158027565 | ||
|
|
419773cde0 | ||
|
|
e64075b913 | ||
|
|
c9e48fc195 | ||
|
|
67b11d3e0c | ||
|
|
b782d8c7cd | ||
|
|
d34256b788 | ||
|
|
4c9553af51 | ||
|
|
dbe7fbea2e | ||
|
|
10ba402437 | ||
|
|
25c67f0c68 | ||
|
|
144485aa0b | ||
|
|
b4c10509f5 | ||
|
|
1cd4e23072 | ||
|
|
8f23c4513d | ||
|
|
a430872e60 | ||
|
|
af838e8ebb | ||
|
|
7f5fdcd54c | ||
|
|
ba5fd32f20 | ||
|
|
9f3d09dc01 | ||
|
|
081942b72e | ||
|
|
2b54b32740 | ||
|
|
1145d67d0d | ||
|
|
3d0dd13d8c | ||
|
|
efb28d55a2 | ||
|
|
e41050359f | ||
|
|
667ed6ab09 | ||
|
|
f5ad063253 | ||
|
|
ef2324d72a | ||
|
|
26a01d544f | ||
|
|
1f5572cf75 | ||
|
|
ad137cdc33 | ||
|
|
250a834f44 | ||
|
|
0cb3a7c654 | ||
|
|
34460984a9 | ||
|
|
4d628c10db | ||
|
|
f240f1a5d0 | ||
|
|
88d2878a11 | ||
|
|
0df8ab51ee | ||
|
|
f66f2b3c71 | ||
|
|
f9366ffeff | ||
|
|
d7fc9604f2 | ||
|
|
cbda3f1c86 | ||
|
|
973b2a9b45 | ||
|
|
5bea0cd431 | ||
|
|
7a01278537 | ||
|
|
ea42d08bc2 | ||
|
|
4d3089f870 | ||
|
|
ebd88f59ad | ||
|
|
cce66d90cc | ||
|
|
67c1f900bb | ||
|
|
8df45ce671 | ||
|
|
cc411fd244 | ||
|
|
eae40cae2b | ||
|
|
1e739dc003 | ||
|
|
ea63e16b69 | ||
|
|
6923a23f31 | ||
|
|
cb0e6da5cf | ||
|
|
ae35d67c9a | ||
|
|
7174768152 | ||
|
|
d750a2c6c0 | ||
|
|
41eafcf47a | ||
|
|
4bcb24eb82 | ||
|
|
926c29b91d | ||
|
|
8dad22ef93 | ||
|
|
172142ce03 | ||
|
|
dc31eaa3f9 | ||
|
|
19371d70fe | ||
|
|
d8d69891c8 | ||
|
|
168875327b | ||
|
|
c7fb3d3906 | ||
|
|
5aa5ca13ec | ||
|
|
eb9edff186 | ||
|
|
839c2e376a | ||
|
|
1ba3e85e68 | ||
|
|
28ee1d911a | ||
|
|
74a2cb7b77 | ||
|
|
e139158a81 | ||
|
|
2b383de39c | ||
|
|
dd136a63a2 | ||
|
|
325f0a4c5b | ||
|
|
7b5ab0d458 | ||
|
|
4fa69176cb | ||
|
|
1418b0546c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -180,7 +180,6 @@ cython_debug/
|
||||
# Scratch folder
|
||||
.scratch/
|
||||
.vscode/
|
||||
.zed/
|
||||
|
||||
# source installer files
|
||||
installer/*zip
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
from fastapi import Body, HTTPException
|
||||
from fastapi.routing import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from invokeai.app.api.dependencies import ApiDependencies
|
||||
from invokeai.app.services.images.images_common import AddImagesToBoardResult, RemoveImagesFromBoardResult
|
||||
|
||||
board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
|
||||
|
||||
|
||||
class AddImagesToBoardResult(BaseModel):
|
||||
board_id: str = Field(description="The id of the board the images were added to")
|
||||
added_image_names: list[str] = Field(description="The image names that were added to the board")
|
||||
|
||||
|
||||
class RemoveImagesFromBoardResult(BaseModel):
|
||||
removed_image_names: list[str] = Field(description="The image names that were removed from their board")
|
||||
|
||||
|
||||
@board_images_router.post(
|
||||
"/",
|
||||
operation_id="add_image_to_board",
|
||||
@@ -14,26 +23,17 @@ board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
|
||||
201: {"description": "The image was added to a board successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
response_model=AddImagesToBoardResult,
|
||||
)
|
||||
async def add_image_to_board(
|
||||
board_id: str = Body(description="The id of the board to add to"),
|
||||
image_name: str = Body(description="The name of the image to add"),
|
||||
) -> AddImagesToBoardResult:
|
||||
):
|
||||
"""Creates a board_image"""
|
||||
try:
|
||||
added_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
|
||||
ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
|
||||
added_images.add(image_name)
|
||||
affected_boards.add(board_id)
|
||||
affected_boards.add(old_board_id)
|
||||
|
||||
return AddImagesToBoardResult(
|
||||
added_images=list(added_images),
|
||||
affected_boards=list(affected_boards),
|
||||
result = ApiDependencies.invoker.services.board_images.add_image_to_board(
|
||||
board_id=board_id, image_name=image_name
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to add image to board")
|
||||
|
||||
@@ -45,25 +45,14 @@ async def add_image_to_board(
|
||||
201: {"description": "The image was removed from the board successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
response_model=RemoveImagesFromBoardResult,
|
||||
)
|
||||
async def remove_image_from_board(
|
||||
image_name: str = Body(description="The name of the image to remove", embed=True),
|
||||
) -> RemoveImagesFromBoardResult:
|
||||
):
|
||||
"""Removes an image from its board, if it had one"""
|
||||
try:
|
||||
removed_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
|
||||
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
|
||||
removed_images.add(image_name)
|
||||
affected_boards.add("none")
|
||||
affected_boards.add(old_board_id)
|
||||
return RemoveImagesFromBoardResult(
|
||||
removed_images=list(removed_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
|
||||
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
|
||||
return result
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to remove image from board")
|
||||
|
||||
@@ -83,25 +72,16 @@ async def add_images_to_board(
|
||||
) -> AddImagesToBoardResult:
|
||||
"""Adds a list of images to a board"""
|
||||
try:
|
||||
added_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
added_image_names: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
|
||||
ApiDependencies.invoker.services.board_images.add_image_to_board(
|
||||
board_id=board_id,
|
||||
image_name=image_name,
|
||||
board_id=board_id, image_name=image_name
|
||||
)
|
||||
added_images.add(image_name)
|
||||
affected_boards.add(board_id)
|
||||
affected_boards.add(old_board_id)
|
||||
|
||||
added_image_names.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return AddImagesToBoardResult(
|
||||
added_images=list(added_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return AddImagesToBoardResult(board_id=board_id, added_image_names=added_image_names)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to add images to board")
|
||||
|
||||
@@ -120,20 +100,13 @@ async def remove_images_from_board(
|
||||
) -> RemoveImagesFromBoardResult:
|
||||
"""Removes a list of images from their board, if they had one"""
|
||||
try:
|
||||
removed_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
removed_image_names: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
|
||||
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
|
||||
removed_images.add(image_name)
|
||||
affected_boards.add("none")
|
||||
affected_boards.add(old_board_id)
|
||||
removed_image_names.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return RemoveImagesFromBoardResult(
|
||||
removed_images=list(removed_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return RemoveImagesFromBoardResult(removed_image_names=removed_image_names)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to remove images from board")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import io
|
||||
import json
|
||||
import traceback
|
||||
from typing import ClassVar, Literal, Optional
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -14,17 +14,10 @@ from invokeai.app.api.extract_metadata_from_image import extract_metadata_from_i
|
||||
from invokeai.app.invocations.fields import MetadataField
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ImageCategory,
|
||||
ImageCollectionCounts,
|
||||
ImageRecordChanges,
|
||||
ResourceOrigin,
|
||||
)
|
||||
from invokeai.app.services.images.images_common import (
|
||||
DeleteImagesResult,
|
||||
ImageDTO,
|
||||
ImageUrlsDTO,
|
||||
StarredImagesResult,
|
||||
UnstarredImagesResult,
|
||||
)
|
||||
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
|
||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
from invokeai.app.util.controlnet_utils import heuristic_resize_fast
|
||||
@@ -160,30 +153,18 @@ async def create_image_upload_entry(
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@images_router.delete("/i/{image_name}", operation_id="delete_image", response_model=DeleteImagesResult)
|
||||
@images_router.delete("/i/{image_name}", operation_id="delete_image")
|
||||
async def delete_image(
|
||||
image_name: str = Path(description="The name of the image to delete"),
|
||||
) -> DeleteImagesResult:
|
||||
) -> None:
|
||||
"""Deletes an image"""
|
||||
|
||||
deleted_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
|
||||
try:
|
||||
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
|
||||
board_id = image_dto.board_id or "none"
|
||||
ApiDependencies.invoker.services.images.delete(image_name)
|
||||
deleted_images.add(image_name)
|
||||
affected_boards.add(board_id)
|
||||
except Exception:
|
||||
# TODO: Does this need any exception handling at all?
|
||||
pass
|
||||
|
||||
return DeleteImagesResult(
|
||||
deleted_images=list(deleted_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
|
||||
|
||||
@images_router.delete("/intermediates", operation_id="clear_intermediates")
|
||||
async def clear_intermediates() -> int:
|
||||
@@ -395,32 +376,31 @@ async def list_image_dtos(
|
||||
return image_dtos
|
||||
|
||||
|
||||
@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesResult)
|
||||
class DeleteImagesFromListResult(BaseModel):
|
||||
deleted_images: list[str]
|
||||
|
||||
|
||||
@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesFromListResult)
|
||||
async def delete_images_from_list(
|
||||
image_names: list[str] = Body(description="The list of names of images to delete", embed=True),
|
||||
) -> DeleteImagesResult:
|
||||
) -> DeleteImagesFromListResult:
|
||||
try:
|
||||
deleted_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
deleted_images: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
|
||||
board_id = image_dto.board_id or "none"
|
||||
ApiDependencies.invoker.services.images.delete(image_name)
|
||||
deleted_images.add(image_name)
|
||||
affected_boards.add(board_id)
|
||||
deleted_images.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return DeleteImagesResult(
|
||||
deleted_images=list(deleted_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return DeleteImagesFromListResult(deleted_images=deleted_images)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to delete images")
|
||||
|
||||
|
||||
@images_router.delete("/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesResult)
|
||||
async def delete_uncategorized_images() -> DeleteImagesResult:
|
||||
@images_router.delete(
|
||||
"/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesFromListResult
|
||||
)
|
||||
async def delete_uncategorized_images() -> DeleteImagesFromListResult:
|
||||
"""Deletes all images that are uncategorized"""
|
||||
|
||||
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
|
||||
@@ -428,19 +408,14 @@ async def delete_uncategorized_images() -> DeleteImagesResult:
|
||||
)
|
||||
|
||||
try:
|
||||
deleted_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
deleted_images: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
ApiDependencies.invoker.services.images.delete(image_name)
|
||||
deleted_images.add(image_name)
|
||||
affected_boards.add("none")
|
||||
deleted_images.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return DeleteImagesResult(
|
||||
deleted_images=list(deleted_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return DeleteImagesFromListResult(deleted_images=deleted_images)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to delete images")
|
||||
|
||||
@@ -449,50 +424,36 @@ class ImagesUpdatedFromListResult(BaseModel):
|
||||
updated_image_names: list[str] = Field(description="The image names that were updated")
|
||||
|
||||
|
||||
@images_router.post("/star", operation_id="star_images_in_list", response_model=StarredImagesResult)
|
||||
@images_router.post("/star", operation_id="star_images_in_list", response_model=ImagesUpdatedFromListResult)
|
||||
async def star_images_in_list(
|
||||
image_names: list[str] = Body(description="The list of names of images to star", embed=True),
|
||||
) -> StarredImagesResult:
|
||||
) -> ImagesUpdatedFromListResult:
|
||||
try:
|
||||
starred_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
updated_image_names: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
updated_image_dto = ApiDependencies.invoker.services.images.update(
|
||||
image_name, changes=ImageRecordChanges(starred=True)
|
||||
)
|
||||
starred_images.add(image_name)
|
||||
affected_boards.add(updated_image_dto.board_id or "none")
|
||||
ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=True))
|
||||
updated_image_names.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return StarredImagesResult(
|
||||
starred_images=list(starred_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return ImagesUpdatedFromListResult(updated_image_names=updated_image_names)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to star images")
|
||||
|
||||
|
||||
@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=UnstarredImagesResult)
|
||||
@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=ImagesUpdatedFromListResult)
|
||||
async def unstar_images_in_list(
|
||||
image_names: list[str] = Body(description="The list of names of images to unstar", embed=True),
|
||||
) -> UnstarredImagesResult:
|
||||
) -> ImagesUpdatedFromListResult:
|
||||
try:
|
||||
unstarred_images: set[str] = set()
|
||||
affected_boards: set[str] = set()
|
||||
updated_image_names: list[str] = []
|
||||
for image_name in image_names:
|
||||
try:
|
||||
updated_image_dto = ApiDependencies.invoker.services.images.update(
|
||||
image_name, changes=ImageRecordChanges(starred=False)
|
||||
)
|
||||
unstarred_images.add(image_name)
|
||||
affected_boards.add(updated_image_dto.board_id or "none")
|
||||
ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=False))
|
||||
updated_image_names.append(image_name)
|
||||
except Exception:
|
||||
pass
|
||||
return UnstarredImagesResult(
|
||||
unstarred_images=list(unstarred_images),
|
||||
affected_boards=list(affected_boards),
|
||||
)
|
||||
return ImagesUpdatedFromListResult(updated_image_names=updated_image_names)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to unstar images")
|
||||
|
||||
@@ -563,92 +524,3 @@ async def get_bulk_download_item(
|
||||
return response
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/collections/counts", operation_id="get_image_collection_counts", response_model=ImageCollectionCounts
|
||||
)
|
||||
async def get_image_collection_counts(
|
||||
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to count."),
|
||||
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
|
||||
is_intermediate: Optional[bool] = Query(default=None, description="Whether to include intermediate images."),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find images without a board.",
|
||||
),
|
||||
search_term: Optional[str] = Query(default=None, description="The term to search for"),
|
||||
) -> ImageCollectionCounts:
|
||||
"""Gets counts for starred and unstarred image collections"""
|
||||
|
||||
try:
|
||||
return ApiDependencies.invoker.services.images.get_collection_counts(
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to get collection counts")
|
||||
|
||||
|
||||
@images_router.get("/collections/{collection}", operation_id="get_image_collection")
|
||||
async def get_image_collection(
|
||||
collection: Literal["starred", "unstarred"] = Path(..., description="The collection to retrieve from"),
|
||||
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
|
||||
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
|
||||
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find images without a board.",
|
||||
),
|
||||
offset: int = Query(default=0, description="The offset within the collection"),
|
||||
limit: int = Query(default=50, description="The number of images to return"),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
|
||||
search_term: Optional[str] = Query(default=None, description="The term to search for"),
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
"""Gets images from a specific collection (starred or unstarred)"""
|
||||
|
||||
try:
|
||||
image_dtos = ApiDependencies.invoker.services.images.get_collection_images(
|
||||
collection=collection,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_dir=order_dir,
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
return image_dtos
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to get collection images")
|
||||
|
||||
|
||||
@images_router.get("/names", operation_id="get_image_names")
|
||||
async def get_image_names(
|
||||
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
|
||||
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
|
||||
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find images without a board.",
|
||||
),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
|
||||
search_term: Optional[str] = Query(default=None, description="The term to search for"),
|
||||
) -> list[str]:
|
||||
"""Gets ordered list of all image names (starred first, then unstarred)"""
|
||||
|
||||
try:
|
||||
image_names = ApiDependencies.invoker.services.images.get_image_names(
|
||||
order_dir=order_dir,
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
return image_names
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Failed to get image names")
|
||||
|
||||
@@ -158,7 +158,7 @@ web_root_path = Path(list(web_dir.__path__)[0])
|
||||
try:
|
||||
app.mount("/", NoCacheStaticFiles(directory=Path(web_root_path, "dist"), html=True), name="ui")
|
||||
except RuntimeError:
|
||||
logger.warning(f"No UI found at {web_root_path}/dist, skipping UI mount")
|
||||
logger.warn(f"No UI found at {web_root_path}/dist, skipping UI mount")
|
||||
app.mount(
|
||||
"/static", NoCacheStaticFiles(directory=Path(web_root_path, "static/")), name="static"
|
||||
) # docs favicon is in here
|
||||
|
||||
@@ -499,7 +499,7 @@ def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None
|
||||
|
||||
ui_type = field.json_schema_extra.get("ui_type", None)
|
||||
if isinstance(ui_type, str) and ui_type.startswith("DEPRECATED_"):
|
||||
logger.warning(f'"UIType.{ui_type.split("_")[-1]}" is deprecated, ignoring')
|
||||
logger.warn(f'"UIType.{ui_type.split("_")[-1]}" is deprecated, ignoring')
|
||||
field.json_schema_extra.pop("ui_type")
|
||||
return None
|
||||
|
||||
@@ -582,8 +582,6 @@ def invocation(
|
||||
|
||||
fields: dict[str, tuple[Any, FieldInfo]] = {}
|
||||
|
||||
original_model_fields: dict[str, OriginalModelField] = {}
|
||||
|
||||
for field_name, field_info in cls.model_fields.items():
|
||||
annotation = field_info.annotation
|
||||
assert annotation is not None, f"{field_name} on invocation {invocation_type} has no type annotation."
|
||||
@@ -591,7 +589,7 @@ def invocation(
|
||||
f"{field_name} on invocation {invocation_type} has a non-dict json_schema_extra, did you forget to use InputField?"
|
||||
)
|
||||
|
||||
original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
|
||||
cls._original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
|
||||
|
||||
validate_field_default(cls.__name__, field_name, invocation_type, annotation, field_info)
|
||||
|
||||
@@ -615,7 +613,7 @@ def invocation(
|
||||
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
|
||||
uiconfig["version"] = version
|
||||
else:
|
||||
logger.warning(f'No version specified for node "{invocation_type}", using "1.0.0"')
|
||||
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
|
||||
uiconfig["version"] = "1.0.0"
|
||||
|
||||
cls.UIConfig = UIConfigBase(**uiconfig)
|
||||
@@ -678,7 +676,6 @@ def invocation(
|
||||
docstring = cls.__doc__
|
||||
new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields) # type: ignore
|
||||
new_class.__doc__ = docstring
|
||||
new_class._original_model_fields = original_model_fields
|
||||
|
||||
InvocationRegistry.register_invocation(new_class)
|
||||
|
||||
|
||||
@@ -114,13 +114,6 @@ class CompelInvocation(BaseInvocation):
|
||||
|
||||
c, _options = compel.build_conditioning_tensor_for_conjunction(conjunction)
|
||||
|
||||
del compel
|
||||
del patched_tokenizer
|
||||
del tokenizer
|
||||
del ti_manager
|
||||
del text_encoder
|
||||
del text_encoder_info
|
||||
|
||||
c = c.detach().to("cpu")
|
||||
|
||||
conditioning_data = ConditioningFieldData(conditionings=[BasicConditioningInfo(embeds=c)])
|
||||
@@ -229,10 +222,7 @@ class SDXLPromptInvocationBase:
|
||||
else:
|
||||
c_pooled = None
|
||||
|
||||
del compel
|
||||
del patched_tokenizer
|
||||
del tokenizer
|
||||
del ti_manager
|
||||
del text_encoder
|
||||
del text_encoder_info
|
||||
|
||||
|
||||
@@ -437,7 +437,7 @@ class WithWorkflow:
|
||||
workflow = None
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
logger.warning(
|
||||
logger.warn(
|
||||
f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow."
|
||||
)
|
||||
super().__init_subclass__()
|
||||
@@ -578,7 +578,7 @@ def InputField(
|
||||
|
||||
if default_factory is not _Unset and default_factory is not None:
|
||||
default = default_factory()
|
||||
logger.warning('"default_factory" is not supported, calling it now to set "default"')
|
||||
logger.warn('"default_factory" is not supported, calling it now to set "default"')
|
||||
|
||||
# These are the args we may wish pass to the pydantic `Field()` function
|
||||
field_args = {
|
||||
|
||||
@@ -24,6 +24,7 @@ from invokeai.frontend.cli.arg_parser import InvokeAIArgs
|
||||
INIT_FILE = Path("invokeai.yaml")
|
||||
DB_FILE = Path("invokeai.db")
|
||||
LEGACY_INIT_FILE = Path("invokeai.init")
|
||||
DEVICE = Literal["auto", "cpu", "cuda", "cuda:1", "mps"]
|
||||
PRECISION = Literal["auto", "float16", "bfloat16", "float32"]
|
||||
ATTENTION_TYPE = Literal["auto", "normal", "xformers", "sliced", "torch-sdp"]
|
||||
ATTENTION_SLICE_SIZE = Literal["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
@@ -92,7 +93,7 @@ class InvokeAIAppConfig(BaseSettings):
|
||||
vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.
|
||||
lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.
|
||||
pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to "backend:cudaMallocAsync" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.
|
||||
device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.<br>Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)
|
||||
device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.<br>Valid values: `auto`, `cpu`, `cuda`, `cuda:1`, `mps`
|
||||
precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.<br>Valid values: `auto`, `float16`, `bfloat16`, `float32`
|
||||
sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.
|
||||
attention_type: Attention type.<br>Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`
|
||||
@@ -175,7 +176,7 @@ class InvokeAIAppConfig(BaseSettings):
|
||||
pytorch_cuda_alloc_conf: Optional[str] = Field(default=None, description="Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.")
|
||||
|
||||
# DEVICE
|
||||
device: str = Field(default="auto", description="Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.<br>Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)", pattern=r"^(auto|cpu|mps|cuda(:\d+)?)$")
|
||||
device: DEVICE = Field(default="auto", description="Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.")
|
||||
precision: PRECISION = Field(default="auto", description="Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.")
|
||||
|
||||
# GENERATION
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
from typing import Optional
|
||||
|
||||
from invokeai.app.invocations.fields import MetadataField
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ImageCategory,
|
||||
ImageCollectionCounts,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
ResourceOrigin,
|
||||
@@ -98,44 +97,3 @@ class ImageRecordStorageBase(ABC):
|
||||
def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
|
||||
"""Gets the most recent image for a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection_counts(
|
||||
self,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> ImageCollectionCounts:
|
||||
"""Gets counts for starred and unstarred image collections."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection_images(
|
||||
self,
|
||||
collection: Literal["starred", "unstarred"],
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> OffsetPaginatedResults[ImageRecord]:
|
||||
"""Gets images from a specific collection (starred or unstarred)."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_image_names(
|
||||
self,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
"""Gets ordered list of all image names (starred first, then unstarred)."""
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,7 @@ import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field, StrictBool, StrictStr
|
||||
from pydantic import Field, StrictBool, StrictStr
|
||||
|
||||
from invokeai.app.util.metaenum import MetaEnum
|
||||
from invokeai.app.util.misc import get_iso_timestamp
|
||||
@@ -207,8 +207,3 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord:
|
||||
starred=starred,
|
||||
has_workflow=has_workflow,
|
||||
)
|
||||
|
||||
|
||||
class ImageCollectionCounts(BaseModel):
|
||||
starred_count: int = Field(description="The number of starred images in the collection.")
|
||||
unstarred_count: int = Field(description="The number of unstarred images in the collection.")
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional, Union, cast
|
||||
from typing import Optional, Union, cast
|
||||
|
||||
from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
|
||||
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
IMAGE_DTO_COLS,
|
||||
ImageCategory,
|
||||
ImageCollectionCounts,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
ImageRecordDeleteException,
|
||||
@@ -387,253 +386,3 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
return None
|
||||
|
||||
return deserialize_image_record(dict(result))
|
||||
|
||||
def get_collection_counts(
|
||||
self,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> ImageCollectionCounts:
|
||||
cursor = self._conn.cursor()
|
||||
|
||||
# Build the base query conditions (same as get_many)
|
||||
base_query = """--sql
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
query_conditions = ""
|
||||
query_params: list[Union[int, str, bool]] = []
|
||||
|
||||
if image_origin is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.image_origin = ?
|
||||
"""
|
||||
query_params.append(image_origin.value)
|
||||
|
||||
if categories is not None:
|
||||
category_strings = [c.value for c in set(categories)]
|
||||
placeholders = ",".join("?" * len(category_strings))
|
||||
query_conditions += f"""--sql
|
||||
AND images.image_category IN ( {placeholders} )
|
||||
"""
|
||||
for c in category_strings:
|
||||
query_params.append(c)
|
||||
|
||||
if is_intermediate is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.is_intermediate = ?
|
||||
"""
|
||||
query_params.append(is_intermediate)
|
||||
|
||||
if board_id == "none":
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
elif board_id is not None:
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
query_params.append(board_id)
|
||||
|
||||
if search_term:
|
||||
query_conditions += """--sql
|
||||
AND (
|
||||
images.metadata LIKE ?
|
||||
OR images.created_at LIKE ?
|
||||
)
|
||||
"""
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
|
||||
# Get starred count
|
||||
starred_query = f"SELECT COUNT(*) {base_query} {query_conditions} AND images.starred = TRUE;"
|
||||
cursor.execute(starred_query, query_params)
|
||||
starred_count = cast(int, cursor.fetchone()[0])
|
||||
|
||||
# Get unstarred count
|
||||
unstarred_query = f"SELECT COUNT(*) {base_query} {query_conditions} AND images.starred = FALSE;"
|
||||
cursor.execute(unstarred_query, query_params)
|
||||
unstarred_count = cast(int, cursor.fetchone()[0])
|
||||
|
||||
return ImageCollectionCounts(starred_count=starred_count, unstarred_count=unstarred_count)
|
||||
|
||||
def get_collection_images(
|
||||
self,
|
||||
collection: Literal["starred", "unstarred"],
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> OffsetPaginatedResults[ImageRecord]:
|
||||
cursor = self._conn.cursor()
|
||||
|
||||
# Base queries
|
||||
count_query = """--sql
|
||||
SELECT COUNT(*)
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
images_query = f"""--sql
|
||||
SELECT {IMAGE_DTO_COLS}
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
query_conditions = ""
|
||||
query_params: list[Union[int, str, bool]] = []
|
||||
|
||||
# Add starred/unstarred filter
|
||||
is_starred = collection == "starred"
|
||||
query_conditions += """--sql
|
||||
AND images.starred = ?
|
||||
"""
|
||||
query_params.append(is_starred)
|
||||
|
||||
if image_origin is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.image_origin = ?
|
||||
"""
|
||||
query_params.append(image_origin.value)
|
||||
|
||||
if categories is not None:
|
||||
category_strings = [c.value for c in set(categories)]
|
||||
placeholders = ",".join("?" * len(category_strings))
|
||||
query_conditions += f"""--sql
|
||||
AND images.image_category IN ( {placeholders} )
|
||||
"""
|
||||
for c in category_strings:
|
||||
query_params.append(c)
|
||||
|
||||
if is_intermediate is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.is_intermediate = ?
|
||||
"""
|
||||
query_params.append(is_intermediate)
|
||||
|
||||
if board_id == "none":
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
elif board_id is not None:
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
query_params.append(board_id)
|
||||
|
||||
if search_term:
|
||||
query_conditions += """--sql
|
||||
AND (
|
||||
images.metadata LIKE ?
|
||||
OR images.created_at LIKE ?
|
||||
)
|
||||
"""
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
|
||||
# Add ordering and pagination
|
||||
query_pagination = f"""--sql
|
||||
ORDER BY images.created_at {order_dir.value} LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
# Execute images query
|
||||
images_query += query_conditions + query_pagination + ";"
|
||||
images_params = query_params.copy()
|
||||
images_params.extend([limit, offset])
|
||||
|
||||
cursor.execute(images_query, images_params)
|
||||
result = cast(list[sqlite3.Row], cursor.fetchall())
|
||||
images = [deserialize_image_record(dict(r)) for r in result]
|
||||
|
||||
# Execute count query
|
||||
count_query += query_conditions + ";"
|
||||
cursor.execute(count_query, query_params)
|
||||
count = cast(int, cursor.fetchone()[0])
|
||||
|
||||
return OffsetPaginatedResults(items=images, offset=offset, limit=limit, total=count)
|
||||
|
||||
def get_image_names(
|
||||
self,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
cursor = self._conn.cursor()
|
||||
|
||||
# Base query to get image names in order (starred first, then unstarred)
|
||||
query = """--sql
|
||||
SELECT images.image_name
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
query_conditions = ""
|
||||
query_params: list[Union[int, str, bool]] = []
|
||||
|
||||
if image_origin is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.image_origin = ?
|
||||
"""
|
||||
query_params.append(image_origin.value)
|
||||
|
||||
if categories is not None:
|
||||
category_strings = [c.value for c in set(categories)]
|
||||
placeholders = ",".join("?" * len(category_strings))
|
||||
query_conditions += f"""--sql
|
||||
AND images.image_category IN ( {placeholders} )
|
||||
"""
|
||||
for c in category_strings:
|
||||
query_params.append(c)
|
||||
|
||||
if is_intermediate is not None:
|
||||
query_conditions += """--sql
|
||||
AND images.is_intermediate = ?
|
||||
"""
|
||||
query_params.append(is_intermediate)
|
||||
|
||||
if board_id == "none":
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
elif board_id is not None:
|
||||
query_conditions += """--sql
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
query_params.append(board_id)
|
||||
|
||||
if search_term:
|
||||
query_conditions += """--sql
|
||||
AND (
|
||||
images.metadata LIKE ?
|
||||
OR images.created_at LIKE ?
|
||||
)
|
||||
"""
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
query_params.append(f"%{search_term.lower()}%")
|
||||
|
||||
# Order by starred first, then by created_at
|
||||
query += (
|
||||
query_conditions
|
||||
+ f"""--sql
|
||||
ORDER BY images.starred DESC, images.created_at {order_dir.value}
|
||||
"""
|
||||
)
|
||||
|
||||
cursor.execute(query, query_params)
|
||||
result = cast(list[sqlite3.Row], cursor.fetchall())
|
||||
|
||||
return [row[0] for row in result]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Literal, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
from PIL.Image import Image as PILImageType
|
||||
|
||||
from invokeai.app.invocations.fields import MetadataField
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ImageCategory,
|
||||
ImageCollectionCounts,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
ResourceOrigin,
|
||||
@@ -126,7 +125,7 @@ class ImageServiceABC(ABC):
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
"""Gets a paginated list of image DTOs with starred images first when starred_first=True."""
|
||||
"""Gets a paginated list of image DTOs."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -148,44 +147,3 @@ class ImageServiceABC(ABC):
|
||||
def delete_images_on_board(self, board_id: str):
|
||||
"""Deletes all images on a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection_counts(
|
||||
self,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> ImageCollectionCounts:
|
||||
"""Gets counts for starred and unstarred image collections."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection_images(
|
||||
self,
|
||||
collection: Literal["starred", "unstarred"],
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
"""Gets images from a specific collection (starred or unstarred)."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_image_names(
|
||||
self,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
"""Gets ordered list of all image names (starred first, then unstarred)."""
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import Field
|
||||
|
||||
from invokeai.app.services.image_records.image_records_common import ImageRecord
|
||||
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||
@@ -39,27 +39,3 @@ def image_record_to_dto(
|
||||
thumbnail_url=thumbnail_url,
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
|
||||
class ResultWithAffectedBoards(BaseModel):
|
||||
affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
|
||||
|
||||
|
||||
class DeleteImagesResult(ResultWithAffectedBoards):
|
||||
deleted_images: list[str] = Field(description="The names of the images that were deleted")
|
||||
|
||||
|
||||
class StarredImagesResult(ResultWithAffectedBoards):
|
||||
starred_images: list[str] = Field(description="The names of the images that were starred")
|
||||
|
||||
|
||||
class UnstarredImagesResult(ResultWithAffectedBoards):
|
||||
unstarred_images: list[str] = Field(description="The names of the images that were unstarred")
|
||||
|
||||
|
||||
class AddImagesToBoardResult(ResultWithAffectedBoards):
|
||||
added_images: list[str] = Field(description="The image names that were added to the board")
|
||||
|
||||
|
||||
class RemoveImagesFromBoardResult(ResultWithAffectedBoards):
|
||||
removed_images: list[str] = Field(description="The image names that were removed from their board")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Literal, Optional
|
||||
from typing import Optional
|
||||
|
||||
from PIL.Image import Image as PILImageType
|
||||
|
||||
@@ -10,7 +10,6 @@ from invokeai.app.services.image_files.image_files_common import (
|
||||
)
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ImageCategory,
|
||||
ImageCollectionCounts,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
ImageRecordDeleteException,
|
||||
@@ -79,7 +78,7 @@ class ImageService(ImageServiceABC):
|
||||
board_id=board_id, image_name=image_name
|
||||
)
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.warning(f"Failed to add image to board {board_id}: {str(e)}")
|
||||
self.__invoker.services.logger.warn(f"Failed to add image to board {board_id}: {str(e)}")
|
||||
self.__invoker.services.image_files.save(
|
||||
image_name=image_name, image=image, metadata=metadata, workflow=workflow, graph=graph
|
||||
)
|
||||
@@ -310,90 +309,3 @@ class ImageService(ImageServiceABC):
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.error("Problem getting intermediates count")
|
||||
raise e
|
||||
|
||||
def get_collection_counts(
|
||||
self,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> ImageCollectionCounts:
|
||||
try:
|
||||
return self.__invoker.services.image_records.get_collection_counts(
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.error("Problem getting collection counts")
|
||||
raise e
|
||||
|
||||
def get_collection_images(
|
||||
self,
|
||||
collection: Literal["starred", "unstarred"],
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> OffsetPaginatedResults[ImageDTO]:
|
||||
try:
|
||||
results = self.__invoker.services.image_records.get_collection_images(
|
||||
collection=collection,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_dir=order_dir,
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
|
||||
image_dtos = [
|
||||
image_record_to_dto(
|
||||
image_record=r,
|
||||
image_url=self.__invoker.services.urls.get_image_url(r.image_name),
|
||||
thumbnail_url=self.__invoker.services.urls.get_image_url(r.image_name, True),
|
||||
board_id=self.__invoker.services.board_image_records.get_board_for_image(r.image_name),
|
||||
)
|
||||
for r in results.items
|
||||
]
|
||||
|
||||
return OffsetPaginatedResults[ImageDTO](
|
||||
items=image_dtos,
|
||||
offset=results.offset,
|
||||
limit=results.limit,
|
||||
total=results.total,
|
||||
)
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.error("Problem getting collection images")
|
||||
raise e
|
||||
|
||||
def get_image_names(
|
||||
self,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
image_origin: Optional[ResourceOrigin] = None,
|
||||
categories: Optional[list[ImageCategory]] = None,
|
||||
is_intermediate: Optional[bool] = None,
|
||||
board_id: Optional[str] = None,
|
||||
search_term: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
try:
|
||||
return self.__invoker.services.image_records.get_image_names(
|
||||
order_dir=order_dir,
|
||||
image_origin=image_origin,
|
||||
categories=categories,
|
||||
is_intermediate=is_intermediate,
|
||||
board_id=board_id,
|
||||
search_term=search_term,
|
||||
)
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.error("Problem getting image names")
|
||||
raise e
|
||||
|
||||
@@ -148,7 +148,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
||||
def _clear_pending_jobs(self) -> None:
|
||||
for job in self.list_jobs():
|
||||
if not job.in_terminal_state:
|
||||
self._logger.warning(f"Cancelling job {job.id}")
|
||||
self._logger.warning("Cancelling job {job.id}")
|
||||
self.cancel_job(job)
|
||||
while True:
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import gc
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from threading import BoundedSemaphore, Thread
|
||||
@@ -440,12 +439,6 @@ class DefaultSessionProcessor(SessionProcessorBase):
|
||||
poll_now_event.wait(self._polling_interval)
|
||||
continue
|
||||
|
||||
# GC-ing here can reduce peak memory usage of the invoke process by freeing allocated memory blocks.
|
||||
# Most queue items take seconds to execute, so the relative cost of a GC is very small.
|
||||
# Python will never cede allocated memory back to the OS, so anything we can do to reduce the peak
|
||||
# allocation is well worth it.
|
||||
gc.collect()
|
||||
|
||||
self._invoker.services.logger.info(
|
||||
f"Executing queue item {self._queue_item.item_id}, session {self._queue_item.session_id}"
|
||||
)
|
||||
|
||||
@@ -101,7 +101,11 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
return cast(Union[int, None], cursor.fetchone()[0]) or 0
|
||||
|
||||
async def enqueue_batch(self, queue_id: str, batch: Batch, prepend: bool) -> EnqueueBatchResult:
|
||||
return await asyncio.to_thread(self._enqueue_batch, queue_id, batch, prepend)
|
||||
|
||||
def _enqueue_batch(self, queue_id: str, batch: Batch, prepend: bool) -> EnqueueBatchResult:
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
# TODO: how does this work in a multi-user scenario?
|
||||
current_queue_size = self._get_current_queue_size(queue_id)
|
||||
max_queue_size = self.__invoker.services.configuration.max_queue_size
|
||||
@@ -111,12 +115,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
if prepend:
|
||||
priority = self._get_highest_priority(queue_id) + 1
|
||||
|
||||
requested_count = await asyncio.to_thread(
|
||||
calc_session_count,
|
||||
batch=batch,
|
||||
)
|
||||
values_to_insert = await asyncio.to_thread(
|
||||
prepare_values_to_insert,
|
||||
requested_count = calc_session_count(batch)
|
||||
values_to_insert = prepare_values_to_insert(
|
||||
queue_id=queue_id,
|
||||
batch=batch,
|
||||
priority=priority,
|
||||
@@ -124,16 +124,19 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
)
|
||||
enqueued_count = len(values_to_insert)
|
||||
|
||||
with self._conn:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.executemany(
|
||||
"""--sql
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values_to_insert,
|
||||
)
|
||||
if requested_count > enqueued_count:
|
||||
values_to_insert = values_to_insert[:max_new_queue_items]
|
||||
|
||||
cursor.executemany(
|
||||
"""--sql
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values_to_insert,
|
||||
)
|
||||
self._conn.commit()
|
||||
except Exception:
|
||||
self._conn.rollback()
|
||||
raise
|
||||
enqueue_result = EnqueueBatchResult(
|
||||
queue_id=queue_id,
|
||||
|
||||
@@ -296,7 +296,7 @@ class LoRAConfigBase(ABC, BaseModel):
|
||||
from invokeai.backend.patches.lora_conversions.formats import flux_format_from_state_dict
|
||||
|
||||
sd = mod.load_state_dict(mod.path)
|
||||
value = flux_format_from_state_dict(sd, mod.metadata())
|
||||
value = flux_format_from_state_dict(sd)
|
||||
mod.cache[key] = value
|
||||
return value
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ from invokeai.backend.model_manager.taxonomy import (
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_aitoolkit_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_aitoolkit_format,
|
||||
lora_model_from_flux_aitoolkit_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import (
|
||||
is_state_dict_likely_flux_control,
|
||||
lora_model_from_flux_control_state_dict,
|
||||
@@ -96,8 +92,6 @@ class LoRALoader(ModelLoader):
|
||||
model = lora_model_from_flux_onetrainer_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_flux_control(state_dict=state_dict):
|
||||
model = lora_model_from_flux_control_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_aitoolkit_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_aitoolkit_state_dict(state_dict=state_dict)
|
||||
else:
|
||||
raise ValueError(f"LoRA model is in unsupported FLUX format: {config.format}")
|
||||
else:
|
||||
|
||||
@@ -137,7 +137,6 @@ class FluxLoRAFormat(str, Enum):
|
||||
Kohya = "flux.kohya"
|
||||
OneTrainer = "flux.onetrainer"
|
||||
Control = "flux.control"
|
||||
AIToolkit = "flux.aitoolkit"
|
||||
|
||||
|
||||
AnyVariant: TypeAlias = Union[ModelVariantType, ClipVariantType, None]
|
||||
|
||||
@@ -46,10 +46,6 @@ class ModelPatcher:
|
||||
text_encoder: Union[CLIPTextModel, CLIPTextModelWithProjection],
|
||||
ti_list: List[Tuple[str, TextualInversionModelRaw]],
|
||||
) -> Iterator[Tuple[CLIPTokenizer, TextualInversionManager]]:
|
||||
if len(ti_list) == 0:
|
||||
yield tokenizer, TextualInversionManager(tokenizer)
|
||||
return
|
||||
|
||||
init_tokens_count = None
|
||||
new_tokens_added = None
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch
|
||||
from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict
|
||||
from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import _group_by_layer
|
||||
from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
|
||||
from invokeai.backend.util import InvokeAILogger
|
||||
|
||||
|
||||
def is_state_dict_likely_in_flux_aitoolkit_format(state_dict: dict[str, Any], metadata: dict[str, Any] = None) -> bool:
|
||||
if metadata:
|
||||
try:
|
||||
software = json.loads(metadata.get("software", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
return software.get("name") == "ai-toolkit"
|
||||
# metadata got lost somewhere
|
||||
return any("diffusion_model" == k.split(".", 1)[0] for k in state_dict.keys())
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupedStateDict:
|
||||
transformer: dict[str, Any] = field(default_factory=dict)
|
||||
# might also grow CLIP and T5 submodels
|
||||
|
||||
|
||||
def _group_state_by_submodel(state_dict: dict[str, Any]) -> GroupedStateDict:
|
||||
logger = InvokeAILogger.get_logger()
|
||||
grouped = GroupedStateDict()
|
||||
for key, value in state_dict.items():
|
||||
submodel_name, param_name = key.split(".", 1)
|
||||
match submodel_name:
|
||||
case "diffusion_model":
|
||||
grouped.transformer[param_name] = value
|
||||
case _:
|
||||
logger.warning(f"Unexpected submodel name: {submodel_name}")
|
||||
return grouped
|
||||
|
||||
|
||||
def _rename_peft_lora_keys(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
||||
"""Renames keys from the PEFT LoRA format to the InvokeAI format."""
|
||||
renamed_state_dict = {}
|
||||
for key, value in state_dict.items():
|
||||
renamed_key = key.replace(".lora_A.", ".lora_down.").replace(".lora_B.", ".lora_up.")
|
||||
renamed_state_dict[renamed_key] = value
|
||||
return renamed_state_dict
|
||||
|
||||
|
||||
def lora_model_from_flux_aitoolkit_state_dict(state_dict: dict[str, torch.Tensor]) -> ModelPatchRaw:
|
||||
state_dict = _rename_peft_lora_keys(state_dict)
|
||||
by_layer = _group_by_layer(state_dict)
|
||||
by_model = _group_state_by_submodel(by_layer)
|
||||
|
||||
layers: dict[str, BaseLayerPatch] = {}
|
||||
for layer_key, layer_state_dict in by_model.transformer.items():
|
||||
layers[FLUX_LORA_TRANSFORMER_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
|
||||
return ModelPatchRaw(layers=layers)
|
||||
@@ -1,7 +1,4 @@
|
||||
from invokeai.backend.model_manager.taxonomy import FluxLoRAFormat
|
||||
from invokeai.backend.patches.lora_conversions.flux_aitoolkit_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_aitoolkit_format,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import is_state_dict_likely_flux_control
|
||||
from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_diffusers_format,
|
||||
@@ -14,7 +11,7 @@ from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_u
|
||||
)
|
||||
|
||||
|
||||
def flux_format_from_state_dict(state_dict: dict, metadata: dict | None = None) -> FluxLoRAFormat | None:
|
||||
def flux_format_from_state_dict(state_dict):
|
||||
if is_state_dict_likely_in_flux_kohya_format(state_dict):
|
||||
return FluxLoRAFormat.Kohya
|
||||
elif is_state_dict_likely_in_flux_onetrainer_format(state_dict):
|
||||
@@ -23,7 +20,5 @@ def flux_format_from_state_dict(state_dict: dict, metadata: dict | None = None)
|
||||
return FluxLoRAFormat.Diffusers
|
||||
elif is_state_dict_likely_flux_control(state_dict):
|
||||
return FluxLoRAFormat.Control
|
||||
elif is_state_dict_likely_in_flux_aitoolkit_format(state_dict, metadata):
|
||||
return FluxLoRAFormat.AIToolkit
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -12,13 +12,11 @@ module.exports = {
|
||||
// TODO: ENABLE THIS RULE BEFORE v6.0.0
|
||||
// 'i18next/no-literal-string': 'error',
|
||||
// https://eslint.org/docs/latest/rules/no-console
|
||||
'no-console': 'warn',
|
||||
'no-console': 'error',
|
||||
// https://eslint.org/docs/latest/rules/no-promise-executor-return
|
||||
'no-promise-executor-return': 'error',
|
||||
// https://eslint.org/docs/latest/rules/require-await
|
||||
'require-await': 'error',
|
||||
// TODO: ENABLE THIS RULE BEFORE v6.0.0
|
||||
'react/display-name': 'off',
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -67,9 +67,8 @@
|
||||
"chakra-react-select": "^4.9.2",
|
||||
"cmdk": "^1.1.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dockview": "^4.3.1",
|
||||
"filesize": "^10.1.6",
|
||||
"fracturedjsonjs": "^4.1.0",
|
||||
"fracturedjsonjs": "^4.0.2",
|
||||
"framer-motion": "^11.10.0",
|
||||
"i18next": "^25.0.1",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
|
||||
24
invokeai/frontend/web/pnpm-lock.yaml
generated
24
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -50,15 +50,12 @@ dependencies:
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
dockview:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(react@18.3.1)
|
||||
filesize:
|
||||
specifier: ^10.1.6
|
||||
version: 10.1.6
|
||||
fracturedjsonjs:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
framer-motion:
|
||||
specifier: ^11.10.0
|
||||
version: 11.10.0(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -4495,19 +4492,6 @@ packages:
|
||||
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
|
||||
dev: false
|
||||
|
||||
/dockview-core@4.3.1:
|
||||
resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==}
|
||||
dev: false
|
||||
|
||||
/dockview@4.3.1(react@18.3.1):
|
||||
resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
dependencies:
|
||||
dockview-core: 4.3.1
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/doctrine@2.1.0:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5296,8 +5280,8 @@ packages:
|
||||
signal-exit: 4.1.0
|
||||
dev: true
|
||||
|
||||
/fracturedjsonjs@4.1.0:
|
||||
resolution: {integrity: sha512-qy6LPA8OOiiyRHt5/sNKDayD7h5r3uHmHxSOLbBsgtU/hkt5vOVWOR51MdfDbeCNfj7k/dKCRbXYm8FBAJcgWQ==}
|
||||
/fracturedjsonjs@4.0.2:
|
||||
resolution: {integrity: sha512-+vGJH9wK0EEhbbn50V2sOebLRaar1VL3EXr02kxchIwpkhQk0ItrPjIOtYPYuU9hNFpVzxjrPgzjtMJih+ae4A==}
|
||||
dev: false
|
||||
|
||||
/framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1):
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator';
|
||||
import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { $globalIsLoading } from 'app/store/nanostores/globalIsLoading';
|
||||
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
const globalIsLoading = useStore($globalIsLoading);
|
||||
const didStudioInit = useStore($didStudioInit);
|
||||
const clearStorage = useClearStorage();
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
@@ -33,7 +33,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||
<Box id="invoke-app-wrapper" w="100dvw" h="100dvh" position="relative" overflow="hidden">
|
||||
<AppContent />
|
||||
{globalIsLoading && <Loading />}
|
||||
{!didStudioInit && <Loading />}
|
||||
</Box>
|
||||
<GlobalHookIsolator config={config} studioInitAction={studioInitAction} />
|
||||
<GlobalModalIsolator />
|
||||
|
||||
@@ -11,9 +11,11 @@ import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
||||
import { toggleImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import i18n from 'i18n';
|
||||
@@ -70,6 +72,12 @@ export const GlobalHookIsolator = memo(
|
||||
useWorkflowBuilderWatcher();
|
||||
useDynamicPromptsWatcher();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'toggleViewer',
|
||||
category: 'viewer',
|
||||
callback: toggleImageViewer,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,13 +7,11 @@ import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const GlobalImageHotkeys = memo(() => {
|
||||
useAssertSingleton('GlobalImageHotkeys');
|
||||
const imageName = useAppSelector(selectLastSelectedImage);
|
||||
const imageDTO = useImageDTO(imageName);
|
||||
const imageDTO = useAppSelector(selectLastSelectedImage);
|
||||
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { ImageViewerModal } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
@@ -60,6 +61,7 @@ export const GlobalModalIsolator = memo(() => {
|
||||
<CanvasPasteModal />
|
||||
</CanvasManagerProviderGate>
|
||||
<LoadWorkflowFromGraphModal />
|
||||
<ImageViewerModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import '@fontsource-variable/inter';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import '@xyflow/react/dist/base.css';
|
||||
import 'common/components/OverlayScrollbars/overlayscrollbars.css';
|
||||
|
||||
import { ChakraProvider, DarkMode, extendTheme, theme as _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
@@ -93,6 +94,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
store.dispatch(canvasReset());
|
||||
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
store.dispatch(sentImageToCanvas());
|
||||
$imageViewer.set(false);
|
||||
toast({
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'info',
|
||||
@@ -162,10 +164,12 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
|
||||
store.dispatch(paramsReset());
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
$imageViewer.set(true);
|
||||
break;
|
||||
case 'canvas':
|
||||
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
|
||||
store.dispatch(canvasReset());
|
||||
$imageViewer.set(false);
|
||||
break;
|
||||
case 'workflows':
|
||||
// Go to the workflows tab
|
||||
|
||||
@@ -15,6 +15,8 @@ import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMi
|
||||
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
||||
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
||||
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
||||
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
||||
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
|
||||
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
|
||||
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
|
||||
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
|
||||
@@ -45,6 +47,10 @@ addImageUploadedFulfilledListener(startAppListening);
|
||||
// Image deleted
|
||||
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
||||
|
||||
// Image starred
|
||||
addImagesStarredListener(startAppListening);
|
||||
addImagesUnstarredListener(startAppListening);
|
||||
|
||||
// Gallery
|
||||
addGalleryImageClickedListener(startAppListening);
|
||||
addGalleryOffsetChangedListener(startAppListening);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
|
||||
matcher: matchAnyBoardDeleted,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
const deletedBoardId = action.meta.arg.originalArgs.board_id;
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
const { autoAddBoardId, selectedBoardId } = state.gallery;
|
||||
|
||||
// If the deleted board was currently selected, we should reset the selected board to uncategorized
|
||||
|
||||
@@ -30,9 +30,9 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
|
||||
const selectedImage = boardImagesData.items.find(
|
||||
(item) => item.image_name === action.payload.selectedImageName
|
||||
);
|
||||
dispatch(imageSelected(selectedImage?.image_name ?? null));
|
||||
dispatch(imageSelected(selectedImage || null));
|
||||
} else if (boardImagesData) {
|
||||
dispatch(imageSelected(boardImagesData.items[0]?.image_name ?? null));
|
||||
dispatch(imageSelected(boardImagesData.items[0] || null));
|
||||
} else {
|
||||
// board has no images - deselect
|
||||
dispatch(imageSelected(null));
|
||||
|
||||
@@ -9,7 +9,7 @@ export const addEnsureImageIsSelectedListener = (startAppListening: AppStartList
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const selection = getState().gallery.selection;
|
||||
if (selection.length === 0) {
|
||||
dispatch(imageSelected(action.payload.items[0]?.image_name ?? null));
|
||||
dispatch(imageSelected(action.payload.items[0] ?? null));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { selectImageCollectionQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageCategory, SQLiteDirection } from 'services/api/types';
|
||||
|
||||
// Type for image collection query arguments
|
||||
type ImageCollectionQueryArgs = {
|
||||
board_id?: string;
|
||||
categories?: ImageCategory[];
|
||||
search_term?: string;
|
||||
order_dir?: SQLiteDirection;
|
||||
is_intermediate: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get cached image names list for selection operations
|
||||
* Returns an ordered array of image names (starred first, then unstarred)
|
||||
*/
|
||||
const getCachedImageNames = (state: RootState, queryArgs: ImageCollectionQueryArgs): string[] => {
|
||||
const queryResult = imagesApi.endpoints.getImageNames.select(queryArgs)(state);
|
||||
return queryResult.data || [];
|
||||
};
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const galleryImageClicked = createAction<{
|
||||
imageName: string;
|
||||
imageDTO: ImageDTO;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
@@ -48,51 +28,45 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
|
||||
startAppListening({
|
||||
actionCreator: galleryImageClicked,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { imageName, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
|
||||
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
|
||||
const state = getState();
|
||||
const queryArgs = selectImageCollectionQueryArgs(state);
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const queryResult = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
|
||||
// Get cached image names for selection operations
|
||||
const imageNames = getCachedImageNames(state, queryArgs);
|
||||
|
||||
// If we don't have the image names cached, we can't perform selection operations
|
||||
// This can happen if the user clicks on an image before the names are loaded
|
||||
if (imageNames.length === 0) {
|
||||
// For basic click without modifiers, we can still set selection
|
||||
if (!shiftKey && !ctrlKey && !metaKey && !altKey) {
|
||||
dispatch(selectionChanged([imageName]));
|
||||
}
|
||||
if (!queryResult.data) {
|
||||
// Should never happen if we have clicked a gallery image
|
||||
return;
|
||||
}
|
||||
|
||||
const imageDTOs = queryResult.data.items;
|
||||
const selection = state.gallery.selection;
|
||||
|
||||
if (altKey) {
|
||||
if (state.gallery.imageToCompare === imageName) {
|
||||
if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(imageToCompareChanged(imageName));
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}
|
||||
} else if (shiftKey) {
|
||||
const rangeEndImageName = imageName;
|
||||
const lastSelectedImage = selection.at(-1);
|
||||
const lastClickedIndex = imageNames.findIndex((name) => name === lastSelectedImage);
|
||||
const currentClickedIndex = imageNames.findIndex((name) => name === rangeEndImageName);
|
||||
const rangeEndImageName = imageDTO.image_name;
|
||||
const lastSelectedImage = selection[selection.length - 1]?.image_name;
|
||||
const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage);
|
||||
const currentClickedIndex = imageDTOs.findIndex((n) => n.image_name === rangeEndImageName);
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
// We have a valid range!
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
const imagesToSelect = imageNames.slice(start, end + 1);
|
||||
dispatch(selectionChanged(uniq(selection.concat(imagesToSelect))));
|
||||
const imagesToSelect = imageDTOs.slice(start, end + 1);
|
||||
dispatch(selectionChanged(selection.concat(imagesToSelect)));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (selection.some((n) => n === imageName) && selection.length > 1) {
|
||||
dispatch(selectionChanged(uniq(selection.filter((n) => n !== imageName))));
|
||||
if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) {
|
||||
dispatch(selectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name)));
|
||||
} else {
|
||||
dispatch(selectionChanged(uniq(selection.concat(imageName))));
|
||||
dispatch(selectionChanged(selection.concat(imageDTO)));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectionChanged([imageName]));
|
||||
dispatch(selectionChanged([imageDTO]));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -84,14 +84,14 @@ export const addGalleryOffsetChangedListener = (startAppListening: AppStartListe
|
||||
if (offset < prevOffset) {
|
||||
// We've gone backwards
|
||||
const lastImage = imageDTOs[imageDTOs.length - 1];
|
||||
if (!selection.some((selectedImage) => selectedImage === lastImage?.image_name)) {
|
||||
dispatch(selectionChanged(lastImage ? [lastImage.image_name] : []));
|
||||
if (!selection.some((selectedImage) => selectedImage.image_name === lastImage?.image_name)) {
|
||||
dispatch(selectionChanged(lastImage ? [lastImage] : []));
|
||||
}
|
||||
} else {
|
||||
// We've gone forwards
|
||||
const firstImage = imageDTOs[0];
|
||||
if (!selection.some((selectedImage) => selectedImage === firstImage?.image_name)) {
|
||||
dispatch(selectionChanged(firstImage ? [firstImage.image_name] : []));
|
||||
if (!selection.some((selectedImage) => selectedImage.image_name === firstImage?.image_name)) {
|
||||
dispatch(selectionChanged(firstImage ? [firstImage] : []));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -102,14 +102,14 @@ export const addGalleryOffsetChangedListener = (startAppListening: AppStartListe
|
||||
if (offset < prevOffset) {
|
||||
// We've gone backwards
|
||||
const lastImage = imageDTOs[imageDTOs.length - 1];
|
||||
if (lastImage && imageToCompare !== lastImage.image_name) {
|
||||
dispatch(imageToCompareChanged(lastImage.image_name));
|
||||
if (lastImage && imageToCompare?.image_name !== lastImage.image_name) {
|
||||
dispatch(imageToCompareChanged(lastImage));
|
||||
}
|
||||
} else {
|
||||
// We've gone forwards
|
||||
const firstImage = imageDTOs[0];
|
||||
if (firstImage && imageToCompare !== firstImage.image_name) {
|
||||
dispatch(imageToCompareChanged(firstImage.image_name));
|
||||
if (firstImage && imageToCompare?.image_name !== firstImage.image_name) {
|
||||
dispatch(imageToCompareChanged(firstImage));
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -8,16 +8,16 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
|
||||
effect: (action) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
log.debug({ board_id, image_name }, 'Image added to board');
|
||||
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||
log.debug({ board_id, imageDTO }, 'Image added to board');
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
|
||||
effect: (action) => {
|
||||
const { board_id, image_name } = action.meta.arg.originalArgs;
|
||||
log.debug({ board_id, image_name }, 'Problem adding image to board');
|
||||
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||
log.debug({ board_id, imageDTO }, 'Problem adding image to board');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const addImagesStarredListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.starImages.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { updated_image_names: starredImages } = action.payload;
|
||||
|
||||
const state = getState();
|
||||
|
||||
const { selection } = state.gallery;
|
||||
const updatedSelection: ImageDTO[] = [];
|
||||
|
||||
selection.forEach((selectedImageDTO) => {
|
||||
if (starredImages.includes(selectedImageDTO.image_name)) {
|
||||
updatedSelection.push({
|
||||
...selectedImageDTO,
|
||||
starred: true,
|
||||
});
|
||||
} else {
|
||||
updatedSelection.push(selectedImageDTO);
|
||||
}
|
||||
});
|
||||
dispatch(selectionChanged(updatedSelection));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const addImagesUnstarredListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.unstarImages.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { updated_image_names: unstarredImages } = action.payload;
|
||||
|
||||
const state = getState();
|
||||
|
||||
const { selection } = state.gallery;
|
||||
const updatedSelection: ImageDTO[] = [];
|
||||
|
||||
selection.forEach((selectedImageDTO) => {
|
||||
if (unstarredImages.includes(selectedImageDTO.image_name)) {
|
||||
updatedSelection.push({
|
||||
...selectedImageDTO,
|
||||
starred: false,
|
||||
});
|
||||
} else {
|
||||
updatedSelection.push(selectedImageDTO);
|
||||
}
|
||||
});
|
||||
dispatch(selectionChanged(updatedSelection));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
export const $isLayoutLoading = atom(false);
|
||||
export const setIsLayoutLoading = (isLoading: boolean) => {
|
||||
flushSync(() => {
|
||||
$isLayoutLoading.set(isLoading);
|
||||
});
|
||||
};
|
||||
export const $globalIsLoading = computed([$didStudioInit, $isLayoutLoading], (didStudioInit, isLayoutLoading) => {
|
||||
return !didStudioInit || isLayoutLoading;
|
||||
});
|
||||
@@ -17,7 +17,6 @@ const Loading = () => {
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
zIndex={99999}
|
||||
>
|
||||
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
|
||||
<Spinner
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.os-scrollbar {
|
||||
/* The size of the scrollbar */
|
||||
--os-size: 8px;
|
||||
--os-size: 9px;
|
||||
/* The axis-perpedicular padding of the scrollbar (horizontal: padding-y, vertical: padding-x) */
|
||||
/* --os-padding-perpendicular: 0; */
|
||||
/* The axis padding of the scrollbar (horizontal: padding-x, vertical: padding-y) */
|
||||
@@ -8,11 +8,11 @@
|
||||
/* The border radius of the scrollbar track */
|
||||
/* --os-track-border-radius: 0; */
|
||||
/* The background of the scrollbar track */
|
||||
--os-track-bg: rgba(0, 0, 0, 0.5);
|
||||
/* --os-track-bg: rgba(0, 0, 0, 0.3); */
|
||||
/* The :hover background of the scrollbar track */
|
||||
--os-track-bg-hover: rgba(0, 0, 0, 0.5);
|
||||
/* --os-track-bg-hover: rgba(0, 0, 0, 0.3); */
|
||||
/* The :active background of the scrollbar track */
|
||||
--os-track-bg-active: rgba(0, 0, 0, 0.6);
|
||||
/* --os-track-bg-active: rgba(0, 0, 0, 0.3); */
|
||||
/* The border of the scrollbar track */
|
||||
/* --os-track-border: none; */
|
||||
/* The :hover background of the scrollbar track */
|
||||
@@ -22,11 +22,11 @@
|
||||
/* The border radius of the scrollbar handle */
|
||||
/* --os-handle-border-radius: 2px; */
|
||||
/* The background of the scrollbar handle */
|
||||
--os-handle-bg: var(--invoke-colors-base-400);
|
||||
/* --os-handle-bg: var(--invokeai-colors-accentAlpha-500); */
|
||||
/* The :hover background of the scrollbar handle */
|
||||
--os-handle-bg-hover: var(--invoke-colors-base-300);
|
||||
/* --os-handle-bg-hover: var(--invokeai-colors-accentAlpha-700); */
|
||||
/* The :active background of the scrollbar handle */
|
||||
--os-handle-bg-active: var(--invoke-colors-base-250);
|
||||
/* --os-handle-bg-active: var(--invokeai-colors-accentAlpha-800); */
|
||||
/* The border of the scrollbar handle */
|
||||
/* --os-handle-border: none; */
|
||||
/* The :hover border of the scrollbar handle */
|
||||
@@ -34,23 +34,23 @@
|
||||
/* The :active border of the scrollbar handle */
|
||||
/* --os-handle-border-active: none; */
|
||||
/* The min size of the scrollbar handle */
|
||||
--os-handle-min-size: 32px;
|
||||
--os-handle-min-size: 50px;
|
||||
/* The max size of the scrollbar handle */
|
||||
/* --os-handle-max-size: none; */
|
||||
/* The axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */
|
||||
/* --os-handle-perpendicular-size: 100%; */
|
||||
/* The :hover axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */
|
||||
--os-handle-perpendicular-size-hover: 100%;
|
||||
/* --os-handle-perpendicular-size-hover: 100%; */
|
||||
/* The :active axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */
|
||||
/* --os-handle-perpendicular-size-active: 100%; */
|
||||
/* Increases the interactive area of the scrollbar handle. */
|
||||
--os-handle-interactive-area-offset: -1px;
|
||||
/* --os-handle-interactive-area-offset: 0; */
|
||||
}
|
||||
|
||||
.os-scrollbar-handle {
|
||||
/* cursor: grab; */
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.os-scrollbar-handle:active {
|
||||
/* cursor: grabbing; */
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { useTimeoutCallback } from 'common/hooks/useTimeoutCallback';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useCallbackOnDragEnter = (cb: () => void, ref: RefObject<HTMLElement>, delay = 300) => {
|
||||
const [run, cancel] = useTimeoutCallback(cb, delay);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element,
|
||||
onDragEnter: run,
|
||||
onDragLeave: cancel,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element,
|
||||
onDragEnter: run,
|
||||
onDragLeave: cancel,
|
||||
})
|
||||
);
|
||||
}, [cancel, ref, run]);
|
||||
};
|
||||
@@ -61,15 +61,6 @@ export const useGlobalHotkeys = () => {
|
||||
dependencies: [clearQueue],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'selectGenerateTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('generate'));
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'selectCanvasTab',
|
||||
category: 'app',
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Selector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import type { Atom, WritableAtom } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const useSelectorAsAtom = <T extends Selector<RootState, any>>(selector: T): Atom<ReturnType<T>> => {
|
||||
const store = useAppStore();
|
||||
const $atom = useState<WritableAtom<ReturnType<T>>>(() => atom<ReturnType<T>>(selector(store.getState())))[0];
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const prev = $atom.get();
|
||||
const next = selector(store.getState());
|
||||
if (prev !== next) {
|
||||
$atom.set(next);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [$atom, selector, store]);
|
||||
|
||||
return $atom;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
export const useTimeoutCallback = (callback: () => void, delay: number, onCancel?: () => void) => {
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const cancel = useCallback(() => {
|
||||
if (timeoutRef.current !== null) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
onCancel?.();
|
||||
}
|
||||
}, [onCancel]);
|
||||
const callWithTimeout = useCallback(() => {
|
||||
cancel();
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
callback();
|
||||
timeoutRef.current = null;
|
||||
}, delay);
|
||||
}, [callback, cancel, delay]);
|
||||
const api = useMemo(() => [callWithTimeout, cancel] as const, [callWithTimeout, cancel]);
|
||||
return api;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 's
|
||||
|
||||
const selectImagesToChange = createMemoizedSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.image_names
|
||||
(changeBoardModal) => changeBoardModal.imagesToChange
|
||||
);
|
||||
|
||||
const selectIsModalOpen = createSelector(
|
||||
@@ -57,10 +57,10 @@ const ChangeBoardModal = () => {
|
||||
}
|
||||
|
||||
if (selectedBoard === 'none') {
|
||||
removeImagesFromBoard({ image_names: imagesToChange });
|
||||
removeImagesFromBoard({ imageDTOs: imagesToChange });
|
||||
} else {
|
||||
addImagesToBoard({
|
||||
image_names: imagesToChange,
|
||||
imageDTOs: imagesToChange,
|
||||
board_id: selectedBoard,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import type { ChangeBoardModalState } from './types';
|
||||
|
||||
export const initialState: ChangeBoardModalState = {
|
||||
isModalOpen: false,
|
||||
image_names: [],
|
||||
imagesToChange: [],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { initialState } from './initialState';
|
||||
|
||||
@@ -11,11 +12,11 @@ export const changeBoardModalSlice = createSlice({
|
||||
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isModalOpen = action.payload;
|
||||
},
|
||||
imagesToChangeSelected: (state, action: PayloadAction<string[]>) => {
|
||||
state.image_names = action.payload;
|
||||
imagesToChangeSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||
state.imagesToChange = action.payload;
|
||||
},
|
||||
changeBoardReset: (state) => {
|
||||
state.image_names = [];
|
||||
state.imagesToChange = [];
|
||||
state.isModalOpen = false;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export type ChangeBoardModalState = {
|
||||
isModalOpen: boolean;
|
||||
image_names: string[];
|
||||
imagesToChange: ImageDTO[];
|
||||
};
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
ContextMenu,
|
||||
Divider,
|
||||
Flex,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
@@ -26,15 +13,12 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
||||
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
|
||||
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
|
||||
@@ -76,107 +60,87 @@ export const AdvancedSession = memo(({ id }: { id: string | null }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tabs w="full" h="full">
|
||||
<TabList>
|
||||
<Tab>Welcome</Tab>
|
||||
<Tab>Workspace</Tab>
|
||||
<Tab>Viewer</Tab>
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel w="full" h="full" justifyContent="center">
|
||||
<GenerateLaunchpadPanel />
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full">
|
||||
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
borderRadius="base"
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="full"
|
||||
width="full"
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
overflow="hidden"
|
||||
>
|
||||
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
borderRadius="base"
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="full"
|
||||
width="full"
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
overflow="hidden"
|
||||
>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasToolbar />
|
||||
</CanvasManagerProviderGate>
|
||||
<Divider />
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
||||
{(ref) => (
|
||||
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
|
||||
<InvokeCanvasComponent />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasToolbar />
|
||||
</CanvasManagerProviderGate>
|
||||
<Divider />
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
||||
{(ref) => (
|
||||
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
|
||||
<InvokeCanvasComponent />
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
top={1}
|
||||
insetInlineStart={1}
|
||||
pointerEvents="none"
|
||||
gap={2}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{showHUD && <CanvasHUD />}
|
||||
<CanvasAlertsSelectedEntityStatus />
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsInvocationProgress />
|
||||
</Flex>
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
||||
<MenuContent />
|
||||
</Menu>
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
{id !== null && (
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider type="advanced" id={id}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
bottom={4}
|
||||
gap={2}
|
||||
align="center"
|
||||
justify="center"
|
||||
left={4}
|
||||
right={4}
|
||||
>
|
||||
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||
<StagingAreaItemsList />
|
||||
</Flex>
|
||||
<Flex gap={2}>
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
<Flex position="absolute" bottom={4}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
<Transform />
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasDropArea />
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
top={1}
|
||||
insetInlineStart={1}
|
||||
pointerEvents="none"
|
||||
gap={2}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{showHUD && <CanvasHUD />}
|
||||
<CanvasAlertsSelectedEntityStatus />
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsInvocationProgress />
|
||||
</Flex>
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
||||
<MenuContent />
|
||||
</Menu>
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
</FocusRegionWrapper>
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full">
|
||||
<Flex flexDir="column" w="full" h="full">
|
||||
<ViewerToolbar />
|
||||
<ImageViewer />
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)}
|
||||
</ContextMenu>
|
||||
{id !== null && (
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasSessionContextProvider type="advanced" id={id}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
bottom={4}
|
||||
gap={2}
|
||||
align="center"
|
||||
justify="center"
|
||||
left={4}
|
||||
right={4}
|
||||
>
|
||||
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||
<StagingAreaItemsList />
|
||||
</Flex>
|
||||
<Flex gap={2}>
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CanvasSessionContextProvider>
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
<Flex position="absolute" bottom={4}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
<Transform />
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasDropArea />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
});
|
||||
AdvancedSession.displayName = 'AdvancedSession';
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
|
||||
|
||||
export const CanvasLayersPanel = memo(() => {
|
||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
};
|
||||
|
||||
export const CanvasLayersPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full" p={2}>
|
||||
<FocusRegionWrapper region="layers" sx={FOCUS_REGION_STYLES}>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
<ParamDenoisingStrength />
|
||||
@@ -22,8 +27,8 @@ export const CanvasLayersPanel = memo(() => {
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasLayersPanel.displayName = 'CanvasLayersPanel';
|
||||
CanvasLayersPanelContent.displayName = 'CanvasLayersPanelContent';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Divider,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
Popover,
|
||||
@@ -12,7 +10,6 @@ import {
|
||||
PopoverContent,
|
||||
Portal,
|
||||
Skeleton,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
|
||||
@@ -23,10 +20,8 @@ import { RefImageHeader } from 'features/controlLayers/components/RefImage/RefIm
|
||||
import { RefImageSettings } from 'features/controlLayers/components/RefImage/RefImageSettings';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import { round } from 'lodash-es';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PiExclamationMarkBold, PiImageBold } from 'react-icons/pi';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
// There is some awkwardness here with closing the popover when clicking outside of it, related to Chakra's
|
||||
@@ -84,75 +79,11 @@ export const RefImage = memo(() => {
|
||||
});
|
||||
RefImage.displayName = 'RefImage';
|
||||
|
||||
const baseSx: SystemStyleObject = {
|
||||
opacity: 0.7,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: 'normal',
|
||||
position: 'relative',
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
},
|
||||
'&[data-is-open="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
'&[data-is-error="true"]': {
|
||||
borderColor: 'error.500',
|
||||
borderWidth: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const weightDisplaySx: SystemStyleObject = {
|
||||
pointerEvents: 'none',
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: 'normal',
|
||||
opacity: 0,
|
||||
'&[data-visible="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const getImageSxWithWeight = (weight: number): SystemStyleObject => {
|
||||
const fillPercentage = Math.max(0, Math.min(100, weight * 100));
|
||||
|
||||
return {
|
||||
...baseSx,
|
||||
_after: {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: `linear-gradient(to top, transparent ${fillPercentage}%, rgba(0, 0, 0, 0.8) ${fillPercentage}%)`,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'base',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
|
||||
const id = useRefImageIdContext();
|
||||
const entity = useRefImageEntity(id);
|
||||
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
|
||||
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
|
||||
|
||||
const sx = useMemo(() => {
|
||||
if (!isIPAdapterConfig(entity.config)) {
|
||||
return baseSx;
|
||||
}
|
||||
return getImageSxWithWeight(entity.config.weight);
|
||||
}, [entity.config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIPAdapterConfig(entity.config)) {
|
||||
return;
|
||||
}
|
||||
setShowWeightDisplay(true);
|
||||
const timeout = window.setTimeout(() => {
|
||||
setShowWeightDisplay(false);
|
||||
}, 1000);
|
||||
return () => {
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [entity.config]);
|
||||
|
||||
if (!entity.config.image) {
|
||||
return (
|
||||
<PopoverAnchor>
|
||||
@@ -176,62 +107,25 @@ const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
|
||||
}
|
||||
return (
|
||||
<PopoverAnchor>
|
||||
<Flex
|
||||
position="relative"
|
||||
<Image
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderRadius="base"
|
||||
aspectRatio="1/1"
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
flexShrink={0}
|
||||
sx={sx}
|
||||
data-is-open={disclosure.isOpen}
|
||||
data-is-error={!entity.config.model}
|
||||
id={getRefImagePopoverTriggerId(id)}
|
||||
role="button"
|
||||
src={imageDTO?.thumbnail_url}
|
||||
objectFit="contain"
|
||||
aspectRatio="1/1"
|
||||
// width={imageDTO?.width}
|
||||
height={imageDTO?.height}
|
||||
fallback={<Skeleton h="full" aspectRatio="1/1" />}
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
onClick={disclosure.toggle}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Image
|
||||
src={imageDTO?.thumbnail_url}
|
||||
objectFit="contain"
|
||||
aspectRatio="1/1"
|
||||
height={imageDTO?.height}
|
||||
fallback={<Skeleton h="full" aspectRatio="1/1" />}
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
{isIPAdapterConfig(entity.config) && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
inset={0}
|
||||
fontWeight="semibold"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex={1}
|
||||
data-visible={showWeightDisplay}
|
||||
sx={weightDisplaySx}
|
||||
>
|
||||
<Text filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))">
|
||||
{`${round(entity.config.weight * 100, 2)}%`}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{!entity.config.model && (
|
||||
<Icon
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translateX(-50%) translateY(-50%)"
|
||||
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))"
|
||||
color="error.500"
|
||||
boxSize={16}
|
||||
as={PiExclamationMarkBold}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
flexShrink={0}
|
||||
// sx={imageSx}
|
||||
// data-is-open={disclosure.isOpen}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
|
||||
|
||||
return (
|
||||
<Tooltip label={selectedModel?.description}>
|
||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full" minW={0}>
|
||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
|
||||
<Combobox
|
||||
options={options}
|
||||
placeholder={t('common.placeholderSelectAModel')}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InitialStateMainModelPicker } from './InitialStateMainModelPicker';
|
||||
import { LaunchpadAddStyleReference } from './LaunchpadAddStyleReference';
|
||||
import { LaunchpadEditImageButton } from './LaunchpadEditImageButton';
|
||||
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||
import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton';
|
||||
|
||||
export const CanvasLaunchpadPanel = memo(() => {
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">
|
||||
<Heading mb={4}>Edit and refine on Canvas.</Heading>
|
||||
<Flex flexDir="column" gap={8}>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||
<InitialStateMainModelPicker />
|
||||
<Flex flexDir="column" gap={2} justifyContent="center">
|
||||
<Text>
|
||||
Want to learn what prompts work best for each model?{' '}
|
||||
<Button as="a" variant="link" href="#" size="sm">
|
||||
Check our our Model Guide.
|
||||
</Button>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<LaunchpadGenerateFromTextButton />
|
||||
<LaunchpadAddStyleReference />
|
||||
<LaunchpadEditImageButton />
|
||||
<LaunchpadUseALayoutImageButton />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
CanvasLaunchpadPanel.displayName = 'CanvasLaunchpadPanel';
|
||||
@@ -1,26 +1,29 @@
|
||||
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
|
||||
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
|
||||
import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
|
||||
import { LaunchpadAddStyleReference } from 'features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||
|
||||
export const GenerateLaunchpadPanel = memo(() => {
|
||||
export const InitialState = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const newCanvasSession = useCallback(() => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">
|
||||
<Heading mb={4}>Generate images from text prompts.</Heading>
|
||||
<Flex flexDir="column" gap={8}>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||
<Flex flexDir="column" h="full" w="full" gap={2}>
|
||||
<Flex px={2} alignItems="center" minH="24px">
|
||||
<Heading size="sm">Get Started</Heading>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<Flex flexDir="column" h="full" justifyContent="center" mx={16}>
|
||||
<Heading mb={4}>Get started with Invoke.</Heading>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap={4}>
|
||||
<InitialStateMainModelPicker />
|
||||
<Flex flexDir="column" gap={2} justifyContent="center">
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text>
|
||||
Want to learn what prompts work best for each model?{' '}
|
||||
<Button as="a" variant="link" href="#" size="sm">
|
||||
@@ -29,8 +32,8 @@ export const GenerateLaunchpadPanel = memo(() => {
|
||||
</Text>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<LaunchpadGenerateFromTextButton />
|
||||
<LaunchpadAddStyleReference />
|
||||
<InitialStateGenerateFromText />
|
||||
<InitialStateAddAStyleReference />
|
||||
<Alert status="info" borderRadius="base" flexDir="column" gap={2} overflow="unset">
|
||||
<Text fontSize="md" fontWeight="semibold">
|
||||
Looking to get more control, edit, and iterate on your images?
|
||||
@@ -44,4 +47,4 @@ export const GenerateLaunchpadPanel = memo(() => {
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateLaunchpadPanel.displayName = 'GenerateLaunchpad';
|
||||
InitialState.displayName = 'InitialState';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
@@ -13,7 +13,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const dndTargetData = addGlobalReferenceImageDndTarget.getData();
|
||||
|
||||
export const LaunchpadAddStyleReference = memo(() => {
|
||||
export const InitialStateAddAStyleReference = memo(() => {
|
||||
const { dispatch, getState } = useAppStore();
|
||||
|
||||
const uploadOptions = useMemo(
|
||||
@@ -32,7 +32,7 @@ export const LaunchpadAddStyleReference = memo(() => {
|
||||
const uploadApi = useImageUploadButton(uploadOptions);
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<InitialStateButtonGridItem {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<Icon as={PiUserCircleGearBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">Add a Style Reference</Heading>
|
||||
@@ -43,7 +43,7 @@ export const LaunchpadAddStyleReference = memo(() => {
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
</LaunchpadButton>
|
||||
</InitialStateButtonGridItem>
|
||||
);
|
||||
});
|
||||
LaunchpadAddStyleReference.displayName = 'LaunchpadAddStyleReference';
|
||||
InitialStateAddAStyleReference.displayName = 'InitialStateAddAStyleReference';
|
||||
@@ -2,7 +2,7 @@ import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||
import { Button, forwardRef } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const LaunchpadButton = memo(
|
||||
export const InitialStateButtonGridItem = memo(
|
||||
forwardRef(({ children, ...rest }: ButtonProps, ref) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -26,4 +26,4 @@ export const LaunchpadButton = memo(
|
||||
})
|
||||
);
|
||||
|
||||
LaunchpadButton.displayName = 'LaunchpadButton';
|
||||
InitialStateButtonGridItem.displayName = 'InitialStateButtonGridItem';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
@@ -13,7 +13,7 @@ const NEW_CANVAS_OPTIONS = { type: 'raster_layer', withInpaintMask: true } as co
|
||||
|
||||
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||
|
||||
export const LaunchpadEditImageButton = memo(() => {
|
||||
export const InitialStateEditImageCard = memo(() => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
|
||||
const onUpload = useCallback(
|
||||
@@ -25,18 +25,16 @@ export const LaunchpadEditImageButton = memo(() => {
|
||||
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<InitialStateButtonGridItem {...uploadApi.getUploadButtonProps()}>
|
||||
<Icon as={PiPencilBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">Edit Image</Heading>
|
||||
<Text color="base.300">Add an image to refine.</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<Heading size="sm">Edit Image</Heading>
|
||||
<Text color="base.300">Add an image to refine.</Text>
|
||||
<Flex w="full" justifyContent="flex-end" p={2}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
</LaunchpadButton>
|
||||
</InitialStateButtonGridItem>
|
||||
);
|
||||
});
|
||||
LaunchpadEditImageButton.displayName = 'LaunchpadEditImageButton';
|
||||
InitialStateEditImageCard.displayName = 'InitialStateEditImageCard';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
|
||||
import { memo } from 'react';
|
||||
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
|
||||
|
||||
@@ -11,9 +11,9 @@ const focusOnPrompt = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const LaunchpadGenerateFromTextButton = memo(() => {
|
||||
export const InitialStateGenerateFromText = memo(() => {
|
||||
return (
|
||||
<LaunchpadButton onClick={focusOnPrompt} position="relative" gap={8}>
|
||||
<InitialStateButtonGridItem onClick={focusOnPrompt} position="relative" gap={8}>
|
||||
<Icon as={PiTextAaBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">Generate from Text</Heading>
|
||||
@@ -22,7 +22,7 @@ export const LaunchpadGenerateFromTextButton = memo(() => {
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiCursorTextBold />
|
||||
</Flex>
|
||||
</LaunchpadButton>
|
||||
</InitialStateButtonGridItem>
|
||||
);
|
||||
});
|
||||
LaunchpadGenerateFromTextButton.displayName = 'LaunchpadGenerateFromTextButton';
|
||||
InitialStateGenerateFromText.displayName = 'InitialStateGenerateFromText';
|
||||
@@ -30,16 +30,16 @@ export const InitialStateMainModelPicker = memo(() => {
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical" alignItems="unset">
|
||||
<FormLabel display="flex" fontSize="md" gap={2}>
|
||||
Select your Model{' '}
|
||||
{isFluxDevSelected && (
|
||||
<InformationalPopover feature="fluxDevLicense" hideDisable={true}>
|
||||
<Flex justifyContent="flex-start">
|
||||
<Icon as={MdMoneyOff} />
|
||||
</Flex>
|
||||
</InformationalPopover>
|
||||
)}
|
||||
</FormLabel>
|
||||
<InformationalPopover feature="paramModel">
|
||||
<FormLabel>Select your Model</FormLabel>
|
||||
</InformationalPopover>
|
||||
{isFluxDevSelected && (
|
||||
<InformationalPopover feature="fluxDevLicense" hideDisable={true}>
|
||||
<Flex justifyContent="flex-start">
|
||||
<Icon as={MdMoneyOff} />
|
||||
</Flex>
|
||||
</InformationalPopover>
|
||||
)}
|
||||
<ModelPicker modelConfigs={modelConfigs} selectedModelConfig={selectedModelConfig} onChange={onChange} grouped />
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { InitialStateButtonGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateButtonGridItem';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
@@ -8,13 +9,11 @@ import { memo, useCallback } from 'react';
|
||||
import { PiRectangleDashedBold, PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { LaunchpadButton } from './LaunchpadButton';
|
||||
|
||||
const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
|
||||
|
||||
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||
|
||||
export const LaunchpadUseALayoutImageButton = memo(() => {
|
||||
export const InitialStateUseALayoutImageCard = memo(() => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
|
||||
const onUpload = useCallback(
|
||||
@@ -26,18 +25,16 @@ export const LaunchpadUseALayoutImageButton = memo(() => {
|
||||
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<InitialStateButtonGridItem {...uploadApi.getUploadButtonProps()}>
|
||||
<Icon as={PiRectangleDashedBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">Use a Layout Image</Heading>
|
||||
<Text color="base.300">Add an image to control composition.</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<Heading size="sm">Use a Layout Image</Heading>
|
||||
<Text color="base.300">Add an image to control composition.</Text>
|
||||
<Flex w="full" justifyContent="flex-end" p={2}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
</LaunchpadButton>
|
||||
</InitialStateButtonGridItem>
|
||||
);
|
||||
});
|
||||
LaunchpadUseALayoutImageButton.displayName = 'LaunchpadUseALayoutImageButton';
|
||||
InitialStateUseALayoutImageCard.displayName = 'InitialStateUseALayoutImageCard';
|
||||
@@ -11,7 +11,6 @@ import { QueueItemProgressImage } from 'features/controlLayers/components/Simple
|
||||
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
@@ -47,28 +46,12 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
|
||||
ctx.$selectedItemId.set(item.item_id);
|
||||
}, [ctx.$selectedItemId, item.item_id]);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
const autoSwitch = ctx.$autoSwitch.get();
|
||||
if (autoSwitch !== 'off') {
|
||||
ctx.$autoSwitch.set('off');
|
||||
toast({
|
||||
title: 'Auto-Switch Disabled',
|
||||
});
|
||||
}
|
||||
}, [ctx.$autoSwitch]);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
ctx.onImageLoad(item.item_id);
|
||||
}, [ctx, item.item_id]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id={getQueueItemElementId(item.item_id)}
|
||||
sx={sx}
|
||||
data-selected={isSelected}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<Flex id={getQueueItemElementId(item.item_id)} sx={sx} data-selected={isSelected} onClick={onClick}>
|
||||
<QueueItemStatusLabel item={item} position="absolute" margin="auto" />
|
||||
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} asThumbnail />}
|
||||
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
|
||||
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const SimpleSession = memo(({ id }: { id: string | null }) => {
|
||||
if (id === null) {
|
||||
return <InitialState />;
|
||||
}
|
||||
return (
|
||||
<CanvasSessionContextProvider type="simple" id={id}>
|
||||
<StagingArea />
|
||||
</CanvasSessionContextProvider>
|
||||
);
|
||||
});
|
||||
SimpleSession.displayName = 'SimpleSession';
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
|
||||
import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const SimpleSessionNoId = memo(() => {
|
||||
return (
|
||||
<Flex flexDir="column" gap={2} w="full" h="full" minW={0} minH={0}>
|
||||
<StagingAreaHeader />
|
||||
<Divider />
|
||||
<StagingAreaNoItems />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea';
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const UpscalingLaunchpadPanel = memo(() => {
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">
|
||||
<Heading mb={4}>Upscale and add detail.</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
UpscalingLaunchpadPanel.displayName = 'UpscalingLaunchpadPanel';
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const WorkflowsLaunchpadPanel = memo(() => {
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">
|
||||
<Heading mb={4}>Go deep with Workflows.</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
WorkflowsLaunchpadPanel.displayName = 'WorkflowsLaunchpadPanel';
|
||||
@@ -2,7 +2,6 @@ import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { buildZodTypeGuard } from 'common/util/zodUtils';
|
||||
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
|
||||
@@ -14,11 +13,6 @@ import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import { $socket } from 'services/events/stores';
|
||||
import { assert, objectEntries } from 'tsafe';
|
||||
import { z } from 'zod';
|
||||
|
||||
const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']);
|
||||
export const isAutoSwitchMode = buildZodTypeGuard(zAutoSwitchMode);
|
||||
export type AutoSwitchMode = z.infer<typeof zAutoSwitchMode>;
|
||||
|
||||
export type ProgressData = {
|
||||
itemId: number;
|
||||
@@ -97,7 +91,7 @@ type CanvasSessionContextValue = {
|
||||
$selectedItem: Atom<S['SessionQueueItem'] | null>;
|
||||
$selectedItemIndex: Atom<number | null>;
|
||||
$selectedItemOutputImageDTO: Atom<ImageDTO | null>;
|
||||
$autoSwitch: WritableAtom<AutoSwitchMode>;
|
||||
$autoSwitch: WritableAtom<boolean>;
|
||||
selectNext: () => void;
|
||||
selectPrev: () => void;
|
||||
selectFirst: () => void;
|
||||
@@ -122,17 +116,8 @@ export const CanvasSessionContextProvider = memo(
|
||||
const store = useAppStore();
|
||||
|
||||
const socket = useStore($socket);
|
||||
|
||||
/**
|
||||
* Track the last completed item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastCompletedItemId = useState(() => atom<number | null>(null))[0];
|
||||
|
||||
/**
|
||||
* Track the last started item. Used to implement autoswitch.
|
||||
*/
|
||||
const $lastStartedItemId = useState(() => atom<number | null>(null))[0];
|
||||
|
||||
/**
|
||||
* Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
|
||||
* and kept in sync with it via a redux subscription.
|
||||
@@ -142,7 +127,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
/**
|
||||
* Whether auto-switch is enabled.
|
||||
*/
|
||||
const $autoSwitch = useState(() => atom<AutoSwitchMode>('switch_on_start'))[0];
|
||||
const $autoSwitch = useState(() => atom(true))[0];
|
||||
|
||||
/**
|
||||
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
|
||||
@@ -292,12 +277,12 @@ export const CanvasSessionContextProvider = memo(
|
||||
imageLoaded: true,
|
||||
});
|
||||
}
|
||||
if ($lastCompletedItemId.get() === itemId && $autoSwitch.get() === 'switch_on_finish') {
|
||||
if ($lastCompletedItemId.get() === itemId) {
|
||||
$selectedItemId.set(itemId);
|
||||
$lastCompletedItemId.set(null);
|
||||
}
|
||||
},
|
||||
[$autoSwitch, $lastCompletedItemId, $progressData, $selectedItemId]
|
||||
[$lastCompletedItemId, $progressData, $selectedItemId]
|
||||
);
|
||||
|
||||
// Set up socket listeners
|
||||
@@ -320,9 +305,6 @@ export const CanvasSessionContextProvider = memo(
|
||||
if (data.status === 'completed') {
|
||||
$lastCompletedItemId.set(data.item_id);
|
||||
}
|
||||
if (data.status === 'in_progress') {
|
||||
$lastStartedItemId.set(data.item_id);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('invocation_progress', onProgress);
|
||||
@@ -332,7 +314,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
socket.off('invocation_progress', onProgress);
|
||||
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
};
|
||||
}, [$autoSwitch, $lastCompletedItemId, $lastStartedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||
}, [$autoSwitch, $lastCompletedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||
|
||||
// Set up state subscriptions and effects
|
||||
useEffect(() => {
|
||||
@@ -351,39 +333,29 @@ export const CanvasSessionContextProvider = memo(
|
||||
});
|
||||
|
||||
// Handle cases that could result in a nonexistent queue item being selected.
|
||||
const unsubEnsureSelectedItemIdExists = effect(
|
||||
[$items, $selectedItemId, $lastStartedItemId],
|
||||
(items, selectedItemId, lastStartedItemId) => {
|
||||
// If there are no items, cannot have a selected item.
|
||||
if (items.length === 0) {
|
||||
$selectedItemId.set(null);
|
||||
return;
|
||||
}
|
||||
// If there is no selected item but there are items, select the first one.
|
||||
if (selectedItemId === null && items.length > 0) {
|
||||
$selectedItemId.set(items[0]?.item_id ?? null);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
$autoSwitch.get() === 'switch_on_start' &&
|
||||
items.findIndex(({ item_id }) => item_id === lastStartedItemId) !== -1
|
||||
) {
|
||||
$selectedItemId.set(lastStartedItemId);
|
||||
$lastStartedItemId.set(null);
|
||||
}
|
||||
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
|
||||
// the above case, selecting the first item if there are any.
|
||||
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
|
||||
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
|
||||
if (prevIndex >= items.length) {
|
||||
prevIndex = items.length - 1;
|
||||
}
|
||||
const nextItem = items[prevIndex];
|
||||
$selectedItemId.set(nextItem?.item_id ?? null);
|
||||
return;
|
||||
}
|
||||
const unsubEnsureSelectedItemIdExists = effect([$items, $selectedItemId], (items, selectedItemId) => {
|
||||
// If there are no items, cannot have a selected item.
|
||||
if (items.length === 0) {
|
||||
$selectedItemId.set(null);
|
||||
return;
|
||||
}
|
||||
);
|
||||
// If there is no selected item but there are items, select the first one.
|
||||
if (selectedItemId === null && items.length > 0) {
|
||||
$selectedItemId.set(items[0]?.item_id ?? null);
|
||||
return;
|
||||
}
|
||||
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
|
||||
// the above case, selecting the first item if there are any.
|
||||
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
|
||||
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
|
||||
if (prevIndex >= items.length) {
|
||||
prevIndex = items.length - 1;
|
||||
}
|
||||
const nextItem = items[prevIndex];
|
||||
$selectedItemId.set(nextItem?.item_id ?? null);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the progress data when a queue item is discarded.
|
||||
const unsubCleanUpProgressData = $items.subscribe(async (items) => {
|
||||
@@ -466,7 +438,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
if (lastLoadedItemId === null) {
|
||||
return;
|
||||
}
|
||||
if ($autoSwitch.get() === 'switch_on_finish') {
|
||||
if ($autoSwitch.get()) {
|
||||
$selectedItemId.set(lastLoadedItemId);
|
||||
}
|
||||
$lastLoadedItemId.set(null);
|
||||
@@ -489,17 +461,7 @@ export const CanvasSessionContextProvider = memo(
|
||||
$progressData.set({});
|
||||
$selectedItemId.set(null);
|
||||
};
|
||||
}, [
|
||||
$autoSwitch,
|
||||
$items,
|
||||
$lastLoadedItemId,
|
||||
$lastStartedItemId,
|
||||
$progressData,
|
||||
$selectedItemId,
|
||||
selectQueueItems,
|
||||
session.id,
|
||||
store,
|
||||
]);
|
||||
}, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
|
||||
|
||||
const value = useMemo<CanvasSessionContextValue>(
|
||||
() => ({
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { isImageField } from 'features/nodes/types/common';
|
||||
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { round } from 'lodash-es';
|
||||
import type { S } from 'services/api/types';
|
||||
import { formatProgressMessage } from 'services/events/stores';
|
||||
import { objectEntries } from 'tsafe';
|
||||
|
||||
export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) => {
|
||||
if (!data) {
|
||||
return 'Generating';
|
||||
}
|
||||
return formatProgressMessage(data);
|
||||
|
||||
let message = data.message;
|
||||
if (data.percentage) {
|
||||
message += ` (${round(data.percentage * 100)}%)`;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { isAutoSwitchMode, useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
@@ -10,22 +9,18 @@ export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
|
||||
|
||||
const onChange = useCallback(
|
||||
(val: string | string[]) => {
|
||||
assert(isAutoSwitchMode(val));
|
||||
ctx.$autoSwitch.set(val);
|
||||
ctx.$autoSwitch.set(val === 'on');
|
||||
},
|
||||
[ctx.$autoSwitch]
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuOptionGroup value={autoSwitch} onChange={onChange} title="Auto-Switch" type="radio">
|
||||
<MenuOptionGroup value={autoSwitch ? 'on' : 'off'} onChange={onChange} title="Auto Switch" type="radio">
|
||||
<MenuItemOption value="off" closeOnSelect={false}>
|
||||
Off
|
||||
</MenuItemOption>
|
||||
<MenuItemOption value="switch_on_start" closeOnSelect={false}>
|
||||
Switch on Start
|
||||
</MenuItemOption>
|
||||
<MenuItemOption value="switch_on_finish" closeOnSelect={false}>
|
||||
Switch on Finish
|
||||
<MenuItemOption value="on" closeOnSelect={false}>
|
||||
On
|
||||
</MenuItemOption>
|
||||
</MenuOptionGroup>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { $simpleId } from 'features/ui/components/MainPanelContent';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const StartOverButton = memo(() => {
|
||||
const startOver = useCallback(() => {
|
||||
// dispatch(canvasSessionTypeChanged({ type: 'simple' }));
|
||||
$simpleId.set(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button size="sm" variant="link" alignSelf="stretch" onClick={startOver} px={2}>
|
||||
Start Over
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
StartOverButton.displayName = 'StartOverButton';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||
@@ -29,6 +29,10 @@ export const CanvasToolbar = memo(() => {
|
||||
|
||||
return (
|
||||
<Flex w="full" gap={2} alignItems="center" px={2}>
|
||||
<Heading size="sm" me={2}>
|
||||
Canvas
|
||||
</Heading>
|
||||
<Divider orientation="vertical" />
|
||||
<ToolColorPicker />
|
||||
<ToolSettings />
|
||||
<Flex alignItems="center" h="full" flexGrow={1} justifyContent="flex-end">
|
||||
@@ -44,6 +48,7 @@ export const CanvasToolbar = memo(() => {
|
||||
<CanvasToolbarNewSessionMenuButton />
|
||||
<CanvasSettingsPopover />
|
||||
</Flex>
|
||||
<Divider orientation="vertical" />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
|
||||
@@ -11,21 +11,17 @@ export const CanvasToolbarResetViewButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const fitLayersToStage = useCallback(() => {
|
||||
canvasManager.stage.fitLayersToStage();
|
||||
}, [canvasManager.stage]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitLayersToCanvas',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.fitLayersToStage(),
|
||||
callback: canvasManager.stage.fitLayersToStage,
|
||||
options: { enabled: isCanvasFocused, preventDefault: true },
|
||||
dependencies: [isCanvasFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitBboxToCanvas',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.fitBboxToStage(),
|
||||
callback: canvasManager.stage.fitBboxToStage,
|
||||
options: { enabled: isCanvasFocused, preventDefault: true },
|
||||
dependencies: [isCanvasFocused],
|
||||
});
|
||||
@@ -62,7 +58,7 @@ export const CanvasToolbarResetViewButton = memo(() => {
|
||||
<IconButton
|
||||
tooltip={t('hotkeys.canvas.fitLayersToCanvas.title')}
|
||||
aria-label={t('hotkeys.canvas.fitLayersToCanvas.title')}
|
||||
onClick={fitLayersToStage}
|
||||
onClick={canvasManager.stage.fitLayersToStage}
|
||||
icon={<PiArrowsOutBold />}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
|
||||
@@ -19,7 +19,6 @@ export const CanvasToolbarSaveToGalleryButton = memo(() => {
|
||||
onClick={shift ? saveBboxToGallery : saveCanvasToGallery}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
|
||||
colorScheme="invokeBlue"
|
||||
tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
import { selectActiveTab, selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useCanvasDeleteLayerHotkey() {
|
||||
useAssertSingleton(useCanvasDeleteLayerHotkey.name);
|
||||
@@ -13,18 +13,25 @@ export function useCanvasDeleteLayerHotkey() {
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const canvasRightPanelTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
const appTab = useAppSelector(selectActiveTab);
|
||||
|
||||
const deleteSelectedLayer = useCallback(() => {
|
||||
if (selectedEntityIdentifier === null || isBusy || canvasRightPanelTab !== 'layers') {
|
||||
if (selectedEntityIdentifier === null) {
|
||||
return;
|
||||
}
|
||||
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [canvasRightPanelTab, dispatch, isBusy, selectedEntityIdentifier]);
|
||||
}, [dispatch, selectedEntityIdentifier]);
|
||||
|
||||
const isDeleteEnabled = useMemo(
|
||||
() => selectedEntityIdentifier !== null && !isBusy && canvasRightPanelTab === 'layers' && appTab === 'canvas',
|
||||
[selectedEntityIdentifier, isBusy, canvasRightPanelTab, appTab]
|
||||
);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'deleteSelected',
|
||||
category: 'canvas',
|
||||
callback: deleteSelectedLayer,
|
||||
dependencies: [deleteSelectedLayer],
|
||||
options: { enabled: isDeleteEnabled },
|
||||
dependencies: [isDeleteEnabled, deleteSelectedLayer],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,36 +154,36 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
// If the stage _had_ no size just before this function was called, that means we've just mounted the stage or
|
||||
// maybe un-hidden it. In that case, the user is about to see the stage for the first time, so we should fit the
|
||||
// layers to the stage. If we don't do this, the layers will not be centered.
|
||||
if (this.konva.stage.width() === 0 || this.konva.stage.height() === 0) {
|
||||
// This fit must happen before the stage size is set, else we can end up with a brief flash of an incorrectly
|
||||
// sized and scaled stage.
|
||||
this.fitLayersToStage({ animate: false, targetWidth: containerWidth, targetHeight: containerHeight });
|
||||
}
|
||||
const shouldFitLayersAfterFittingStage = this.konva.stage.width() === 0 || this.konva.stage.height() === 0;
|
||||
|
||||
this.konva.stage.width(containerWidth);
|
||||
this.konva.stage.height(containerHeight);
|
||||
this.syncStageAttrs();
|
||||
|
||||
if (shouldFitLayersAfterFittingStage) {
|
||||
this.fitLayersToStage();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
|
||||
*/
|
||||
fitBboxToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
|
||||
fitBboxToStage = (): void => {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
this.log.trace({ rect }, 'Fitting bbox to stage');
|
||||
this.fitRect(rect, options);
|
||||
this.fitRect(rect);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
|
||||
*/
|
||||
fitLayersToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
|
||||
fitLayersToStage = (): void => {
|
||||
const rect = this.manager.compositor.getVisibleRectOfType();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
this.fitBboxToStage(options);
|
||||
this.fitBboxToStage();
|
||||
} else {
|
||||
this.log.trace({ rect }, 'Fitting layers to stage');
|
||||
this.fitRect(rect, options);
|
||||
this.fitRect(rect);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -191,12 +191,12 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
* Fits the bbox and layers to the stage. The union of the bbox and the visible layers will be centered and scaled
|
||||
* to fit the stage with some padding.
|
||||
*/
|
||||
fitBboxAndLayersToStage = (options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
|
||||
fitBboxAndLayersToStage = (): void => {
|
||||
const layersRect = this.manager.compositor.getVisibleRectOfType();
|
||||
const bboxRect = this.manager.stateApi.getBbox().rect;
|
||||
const unionRect = getRectUnion(layersRect, bboxRect);
|
||||
this.log.trace({ bboxRect, layersRect, unionRect }, 'Fitting bbox and layers to stage');
|
||||
this.fitRect(unionRect, options);
|
||||
this.fitRect(unionRect);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -204,22 +204,16 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
*
|
||||
* The max scale is 1, but the stage can be scaled down to fit the rect.
|
||||
*/
|
||||
fitRect = (rect: Rect, options?: { animate?: boolean; targetWidth?: number; targetHeight?: number }): void => {
|
||||
const size = this.getSize();
|
||||
const { animate, targetWidth, targetHeight } = {
|
||||
animate: true,
|
||||
targetWidth: size.width,
|
||||
targetHeight: size.height,
|
||||
...options,
|
||||
};
|
||||
fitRect = (rect: Rect): void => {
|
||||
const { width, height } = this.getSize();
|
||||
|
||||
// If the stage has no size, we can't fit anything to it
|
||||
if (targetWidth === 0 || targetHeight === 0) {
|
||||
if (width === 0 || height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableWidth = targetWidth - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
|
||||
const availableHeight = targetHeight - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
|
||||
const availableWidth = width - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
|
||||
const availableHeight = height - this.config.FIT_LAYERS_TO_STAGE_PADDING_PX * 2;
|
||||
|
||||
// Make sure we don't accidentally set the scale to something nonsensical, like a negative number, 0 or something
|
||||
// outside the valid range
|
||||
@@ -237,33 +231,23 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
this._intendedScale = scale;
|
||||
this._activeSnapPoint = null;
|
||||
|
||||
if (animate) {
|
||||
const tween = new Konva.Tween({
|
||||
node: this.konva.stage,
|
||||
duration: 0.15,
|
||||
x,
|
||||
y,
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
easing: Konva.Easings.EaseInOut,
|
||||
onUpdate: () => {
|
||||
this.syncStageAttrs();
|
||||
},
|
||||
onFinish: () => {
|
||||
this.syncStageAttrs();
|
||||
tween.destroy();
|
||||
},
|
||||
});
|
||||
tween.play();
|
||||
} else {
|
||||
this.konva.stage.setAttrs({
|
||||
x,
|
||||
y,
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
});
|
||||
this.syncStageAttrs();
|
||||
}
|
||||
const tween = new Konva.Tween({
|
||||
node: this.konva.stage,
|
||||
duration: 0.15,
|
||||
x,
|
||||
y,
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
easing: Konva.Easings.EaseInOut,
|
||||
onUpdate: () => {
|
||||
this.syncStageAttrs();
|
||||
},
|
||||
onFinish: () => {
|
||||
this.syncStageAttrs();
|
||||
tween.destroy();
|
||||
},
|
||||
});
|
||||
tween.play();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import type { ProgressData, ProgressDataMap } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import type { ProgressDataMap } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
@@ -135,8 +135,8 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.$isStaging.set(this.manager.stateApi.runSelector(selectIsStaging));
|
||||
};
|
||||
|
||||
connectToSession = ($selectedItemId: Atom<number | null>, $progressData: ProgressDataMap) => {
|
||||
const cb = (selectedItemId: number | null, progressData: Record<number, ProgressData | undefined>) => {
|
||||
connectToSession = ($selectedItemId: Atom<number | null>, $progressData: ProgressDataMap) =>
|
||||
effect([$selectedItemId, $progressData], (selectedItemId, progressData) => {
|
||||
if (!selectedItemId) {
|
||||
this.$imageSrc.set(null);
|
||||
return;
|
||||
@@ -153,14 +153,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
} else {
|
||||
this.$imageSrc.set(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Run the effect & forcibly render once to initialize
|
||||
cb($selectedItemId.get(), $progressData.get());
|
||||
this.render();
|
||||
|
||||
return effect([$selectedItemId, $progressData], cb);
|
||||
};
|
||||
});
|
||||
|
||||
private _getImageFromSrc = (
|
||||
{ type, data }: ImageNameSrc | DataURLSrc,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const DeleteImageModal = memo(() => {
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
title={`${t('gallery.deleteImage', { count: state.image_names.length })}2`}
|
||||
title={`${t('gallery.deleteImage', { count: state.imageDTOs.length })}2`}
|
||||
isOpen={state.isOpen}
|
||||
onClose={api.close}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
|
||||
@@ -19,16 +19,17 @@ import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from '
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { selectUpscaleSlice, type UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { forEach, intersection, some } from 'lodash-es';
|
||||
import { forEach, intersectionBy, some } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
// Implements an awaitable modal dialog for deleting images
|
||||
|
||||
type DeleteImagesModalState = {
|
||||
image_names: string[];
|
||||
imageDTOs: ImageDTO[];
|
||||
usagePerImage: ImageUsage[];
|
||||
usageSummary: ImageUsage;
|
||||
isOpen: boolean;
|
||||
@@ -37,7 +38,7 @@ type DeleteImagesModalState = {
|
||||
};
|
||||
|
||||
const getInitialState = (): DeleteImagesModalState => ({
|
||||
image_names: [],
|
||||
imageDTOs: [],
|
||||
usagePerImage: [],
|
||||
usageSummary: {
|
||||
isControlLayerImage: false,
|
||||
@@ -53,21 +54,21 @@ const getInitialState = (): DeleteImagesModalState => ({
|
||||
|
||||
const $deleteModalState = atom<DeleteImagesModalState>(getInitialState());
|
||||
|
||||
const deleteImagesWithDialog = async (image_names: string[]): Promise<void> => {
|
||||
const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise<void> => {
|
||||
const { getState, dispatch } = getStore();
|
||||
const imageUsage = getImageUsageFromImageNames(image_names, getState());
|
||||
const imageUsage = getImageUsageFromImageDTOs(imageDTOs, getState());
|
||||
const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState());
|
||||
|
||||
if (!shouldConfirmOnDelete && !isAnyImageInUse(imageUsage)) {
|
||||
// If we don't need to confirm and the images are not in use, delete them directly
|
||||
await handleDeletions(image_names, dispatch, getState);
|
||||
await handleDeletions(imageDTOs, dispatch, getState);
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
$deleteModalState.set({
|
||||
usagePerImage: imageUsage,
|
||||
usageSummary: getImageUsageSummary(imageUsage),
|
||||
image_names,
|
||||
imageDTOs,
|
||||
isOpen: true,
|
||||
resolve,
|
||||
reject,
|
||||
@@ -75,12 +76,12 @@ const deleteImagesWithDialog = async (image_names: string[]): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletions = async (image_names: string[], dispatch: AppDispatch, getState: AppGetState) => {
|
||||
const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, getState: AppGetState) => {
|
||||
try {
|
||||
const state = getState();
|
||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })).unwrap();
|
||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
||||
|
||||
if (intersection(state.gallery.selection, image_names).length > 0) {
|
||||
if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
|
||||
// Some selected images were deleted, need to select the next image
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
@@ -92,11 +93,11 @@ const handleDeletions = async (image_names: string[], dispatch: AppDispatch, get
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
for (const image_name of image_names) {
|
||||
deleteNodesImages(state, dispatch, image_name);
|
||||
deleteControlLayerImages(state, dispatch, image_name);
|
||||
deleteReferenceImages(state, dispatch, image_name);
|
||||
deleteRasterLayerImages(state, dispatch, image_name);
|
||||
for (const imageDTO of imageDTOs) {
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
deleteReferenceImages(state, dispatch, imageDTO);
|
||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
@@ -105,7 +106,7 @@ const handleDeletions = async (image_names: string[], dispatch: AppDispatch, get
|
||||
|
||||
const confirmDeletion = async (dispatch: AppDispatch, getState: AppGetState) => {
|
||||
const state = $deleteModalState.get();
|
||||
await handleDeletions(state.image_names, dispatch, getState);
|
||||
await handleDeletions(state.imageDTOs, dispatch, getState);
|
||||
state.resolve?.();
|
||||
closeSilently();
|
||||
};
|
||||
@@ -141,8 +142,8 @@ export const useDeleteImageModalApi = () => {
|
||||
return api;
|
||||
};
|
||||
|
||||
const getImageUsageFromImageNames = (image_names: string[], state: RootState): ImageUsage[] => {
|
||||
if (image_names.length === 0) {
|
||||
const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): ImageUsage[] => {
|
||||
if (imageDTOs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ const getImageUsageFromImageNames = (image_names: string[], state: RootState): I
|
||||
const upscale = selectUpscaleSlice(state);
|
||||
const refImages = selectRefImagesSlice(state);
|
||||
|
||||
return image_names.map((image_name) => getImageUsage(nodes, canvas, upscale, refImages, image_name));
|
||||
return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, refImages, image_name));
|
||||
};
|
||||
|
||||
const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({
|
||||
@@ -177,7 +178,7 @@ const isAnyImageInUse = (imageUsage: ImageUsage[]): boolean =>
|
||||
);
|
||||
|
||||
// Some utils to delete images from different parts of the app
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name: string) => {
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
const actions: Param0<typeof dispatch>[] = [];
|
||||
state.nodes.present.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
@@ -185,7 +186,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name:
|
||||
}
|
||||
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (isImageFieldInputInstance(input) && input.value?.image_name === image_name) {
|
||||
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
|
||||
actions.push(
|
||||
fieldImageValueChanged({
|
||||
nodeId: node.data.id,
|
||||
@@ -200,7 +201,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name:
|
||||
fieldImageCollectionValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: input.value?.filter((value) => value?.image_name !== image_name),
|
||||
value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -210,11 +211,11 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name:
|
||||
actions.forEach(dispatch);
|
||||
};
|
||||
|
||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => {
|
||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
@@ -225,19 +226,19 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
|
||||
});
|
||||
};
|
||||
|
||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, image_name: string) => {
|
||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectReferenceImageEntities(state).forEach((entity) => {
|
||||
if (entity.config.image?.image_name === image_name) {
|
||||
if (entity.config.image?.image_name === imageDTO.image_name) {
|
||||
dispatch(refImageImageChanged({ id: entity.id, imageDTO: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => {
|
||||
const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } f
|
||||
import { memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string[] }) => {
|
||||
const DndDragPreviewMultipleImage = memo(({ imageDTOs }: { imageDTOs: ImageDTO[] }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex
|
||||
@@ -20,7 +21,7 @@ const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string
|
||||
bg="base.900"
|
||||
borderRadius="base"
|
||||
>
|
||||
<Heading>{image_names.length}</Heading>
|
||||
<Heading>{imageDTOs.length}</Heading>
|
||||
<Heading size="sm">{t('parameters.images')}</Heading>
|
||||
</Flex>
|
||||
);
|
||||
@@ -31,11 +32,11 @@ DndDragPreviewMultipleImage.displayName = 'DndDragPreviewMultipleImage';
|
||||
export type DndDragPreviewMultipleImageState = {
|
||||
type: 'multiple-image';
|
||||
container: HTMLElement;
|
||||
image_names: string[];
|
||||
imageDTOs: ImageDTO[];
|
||||
};
|
||||
|
||||
export const createMultipleImageDragPreview = (arg: DndDragPreviewMultipleImageState) =>
|
||||
createPortal(<DndDragPreviewMultipleImage image_names={arg.image_names} />, arg.container);
|
||||
createPortal(<DndDragPreviewMultipleImage imageDTOs={arg.imageDTOs} />, arg.container);
|
||||
|
||||
type SetMultipleDragPreviewArg = {
|
||||
multipleImageDndData: MultipleImageDndSourceData;
|
||||
@@ -51,7 +52,7 @@ export const setMultipleImageDragPreview = ({
|
||||
const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs;
|
||||
setCustomNativeDragPreview({
|
||||
render({ container }) {
|
||||
setDragPreviewState({ type: 'multiple-image', container, image_names: multipleImageDndData.payload.image_names });
|
||||
setDragPreviewState({ type: 'multiple-image', container, imageDTOs: multipleImageDndData.payload.imageDTOs });
|
||||
return () => setDragPreviewState(null);
|
||||
},
|
||||
nativeSetDragImage,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreview
|
||||
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@@ -47,6 +48,9 @@ export const DndImage = memo(
|
||||
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
if ($imageViewer.get()) {
|
||||
$imageViewer.set(false);
|
||||
}
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/exter
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $focusedRegion } from 'common/hooks/focus';
|
||||
@@ -11,6 +12,7 @@ import { useClientSideUpload } from 'common/hooks/useClientSideUpload';
|
||||
import { setFileToPaste } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -68,6 +70,7 @@ export const FullscreenDropzone = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndState, setDndState] = useState<DndTargetState>('idle');
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
const isImageViewerOpen = useStore($imageViewer);
|
||||
const isClientSideUploadEnabled = useAppSelector(selectIsClientSideUploadEnabled);
|
||||
const clientSideUpload = useClientSideUpload();
|
||||
|
||||
@@ -93,7 +96,13 @@ export const FullscreenDropzone = memo(() => {
|
||||
// While on the canvas tab and when pasting a single image, canvas may want to create a new layer. Let it handle
|
||||
// the paste event.
|
||||
const [firstImageFile] = files;
|
||||
if (focusedRegion === 'canvas' && activeTab === 'canvas' && files.length === 1 && firstImageFile) {
|
||||
if (
|
||||
focusedRegion === 'canvas' &&
|
||||
!isImageViewerOpen &&
|
||||
activeTab === 'canvas' &&
|
||||
files.length === 1 &&
|
||||
firstImageFile
|
||||
) {
|
||||
setFileToPaste(firstImageFile);
|
||||
return;
|
||||
}
|
||||
@@ -116,7 +125,7 @@ export const FullscreenDropzone = memo(() => {
|
||||
uploadImages(uploadArgs);
|
||||
}
|
||||
},
|
||||
[activeTab, t, isClientSideUploadEnabled, clientSideUpload]
|
||||
[activeTab, isImageViewerOpen, t, isClientSideUploadEnabled, clientSideUpload]
|
||||
);
|
||||
|
||||
const onPaste = useCallback(
|
||||
|
||||
@@ -87,7 +87,7 @@ const _multipleImage = buildTypeAndKey('multiple-image');
|
||||
export type MultipleImageDndSourceData = DndData<
|
||||
typeof _multipleImage.type,
|
||||
typeof _multipleImage.key,
|
||||
{ image_names: string[]; board_id: BoardId }
|
||||
{ imageDTOs: ImageDTO[]; boardId: BoardId }
|
||||
>;
|
||||
export const multipleImageDndSource: DndSource<MultipleImageDndSourceData> = {
|
||||
..._multipleImage,
|
||||
@@ -305,7 +305,7 @@ export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget<
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
newValue.push({ image_name: sourceData.payload.imageDTO.image_name });
|
||||
} else {
|
||||
newValue.push(...sourceData.payload.image_names.map((image_name) => ({ image_name })));
|
||||
newValue.push(...sourceData.payload.imageDTOs.map(({ image_name }) => ({ image_name })));
|
||||
}
|
||||
|
||||
dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: newValue }));
|
||||
@@ -330,17 +330,17 @@ export const setComparisonImageDndTarget: DndTarget<SetComparisonImageDndTargetD
|
||||
}
|
||||
const { firstImage, secondImage } = selectComparisonImages(getState());
|
||||
// Do not allow the same images to be selected for comparison
|
||||
if (sourceData.payload.imageDTO.image_name === firstImage) {
|
||||
if (sourceData.payload.imageDTO.image_name === firstImage?.image_name) {
|
||||
return false;
|
||||
}
|
||||
if (sourceData.payload.imageDTO.image_name === secondImage) {
|
||||
if (sourceData.payload.imageDTO.image_name === secondImage?.image_name) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: ({ sourceData, dispatch }) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
setComparisonImage({ image_name: imageDTO.image_name, dispatch });
|
||||
setComparisonImage({ imageDTO, dispatch });
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
@@ -450,7 +450,7 @@ export const addImageToBoardDndTarget: DndTarget<
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
if (multipleImageDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.board_id;
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
@@ -460,13 +460,13 @@ export const addImageToBoardDndTarget: DndTarget<
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
addImagesToBoard({ image_names: [imageDTO.image_name], boardId, dispatch });
|
||||
addImagesToBoard({ imageDTOs: [imageDTO], boardId, dispatch });
|
||||
}
|
||||
|
||||
if (multipleImageDndSource.typeGuard(sourceData)) {
|
||||
const { image_names } = sourceData.payload;
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
addImagesToBoard({ image_names, boardId, dispatch });
|
||||
addImagesToBoard({ imageDTOs, boardId, dispatch });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -494,7 +494,7 @@ export const removeImageFromBoardDndTarget: DndTarget<
|
||||
}
|
||||
|
||||
if (multipleImageDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.board_id;
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
@@ -503,12 +503,12 @@ export const removeImageFromBoardDndTarget: DndTarget<
|
||||
handler: ({ sourceData, dispatch }) => {
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
removeImagesFromBoard({ image_names: [imageDTO.image_name], dispatch });
|
||||
removeImagesFromBoard({ imageDTOs: [imageDTO], dispatch });
|
||||
}
|
||||
|
||||
if (multipleImageDndSource.typeGuard(sourceData)) {
|
||||
const { image_names } = sourceData.payload;
|
||||
removeImagesFromBoard({ image_names, dispatch });
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
removeImagesFromBoard({ imageDTOs, dispatch });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildUseDisclosure } from 'common/hooks/useBoolean';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
@@ -8,8 +7,6 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
export const [useBoardSearchDisclosure, $boardSearchIsOpen] = buildUseDisclosure(false);
|
||||
|
||||
export const BoardsSearch = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
|
||||
@@ -91,7 +91,7 @@ const DeleteBoardModal = () => {
|
||||
if (!boardToDelete || boardToDelete === 'none') {
|
||||
return;
|
||||
}
|
||||
deleteBoardOnly({ board_id: boardToDelete.board_id });
|
||||
deleteBoardOnly(boardToDelete.board_id);
|
||||
$boardToDelete.set(null);
|
||||
}, [boardToDelete, deleteBoardOnly]);
|
||||
|
||||
@@ -99,7 +99,7 @@ const DeleteBoardModal = () => {
|
||||
if (!boardToDelete || boardToDelete === 'none') {
|
||||
return;
|
||||
}
|
||||
deleteBoardAndImages({ board_id: boardToDelete.board_id });
|
||||
deleteBoardAndImages(boardToDelete.board_id);
|
||||
$boardToDelete.set(null);
|
||||
}, [boardToDelete, deleteBoardAndImages]);
|
||||
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
|
||||
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
|
||||
import { $boardSearchIsOpen, BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
|
||||
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
|
||||
import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
|
||||
export const BoardsPanel = memo(() => {
|
||||
const boardSearchDisclosure = useStore($boardSearchIsOpen);
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" p={2}>
|
||||
<GalleryTopBar />
|
||||
<Collapse in={boardSearchDisclosure} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<BoardsSearch />
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Divider pt={2} />
|
||||
<BoardsListWrapper />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
BoardsPanel.displayName = 'BoardsPanel';
|
||||
export const BoardsListPanelContent = memo(
|
||||
({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full">
|
||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<BoardsSearch />
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Divider pt={2} />
|
||||
<BoardsListWrapper />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
BoardsListPanelContent.displayName = 'BoardsListPanelContent';
|
||||
|
||||
@@ -25,8 +25,9 @@ import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import { GalleryUploadButton } from './GalleryUploadButton';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
import { GallerySearch } from './ImageGrid/GallerySearch';
|
||||
import { NewGallery } from './NewGallery';
|
||||
|
||||
const BASE_STYLES: ChakraProps['sx'] = {
|
||||
fontWeight: 'semibold',
|
||||
@@ -45,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10
|
||||
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
|
||||
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
|
||||
export const GalleryPanel = memo(() => {
|
||||
export const Gallery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const galleryView = useAppSelector(selectGalleryView);
|
||||
@@ -69,8 +70,8 @@ export const GalleryPanel = memo(() => {
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" p={2} minH={0}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full" pb={2}>
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1} minH={0}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
||||
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
|
||||
{boardName}
|
||||
@@ -89,7 +90,6 @@ export const GalleryPanel = memo(() => {
|
||||
<Flex h="full" justifyContent="flex-end">
|
||||
<GalleryUploadButton />
|
||||
<GallerySettingsPopover />
|
||||
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
@@ -101,21 +101,20 @@ export const GalleryPanel = memo(() => {
|
||||
/>
|
||||
</Flex>
|
||||
</TabList>
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Tabs>
|
||||
|
||||
{/* <GalleryImageGrid />
|
||||
<GalleryPagination /> */}
|
||||
<NewGallery />
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
<GalleryImageGrid />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GalleryPanel.displayName = 'Gallery';
|
||||
Gallery.displayName = 'Gallery';
|
||||
|
||||
@@ -1,51 +1,67 @@
|
||||
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
|
||||
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useBoardSearchDisclosure } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
|
||||
import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
|
||||
export const GalleryTopBar = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const boardSearchDisclosure = useBoardSearchDisclosure();
|
||||
export const GalleryTopBar = memo(
|
||||
({
|
||||
boardsListPanel,
|
||||
boardSearchDisclosure,
|
||||
}: {
|
||||
boardsListPanel: UsePanelReturn;
|
||||
boardSearchDisclosure: UseDisclosureReturn;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
|
||||
const onClickBoardSearch = useCallback(() => {
|
||||
if (boardSearchText.length) {
|
||||
dispatch(boardSearchTextChanged(''));
|
||||
}
|
||||
boardSearchDisclosure.toggle();
|
||||
}, [boardSearchText.length, boardSearchDisclosure, dispatch]);
|
||||
const onClickBoardSearch = useCallback(() => {
|
||||
if (boardSearchText.length) {
|
||||
dispatch(boardSearchTextChanged(''));
|
||||
}
|
||||
boardSearchDisclosure.onToggle();
|
||||
boardsListPanel.expand();
|
||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Flex flexGrow={1} flexBasis={0}>
|
||||
<Text>Boards</Text>
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Flex flexGrow={1} flexBasis={0}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={boardsListPanel.toggle}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<GalleryHeader />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
|
||||
<BoardsSettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={onClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<GalleryHeader />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
|
||||
<BoardsSettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={onClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
GalleryTopBar.displayName = 'GalleryTopBar';
|
||||
|
||||
@@ -12,7 +12,7 @@ export const ImageMenuItemChangeBoard = memo(() => {
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imagesToChangeSelected([imageDTO.image_name]));
|
||||
dispatch(imagesToChangeSelected([imageDTO]));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export const ImageMenuItemDelete = memo(() => {
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
try {
|
||||
await deleteImageModal.delete([imageDTO.image_name]);
|
||||
await deleteImageModal.delete([imageDTO]);
|
||||
} catch {
|
||||
// noop;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -15,51 +16,56 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
|
||||
@@ -17,6 +18,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewRasterLayerFromImage = useCallback(() => {
|
||||
@@ -24,60 +26,65 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewControlLayerFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewInpaintMaskFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'inpaint_mask', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
const onClickNewRegionalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('canvas'));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
// TODO: figure out how to select the closest image viewer...
|
||||
}, [dispatch, imageDTO]);
|
||||
imageViewer.openImageInViewer(imageDTO);
|
||||
}, [imageDTO, imageViewer]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
|
||||
@@ -12,13 +12,13 @@ export const ImageMenuItemSelectForCompare = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const selectMaySelectForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare !== imageDTO.image_name),
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(imageDTO.image_name));
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,13 +16,13 @@ export const ImageMenuItemStarUnstar = memo(() => {
|
||||
|
||||
const starImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
starImages({ image_names: [imageDTO.image_name] });
|
||||
starImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [starImages, imageDTO]);
|
||||
|
||||
const unstarImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
unstarImages({ image_names: [imageDTO.image_name] });
|
||||
unstarImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [unstarImages, imageDTO]);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -13,18 +14,20 @@ export const ImageMenuItemUseAsRefImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
imageViewer.close();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
}, [imageDTO, imageViewer, store, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiImageBold />} onClickCapture={onClickNewGlobalReferenceImageFromImage}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import {
|
||||
@@ -37,25 +37,37 @@ const MultipleSelectionMenuItems = () => {
|
||||
}, [deleteImageModal, selection]);
|
||||
|
||||
const handleStarSelection = useCallback(() => {
|
||||
starImages({ image_names: selection });
|
||||
starImages({ imageDTOs: selection });
|
||||
}, [starImages, selection]);
|
||||
|
||||
const handleUnstarSelection = useCallback(() => {
|
||||
unstarImages({ image_names: selection });
|
||||
unstarImages({ imageDTOs: selection });
|
||||
}, [unstarImages, selection]);
|
||||
|
||||
const handleBulkDownload = useCallback(() => {
|
||||
bulkDownload({ image_names: selection });
|
||||
bulkDownload({ image_names: selection.map((img) => img.image_name) });
|
||||
}, [selection, bulkDownload]);
|
||||
|
||||
const areAllStarred = useMemo(() => {
|
||||
return selection.every((img) => img.starred);
|
||||
}, [selection]);
|
||||
|
||||
const areAllUnstarred = useMemo(() => {
|
||||
return selection.every((img) => !img.starred);
|
||||
}, [selection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleUnstarSelection}>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
</MenuItem>
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarFill />} onClickCapture={handleStarSelection}>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</MenuItem>
|
||||
{areAllStarred && (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleUnstarSelection}>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarFill />} onClickCapture={handleStarSelection}>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isBulkDownloadEnabled && (
|
||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleBulkDownload}>
|
||||
{t('gallery.downloadSelection')}
|
||||
|
||||
@@ -15,9 +15,8 @@ import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -85,7 +84,6 @@ interface Props {
|
||||
|
||||
export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
const store = useAppStore();
|
||||
const autoLayoutContext = useAutoLayoutContext();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPreviewState, setDragPreviewState] = useState<
|
||||
DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null
|
||||
@@ -93,7 +91,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
const ref = useRef<HTMLImageElement>(null);
|
||||
const dndId = useId();
|
||||
const selectIsSelectedForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare === imageDTO.image_name),
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const isSelectedForCompare = useAppSelector(selectIsSelectedForCompare);
|
||||
@@ -101,7 +99,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
() =>
|
||||
createSelector(selectGallerySlice, (gallery) => {
|
||||
for (const selectedImage of gallery.selection) {
|
||||
if (selectedImage === imageDTO.image_name) {
|
||||
if (selectedImage.image_name === imageDTO.image_name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -126,11 +124,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
// multi-image drag.
|
||||
if (
|
||||
gallery.selection.length > 1 &&
|
||||
gallery.selection.find((image_name) => image_name === imageDTO.image_name) !== undefined
|
||||
gallery.selection.find(({ image_name }) => image_name === imageDTO.image_name) !== undefined
|
||||
) {
|
||||
return multipleImageDndSource.getData({
|
||||
image_names: gallery.selection,
|
||||
board_id: gallery.selectedBoardId,
|
||||
imageDTOs: gallery.selection,
|
||||
boardId: gallery.selectedBoardId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,10 +165,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
onDragStart: ({ source }) => {
|
||||
// When we start dragging multiple images, set the dragging state to true if the dragged image is part of the
|
||||
// selection. This is called for all drag events.
|
||||
if (
|
||||
multipleImageDndSource.typeGuard(source.data) &&
|
||||
source.data.payload.image_names.includes(imageDTO.image_name)
|
||||
) {
|
||||
if (multipleImageDndSource.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
},
|
||||
@@ -196,7 +191,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
(e) => {
|
||||
store.dispatch(
|
||||
galleryImageClicked({
|
||||
imageName: imageDTO.image_name,
|
||||
imageDTO,
|
||||
shiftKey: e.shiftKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
@@ -208,9 +203,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
|
||||
// Use the atom here directly instead of the `useImageViewer` to avoid re-rendering the gallery when the viewer
|
||||
// opened state changes.
|
||||
$imageViewer.set(true);
|
||||
store.dispatch(imageToCompareChanged(null));
|
||||
autoLayoutContext.focusPanel(VIEWER_PANEL_ID);
|
||||
}, [autoLayoutContext, store]);
|
||||
}, [store]);
|
||||
|
||||
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user