mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into refactor/model_manager_instantiate
This commit is contained in:
commit
91ebf9f76e
1
.github/workflows/style-checks.yml
vendored
1
.github/workflows/style-checks.yml
vendored
@ -6,7 +6,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: main
|
branches: main
|
||||||
tags: "*"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
black:
|
black:
|
||||||
|
50
.github/workflows/test-invoke-pip-skip.yml
vendored
50
.github/workflows/test-invoke-pip-skip.yml
vendored
@ -1,50 +0,0 @@
|
|||||||
name: Test invoke.py pip
|
|
||||||
|
|
||||||
# This is a dummy stand-in for the actual tests
|
|
||||||
# we don't need to run python tests on non-Python changes
|
|
||||||
# But PRs require passing tests to be mergeable
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**'
|
|
||||||
- '!pyproject.toml'
|
|
||||||
- '!invokeai/**'
|
|
||||||
- '!tests/**'
|
|
||||||
- 'invokeai/frontend/web/**'
|
|
||||||
merge_group:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
matrix:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version:
|
|
||||||
- '3.10'
|
|
||||||
pytorch:
|
|
||||||
- linux-cuda-11_7
|
|
||||||
- linux-rocm-5_2
|
|
||||||
- linux-cpu
|
|
||||||
- macos-default
|
|
||||||
- windows-cpu
|
|
||||||
include:
|
|
||||||
- pytorch: linux-cuda-11_7
|
|
||||||
os: ubuntu-22.04
|
|
||||||
- pytorch: linux-rocm-5_2
|
|
||||||
os: ubuntu-22.04
|
|
||||||
- pytorch: linux-cpu
|
|
||||||
os: ubuntu-22.04
|
|
||||||
- pytorch: macos-default
|
|
||||||
os: macOS-12
|
|
||||||
- pytorch: windows-cpu
|
|
||||||
os: windows-2022
|
|
||||||
name: ${{ matrix.pytorch }} on ${{ matrix.python-version }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: skip
|
|
||||||
run: echo "no build required"
|
|
24
.github/workflows/test-invoke-pip.yml
vendored
24
.github/workflows/test-invoke-pip.yml
vendored
@ -3,16 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
paths:
|
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'invokeai/**'
|
|
||||||
- '!invokeai/frontend/web/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'invokeai/**'
|
|
||||||
- 'tests/**'
|
|
||||||
- '!invokeai/frontend/web/**'
|
|
||||||
types:
|
types:
|
||||||
- 'ready_for_review'
|
- 'ready_for_review'
|
||||||
- 'opened'
|
- 'opened'
|
||||||
@ -65,10 +56,23 @@ jobs:
|
|||||||
id: checkout-sources
|
id: checkout-sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Check for changed python files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v37
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
python:
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'invokeai/**'
|
||||||
|
- '!invokeai/frontend/web/**'
|
||||||
|
- 'tests/**'
|
||||||
|
|
||||||
- name: set test prompt to main branch validation
|
- name: set test prompt to main branch validation
|
||||||
|
if: steps.changed-files.outputs.python_any_changed == 'true'
|
||||||
run: echo "TEST_PROMPTS=tests/validate_pr_prompt.txt" >> ${{ matrix.github-env }}
|
run: echo "TEST_PROMPTS=tests/validate_pr_prompt.txt" >> ${{ matrix.github-env }}
|
||||||
|
|
||||||
- name: setup python
|
- name: setup python
|
||||||
|
if: steps.changed-files.outputs.python_any_changed == 'true'
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -76,6 +80,7 @@ jobs:
|
|||||||
cache-dependency-path: pyproject.toml
|
cache-dependency-path: pyproject.toml
|
||||||
|
|
||||||
- name: install invokeai
|
- name: install invokeai
|
||||||
|
if: steps.changed-files.outputs.python_any_changed == 'true'
|
||||||
env:
|
env:
|
||||||
PIP_EXTRA_INDEX_URL: ${{ matrix.extra-index-url }}
|
PIP_EXTRA_INDEX_URL: ${{ matrix.extra-index-url }}
|
||||||
run: >
|
run: >
|
||||||
@ -83,6 +88,7 @@ jobs:
|
|||||||
--editable=".[test]"
|
--editable=".[test]"
|
||||||
|
|
||||||
- name: run pytest
|
- name: run pytest
|
||||||
|
if: steps.changed-files.outputs.python_any_changed == 'true'
|
||||||
id: run-pytest
|
id: run-pytest
|
||||||
run: pytest
|
run: pytest
|
||||||
|
|
||||||
|
@ -1,24 +1,30 @@
|
|||||||
from fastapi import Body, HTTPException, Path, Query
|
from fastapi import Body, HTTPException
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from invokeai.app.services.board_record_storage import BoardRecord, BoardChanges
|
from pydantic import BaseModel, Field
|
||||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
|
||||||
from invokeai.app.services.models.board_record import BoardDTO
|
|
||||||
from invokeai.app.services.models.image_record import ImageDTO
|
|
||||||
|
|
||||||
from ..dependencies import ApiDependencies
|
from ..dependencies import ApiDependencies
|
||||||
|
|
||||||
board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
|
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(
|
@board_images_router.post(
|
||||||
"/",
|
"/",
|
||||||
operation_id="create_board_image",
|
operation_id="add_image_to_board",
|
||||||
responses={
|
responses={
|
||||||
201: {"description": "The image was added to a board successfully"},
|
201: {"description": "The image was added to a board successfully"},
|
||||||
},
|
},
|
||||||
status_code=201,
|
status_code=201,
|
||||||
)
|
)
|
||||||
async def create_board_image(
|
async def add_image_to_board(
|
||||||
board_id: str = Body(description="The id of the board to add to"),
|
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"),
|
image_name: str = Body(description="The name of the image to add"),
|
||||||
):
|
):
|
||||||
@ -29,26 +35,78 @@ async def create_board_image(
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail="Failed to add to board")
|
raise HTTPException(status_code=500, detail="Failed to add image to board")
|
||||||
|
|
||||||
|
|
||||||
@board_images_router.delete(
|
@board_images_router.delete(
|
||||||
"/",
|
"/",
|
||||||
operation_id="remove_board_image",
|
operation_id="remove_image_from_board",
|
||||||
responses={
|
responses={
|
||||||
201: {"description": "The image was removed from the board successfully"},
|
201: {"description": "The image was removed from the board successfully"},
|
||||||
},
|
},
|
||||||
status_code=201,
|
status_code=201,
|
||||||
)
|
)
|
||||||
async def remove_board_image(
|
async def remove_image_from_board(
|
||||||
board_id: str = Body(description="The id of the board"),
|
image_name: str = Body(description="The name of the image to remove", embed=True),
|
||||||
image_name: str = Body(description="The name of the image to remove"),
|
|
||||||
):
|
):
|
||||||
"""Deletes a board_image"""
|
"""Removes an image from its board, if it had one"""
|
||||||
try:
|
try:
|
||||||
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(
|
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
|
||||||
board_id=board_id, image_name=image_name
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail="Failed to update board")
|
raise HTTPException(status_code=500, detail="Failed to remove image from board")
|
||||||
|
|
||||||
|
|
||||||
|
@board_images_router.post(
|
||||||
|
"/batch",
|
||||||
|
operation_id="add_images_to_board",
|
||||||
|
responses={
|
||||||
|
201: {"description": "Images were added to board successfully"},
|
||||||
|
},
|
||||||
|
status_code=201,
|
||||||
|
response_model=AddImagesToBoardResult,
|
||||||
|
)
|
||||||
|
async def add_images_to_board(
|
||||||
|
board_id: str = Body(description="The id of the board to add to"),
|
||||||
|
image_names: list[str] = Body(description="The names of the images to add", embed=True),
|
||||||
|
) -> AddImagesToBoardResult:
|
||||||
|
"""Adds a list of images to a board"""
|
||||||
|
try:
|
||||||
|
added_image_names: list[str] = []
|
||||||
|
for image_name in image_names:
|
||||||
|
try:
|
||||||
|
ApiDependencies.invoker.services.board_images.add_image_to_board(
|
||||||
|
board_id=board_id, image_name=image_name
|
||||||
|
)
|
||||||
|
added_image_names.append(image_name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return AddImagesToBoardResult(board_id=board_id, added_image_names=added_image_names)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to add images to board")
|
||||||
|
|
||||||
|
|
||||||
|
@board_images_router.post(
|
||||||
|
"/batch/delete",
|
||||||
|
operation_id="remove_images_from_board",
|
||||||
|
responses={
|
||||||
|
201: {"description": "Images were removed from board successfully"},
|
||||||
|
},
|
||||||
|
status_code=201,
|
||||||
|
response_model=RemoveImagesFromBoardResult,
|
||||||
|
)
|
||||||
|
async def remove_images_from_board(
|
||||||
|
image_names: list[str] = Body(description="The names of the images to remove", embed=True),
|
||||||
|
) -> RemoveImagesFromBoardResult:
|
||||||
|
"""Removes a list of images from their board, if they had one"""
|
||||||
|
try:
|
||||||
|
removed_image_names: list[str] = []
|
||||||
|
for image_name in image_names:
|
||||||
|
try:
|
||||||
|
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
|
||||||
|
removed_image_names.append(image_name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return RemoveImagesFromBoardResult(removed_image_names=removed_image_names)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to remove images from board")
|
||||||
|
@ -5,6 +5,7 @@ from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadF
|
|||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from invokeai.app.invocations.metadata import ImageMetadata
|
from invokeai.app.invocations.metadata import ImageMetadata
|
||||||
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
||||||
@ -25,7 +26,7 @@ IMAGE_MAX_AGE = 31536000
|
|||||||
|
|
||||||
|
|
||||||
@images_router.post(
|
@images_router.post(
|
||||||
"/",
|
"/upload",
|
||||||
operation_id="upload_image",
|
operation_id="upload_image",
|
||||||
responses={
|
responses={
|
||||||
201: {"description": "The image was uploaded successfully"},
|
201: {"description": "The image was uploaded successfully"},
|
||||||
@ -77,7 +78,7 @@ async def upload_image(
|
|||||||
raise HTTPException(status_code=500, detail="Failed to create image")
|
raise HTTPException(status_code=500, detail="Failed to create image")
|
||||||
|
|
||||||
|
|
||||||
@images_router.delete("/{image_name}", operation_id="delete_image")
|
@images_router.delete("/i/{image_name}", operation_id="delete_image")
|
||||||
async def delete_image(
|
async def delete_image(
|
||||||
image_name: str = Path(description="The name of the image to delete"),
|
image_name: str = Path(description="The name of the image to delete"),
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -103,7 +104,7 @@ async def clear_intermediates() -> int:
|
|||||||
|
|
||||||
|
|
||||||
@images_router.patch(
|
@images_router.patch(
|
||||||
"/{image_name}",
|
"/i/{image_name}",
|
||||||
operation_id="update_image",
|
operation_id="update_image",
|
||||||
response_model=ImageDTO,
|
response_model=ImageDTO,
|
||||||
)
|
)
|
||||||
@ -120,7 +121,7 @@ async def update_image(
|
|||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/{image_name}",
|
"/i/{image_name}",
|
||||||
operation_id="get_image_dto",
|
operation_id="get_image_dto",
|
||||||
response_model=ImageDTO,
|
response_model=ImageDTO,
|
||||||
)
|
)
|
||||||
@ -136,7 +137,7 @@ async def get_image_dto(
|
|||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/{image_name}/metadata",
|
"/i/{image_name}/metadata",
|
||||||
operation_id="get_image_metadata",
|
operation_id="get_image_metadata",
|
||||||
response_model=ImageMetadata,
|
response_model=ImageMetadata,
|
||||||
)
|
)
|
||||||
@ -152,7 +153,7 @@ async def get_image_metadata(
|
|||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/{image_name}/full",
|
"/i/{image_name}/full",
|
||||||
operation_id="get_image_full",
|
operation_id="get_image_full",
|
||||||
response_class=Response,
|
response_class=Response,
|
||||||
responses={
|
responses={
|
||||||
@ -187,7 +188,7 @@ async def get_image_full(
|
|||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/{image_name}/thumbnail",
|
"/i/{image_name}/thumbnail",
|
||||||
operation_id="get_image_thumbnail",
|
operation_id="get_image_thumbnail",
|
||||||
response_class=Response,
|
response_class=Response,
|
||||||
responses={
|
responses={
|
||||||
@ -216,7 +217,7 @@ async def get_image_thumbnail(
|
|||||||
|
|
||||||
|
|
||||||
@images_router.get(
|
@images_router.get(
|
||||||
"/{image_name}/urls",
|
"/i/{image_name}/urls",
|
||||||
operation_id="get_image_urls",
|
operation_id="get_image_urls",
|
||||||
response_model=ImageUrlsDTO,
|
response_model=ImageUrlsDTO,
|
||||||
)
|
)
|
||||||
@ -265,3 +266,24 @@ async def list_image_dtos(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return image_dtos
|
return image_dtos
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
) -> DeleteImagesFromListResult:
|
||||||
|
try:
|
||||||
|
deleted_images: list[str] = []
|
||||||
|
for image_name in image_names:
|
||||||
|
try:
|
||||||
|
ApiDependencies.invoker.services.images.delete(image_name)
|
||||||
|
deleted_images.append(image_name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return DeleteImagesFromListResult(deleted_images=deleted_images)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to delete images")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from typing import Literal, Optional, Union
|
from typing import Literal, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import Field
|
||||||
|
|
||||||
from invokeai.app.invocations.baseinvocation import (
|
from invokeai.app.invocations.baseinvocation import (
|
||||||
BaseInvocation,
|
BaseInvocation,
|
||||||
@ -10,16 +10,17 @@ from invokeai.app.invocations.baseinvocation import (
|
|||||||
)
|
)
|
||||||
from invokeai.app.invocations.controlnet_image_processors import ControlField
|
from invokeai.app.invocations.controlnet_image_processors import ControlField
|
||||||
from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField
|
from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField
|
||||||
|
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||||
|
|
||||||
|
|
||||||
class LoRAMetadataField(BaseModel):
|
class LoRAMetadataField(BaseModelExcludeNull):
|
||||||
"""LoRA metadata for an image generated in InvokeAI."""
|
"""LoRA metadata for an image generated in InvokeAI."""
|
||||||
|
|
||||||
lora: LoRAModelField = Field(description="The LoRA model")
|
lora: LoRAModelField = Field(description="The LoRA model")
|
||||||
weight: float = Field(description="The weight of the LoRA model")
|
weight: float = Field(description="The weight of the LoRA model")
|
||||||
|
|
||||||
|
|
||||||
class CoreMetadata(BaseModel):
|
class CoreMetadata(BaseModelExcludeNull):
|
||||||
"""Core generation metadata for an image generated in InvokeAI."""
|
"""Core generation metadata for an image generated in InvokeAI."""
|
||||||
|
|
||||||
generation_mode: str = Field(
|
generation_mode: str = Field(
|
||||||
@ -70,7 +71,7 @@ class CoreMetadata(BaseModel):
|
|||||||
refiner_start: Union[float, None] = Field(default=None, description="The start value used for refiner denoising")
|
refiner_start: Union[float, None] = Field(default=None, description="The start value used for refiner denoising")
|
||||||
|
|
||||||
|
|
||||||
class ImageMetadata(BaseModel):
|
class ImageMetadata(BaseModelExcludeNull):
|
||||||
"""An image's generation metadata"""
|
"""An image's generation metadata"""
|
||||||
|
|
||||||
metadata: Optional[dict] = Field(
|
metadata: Optional[dict] = Field(
|
||||||
|
@ -25,7 +25,6 @@ class BoardImageRecordStorageBase(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def remove_image_from_board(
|
def remove_image_from_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
|
||||||
image_name: str,
|
image_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Removes an image from a board."""
|
"""Removes an image from a board."""
|
||||||
@ -154,7 +153,6 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
|||||||
|
|
||||||
def remove_image_from_board(
|
def remove_image_from_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
|
||||||
image_name: str,
|
image_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
@ -162,9 +160,9 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
|||||||
self._cursor.execute(
|
self._cursor.execute(
|
||||||
"""--sql
|
"""--sql
|
||||||
DELETE FROM board_images
|
DELETE FROM board_images
|
||||||
WHERE board_id = ? AND image_name = ?;
|
WHERE image_name = ?;
|
||||||
""",
|
""",
|
||||||
(board_id, image_name),
|
(image_name,),
|
||||||
)
|
)
|
||||||
self._conn.commit()
|
self._conn.commit()
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
|
@ -31,7 +31,6 @@ class BoardImagesServiceABC(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def remove_image_from_board(
|
def remove_image_from_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
|
||||||
image_name: str,
|
image_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Removes an image from a board."""
|
"""Removes an image from a board."""
|
||||||
@ -93,10 +92,9 @@ class BoardImagesService(BoardImagesServiceABC):
|
|||||||
|
|
||||||
def remove_image_from_board(
|
def remove_image_from_board(
|
||||||
self,
|
self,
|
||||||
board_id: str,
|
|
||||||
image_name: str,
|
image_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._services.board_image_records.remove_image_from_board(board_id, image_name)
|
self._services.board_image_records.remove_image_from_board(image_name)
|
||||||
|
|
||||||
def get_all_board_image_names_for_board(
|
def get_all_board_image_names_for_board(
|
||||||
self,
|
self,
|
||||||
|
@ -289,9 +289,10 @@ class ImageService(ImageServiceABC):
|
|||||||
def get_metadata(self, image_name: str) -> Optional[ImageMetadata]:
|
def get_metadata(self, image_name: str) -> Optional[ImageMetadata]:
|
||||||
try:
|
try:
|
||||||
image_record = self._services.image_records.get(image_name)
|
image_record = self._services.image_records.get(image_name)
|
||||||
|
metadata = self._services.image_records.get_metadata(image_name)
|
||||||
|
|
||||||
if not image_record.session_id:
|
if not image_record.session_id:
|
||||||
return ImageMetadata()
|
return ImageMetadata(metadata=metadata)
|
||||||
|
|
||||||
session_raw = self._services.graph_execution_manager.get_raw(image_record.session_id)
|
session_raw = self._services.graph_execution_manager.get_raw(image_record.session_id)
|
||||||
graph = None
|
graph = None
|
||||||
@ -303,7 +304,6 @@ class ImageService(ImageServiceABC):
|
|||||||
self._services.logger.warn(f"Failed to parse session graph: {e}")
|
self._services.logger.warn(f"Failed to parse session graph: {e}")
|
||||||
graph = None
|
graph = None
|
||||||
|
|
||||||
metadata = self._services.image_records.get_metadata(image_name)
|
|
||||||
return ImageMetadata(graph=graph, metadata=metadata)
|
return ImageMetadata(graph=graph, metadata=metadata)
|
||||||
except ImageRecordNotFoundException:
|
except ImageRecordNotFoundException:
|
||||||
self._services.logger.error("Image record not found")
|
self._services.logger.error("Image record not found")
|
||||||
|
8
invokeai/app/services/models/board_image.py
Normal file
8
invokeai/app/services/models/board_image.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||||
|
|
||||||
|
|
||||||
|
class BoardImage(BaseModelExcludeNull):
|
||||||
|
board_id: str = Field(description="The id of the board")
|
||||||
|
image_name: str = Field(description="The name of the image")
|
@ -1,10 +1,11 @@
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, Extra, Field, StrictBool, StrictStr
|
from pydantic import Field
|
||||||
from invokeai.app.util.misc import get_iso_timestamp
|
from invokeai.app.util.misc import get_iso_timestamp
|
||||||
|
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||||
|
|
||||||
|
|
||||||
class BoardRecord(BaseModel):
|
class BoardRecord(BaseModelExcludeNull):
|
||||||
"""Deserialized board record."""
|
"""Deserialized board record."""
|
||||||
|
|
||||||
board_id: str = Field(description="The unique ID of the board.")
|
board_id: str = Field(description="The unique ID of the board.")
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Extra, Field, StrictBool, StrictStr
|
from pydantic import Extra, Field, StrictBool, StrictStr
|
||||||
|
|
||||||
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
||||||
from invokeai.app.util.misc import get_iso_timestamp
|
from invokeai.app.util.misc import get_iso_timestamp
|
||||||
|
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||||
|
|
||||||
|
|
||||||
class ImageRecord(BaseModel):
|
class ImageRecord(BaseModelExcludeNull):
|
||||||
"""Deserialized image record without metadata."""
|
"""Deserialized image record without metadata."""
|
||||||
|
|
||||||
image_name: str = Field(description="The unique name of the image.")
|
image_name: str = Field(description="The unique name of the image.")
|
||||||
@ -40,7 +41,7 @@ class ImageRecord(BaseModel):
|
|||||||
"""The node ID that generated this image, if it is a generated image."""
|
"""The node ID that generated this image, if it is a generated image."""
|
||||||
|
|
||||||
|
|
||||||
class ImageRecordChanges(BaseModel, extra=Extra.forbid):
|
class ImageRecordChanges(BaseModelExcludeNull, extra=Extra.forbid):
|
||||||
"""A set of changes to apply to an image record.
|
"""A set of changes to apply to an image record.
|
||||||
|
|
||||||
Only limited changes are valid:
|
Only limited changes are valid:
|
||||||
@ -60,7 +61,7 @@ class ImageRecordChanges(BaseModel, extra=Extra.forbid):
|
|||||||
"""The image's new `is_intermediate` flag."""
|
"""The image's new `is_intermediate` flag."""
|
||||||
|
|
||||||
|
|
||||||
class ImageUrlsDTO(BaseModel):
|
class ImageUrlsDTO(BaseModelExcludeNull):
|
||||||
"""The URLs for an image and its thumbnail."""
|
"""The URLs for an image and its thumbnail."""
|
||||||
|
|
||||||
image_name: str = Field(description="The unique name of the image.")
|
image_name: str = Field(description="The unique name of the image.")
|
||||||
@ -76,11 +77,15 @@ class ImageDTO(ImageRecord, ImageUrlsDTO):
|
|||||||
|
|
||||||
board_id: Optional[str] = Field(description="The id of the board the image belongs to, if one exists.")
|
board_id: Optional[str] = Field(description="The id of the board the image belongs to, if one exists.")
|
||||||
"""The id of the board the image belongs to, if one exists."""
|
"""The id of the board the image belongs to, if one exists."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def image_record_to_dto(
|
def image_record_to_dto(
|
||||||
image_record: ImageRecord, image_url: str, thumbnail_url: str, board_id: Optional[str]
|
image_record: ImageRecord,
|
||||||
|
image_url: str,
|
||||||
|
thumbnail_url: str,
|
||||||
|
board_id: Optional[str],
|
||||||
) -> ImageDTO:
|
) -> ImageDTO:
|
||||||
"""Converts an image record to an image DTO."""
|
"""Converts an image record to an image DTO."""
|
||||||
return ImageDTO(
|
return ImageDTO(
|
||||||
|
@ -20,6 +20,6 @@ class LocalUrlService(UrlServiceBase):
|
|||||||
|
|
||||||
# These paths are determined by the routes in invokeai/app/api/routers/images.py
|
# These paths are determined by the routes in invokeai/app/api/routers/images.py
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
return f"{self._base_url}/images/{image_basename}/thumbnail"
|
return f"{self._base_url}/images/i/{image_basename}/thumbnail"
|
||||||
|
|
||||||
return f"{self._base_url}/images/{image_basename}/full"
|
return f"{self._base_url}/images/i/{image_basename}/full"
|
||||||
|
23
invokeai/app/util/model_exclude_null.py
Normal file
23
invokeai/app/util/model_exclude_null.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from typing import Any
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
We want to exclude null values from objects that make their way to the client.
|
||||||
|
|
||||||
|
Unfortunately there is no built-in way to do this in pydantic, so we need to override the default
|
||||||
|
dict method to do this.
|
||||||
|
|
||||||
|
From https://github.com/tiangolo/fastapi/discussions/8882#discussioncomment-5154541
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModelExcludeNull(BaseModel):
|
||||||
|
def dict(self, *args, **kwargs) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Override the default dict method to exclude None values in the response
|
||||||
|
"""
|
||||||
|
kwargs.pop("exclude_none", None)
|
||||||
|
return super().dict(*args, exclude_none=True, **kwargs)
|
||||||
|
|
||||||
|
pass
|
@ -292,8 +292,9 @@ class DiffusersModel(ModelBase):
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print("====ERR LOAD====")
|
if not str(e).startswith("Error no file"):
|
||||||
# print(f"{variant}: {e}")
|
print("====ERR LOAD====")
|
||||||
|
print(f"{variant}: {e}")
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Failed to load {self.base_model}:{self.model_type}:{child_type} model")
|
raise Exception(f"Failed to load {self.base_model}:{self.model_type}:{child_type} model")
|
||||||
|
@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, Optional, Union
|
from typing import Literal, Optional, Union
|
||||||
|
from diffusers import StableDiffusionInpaintPipeline, StableDiffusionPipeline
|
||||||
from .base import (
|
from .base import (
|
||||||
ModelConfigBase,
|
ModelConfigBase,
|
||||||
BaseModelType,
|
BaseModelType,
|
||||||
@ -263,6 +264,8 @@ def _convert_ckpt_and_cache(
|
|||||||
weights = app_config.models_path / model_config.path
|
weights = app_config.models_path / model_config.path
|
||||||
config_file = app_config.root_path / model_config.config
|
config_file = app_config.root_path / model_config.config
|
||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
|
variant = model_config.variant
|
||||||
|
pipeline_class = StableDiffusionInpaintPipeline if variant == "inpaint" else StableDiffusionPipeline
|
||||||
|
|
||||||
# return cached version if it exists
|
# return cached version if it exists
|
||||||
if output_path.exists():
|
if output_path.exists():
|
||||||
@ -289,6 +292,7 @@ def _convert_ckpt_and_cache(
|
|||||||
original_config_file=config_file,
|
original_config_file=config_file,
|
||||||
extract_ema=True,
|
extract_ema=True,
|
||||||
scan_needed=True,
|
scan_needed=True,
|
||||||
|
pipeline_class=pipeline_class,
|
||||||
from_safetensors=weights.suffix == ".safetensors",
|
from_safetensors=weights.suffix == ".safetensors",
|
||||||
precision=torch_dtype(choose_torch_device()),
|
precision=torch_dtype(choose_torch_device()),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"dev": "concurrently \"vite dev\" \"yarn run theme:watch\"",
|
"dev": "concurrently \"vite dev\" \"yarn run theme:watch\"",
|
||||||
"dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"",
|
"dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"",
|
||||||
"build": "yarn run lint && vite build",
|
"build": "yarn run lint && vite build",
|
||||||
"typegen": "npx ts-node scripts/typegen.ts",
|
"typegen": "node scripts/typegen.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint:madge": "madge --circular src/main.tsx",
|
"lint:madge": "madge --circular src/main.tsx",
|
||||||
"lint:eslint": "eslint --max-warnings=0 .",
|
"lint:eslint": "eslint --max-warnings=0 .",
|
||||||
|
@ -4,8 +4,9 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { PartialAppConfig } from 'app/types/invokeai';
|
import { PartialAppConfig } from 'app/types/invokeai';
|
||||||
import ImageUploader from 'common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
|
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||||
|
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
||||||
import DeleteImageModal from 'features/imageDeletion/components/DeleteImageModal';
|
|
||||||
import SiteHeader from 'features/system/components/SiteHeader';
|
import SiteHeader from 'features/system/components/SiteHeader';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
@ -16,7 +17,6 @@ import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
|||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { ReactNode, memo, useEffect } from 'react';
|
import { ReactNode, memo, useEffect } from 'react';
|
||||||
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
|
|
||||||
import GlobalHotkeys from './GlobalHotkeys';
|
import GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
</Portal>
|
</Portal>
|
||||||
</Grid>
|
</Grid>
|
||||||
<DeleteImageModal />
|
<DeleteImageModal />
|
||||||
<UpdateImageBoardModal />
|
<ChangeBoardModal />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<GlobalHotkeys />
|
<GlobalHotkeys />
|
||||||
</>
|
</>
|
||||||
|
@ -58,7 +58,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.dragData.payloadType === 'IMAGE_NAMES') {
|
if (props.dragData.payloadType === 'IMAGE_DTOS') {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -71,7 +71,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
|||||||
...STYLES,
|
...STYLES,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading>{props.dragData.payload.image_names.length}</Heading>
|
<Heading>{props.dragData.payload.imageDTOs.length}</Heading>
|
||||||
<Heading size="sm">Images</Heading>
|
<Heading size="sm">Images</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -18,27 +18,32 @@ import {
|
|||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
TypesafeDraggableData,
|
TypesafeDraggableData,
|
||||||
} from './typesafeDnd';
|
} from './typesafeDnd';
|
||||||
|
import { logger } from 'app/logging/logger';
|
||||||
|
|
||||||
type ImageDndContextProps = PropsWithChildren;
|
type ImageDndContextProps = PropsWithChildren;
|
||||||
|
|
||||||
const ImageDndContext = (props: ImageDndContextProps) => {
|
const ImageDndContext = (props: ImageDndContextProps) => {
|
||||||
const [activeDragData, setActiveDragData] =
|
const [activeDragData, setActiveDragData] =
|
||||||
useState<TypesafeDraggableData | null>(null);
|
useState<TypesafeDraggableData | null>(null);
|
||||||
|
const log = logger('images');
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback(
|
||||||
console.log('dragStart', event.active.data.current);
|
(event: DragStartEvent) => {
|
||||||
|
log.trace({ dragData: event.active.data.current }, 'Drag started');
|
||||||
const activeData = event.active.data.current;
|
const activeData = event.active.data.current;
|
||||||
if (!activeData) {
|
if (!activeData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setActiveDragData(activeData);
|
setActiveDragData(activeData);
|
||||||
}, []);
|
},
|
||||||
|
[log]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
console.log('dragEnd', event.active.data.current);
|
log.trace({ dragData: event.active.data.current }, 'Drag ended');
|
||||||
const overData = event.over?.data.current;
|
const overData = event.over?.data.current;
|
||||||
if (!activeDragData || !overData) {
|
if (!activeDragData || !overData) {
|
||||||
return;
|
return;
|
||||||
@ -46,7 +51,7 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
|||||||
dispatch(dndDropped({ overData, activeData: activeDragData }));
|
dispatch(dndDropped({ overData, activeData: activeDragData }));
|
||||||
setActiveDragData(null);
|
setActiveDragData(null);
|
||||||
},
|
},
|
||||||
[activeDragData, dispatch]
|
[activeDragData, dispatch, log]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseSensor = useSensor(MouseSensor, {
|
const mouseSensor = useSensor(MouseSensor, {
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
useDraggable as useOriginalDraggable,
|
useDraggable as useOriginalDraggable,
|
||||||
useDroppable as useOriginalDroppable,
|
useDroppable as useOriginalDroppable,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { BoardId } from 'features/gallery/store/types';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
type BaseDropData = {
|
type BaseDropData = {
|
||||||
@ -54,9 +53,13 @@ export type AddToBatchDropData = BaseDropData & {
|
|||||||
actionType: 'ADD_TO_BATCH';
|
actionType: 'ADD_TO_BATCH';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MoveBoardDropData = BaseDropData & {
|
export type AddToBoardDropData = BaseDropData & {
|
||||||
actionType: 'MOVE_BOARD';
|
actionType: 'ADD_TO_BOARD';
|
||||||
context: { boardId: BoardId };
|
context: { boardId: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveFromBoardDropData = BaseDropData & {
|
||||||
|
actionType: 'REMOVE_FROM_BOARD';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDroppableData =
|
export type TypesafeDroppableData =
|
||||||
@ -67,7 +70,8 @@ export type TypesafeDroppableData =
|
|||||||
| NodesImageDropData
|
| NodesImageDropData
|
||||||
| AddToBatchDropData
|
| AddToBatchDropData
|
||||||
| NodesMultiImageDropData
|
| NodesMultiImageDropData
|
||||||
| MoveBoardDropData;
|
| AddToBoardDropData
|
||||||
|
| RemoveFromBoardDropData;
|
||||||
|
|
||||||
type BaseDragData = {
|
type BaseDragData = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -78,14 +82,12 @@ export type ImageDraggableData = BaseDragData & {
|
|||||||
payload: { imageDTO: ImageDTO };
|
payload: { imageDTO: ImageDTO };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImageNamesDraggableData = BaseDragData & {
|
export type ImageDTOsDraggableData = BaseDragData & {
|
||||||
payloadType: 'IMAGE_NAMES';
|
payloadType: 'IMAGE_DTOS';
|
||||||
payload: { image_names: string[] };
|
payload: { imageDTOs: ImageDTO[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TypesafeDraggableData =
|
export type TypesafeDraggableData = ImageDraggableData | ImageDTOsDraggableData;
|
||||||
| ImageDraggableData
|
|
||||||
| ImageNamesDraggableData;
|
|
||||||
|
|
||||||
interface UseDroppableTypesafeArguments
|
interface UseDroppableTypesafeArguments
|
||||||
extends Omit<UseDroppableArguments, 'data'> {
|
extends Omit<UseDroppableArguments, 'data'> {
|
||||||
@ -156,14 +158,39 @@ export const isValidDrop = (
|
|||||||
case 'SET_NODES_IMAGE':
|
case 'SET_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO';
|
return payloadType === 'IMAGE_DTO';
|
||||||
case 'SET_MULTI_NODES_IMAGE':
|
case 'SET_MULTI_NODES_IMAGE':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||||
case 'ADD_TO_BATCH':
|
case 'ADD_TO_BATCH':
|
||||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
return payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||||
case 'MOVE_BOARD': {
|
case 'ADD_TO_BOARD': {
|
||||||
// If the board is the same, don't allow the drop
|
// If the board is the same, don't allow the drop
|
||||||
|
|
||||||
// Check the payload types
|
// Check the payload types
|
||||||
const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_NAMES';
|
const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||||
|
if (!isPayloadValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the image's board is the board we are dragging onto
|
||||||
|
if (payloadType === 'IMAGE_DTO') {
|
||||||
|
const { imageDTO } = active.data.current.payload;
|
||||||
|
const currentBoard = imageDTO.board_id ?? 'none';
|
||||||
|
const destinationBoard = overData.context.boardId;
|
||||||
|
|
||||||
|
return currentBoard !== destinationBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadType === 'IMAGE_DTOS') {
|
||||||
|
// TODO (multi-select)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case 'REMOVE_FROM_BOARD': {
|
||||||
|
// If the board is the same, don't allow the drop
|
||||||
|
|
||||||
|
// Check the payload types
|
||||||
|
const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||||
if (!isPayloadValid) {
|
if (!isPayloadValid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -172,20 +199,16 @@ export const isValidDrop = (
|
|||||||
if (payloadType === 'IMAGE_DTO') {
|
if (payloadType === 'IMAGE_DTO') {
|
||||||
const { imageDTO } = active.data.current.payload;
|
const { imageDTO } = active.data.current.payload;
|
||||||
const currentBoard = imageDTO.board_id;
|
const currentBoard = imageDTO.board_id;
|
||||||
const destinationBoard = overData.context.boardId;
|
|
||||||
|
|
||||||
const isSameBoard = currentBoard === destinationBoard;
|
return currentBoard !== 'none';
|
||||||
const isDestinationValid = !currentBoard ? destinationBoard : true;
|
|
||||||
|
|
||||||
return !isSameBoard && isDestinationValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payloadType === 'IMAGE_NAMES') {
|
if (payloadType === 'IMAGE_DTOS') {
|
||||||
// TODO (multi-select)
|
// TODO (multi-select)
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { Middleware } from '@reduxjs/toolkit';
|
||||||
import { store } from 'app/store/store';
|
import { store } from 'app/store/store';
|
||||||
|
import { PartialAppConfig } from 'app/types/invokeai';
|
||||||
import React, {
|
import React, {
|
||||||
lazy,
|
lazy,
|
||||||
memo,
|
memo,
|
||||||
@ -7,16 +9,11 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { PartialAppConfig } from 'app/types/invokeai';
|
|
||||||
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
|
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
|
||||||
import Loading from '../../common/components/Loading/Loading';
|
import { $authToken, $baseUrl, $projectId } from 'services/api/client';
|
||||||
|
|
||||||
import { Middleware } from '@reduxjs/toolkit';
|
|
||||||
import { $authToken, $baseUrl } from 'services/api/client';
|
|
||||||
import { socketMiddleware } from 'services/events/middleware';
|
import { socketMiddleware } from 'services/events/middleware';
|
||||||
|
import Loading from '../../common/components/Loading/Loading';
|
||||||
import '../../i18n';
|
import '../../i18n';
|
||||||
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
|
|
||||||
import ImageDndContext from './ImageDnd/ImageDndContext';
|
import ImageDndContext from './ImageDnd/ImageDndContext';
|
||||||
|
|
||||||
const App = lazy(() => import('./App'));
|
const App = lazy(() => import('./App'));
|
||||||
@ -37,6 +34,7 @@ const InvokeAIUI = ({
|
|||||||
config,
|
config,
|
||||||
headerComponent,
|
headerComponent,
|
||||||
middleware,
|
middleware,
|
||||||
|
projectId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// configure API client token
|
// configure API client token
|
||||||
@ -49,6 +47,11 @@ const InvokeAIUI = ({
|
|||||||
$baseUrl.set(apiUrl);
|
$baseUrl.set(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configure API client project header
|
||||||
|
if (projectId) {
|
||||||
|
$projectId.set(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
// reset dynamically added middlewares
|
// reset dynamically added middlewares
|
||||||
resetMiddlewares();
|
resetMiddlewares();
|
||||||
|
|
||||||
@ -68,8 +71,9 @@ const InvokeAIUI = ({
|
|||||||
// Reset the API client token and base url on unmount
|
// Reset the API client token and base url on unmount
|
||||||
$baseUrl.set(undefined);
|
$baseUrl.set(undefined);
|
||||||
$authToken.set(undefined);
|
$authToken.set(undefined);
|
||||||
|
$projectId.set(undefined);
|
||||||
};
|
};
|
||||||
}, [apiUrl, token, middleware]);
|
}, [apiUrl, token, middleware, projectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@ -77,9 +81,7 @@ const InvokeAIUI = ({
|
|||||||
<React.Suspense fallback={<Loading />}>
|
<React.Suspense fallback={<Loading />}>
|
||||||
<ThemeLocaleProvider>
|
<ThemeLocaleProvider>
|
||||||
<ImageDndContext>
|
<ImageDndContext>
|
||||||
<AddImageToBoardContextProvider>
|
|
||||||
<App config={config} headerComponent={headerComponent} />
|
<App config={config} headerComponent={headerComponent} />
|
||||||
</AddImageToBoardContextProvider>
|
|
||||||
</ImageDndContext>
|
</ImageDndContext>
|
||||||
</ThemeLocaleProvider>
|
</ThemeLocaleProvider>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import { useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
|
||||||
import { useAppDispatch } from '../store/storeHooks';
|
|
||||||
|
|
||||||
export type ImageUsage = {
|
|
||||||
isInitialImage: boolean;
|
|
||||||
isCanvasImage: boolean;
|
|
||||||
isNodesImage: boolean;
|
|
||||||
isControlNetImage: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddImageToBoardContextValue = {
|
|
||||||
/**
|
|
||||||
* Whether the move image dialog is open.
|
|
||||||
*/
|
|
||||||
isOpen: boolean;
|
|
||||||
/**
|
|
||||||
* Closes the move image dialog.
|
|
||||||
*/
|
|
||||||
onClose: () => void;
|
|
||||||
/**
|
|
||||||
* The image pending movement
|
|
||||||
*/
|
|
||||||
image?: ImageDTO;
|
|
||||||
onClickAddToBoard: (image: ImageDTO) => void;
|
|
||||||
handleAddToBoard: (boardId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AddImageToBoardContext =
|
|
||||||
createContext<AddImageToBoardContextValue>({
|
|
||||||
isOpen: false,
|
|
||||||
onClose: () => undefined,
|
|
||||||
onClickAddToBoard: () => undefined,
|
|
||||||
handleAddToBoard: () => undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = PropsWithChildren;
|
|
||||||
|
|
||||||
export const AddImageToBoardContextProvider = (props: Props) => {
|
|
||||||
const [imageToMove, setImageToMove] = useState<ImageDTO>();
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
// Clean up after deleting or dismissing the modal
|
|
||||||
const closeAndClearImageToDelete = useCallback(() => {
|
|
||||||
setImageToMove(undefined);
|
|
||||||
onClose();
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const onClickAddToBoard = useCallback(
|
|
||||||
(image?: ImageDTO) => {
|
|
||||||
if (!image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImageToMove(image);
|
|
||||||
onOpen();
|
|
||||||
},
|
|
||||||
[setImageToMove, onOpen]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddToBoard = useCallback(
|
|
||||||
(boardId: string) => {
|
|
||||||
if (imageToMove) {
|
|
||||||
dispatch(
|
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
|
||||||
imageDTO: imageToMove,
|
|
||||||
board_id: boardId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
closeAndClearImageToDelete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, closeAndClearImageToDelete, imageToMove]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AddImageToBoardContext.Provider
|
|
||||||
value={{
|
|
||||||
isOpen,
|
|
||||||
image: imageToMove,
|
|
||||||
onClose: closeAndClearImageToDelete,
|
|
||||||
onClickAddToBoard,
|
|
||||||
handleAddToBoard,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</AddImageToBoardContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,8 +0,0 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
|
|
||||||
type VoidFunc = () => void;
|
|
||||||
|
|
||||||
type ImageUploaderTriggerContextType = VoidFunc | null;
|
|
||||||
|
|
||||||
export const ImageUploaderTriggerContext =
|
|
||||||
createContext<ImageUploaderTriggerContextType>(null);
|
|
@ -23,6 +23,6 @@ const serializationDenylist: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const serialize: SerializeFunction = (data, key) => {
|
export const serialize: SerializeFunction = (data, key) => {
|
||||||
const result = omit(data, serializationDenylist[key]);
|
const result = omit(data, serializationDenylist[key] ?? []);
|
||||||
return JSON.stringify(result);
|
return JSON.stringify(result);
|
||||||
};
|
};
|
||||||
|
@ -27,7 +27,8 @@ import {
|
|||||||
addImageDeletedFulfilledListener,
|
addImageDeletedFulfilledListener,
|
||||||
addImageDeletedPendingListener,
|
addImageDeletedPendingListener,
|
||||||
addImageDeletedRejectedListener,
|
addImageDeletedRejectedListener,
|
||||||
addRequestedImageDeletionListener,
|
addRequestedSingleImageDeletionListener,
|
||||||
|
addRequestedMultipleImageDeletionListener,
|
||||||
} from './listeners/imageDeleted';
|
} from './listeners/imageDeleted';
|
||||||
import { addImageDroppedListener } from './listeners/imageDropped';
|
import { addImageDroppedListener } from './listeners/imageDropped';
|
||||||
import {
|
import {
|
||||||
@ -111,7 +112,8 @@ addImageUploadedRejectedListener();
|
|||||||
addInitialImageSelectedListener();
|
addInitialImageSelectedListener();
|
||||||
|
|
||||||
// Image deleted
|
// Image deleted
|
||||||
addRequestedImageDeletionListener();
|
addRequestedSingleImageDeletionListener();
|
||||||
|
addRequestedMultipleImageDeletionListener();
|
||||||
addImageDeletedPendingListener();
|
addImageDeletedPendingListener();
|
||||||
addImageDeletedFulfilledListener();
|
addImageDeletedFulfilledListener();
|
||||||
addImageDeletedRejectedListener();
|
addImageDeletedRejectedListener();
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import {
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
ImageCache,
|
|
||||||
getListImagesUrl,
|
|
||||||
imagesApi,
|
|
||||||
} from 'services/api/endpoints/images';
|
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
import { getListImagesUrl, imagesAdapter } from 'services/api/util';
|
||||||
|
import { ImageCache } from 'services/api/types';
|
||||||
|
|
||||||
export const appStarted = createAction('app/appStarted');
|
export const appStarted = createAction('app/appStarted');
|
||||||
|
|
||||||
@ -34,7 +32,8 @@ export const addFirstListImagesListener = () => {
|
|||||||
|
|
||||||
if (data.ids.length > 0) {
|
if (data.ids.length > 0) {
|
||||||
// Select the first image
|
// Select the first image
|
||||||
dispatch(imageSelected(data.ids[0] as string));
|
const firstImage = imagesAdapter.getSelectors().selectAll(data)[0];
|
||||||
|
dispatch(imageSelected(firstImage ?? null));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,9 @@ export const addAppConfigReceivedListener = () => {
|
|||||||
const infillMethod = getState().generation.infillMethod;
|
const infillMethod = getState().generation.infillMethod;
|
||||||
|
|
||||||
if (!infill_methods.includes(infillMethod)) {
|
if (!infill_methods.includes(infillMethod)) {
|
||||||
dispatch(setInfillMethod(infill_methods[0]));
|
// if there is no infill method, set it to the first one
|
||||||
|
// if there is no first one... god help us
|
||||||
|
dispatch(setInfillMethod(infill_methods[0] as string));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nsfw_methods.includes('nsfw_checker')) {
|
if (!nsfw_methods.includes('nsfw_checker')) {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
|
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { boardsApi } from '../../../../../services/api/endpoints/boards';
|
|
||||||
|
|
||||||
export const addDeleteBoardAndImagesFulfilledListener = () => {
|
export const addDeleteBoardAndImagesFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
matcher: boardsApi.endpoints.deleteBoardAndImages.matchFulfilled,
|
matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
|
||||||
effect: async (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
const { deleted_images } = action.payload;
|
const { deleted_images } = action.payload;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from 'features/gallery/store/types';
|
} from 'features/gallery/store/types';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
import { imagesSelectors } from 'services/api/util';
|
||||||
|
|
||||||
export const addBoardIdSelectedListener = () => {
|
export const addBoardIdSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -52,8 +53,9 @@ export const addBoardIdSelectedListener = () => {
|
|||||||
queryArgs
|
queryArgs
|
||||||
)(getState());
|
)(getState());
|
||||||
|
|
||||||
if (boardImagesData?.ids.length) {
|
if (boardImagesData) {
|
||||||
dispatch(imageSelected((boardImagesData.ids[0] as string) ?? null));
|
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
|
||||||
|
dispatch(imageSelected(firstImage ?? null));
|
||||||
} else {
|
} else {
|
||||||
// board has no images - deselect
|
// board has no images - deselect
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelected(null));
|
||||||
|
@ -26,6 +26,8 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { autoAddBoardId } = state.gallery;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.uploadImage.initiate({
|
imagesApi.endpoints.uploadImage.initiate({
|
||||||
file: new File([blob], 'savedCanvas.png', {
|
file: new File([blob], 'savedCanvas.png', {
|
||||||
@ -33,7 +35,7 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
}),
|
}),
|
||||||
image_category: 'general',
|
image_category: 'general',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
board_id: state.gallery.autoAddBoardId,
|
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||||
crop_visible: true,
|
crop_visible: true,
|
||||||
postUploadAction: {
|
postUploadAction: {
|
||||||
type: 'TOAST',
|
type: 'TOAST',
|
||||||
|
@ -31,15 +31,20 @@ const predicate: AnyListenerPredicate<RootState> = (
|
|||||||
// do not process if the user just disabled auto-config
|
// do not process if the user just disabled auto-config
|
||||||
if (
|
if (
|
||||||
prevState.controlNet.controlNets[action.payload.controlNetId]
|
prevState.controlNet.controlNets[action.payload.controlNetId]
|
||||||
.shouldAutoConfig === true
|
?.shouldAutoConfig === true
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { controlImage, processorType, shouldAutoConfig } =
|
const cn = state.controlNet.controlNets[action.payload.controlNetId];
|
||||||
state.controlNet.controlNets[action.payload.controlNetId];
|
|
||||||
|
|
||||||
|
if (!cn) {
|
||||||
|
// something is wrong, the controlNet should exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { controlImage, processorType, shouldAutoConfig } = cn;
|
||||||
if (controlNetModelChanged.match(action) && !shouldAutoConfig) {
|
if (controlNetModelChanged.match(action) && !shouldAutoConfig) {
|
||||||
// do not process if the action is a model change but the processor settings are dirty
|
// do not process if the action is a model change but the processor settings are dirty
|
||||||
return false;
|
return false;
|
||||||
|
@ -17,7 +17,7 @@ export const addControlNetImageProcessedListener = () => {
|
|||||||
const { controlNetId } = action.payload;
|
const { controlNetId } = action.payload;
|
||||||
const controlNet = getState().controlNet.controlNets[controlNetId];
|
const controlNet = getState().controlNet.controlNets[controlNetId];
|
||||||
|
|
||||||
if (!controlNet.controlImage) {
|
if (!controlNet?.controlImage) {
|
||||||
log.error('Unable to process ControlNet image');
|
log.error('Unable to process ControlNet image');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,72 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||||
|
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||||
|
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { imageDeletionConfirmed } from 'features/imageDeletion/store/actions';
|
|
||||||
import { isModalOpenChanged } from 'features/imageDeletion/store/imageDeletionSlice';
|
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { api } from 'services/api';
|
import { api } from 'services/api';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
|
import { imagesAdapter } from 'services/api/util';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
/**
|
export const addRequestedSingleImageDeletionListener = () => {
|
||||||
* Called when the user requests an image deletion
|
|
||||||
*/
|
|
||||||
export const addRequestedImageDeletionListener = () => {
|
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageDeletionConfirmed,
|
actionCreator: imageDeletionConfirmed,
|
||||||
effect: async (action, { dispatch, getState, condition }) => {
|
effect: async (action, { dispatch, getState, condition }) => {
|
||||||
const { imageDTO, imageUsage } = action.payload;
|
const { imageDTOs, imagesUsage } = action.payload;
|
||||||
|
|
||||||
|
if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
|
||||||
|
// handle multiples in separate listener
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageDTO = imageDTOs[0];
|
||||||
|
const imageUsage = imagesUsage[0];
|
||||||
|
|
||||||
|
if (!imageDTO || !imageUsage) {
|
||||||
|
// satisfy noUncheckedIndexedAccess
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(isModalOpenChanged(false));
|
dispatch(isModalOpenChanged(false));
|
||||||
|
|
||||||
const { image_name } = imageDTO;
|
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const lastSelectedImage =
|
const lastSelectedImage =
|
||||||
state.gallery.selection[state.gallery.selection.length - 1];
|
state.gallery.selection[state.gallery.selection.length - 1]?.image_name;
|
||||||
|
|
||||||
|
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
|
||||||
|
const { image_name } = imageDTO;
|
||||||
|
|
||||||
if (lastSelectedImage === image_name) {
|
|
||||||
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
|
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
|
||||||
const { data } =
|
const { data } =
|
||||||
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||||
|
|
||||||
const ids = data?.ids ?? [];
|
const cachedImageDTOs = data
|
||||||
|
? imagesAdapter.getSelectors().selectAll(data)
|
||||||
|
: [];
|
||||||
|
|
||||||
const deletedImageIndex = ids.findIndex(
|
const deletedImageIndex = cachedImageDTOs.findIndex(
|
||||||
(result) => result.toString() === image_name
|
(i) => i.image_name === image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredIds = ids.filter((id) => id.toString() !== image_name);
|
const filteredImageDTOs = cachedImageDTOs.filter(
|
||||||
|
(i) => i.image_name !== image_name
|
||||||
|
);
|
||||||
|
|
||||||
const newSelectedImageIndex = clamp(
|
const newSelectedImageIndex = clamp(
|
||||||
deletedImageIndex,
|
deletedImageIndex,
|
||||||
0,
|
0,
|
||||||
filteredIds.length - 1
|
filteredImageDTOs.length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
|
||||||
|
|
||||||
if (newSelectedImageId) {
|
if (newSelectedImageDTO) {
|
||||||
dispatch(imageSelected(newSelectedImageId as string));
|
dispatch(imageSelected(newSelectedImageDTO));
|
||||||
} else {
|
} else {
|
||||||
dispatch(imageSelected(null));
|
dispatch(imageSelected(null));
|
||||||
}
|
}
|
||||||
@ -97,6 +112,66 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user requests an image deletion
|
||||||
|
*/
|
||||||
|
export const addRequestedMultipleImageDeletionListener = () => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: imageDeletionConfirmed,
|
||||||
|
effect: async (action, { dispatch, getState }) => {
|
||||||
|
const { imageDTOs, imagesUsage } = action.payload;
|
||||||
|
|
||||||
|
if (imageDTOs.length < 1 || imagesUsage.length < 1) {
|
||||||
|
// handle singles in separate listener
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete from server
|
||||||
|
await dispatch(
|
||||||
|
imagesApi.endpoints.deleteImages.initiate({ imageDTOs })
|
||||||
|
).unwrap();
|
||||||
|
const state = getState();
|
||||||
|
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
|
||||||
|
const { data } =
|
||||||
|
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||||
|
|
||||||
|
const newSelectedImageDTO = data
|
||||||
|
? imagesAdapter.getSelectors().selectAll(data)[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (newSelectedImageDTO) {
|
||||||
|
dispatch(imageSelected(newSelectedImageDTO));
|
||||||
|
} else {
|
||||||
|
dispatch(imageSelected(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(isModalOpenChanged(false));
|
||||||
|
|
||||||
|
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||||
|
|
||||||
|
if (imagesUsage.some((i) => i.isCanvasImage)) {
|
||||||
|
dispatch(resetCanvas());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesUsage.some((i) => i.isControlNetImage)) {
|
||||||
|
dispatch(controlNetReset());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesUsage.some((i) => i.isInitialImage)) {
|
||||||
|
dispatch(clearInitialImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesUsage.some((i) => i.isNodesImage)) {
|
||||||
|
dispatch(nodeEditorReset());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the actual delete request is sent to the server
|
* Called when the actual delete request is sent to the server
|
||||||
*/
|
*/
|
||||||
|
@ -6,10 +6,7 @@ import {
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import {
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
imageSelected,
|
|
||||||
imagesAddedToBatch,
|
|
||||||
} from 'features/gallery/store/gallerySlice';
|
|
||||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@ -27,19 +24,32 @@ export const addImageDroppedListener = () => {
|
|||||||
const log = logger('images');
|
const log = logger('images');
|
||||||
const { activeData, overData } = action.payload;
|
const { activeData, overData } = action.payload;
|
||||||
|
|
||||||
log.debug({ activeData, overData }, 'Image or selection dropped');
|
if (activeData.payloadType === 'IMAGE_DTO') {
|
||||||
|
log.debug({ activeData, overData }, 'Image dropped');
|
||||||
|
} else if (activeData.payloadType === 'IMAGE_DTOS') {
|
||||||
|
log.debug(
|
||||||
|
{ activeData, overData },
|
||||||
|
`Images (${activeData.payload.imageDTOs.length}) dropped`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.debug({ activeData, overData }, `Unknown payload dropped`);
|
||||||
|
}
|
||||||
|
|
||||||
// set current image
|
/**
|
||||||
|
* Image dropped on current image
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
dispatch(imageSelected(activeData.payload.imageDTO.image_name));
|
dispatch(imageSelected(activeData.payload.imageDTO));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set initial image
|
/**
|
||||||
|
* Image dropped on initial image
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_INITIAL_IMAGE' &&
|
overData.actionType === 'SET_INITIAL_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
@ -49,27 +59,9 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add image to batch
|
/**
|
||||||
if (
|
* Image dropped on ControlNet
|
||||||
overData.actionType === 'ADD_TO_BATCH' &&
|
*/
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
|
||||||
activeData.payload.imageDTO
|
|
||||||
) {
|
|
||||||
dispatch(imagesAddedToBatch([activeData.payload.imageDTO.image_name]));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add multiple images to batch
|
|
||||||
if (
|
|
||||||
overData.actionType === 'ADD_TO_BATCH' &&
|
|
||||||
activeData.payloadType === 'IMAGE_NAMES'
|
|
||||||
) {
|
|
||||||
dispatch(imagesAddedToBatch(activeData.payload.image_names));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set control image
|
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_CONTROLNET_IMAGE' &&
|
overData.actionType === 'SET_CONTROLNET_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
@ -85,7 +77,9 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set canvas image
|
/**
|
||||||
|
* Image dropped on Canvas
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
@ -95,7 +89,9 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set nodes image
|
/**
|
||||||
|
* Image dropped on node image field
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'SET_NODES_IMAGE' &&
|
overData.actionType === 'SET_NODES_IMAGE' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
@ -112,61 +108,36 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set multiple nodes images (single image handler)
|
/**
|
||||||
if (
|
* TODO
|
||||||
overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
* Image selection dropped on node image collection field
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
*/
|
||||||
activeData.payload.imageDTO
|
|
||||||
) {
|
|
||||||
const { fieldName, nodeId } = overData.context;
|
|
||||||
dispatch(
|
|
||||||
fieldValueChanged({
|
|
||||||
nodeId,
|
|
||||||
fieldName,
|
|
||||||
value: [activeData.payload.imageDTO],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// // set multiple nodes images (multiple images handler)
|
|
||||||
// if (
|
// if (
|
||||||
// overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
// overData.actionType === 'SET_MULTI_NODES_IMAGE' &&
|
||||||
// activeData.payloadType === 'IMAGE_NAMES'
|
// activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
|
// activeData.payload.imageDTO
|
||||||
// ) {
|
// ) {
|
||||||
// const { fieldName, nodeId } = overData.context;
|
// const { fieldName, nodeId } = overData.context;
|
||||||
// dispatch(
|
// dispatch(
|
||||||
// imageCollectionFieldValueChanged({
|
// fieldValueChanged({
|
||||||
// nodeId,
|
// nodeId,
|
||||||
// fieldName,
|
// fieldName,
|
||||||
// value: activeData.payload.image_names.map((image_name) => ({
|
// value: [activeData.payload.imageDTO],
|
||||||
// image_name,
|
|
||||||
// })),
|
|
||||||
// })
|
// })
|
||||||
// );
|
// );
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// add image to board
|
/**
|
||||||
|
* Image dropped on user board
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
overData.actionType === 'MOVE_BOARD' &&
|
overData.actionType === 'ADD_TO_BOARD' &&
|
||||||
activeData.payloadType === 'IMAGE_DTO' &&
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
activeData.payload.imageDTO
|
activeData.payload.imageDTO
|
||||||
) {
|
) {
|
||||||
const { imageDTO } = activeData.payload;
|
const { imageDTO } = activeData.payload;
|
||||||
const { boardId } = overData.context;
|
const { boardId } = overData.context;
|
||||||
|
|
||||||
// image was droppe on the "NoBoardBoard"
|
|
||||||
if (!boardId) {
|
|
||||||
dispatch(
|
|
||||||
imagesApi.endpoints.removeImageFromBoard.initiate({
|
|
||||||
imageDTO,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// image was dropped on a user board
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
imageDTO,
|
imageDTO,
|
||||||
@ -176,67 +147,58 @@ export const addImageDroppedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // add gallery selection to board
|
/**
|
||||||
// if (
|
* Image dropped on 'none' board
|
||||||
// overData.actionType === 'MOVE_BOARD' &&
|
*/
|
||||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
if (
|
||||||
// overData.context.boardId
|
overData.actionType === 'REMOVE_FROM_BOARD' &&
|
||||||
// ) {
|
activeData.payloadType === 'IMAGE_DTO' &&
|
||||||
// console.log('adding gallery selection to board');
|
activeData.payload.imageDTO
|
||||||
// const board_id = overData.context.boardId;
|
) {
|
||||||
// dispatch(
|
const { imageDTO } = activeData.payload;
|
||||||
// boardImagesApi.endpoints.addManyBoardImages.initiate({
|
dispatch(
|
||||||
// board_id,
|
imagesApi.endpoints.removeImageFromBoard.initiate({
|
||||||
// image_names: activeData.payload.image_names,
|
imageDTO,
|
||||||
// })
|
})
|
||||||
// );
|
);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // remove gallery selection from board
|
/**
|
||||||
// if (
|
* Multiple images dropped on user board
|
||||||
// overData.actionType === 'MOVE_BOARD' &&
|
*/
|
||||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
if (
|
||||||
// overData.context.boardId === null
|
overData.actionType === 'ADD_TO_BOARD' &&
|
||||||
// ) {
|
activeData.payloadType === 'IMAGE_DTOS' &&
|
||||||
// console.log('removing gallery selection to board');
|
activeData.payload.imageDTOs
|
||||||
// dispatch(
|
) {
|
||||||
// boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
const { imageDTOs } = activeData.payload;
|
||||||
// image_names: activeData.payload.image_names,
|
const { boardId } = overData.context;
|
||||||
// })
|
dispatch(
|
||||||
// );
|
imagesApi.endpoints.addImagesToBoard.initiate({
|
||||||
// return;
|
imageDTOs,
|
||||||
// }
|
board_id: boardId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// // add batch selection to board
|
/**
|
||||||
// if (
|
* Multiple images dropped on 'none' board
|
||||||
// overData.actionType === 'MOVE_BOARD' &&
|
*/
|
||||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
if (
|
||||||
// overData.context.boardId
|
overData.actionType === 'REMOVE_FROM_BOARD' &&
|
||||||
// ) {
|
activeData.payloadType === 'IMAGE_DTOS' &&
|
||||||
// const board_id = overData.context.boardId;
|
activeData.payload.imageDTOs
|
||||||
// dispatch(
|
) {
|
||||||
// boardImagesApi.endpoints.addManyBoardImages.initiate({
|
const { imageDTOs } = activeData.payload;
|
||||||
// board_id,
|
dispatch(
|
||||||
// image_names: activeData.payload.image_names,
|
imagesApi.endpoints.removeImagesFromBoard.initiate({
|
||||||
// })
|
imageDTOs,
|
||||||
// );
|
})
|
||||||
// return;
|
);
|
||||||
// }
|
return;
|
||||||
|
}
|
||||||
// // remove batch selection from board
|
|
||||||
// if (
|
|
||||||
// overData.actionType === 'MOVE_BOARD' &&
|
|
||||||
// activeData.payloadType === 'IMAGE_NAMES' &&
|
|
||||||
// overData.context.boardId === null
|
|
||||||
// ) {
|
|
||||||
// dispatch(
|
|
||||||
// boardImagesApi.endpoints.deleteManyBoardImages.initiate({
|
|
||||||
// image_names: activeData.payload.image_names,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,37 +1,32 @@
|
|||||||
import { imageDeletionConfirmed } from 'features/imageDeletion/store/actions';
|
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||||
import { selectImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
|
import { selectImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||||
import {
|
import {
|
||||||
imageToDeleteSelected,
|
imagesToDeleteSelected,
|
||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
} from 'features/imageDeletion/store/imageDeletionSlice';
|
} from 'features/deleteImageModal/store/slice';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
export const addImageToDeleteSelectedListener = () => {
|
export const addImageToDeleteSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageToDeleteSelected,
|
actionCreator: imagesToDeleteSelected,
|
||||||
effect: async (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
const imageDTO = action.payload;
|
const imageDTOs = action.payload;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { shouldConfirmOnDelete } = state.system;
|
const { shouldConfirmOnDelete } = state.system;
|
||||||
const imageUsage = selectImageUsage(getState());
|
const imagesUsage = selectImageUsage(getState());
|
||||||
|
|
||||||
if (!imageUsage) {
|
|
||||||
// should never happen
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isImageInUse =
|
const isImageInUse =
|
||||||
imageUsage.isCanvasImage ||
|
imagesUsage.some((i) => i.isCanvasImage) ||
|
||||||
imageUsage.isInitialImage ||
|
imagesUsage.some((i) => i.isInitialImage) ||
|
||||||
imageUsage.isControlNetImage ||
|
imagesUsage.some((i) => i.isControlNetImage) ||
|
||||||
imageUsage.isNodesImage;
|
imagesUsage.some((i) => i.isNodesImage);
|
||||||
|
|
||||||
if (shouldConfirmOnDelete || isImageInUse) {
|
if (shouldConfirmOnDelete || isImageInUse) {
|
||||||
dispatch(isModalOpenChanged(true));
|
dispatch(isModalOpenChanged(true));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(imageDeletionConfirmed({ imageDTO, imageUsage }));
|
dispatch(imageDeletionConfirmed({ imageDTOs, imagesUsage }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -2,14 +2,13 @@ import { UseToastOptions } from '@chakra-ui/react';
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||||
import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { imagesApi } from '../../../../../services/api/endpoints/images';
|
import { imagesApi } from '../../../../../services/api/endpoints/images';
|
||||||
import { omit } from 'lodash-es';
|
|
||||||
|
|
||||||
const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
|
const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
|
||||||
title: 'Image Uploaded',
|
title: 'Image Uploaded',
|
||||||
@ -121,17 +120,6 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postUploadAction?.type === 'ADD_TO_BATCH') {
|
|
||||||
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
|
||||||
dispatch(
|
|
||||||
addToast({
|
|
||||||
...DEFAULT_UPLOADED_TOAST,
|
|
||||||
description: 'Added to batch',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
setShouldUseSDXLRefiner,
|
setShouldUseSDXLRefiner,
|
||||||
} from 'features/sdxl/store/sdxlSlice';
|
} from 'features/sdxl/store/sdxlSlice';
|
||||||
import { forEach, some } from 'lodash-es';
|
import { forEach, some } from 'lodash-es';
|
||||||
import { modelsApi } from 'services/api/endpoints/models';
|
import { modelsApi, vaeModelsAdapter } from 'services/api/endpoints/models';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
|
||||||
export const addModelsLoadedListener = () => {
|
export const addModelsLoadedListener = () => {
|
||||||
@ -144,8 +144,9 @@ export const addModelsLoadedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstModelId = action.payload.ids[0];
|
const firstModel = vaeModelsAdapter
|
||||||
const firstModel = action.payload.entities[firstModelId];
|
.getSelectors()
|
||||||
|
.selectAll(action.payload)[0];
|
||||||
|
|
||||||
if (!firstModel) {
|
if (!firstModel) {
|
||||||
// No custom VAEs loaded at all; use the default
|
// No custom VAEs loaded at all; use the default
|
||||||
|
@ -8,9 +8,10 @@ import {
|
|||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||||
import { imagesAdapter, imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { isImageOutput } from 'services/api/guards';
|
import { isImageOutput } from 'services/api/guards';
|
||||||
import { sessionCanceled } from 'services/api/thunks/session';
|
import { sessionCanceled } from 'services/api/thunks/session';
|
||||||
|
import { imagesAdapter } from 'services/api/util';
|
||||||
import {
|
import {
|
||||||
appSocketInvocationComplete,
|
appSocketInvocationComplete,
|
||||||
socketInvocationComplete,
|
socketInvocationComplete,
|
||||||
@ -67,7 +68,7 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { autoAddBoardId } = gallery;
|
const { autoAddBoardId } = gallery;
|
||||||
if (autoAddBoardId) {
|
if (autoAddBoardId && autoAddBoardId !== 'none') {
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.endpoints.addImageToBoard.initiate({
|
imagesApi.endpoints.addImageToBoard.initiate({
|
||||||
board_id: autoAddBoardId,
|
board_id: autoAddBoardId,
|
||||||
@ -83,10 +84,7 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
categories: IMAGE_CATEGORIES,
|
categories: IMAGE_CATEGORIES,
|
||||||
},
|
},
|
||||||
(draft) => {
|
(draft) => {
|
||||||
const oldTotal = draft.total;
|
imagesAdapter.addOne(draft, imageDTO);
|
||||||
const newState = imagesAdapter.addOne(draft, imageDTO);
|
|
||||||
const delta = newState.total - oldTotal;
|
|
||||||
draft.total = draft.total + delta;
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -94,8 +92,8 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imagesApi.util.invalidateTags([
|
imagesApi.util.invalidateTags([
|
||||||
{ type: 'BoardImagesTotal', id: autoAddBoardId ?? 'none' },
|
{ type: 'BoardImagesTotal', id: autoAddBoardId },
|
||||||
{ type: 'BoardAssetsTotal', id: autoAddBoardId ?? 'none' },
|
{ type: 'BoardAssetsTotal', id: autoAddBoardId },
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,7 +108,7 @@ export const addInvocationCompleteEventListener = () => {
|
|||||||
} else if (!autoAddBoardId) {
|
} else if (!autoAddBoardId) {
|
||||||
dispatch(galleryViewChanged('images'));
|
dispatch(galleryViewChanged('images'));
|
||||||
}
|
}
|
||||||
dispatch(imageSelected(imageDTO.image_name));
|
dispatch(imageSelected(imageDTO));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ import {
|
|||||||
import canvasReducer from 'features/canvas/store/canvasSlice';
|
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||||
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
|
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
|
||||||
import dynamicPromptsReducer from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import dynamicPromptsReducer from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import boardsReducer from 'features/gallery/store/boardSlice';
|
|
||||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||||
import imageDeletionReducer from 'features/imageDeletion/store/imageDeletionSlice';
|
import deleteImageModalReducer from 'features/deleteImageModal/store/slice';
|
||||||
|
import changeBoardModalReducer from 'features/changeBoardModal/store/slice';
|
||||||
import loraReducer from 'features/lora/store/loraSlice';
|
import loraReducer from 'features/lora/store/loraSlice';
|
||||||
import nodesReducer from 'features/nodes/store/nodesSlice';
|
import nodesReducer from 'features/nodes/store/nodesSlice';
|
||||||
import generationReducer from 'features/parameters/store/generationSlice';
|
import generationReducer from 'features/parameters/store/generationSlice';
|
||||||
@ -43,9 +43,9 @@ const allReducers = {
|
|||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
hotkeys: hotkeysReducer,
|
hotkeys: hotkeysReducer,
|
||||||
controlNet: controlNetReducer,
|
controlNet: controlNetReducer,
|
||||||
boards: boardsReducer,
|
|
||||||
dynamicPrompts: dynamicPromptsReducer,
|
dynamicPrompts: dynamicPromptsReducer,
|
||||||
imageDeletion: imageDeletionReducer,
|
deleteImageModal: deleteImageModalReducer,
|
||||||
|
changeBoardModal: changeBoardModalReducer,
|
||||||
lora: loraReducer,
|
lora: loraReducer,
|
||||||
modelmanager: modelmanagerReducer,
|
modelmanager: modelmanagerReducer,
|
||||||
sdxl: sdxlReducer,
|
sdxl: sdxlReducer,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, Text, useColorMode } from '@chakra-ui/react';
|
import { Box, Flex, useColorMode } from '@chakra-ui/react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ReactNode, memo, useRef } from 'react';
|
import { ReactNode, memo, useRef } from 'react';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
@ -74,7 +74,7 @@ export const IAIDropOverlay = (props: Props) => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '2xl',
|
fontSize: '2xl',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -87,7 +87,7 @@ export const IAIDropOverlay = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
@ -53,7 +53,9 @@ const IAIMantineSearchableSelect = (props: IAISelectProps) => {
|
|||||||
// wrap onChange to clear search value on select
|
// wrap onChange to clear search value on select
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(v: string | null) => {
|
(v: string | null) => {
|
||||||
setSearchValue('');
|
// cannot figure out why we were doing this, but it was causing an issue where if you
|
||||||
|
// select the currently-selected item, it reset the search value to empty
|
||||||
|
// setSearchValue('');
|
||||||
|
|
||||||
if (!onChange) {
|
if (!onChange) {
|
||||||
return;
|
return;
|
||||||
|
@ -78,7 +78,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
board_id: autoAddBoardId,
|
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[autoAddBoardId, postUploadAction, uploadImage]
|
[autoAddBoardId, postUploadAction, uploadImage]
|
||||||
|
@ -49,7 +49,7 @@ export const useImageUploadButton = ({
|
|||||||
image_category: 'user',
|
image_category: 'user',
|
||||||
is_intermediate: false,
|
is_intermediate: false,
|
||||||
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
postUploadAction: postUploadAction ?? { type: 'TOAST' },
|
||||||
board_id: autoAddBoardId,
|
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[autoAddBoardId, postUploadAction, uploadImage]
|
[autoAddBoardId, postUploadAction, uploadImage]
|
||||||
|
@ -33,6 +33,10 @@ const useColorPicker = () => {
|
|||||||
1
|
1
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
|
if (!(a && r && g && b)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(setColorPickerColor({ r, g, b, a }));
|
dispatch(setColorPickerColor({ r, g, b, a }));
|
||||||
},
|
},
|
||||||
commitColorUnderCursor: () => {
|
commitColorUnderCursor: () => {
|
||||||
|
@ -727,10 +727,13 @@ export const canvasSlice = createSlice({
|
|||||||
state.pastLayerStates.shift();
|
state.pastLayerStates.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.layerState.objects.push({
|
const imageToCommit = images[selectedImageIndex];
|
||||||
...images[selectedImageIndex],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (imageToCommit) {
|
||||||
|
state.layerState.objects.push({
|
||||||
|
...imageToCommit,
|
||||||
|
});
|
||||||
|
}
|
||||||
state.layerState.stagingArea = {
|
state.layerState.stagingArea = {
|
||||||
...initialLayerState.stagingArea,
|
...initialLayerState.stagingArea,
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||||
|
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
|
import {
|
||||||
|
useAddImagesToBoardMutation,
|
||||||
|
useRemoveImagesFromBoardMutation,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
|
import { changeBoardReset, isModalOpenChanged } from '../store/slice';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
({ changeBoardModal }) => {
|
||||||
|
const { isModalOpen, imagesToChange } = changeBoardModal;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isModalOpen,
|
||||||
|
imagesToChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const ChangeBoardModal = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||||
|
const { data: boards, isFetching } = useListAllBoardsQuery();
|
||||||
|
const { imagesToChange, isModalOpen } = useAppSelector(selector);
|
||||||
|
const [addImagesToBoard] = useAddImagesToBoardMutation();
|
||||||
|
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
const data: { label: string; value: string }[] = [
|
||||||
|
{ label: 'Uncategorized', value: 'none' },
|
||||||
|
];
|
||||||
|
(boards ?? []).forEach((board) =>
|
||||||
|
data.push({
|
||||||
|
label: board.board_name,
|
||||||
|
value: board.board_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [boards]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
dispatch(changeBoardReset());
|
||||||
|
dispatch(isModalOpenChanged(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleChangeBoard = useCallback(() => {
|
||||||
|
if (!imagesToChange.length || !selectedBoard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedBoard === 'none') {
|
||||||
|
removeImagesFromBoard({ imageDTOs: imagesToChange });
|
||||||
|
} else {
|
||||||
|
addImagesToBoard({
|
||||||
|
imageDTOs: imagesToChange,
|
||||||
|
board_id: selectedBoard,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedBoard(null);
|
||||||
|
dispatch(changeBoardReset());
|
||||||
|
}, [
|
||||||
|
addImagesToBoard,
|
||||||
|
dispatch,
|
||||||
|
imagesToChange,
|
||||||
|
removeImagesFromBoard,
|
||||||
|
selectedBoard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
leastDestructiveRef={cancelRef}
|
||||||
|
isCentered
|
||||||
|
>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
Change Board
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
<Flex sx={{ flexDir: 'column', gap: 4 }}>
|
||||||
|
<Text>
|
||||||
|
Moving {`${imagesToChange.length}`} image
|
||||||
|
{`${imagesToChange.length > 1 ? 's' : ''}`} to board:
|
||||||
|
</Text>
|
||||||
|
<IAIMantineSearchableSelect
|
||||||
|
placeholder={isFetching ? 'Loading...' : 'Select Board'}
|
||||||
|
disabled={isFetching}
|
||||||
|
onChange={(v) => setSelectedBoard(v)}
|
||||||
|
value={selectedBoard}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialogBody>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<IAIButton ref={cancelRef} onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</IAIButton>
|
||||||
|
<IAIButton colorScheme="accent" onClick={handleChangeBoard} ml={3}>
|
||||||
|
Move
|
||||||
|
</IAIButton>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChangeBoardModal);
|
@ -0,0 +1,6 @@
|
|||||||
|
import { ChangeBoardModalState } from './types';
|
||||||
|
|
||||||
|
export const initialState: ChangeBoardModalState = {
|
||||||
|
isModalOpen: false,
|
||||||
|
imagesToChange: [],
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { initialState } from './initialState';
|
||||||
|
|
||||||
|
const changeBoardModal = createSlice({
|
||||||
|
name: 'changeBoardModal',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isModalOpen = action.payload;
|
||||||
|
},
|
||||||
|
imagesToChangeSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||||
|
state.imagesToChange = action.payload;
|
||||||
|
},
|
||||||
|
changeBoardReset: (state) => {
|
||||||
|
state.imagesToChange = [];
|
||||||
|
state.isModalOpen = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } =
|
||||||
|
changeBoardModal.actions;
|
||||||
|
|
||||||
|
export default changeBoardModal.reducer;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export type ChangeBoardModalState = {
|
||||||
|
isModalOpen: boolean;
|
||||||
|
imagesToChange: ImageDTO[];
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { FaCopy, FaTrash } from 'react-icons/fa';
|
import { FaCopy, FaTrash } from 'react-icons/fa';
|
||||||
import {
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
controlNetDuplicated,
|
controlNetDuplicated,
|
||||||
controlNetRemoved,
|
controlNetRemoved,
|
||||||
controlNetToggled,
|
controlNetToggled,
|
||||||
@ -27,18 +28,27 @@ import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcesso
|
|||||||
import ParamControlNetResizeMode from './parameters/ParamControlNetResizeMode';
|
import ParamControlNetResizeMode from './parameters/ParamControlNetResizeMode';
|
||||||
|
|
||||||
type ControlNetProps = {
|
type ControlNetProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlNet = (props: ControlNetProps) => {
|
const ControlNet = (props: ControlNetProps) => {
|
||||||
const { controlNetId } = props;
|
const { controlNet } = props;
|
||||||
|
const { controlNetId } = controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
stateSelector,
|
stateSelector,
|
||||||
({ controlNet }) => {
|
({ controlNet }) => {
|
||||||
const { isEnabled, shouldAutoConfig } =
|
const cn = controlNet.controlNets[controlNetId];
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
|
if (!cn) {
|
||||||
|
return {
|
||||||
|
isEnabled: false,
|
||||||
|
shouldAutoConfig: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isEnabled, shouldAutoConfig } = cn;
|
||||||
|
|
||||||
return { isEnabled, shouldAutoConfig };
|
return { isEnabled, shouldAutoConfig };
|
||||||
},
|
},
|
||||||
@ -96,7 +106,7 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ParamControlNetModel controlNetId={controlNetId} />
|
<ParamControlNetModel controlNet={controlNet} />
|
||||||
</Box>
|
</Box>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -171,8 +181,8 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ParamControlNetWeight controlNetId={controlNetId} />
|
<ParamControlNetWeight controlNet={controlNet} />
|
||||||
<ParamControlNetBeginEnd controlNetId={controlNetId} />
|
<ParamControlNetBeginEnd controlNet={controlNet} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<Flex
|
<Flex
|
||||||
@ -184,22 +194,22 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ControlNetImagePreview controlNetId={controlNetId} height={28} />
|
<ControlNetImagePreview controlNet={controlNet} height={28} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex sx={{ gap: 2 }}>
|
<Flex sx={{ gap: 2 }}>
|
||||||
<ParamControlNetControlMode controlNetId={controlNetId} />
|
<ParamControlNetControlMode controlNet={controlNet} />
|
||||||
<ParamControlNetResizeMode controlNetId={controlNetId} />
|
<ParamControlNetResizeMode controlNet={controlNet} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<ParamControlNetProcessorSelect controlNetId={controlNetId} />
|
<ParamControlNetProcessorSelect controlNet={controlNet} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<ControlNetImagePreview controlNetId={controlNetId} height="392px" />
|
<ControlNetImagePreview controlNet={controlNet} height="392px" />
|
||||||
<ParamControlNetShouldAutoConfig controlNetId={controlNetId} />
|
<ParamControlNetShouldAutoConfig controlNet={controlNet} />
|
||||||
<ControlNetProcessorComponent controlNetId={controlNetId} />
|
<ControlNetProcessorComponent controlNet={controlNet} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -12,50 +12,41 @@ import IAIDndImage from 'common/components/IAIDndImage';
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { PostUploadAction } from 'services/api/types';
|
import { PostUploadAction } from 'services/api/types';
|
||||||
import { controlNetImageChanged } from '../store/controlNetSlice';
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
|
controlNetImageChanged,
|
||||||
|
} from '../store/controlNetSlice';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
height: SystemStyleObject['h'];
|
height: SystemStyleObject['h'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlNetImagePreview = (props: Props) => {
|
const selector = createSelector(
|
||||||
const { height, controlNetId } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
stateSelector,
|
||||||
({ controlNet }) => {
|
({ controlNet }) => {
|
||||||
const { pendingControlImages } = controlNet;
|
const { pendingControlImages } = controlNet;
|
||||||
const {
|
|
||||||
controlImage,
|
|
||||||
processedControlImage,
|
|
||||||
processorType,
|
|
||||||
isEnabled,
|
|
||||||
} = controlNet.controlNets[controlNetId];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
controlImageName: controlImage,
|
|
||||||
processedControlImageName: processedControlImage,
|
|
||||||
processorType,
|
|
||||||
isEnabled,
|
|
||||||
pendingControlImages,
|
pendingControlImages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ControlNetImagePreview = (props: Props) => {
|
||||||
|
const { height } = props;
|
||||||
const {
|
const {
|
||||||
controlImageName,
|
controlImage: controlImageName,
|
||||||
processedControlImageName,
|
processedControlImage: processedControlImageName,
|
||||||
processorType,
|
processorType,
|
||||||
pendingControlImages,
|
|
||||||
isEnabled,
|
isEnabled,
|
||||||
} = useAppSelector(selector);
|
controlNetId,
|
||||||
|
} = props.controlNet;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { pendingControlImages } = useAppSelector(selector);
|
||||||
|
|
||||||
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { memo } from 'react';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { ControlNetConfig } from '../store/controlNetSlice';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import CannyProcessor from './processors/CannyProcessor';
|
import CannyProcessor from './processors/CannyProcessor';
|
||||||
import ContentShuffleProcessor from './processors/ContentShuffleProcessor';
|
import ContentShuffleProcessor from './processors/ContentShuffleProcessor';
|
||||||
import HedProcessor from './processors/HedProcessor';
|
import HedProcessor from './processors/HedProcessor';
|
||||||
@ -17,28 +14,11 @@ import PidiProcessor from './processors/PidiProcessor';
|
|||||||
import ZoeDepthProcessor from './processors/ZoeDepthProcessor';
|
import ZoeDepthProcessor from './processors/ZoeDepthProcessor';
|
||||||
|
|
||||||
export type ControlNetProcessorProps = {
|
export type ControlNetProcessorProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => {
|
const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => {
|
||||||
const { controlNetId } = props;
|
const { controlNetId, isEnabled, processorNode } = props.controlNet;
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { isEnabled, processorNode } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
|
|
||||||
return { isEnabled, processorNode };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isEnabled, processorNode } = useAppSelector(selector);
|
|
||||||
|
|
||||||
if (processorNode.type === 'canny_image_processor') {
|
if (processorNode.type === 'canny_image_processor') {
|
||||||
return (
|
return (
|
||||||
|
@ -1,34 +1,19 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAISwitch from 'common/components/IAISwitch';
|
import IAISwitch from 'common/components/IAISwitch';
|
||||||
import { controlNetAutoConfigToggled } from 'features/controlNet/store/controlNetSlice';
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
|
controlNetAutoConfigToggled,
|
||||||
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParamControlNetShouldAutoConfig = (props: Props) => {
|
const ParamControlNetShouldAutoConfig = (props: Props) => {
|
||||||
const { controlNetId } = props;
|
const { controlNetId, isEnabled, shouldAutoConfig } = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { isEnabled, shouldAutoConfig } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
return { isEnabled, shouldAutoConfig };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isEnabled, shouldAutoConfig } = useAppSelector(selector);
|
|
||||||
const isBusy = useAppSelector(selectIsBusy);
|
const isBusy = useAppSelector(selectIsBusy);
|
||||||
|
|
||||||
const handleShouldAutoConfigChanged = useCallback(() => {
|
const handleShouldAutoConfigChanged = useCallback(() => {
|
||||||
|
@ -9,48 +9,39 @@ import {
|
|||||||
RangeSliderTrack,
|
RangeSliderTrack,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import {
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
controlNetBeginStepPctChanged,
|
controlNetBeginStepPctChanged,
|
||||||
controlNetEndStepPctChanged,
|
controlNetEndStepPctChanged,
|
||||||
} from 'features/controlNet/store/controlNetSlice';
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
||||||
|
|
||||||
const ParamControlNetBeginEnd = (props: Props) => {
|
const ParamControlNetBeginEnd = (props: Props) => {
|
||||||
const { controlNetId } = props;
|
const { beginStepPct, endStepPct, isEnabled, controlNetId } =
|
||||||
|
props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { beginStepPct, endStepPct, isEnabled } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
return { beginStepPct, endStepPct, isEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { beginStepPct, endStepPct, isEnabled } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleStepPctChanged = useCallback(
|
const handleStepPctChanged = useCallback(
|
||||||
(v: number[]) => {
|
(v: number[]) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
controlNetBeginStepPctChanged({ controlNetId, beginStepPct: v[0] })
|
controlNetBeginStepPctChanged({
|
||||||
|
controlNetId,
|
||||||
|
beginStepPct: v[0] as number,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
controlNetEndStepPctChanged({
|
||||||
|
controlNetId,
|
||||||
|
endStepPct: v[1] as number,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: v[1] }));
|
|
||||||
},
|
},
|
||||||
[controlNetId, dispatch]
|
[controlNetId, dispatch]
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||||
import {
|
import {
|
||||||
ControlModes,
|
ControlModes,
|
||||||
|
ControlNetConfig,
|
||||||
controlNetControlModeChanged,
|
controlNetControlModeChanged,
|
||||||
} from 'features/controlNet/store/controlNetSlice';
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
type ParamControlNetControlModeProps = {
|
type ParamControlNetControlModeProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROL_MODE_DATA = [
|
const CONTROL_MODE_DATA = [
|
||||||
@ -23,23 +21,8 @@ const CONTROL_MODE_DATA = [
|
|||||||
export default function ParamControlNetControlMode(
|
export default function ParamControlNetControlMode(
|
||||||
props: ParamControlNetControlModeProps
|
props: ParamControlNetControlModeProps
|
||||||
) {
|
) {
|
||||||
const { controlNetId } = props;
|
const { controlMode, isEnabled, controlNetId } = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { controlMode, isEnabled } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
return { controlMode, isEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { controlMode, isEnabled } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleControlModeChange = useCallback(
|
const handleControlModeChange = useCallback(
|
||||||
(controlMode: ControlModes) => {
|
(controlMode: ControlModes) => {
|
||||||
|
@ -5,7 +5,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||||
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
||||||
import { controlNetModelChanged } from 'features/controlNet/store/controlNetSlice';
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
|
controlNetModelChanged,
|
||||||
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
||||||
import { modelIdToControlNetModelParam } from 'features/parameters/util/modelIdToControlNetModelParam';
|
import { modelIdToControlNetModelParam } from 'features/parameters/util/modelIdToControlNetModelParam';
|
||||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
||||||
@ -14,30 +17,24 @@ import { memo, useCallback, useMemo } from 'react';
|
|||||||
import { useGetControlNetModelsQuery } from 'services/api/endpoints/models';
|
import { useGetControlNetModelsQuery } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
type ParamControlNetModelProps = {
|
type ParamControlNetModelProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ generation }) => {
|
||||||
|
const { model } = generation;
|
||||||
|
return { mainModel: model };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
const ParamControlNetModel = (props: ParamControlNetModelProps) => {
|
const ParamControlNetModel = (props: ParamControlNetModelProps) => {
|
||||||
const { controlNetId } = props;
|
const { controlNetId, model: controlNetModel, isEnabled } = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isBusy = useAppSelector(selectIsBusy);
|
const isBusy = useAppSelector(selectIsBusy);
|
||||||
|
|
||||||
const selector = useMemo(
|
const { mainModel } = useAppSelector(selector);
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ generation, controlNet }) => {
|
|
||||||
const { model } = generation;
|
|
||||||
const controlNetModel = controlNet.controlNets[controlNetId]?.model;
|
|
||||||
const isEnabled = controlNet.controlNets[controlNetId]?.isEnabled;
|
|
||||||
return { mainModel: model, controlNetModel, isEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mainModel, controlNetModel, isEnabled } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const { data: controlNetModels } = useGetControlNetModelsQuery();
|
const { data: controlNetModels } = useGetControlNetModelsQuery();
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIMantineSearchableSelect, {
|
import IAIMantineSearchableSelect, {
|
||||||
IAISelectDataType,
|
IAISelectDataType,
|
||||||
@ -9,13 +8,16 @@ import IAIMantineSearchableSelect, {
|
|||||||
import { configSelector } from 'features/system/store/configSelectors';
|
import { configSelector } from 'features/system/store/configSelectors';
|
||||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { CONTROLNET_PROCESSORS } from '../../store/constants';
|
import { CONTROLNET_PROCESSORS } from '../../store/constants';
|
||||||
import { controlNetProcessorTypeChanged } from '../../store/controlNetSlice';
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
|
controlNetProcessorTypeChanged,
|
||||||
|
} from '../../store/controlNetSlice';
|
||||||
import { ControlNetProcessorType } from '../../store/types';
|
import { ControlNetProcessorType } from '../../store/types';
|
||||||
|
|
||||||
type ParamControlNetProcessorSelectProps = {
|
type ParamControlNetProcessorSelectProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
@ -52,23 +54,9 @@ const ParamControlNetProcessorSelect = (
|
|||||||
props: ParamControlNetProcessorSelectProps
|
props: ParamControlNetProcessorSelectProps
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { controlNetId } = props;
|
const { controlNetId, isEnabled, processorNode } = props.controlNet;
|
||||||
const processorNodeSelector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { isEnabled, processorNode } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
return { isEnabled, processorNode };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
const isBusy = useAppSelector(selectIsBusy);
|
const isBusy = useAppSelector(selectIsBusy);
|
||||||
const controlNetProcessors = useAppSelector(selector);
|
const controlNetProcessors = useAppSelector(selector);
|
||||||
const { isEnabled, processorNode } = useAppSelector(processorNodeSelector);
|
|
||||||
|
|
||||||
const handleProcessorTypeChanged = useCallback(
|
const handleProcessorTypeChanged = useCallback(
|
||||||
(v: string | null) => {
|
(v: string | null) => {
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||||
import {
|
import {
|
||||||
|
ControlNetConfig,
|
||||||
ResizeModes,
|
ResizeModes,
|
||||||
controlNetResizeModeChanged,
|
controlNetResizeModeChanged,
|
||||||
} from 'features/controlNet/store/controlNetSlice';
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
type ParamControlNetResizeModeProps = {
|
type ParamControlNetResizeModeProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESIZE_MODE_DATA = [
|
const RESIZE_MODE_DATA = [
|
||||||
@ -22,23 +20,8 @@ const RESIZE_MODE_DATA = [
|
|||||||
export default function ParamControlNetResizeMode(
|
export default function ParamControlNetResizeMode(
|
||||||
props: ParamControlNetResizeModeProps
|
props: ParamControlNetResizeModeProps
|
||||||
) {
|
) {
|
||||||
const { controlNetId } = props;
|
const { resizeMode, isEnabled, controlNetId } = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { resizeMode, isEnabled } =
|
|
||||||
controlNet.controlNets[controlNetId];
|
|
||||||
return { resizeMode, isEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { resizeMode, isEnabled } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleResizeModeChange = useCallback(
|
const handleResizeModeChange = useCallback(
|
||||||
(resizeMode: ResizeModes) => {
|
(resizeMode: ResizeModes) => {
|
||||||
|
@ -1,32 +1,18 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import { controlNetWeightChanged } from 'features/controlNet/store/controlNetSlice';
|
import {
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
ControlNetConfig,
|
||||||
|
controlNetWeightChanged,
|
||||||
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type ParamControlNetWeightProps = {
|
type ParamControlNetWeightProps = {
|
||||||
controlNetId: string;
|
controlNet: ControlNetConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
|
const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
|
||||||
const { controlNetId } = props;
|
const { weight, isEnabled, controlNetId } = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ controlNet }) => {
|
|
||||||
const { weight, isEnabled } = controlNet.controlNets[controlNetId];
|
|
||||||
return { weight, isEnabled };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[controlNetId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { weight, isEnabled } = useAppSelector(selector);
|
|
||||||
const handleWeightChanged = useCallback(
|
const handleWeightChanged = useCallback(
|
||||||
(weight: number) => {
|
(weight: number) => {
|
||||||
dispatch(controlNetWeightChanged({ controlNetId, weight }));
|
dispatch(controlNetWeightChanged({ controlNetId, weight }));
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
type ControlNetProcessorsDict = Record<
|
type ControlNetProcessorsDict = Record<
|
||||||
string,
|
ControlNetProcessorType,
|
||||||
{
|
{
|
||||||
type: ControlNetProcessorType | 'none';
|
type: ControlNetProcessorType | 'none';
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -96,8 +96,11 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { sourceControlNetId, newControlNetId } = action.payload;
|
const { sourceControlNetId, newControlNetId } = action.payload;
|
||||||
|
const oldControlNet = state.controlNets[sourceControlNetId];
|
||||||
const newControlnet = cloneDeep(state.controlNets[sourceControlNetId]);
|
if (!oldControlNet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newControlnet = cloneDeep(oldControlNet);
|
||||||
newControlnet.controlNetId = newControlNetId;
|
newControlnet.controlNetId = newControlNetId;
|
||||||
state.controlNets[newControlNetId] = newControlnet;
|
state.controlNets[newControlNetId] = newControlnet;
|
||||||
},
|
},
|
||||||
@ -124,8 +127,11 @@ export const controlNetSlice = createSlice({
|
|||||||
action: PayloadAction<{ controlNetId: string }>
|
action: PayloadAction<{ controlNetId: string }>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId } = action.payload;
|
const { controlNetId } = action.payload;
|
||||||
state.controlNets[controlNetId].isEnabled =
|
const cn = state.controlNets[controlNetId];
|
||||||
!state.controlNets[controlNetId].isEnabled;
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cn.isEnabled = !cn.isEnabled;
|
||||||
},
|
},
|
||||||
controlNetImageChanged: (
|
controlNetImageChanged: (
|
||||||
state,
|
state,
|
||||||
@ -135,12 +141,14 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, controlImage } = action.payload;
|
const { controlNetId, controlImage } = action.payload;
|
||||||
state.controlNets[controlNetId].controlImage = controlImage;
|
const cn = state.controlNets[controlNetId];
|
||||||
state.controlNets[controlNetId].processedControlImage = null;
|
if (!cn) {
|
||||||
if (
|
return;
|
||||||
controlImage !== null &&
|
}
|
||||||
state.controlNets[controlNetId].processorType !== 'none'
|
|
||||||
) {
|
cn.controlImage = controlImage;
|
||||||
|
cn.processedControlImage = null;
|
||||||
|
if (controlImage !== null && cn.processorType !== 'none') {
|
||||||
state.pendingControlImages.push(controlNetId);
|
state.pendingControlImages.push(controlNetId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -152,8 +160,12 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, processedControlImage } = action.payload;
|
const { controlNetId, processedControlImage } = action.payload;
|
||||||
state.controlNets[controlNetId].processedControlImage =
|
const cn = state.controlNets[controlNetId];
|
||||||
processedControlImage;
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.processedControlImage = processedControlImage;
|
||||||
state.pendingControlImages = state.pendingControlImages.filter(
|
state.pendingControlImages = state.pendingControlImages.filter(
|
||||||
(id) => id !== controlNetId
|
(id) => id !== controlNetId
|
||||||
);
|
);
|
||||||
@ -166,10 +178,15 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, model } = action.payload;
|
const { controlNetId, model } = action.payload;
|
||||||
state.controlNets[controlNetId].model = model;
|
const cn = state.controlNets[controlNetId];
|
||||||
state.controlNets[controlNetId].processedControlImage = null;
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.controlNets[controlNetId].shouldAutoConfig) {
|
cn.model = model;
|
||||||
|
cn.processedControlImage = null;
|
||||||
|
|
||||||
|
if (cn.shouldAutoConfig) {
|
||||||
let processorType: ControlNetProcessorType | undefined = undefined;
|
let processorType: ControlNetProcessorType | undefined = undefined;
|
||||||
|
|
||||||
for (const modelSubstring in CONTROLNET_MODEL_DEFAULT_PROCESSORS) {
|
for (const modelSubstring in CONTROLNET_MODEL_DEFAULT_PROCESSORS) {
|
||||||
@ -180,14 +197,13 @@ export const controlNetSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (processorType) {
|
if (processorType) {
|
||||||
state.controlNets[controlNetId].processorType = processorType;
|
cn.processorType = processorType;
|
||||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
cn.processorNode = CONTROLNET_PROCESSORS[processorType]
|
||||||
processorType
|
.default as RequiredControlNetProcessorNode;
|
||||||
].default as RequiredControlNetProcessorNode;
|
|
||||||
} else {
|
} else {
|
||||||
state.controlNets[controlNetId].processorType = 'none';
|
cn.processorType = 'none';
|
||||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS
|
cn.processorNode = CONTROLNET_PROCESSORS.none
|
||||||
.none.default as RequiredControlNetProcessorNode;
|
.default as RequiredControlNetProcessorNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -196,28 +212,48 @@ export const controlNetSlice = createSlice({
|
|||||||
action: PayloadAction<{ controlNetId: string; weight: number }>
|
action: PayloadAction<{ controlNetId: string; weight: number }>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, weight } = action.payload;
|
const { controlNetId, weight } = action.payload;
|
||||||
state.controlNets[controlNetId].weight = weight;
|
const cn = state.controlNets[controlNetId];
|
||||||
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.weight = weight;
|
||||||
},
|
},
|
||||||
controlNetBeginStepPctChanged: (
|
controlNetBeginStepPctChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ controlNetId: string; beginStepPct: number }>
|
action: PayloadAction<{ controlNetId: string; beginStepPct: number }>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, beginStepPct } = action.payload;
|
const { controlNetId, beginStepPct } = action.payload;
|
||||||
state.controlNets[controlNetId].beginStepPct = beginStepPct;
|
const cn = state.controlNets[controlNetId];
|
||||||
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.beginStepPct = beginStepPct;
|
||||||
},
|
},
|
||||||
controlNetEndStepPctChanged: (
|
controlNetEndStepPctChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ controlNetId: string; endStepPct: number }>
|
action: PayloadAction<{ controlNetId: string; endStepPct: number }>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, endStepPct } = action.payload;
|
const { controlNetId, endStepPct } = action.payload;
|
||||||
state.controlNets[controlNetId].endStepPct = endStepPct;
|
const cn = state.controlNets[controlNetId];
|
||||||
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.endStepPct = endStepPct;
|
||||||
},
|
},
|
||||||
controlNetControlModeChanged: (
|
controlNetControlModeChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ controlNetId: string; controlMode: ControlModes }>
|
action: PayloadAction<{ controlNetId: string; controlMode: ControlModes }>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, controlMode } = action.payload;
|
const { controlNetId, controlMode } = action.payload;
|
||||||
state.controlNets[controlNetId].controlMode = controlMode;
|
const cn = state.controlNets[controlNetId];
|
||||||
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.controlMode = controlMode;
|
||||||
},
|
},
|
||||||
controlNetResizeModeChanged: (
|
controlNetResizeModeChanged: (
|
||||||
state,
|
state,
|
||||||
@ -227,7 +263,12 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, resizeMode } = action.payload;
|
const { controlNetId, resizeMode } = action.payload;
|
||||||
state.controlNets[controlNetId].resizeMode = resizeMode;
|
const cn = state.controlNets[controlNetId];
|
||||||
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.resizeMode = resizeMode;
|
||||||
},
|
},
|
||||||
controlNetProcessorParamsChanged: (
|
controlNetProcessorParamsChanged: (
|
||||||
state,
|
state,
|
||||||
@ -240,12 +281,17 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, changes } = action.payload;
|
const { controlNetId, changes } = action.payload;
|
||||||
const processorNode = state.controlNets[controlNetId].processorNode;
|
const cn = state.controlNets[controlNetId];
|
||||||
state.controlNets[controlNetId].processorNode = {
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processorNode = cn.processorNode;
|
||||||
|
cn.processorNode = {
|
||||||
...processorNode,
|
...processorNode,
|
||||||
...changes,
|
...changes,
|
||||||
};
|
};
|
||||||
state.controlNets[controlNetId].shouldAutoConfig = false;
|
cn.shouldAutoConfig = false;
|
||||||
},
|
},
|
||||||
controlNetProcessorTypeChanged: (
|
controlNetProcessorTypeChanged: (
|
||||||
state,
|
state,
|
||||||
@ -255,12 +301,16 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId, processorType } = action.payload;
|
const { controlNetId, processorType } = action.payload;
|
||||||
state.controlNets[controlNetId].processedControlImage = null;
|
const cn = state.controlNets[controlNetId];
|
||||||
state.controlNets[controlNetId].processorType = processorType;
|
if (!cn) {
|
||||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
return;
|
||||||
processorType
|
}
|
||||||
].default as RequiredControlNetProcessorNode;
|
|
||||||
state.controlNets[controlNetId].shouldAutoConfig = false;
|
cn.processedControlImage = null;
|
||||||
|
cn.processorType = processorType;
|
||||||
|
cn.processorNode = CONTROLNET_PROCESSORS[processorType]
|
||||||
|
.default as RequiredControlNetProcessorNode;
|
||||||
|
cn.shouldAutoConfig = false;
|
||||||
},
|
},
|
||||||
controlNetAutoConfigToggled: (
|
controlNetAutoConfigToggled: (
|
||||||
state,
|
state,
|
||||||
@ -269,37 +319,36 @@ export const controlNetSlice = createSlice({
|
|||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { controlNetId } = action.payload;
|
const { controlNetId } = action.payload;
|
||||||
const newShouldAutoConfig =
|
const cn = state.controlNets[controlNetId];
|
||||||
!state.controlNets[controlNetId].shouldAutoConfig;
|
if (!cn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newShouldAutoConfig = !cn.shouldAutoConfig;
|
||||||
|
|
||||||
if (newShouldAutoConfig) {
|
if (newShouldAutoConfig) {
|
||||||
// manage the processor for the user
|
// manage the processor for the user
|
||||||
let processorType: ControlNetProcessorType | undefined = undefined;
|
let processorType: ControlNetProcessorType | undefined = undefined;
|
||||||
|
|
||||||
for (const modelSubstring in CONTROLNET_MODEL_DEFAULT_PROCESSORS) {
|
for (const modelSubstring in CONTROLNET_MODEL_DEFAULT_PROCESSORS) {
|
||||||
if (
|
if (cn.model?.model_name.includes(modelSubstring)) {
|
||||||
state.controlNets[controlNetId].model?.model_name.includes(
|
|
||||||
modelSubstring
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
processorType = CONTROLNET_MODEL_DEFAULT_PROCESSORS[modelSubstring];
|
processorType = CONTROLNET_MODEL_DEFAULT_PROCESSORS[modelSubstring];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processorType) {
|
if (processorType) {
|
||||||
state.controlNets[controlNetId].processorType = processorType;
|
cn.processorType = processorType;
|
||||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
cn.processorNode = CONTROLNET_PROCESSORS[processorType]
|
||||||
processorType
|
.default as RequiredControlNetProcessorNode;
|
||||||
].default as RequiredControlNetProcessorNode;
|
|
||||||
} else {
|
} else {
|
||||||
state.controlNets[controlNetId].processorType = 'none';
|
cn.processorType = 'none';
|
||||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS
|
cn.processorNode = CONTROLNET_PROCESSORS.none
|
||||||
.none.default as RequiredControlNetProcessorNode;
|
.default as RequiredControlNetProcessorNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.controlNets[controlNetId].shouldAutoConfig = newShouldAutoConfig;
|
cn.shouldAutoConfig = newShouldAutoConfig;
|
||||||
},
|
},
|
||||||
controlNetReset: () => {
|
controlNetReset: () => {
|
||||||
return { ...initialControlNetState };
|
return { ...initialControlNetState };
|
||||||
@ -307,9 +356,11 @@ export const controlNetSlice = createSlice({
|
|||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(controlNetImageProcessed, (state, action) => {
|
builder.addCase(controlNetImageProcessed, (state, action) => {
|
||||||
if (
|
const cn = state.controlNets[action.payload.controlNetId];
|
||||||
state.controlNets[action.payload.controlNetId].controlImage !== null
|
if (!cn) {
|
||||||
) {
|
return;
|
||||||
|
}
|
||||||
|
if (cn.controlImage !== null) {
|
||||||
state.pendingControlImages.push(action.payload.controlNetId);
|
state.pendingControlImages.push(action.payload.controlNetId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -15,30 +15,42 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import IAISwitch from 'common/components/IAISwitch';
|
import IAISwitch from 'common/components/IAISwitch';
|
||||||
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { some } from 'lodash-es';
|
||||||
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
import { ChangeEvent, memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { imageDeletionConfirmed } from '../store/actions';
|
import { imageDeletionConfirmed } from '../store/actions';
|
||||||
import { selectImageUsage } from '../store/imageDeletionSelectors';
|
import { getImageUsage, selectImageUsage } from '../store/selectors';
|
||||||
import {
|
import { imageDeletionCanceled, isModalOpenChanged } from '../store/slice';
|
||||||
imageToDeleteCleared,
|
|
||||||
isModalOpenChanged,
|
|
||||||
} from '../store/imageDeletionSlice';
|
|
||||||
import ImageUsageMessage from './ImageUsageMessage';
|
import ImageUsageMessage from './ImageUsageMessage';
|
||||||
|
import { ImageUsage } from '../store/types';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector, selectImageUsage],
|
[stateSelector, selectImageUsage],
|
||||||
({ system, config, imageDeletion }, imageUsage) => {
|
(state, imagesUsage) => {
|
||||||
|
const { system, config, deleteImageModal } = state;
|
||||||
const { shouldConfirmOnDelete } = system;
|
const { shouldConfirmOnDelete } = system;
|
||||||
const { canRestoreDeletedImagesFromBin } = config;
|
const { canRestoreDeletedImagesFromBin } = config;
|
||||||
const { imageToDelete, isModalOpen } = imageDeletion;
|
const { imagesToDelete, isModalOpen } = deleteImageModal;
|
||||||
|
|
||||||
|
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
|
||||||
|
getImageUsage(state, image_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageUsageSummary: ImageUsage = {
|
||||||
|
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
|
||||||
|
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
|
||||||
|
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
|
||||||
|
isControlNetImage: some(allImageUsage, (i) => i.isControlNetImage),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shouldConfirmOnDelete,
|
shouldConfirmOnDelete,
|
||||||
canRestoreDeletedImagesFromBin,
|
canRestoreDeletedImagesFromBin,
|
||||||
imageToDelete,
|
imagesToDelete,
|
||||||
imageUsage,
|
imagesUsage,
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
|
imageUsageSummary,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -51,9 +63,10 @@ const DeleteImageModal = () => {
|
|||||||
const {
|
const {
|
||||||
shouldConfirmOnDelete,
|
shouldConfirmOnDelete,
|
||||||
canRestoreDeletedImagesFromBin,
|
canRestoreDeletedImagesFromBin,
|
||||||
imageToDelete,
|
imagesToDelete,
|
||||||
imageUsage,
|
imagesUsage,
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
|
imageUsageSummary,
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||||
@ -63,17 +76,19 @@ const DeleteImageModal = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
dispatch(imageToDeleteCleared());
|
dispatch(imageDeletionCanceled());
|
||||||
dispatch(isModalOpenChanged(false));
|
dispatch(isModalOpenChanged(false));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!imageToDelete || !imageUsage) {
|
if (!imagesToDelete.length || !imagesUsage.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteCleared());
|
dispatch(imageDeletionCanceled());
|
||||||
dispatch(imageDeletionConfirmed({ imageDTO: imageToDelete, imageUsage }));
|
dispatch(
|
||||||
}, [dispatch, imageToDelete, imageUsage]);
|
imageDeletionConfirmed({ imageDTOs: imagesToDelete, imagesUsage })
|
||||||
|
);
|
||||||
|
}, [dispatch, imagesToDelete, imagesUsage]);
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@ -92,7 +107,7 @@ const DeleteImageModal = () => {
|
|||||||
|
|
||||||
<AlertDialogBody>
|
<AlertDialogBody>
|
||||||
<Flex direction="column" gap={3}>
|
<Flex direction="column" gap={3}>
|
||||||
<ImageUsageMessage imageUsage={imageUsage} />
|
<ImageUsageMessage imageUsage={imageUsageSummary} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text>
|
<Text>
|
||||||
{canRestoreDeletedImagesFromBin
|
{canRestoreDeletedImagesFromBin
|
@ -3,6 +3,6 @@ import { ImageDTO } from 'services/api/types';
|
|||||||
import { ImageUsage } from './types';
|
import { ImageUsage } from './types';
|
||||||
|
|
||||||
export const imageDeletionConfirmed = createAction<{
|
export const imageDeletionConfirmed = createAction<{
|
||||||
imageDTO: ImageDTO;
|
imageDTOs: ImageDTO[];
|
||||||
imageUsage: ImageUsage;
|
imagesUsage: ImageUsage[];
|
||||||
}>('imageDeletion/imageDeletionConfirmed');
|
}>('deleteImageModal/imageDeletionConfirmed');
|
@ -0,0 +1,6 @@
|
|||||||
|
import { DeleteImageState } from './types';
|
||||||
|
|
||||||
|
export const initialDeleteImageState: DeleteImageState = {
|
||||||
|
imagesToDelete: [],
|
||||||
|
isModalOpen: false,
|
||||||
|
};
|
@ -39,17 +39,17 @@ export const getImageUsage = (state: RootState, image_name: string) => {
|
|||||||
export const selectImageUsage = createSelector(
|
export const selectImageUsage = createSelector(
|
||||||
[(state: RootState) => state],
|
[(state: RootState) => state],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { imageToDelete } = state.imageDeletion;
|
const { imagesToDelete } = state.deleteImageModal;
|
||||||
|
|
||||||
if (!imageToDelete) {
|
if (!imagesToDelete.length) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { image_name } = imageToDelete;
|
const imagesUsage = imagesToDelete.map((i) =>
|
||||||
|
getImageUsage(state, i.image_name)
|
||||||
|
);
|
||||||
|
|
||||||
const imageUsage = getImageUsage(state, image_name);
|
return imagesUsage;
|
||||||
|
|
||||||
return imageUsage;
|
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
@ -0,0 +1,28 @@
|
|||||||
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { initialDeleteImageState } from './initialState';
|
||||||
|
|
||||||
|
const deleteImageModal = createSlice({
|
||||||
|
name: 'deleteImageModal',
|
||||||
|
initialState: initialDeleteImageState,
|
||||||
|
reducers: {
|
||||||
|
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isModalOpen = action.payload;
|
||||||
|
},
|
||||||
|
imagesToDeleteSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||||
|
state.imagesToDelete = action.payload;
|
||||||
|
},
|
||||||
|
imageDeletionCanceled: (state) => {
|
||||||
|
state.imagesToDelete = [];
|
||||||
|
state.isModalOpen = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
isModalOpenChanged,
|
||||||
|
imagesToDeleteSelected,
|
||||||
|
imageDeletionCanceled,
|
||||||
|
} = deleteImageModal.actions;
|
||||||
|
|
||||||
|
export default deleteImageModal.reducer;
|
@ -0,0 +1,13 @@
|
|||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export type DeleteImageState = {
|
||||||
|
imagesToDelete: ImageDTO[];
|
||||||
|
isModalOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageUsage = {
|
||||||
|
isInitialImage: boolean;
|
||||||
|
isCanvasImage: boolean;
|
||||||
|
isNodesImage: boolean;
|
||||||
|
isControlNetImage: boolean;
|
||||||
|
};
|
@ -56,7 +56,7 @@ const BoardAutoAddSelect = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
|
dispatch(autoAddBoardIdChanged(v));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -11,10 +11,11 @@ import { BoardDTO } from 'services/api/types';
|
|||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||||
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
||||||
|
import { BoardId } from 'features/gallery/store/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board?: BoardDTO;
|
board?: BoardDTO;
|
||||||
board_id?: string;
|
board_id: BoardId;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||||
};
|
};
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { AddToBatchDropData } from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { FaLayerGroup } from 'react-icons/fa';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import GenericBoard from './GenericBoard';
|
|
||||||
|
|
||||||
const selector = createSelector(stateSelector, (state) => {
|
|
||||||
return {
|
|
||||||
count: state.gallery.batchImageNames.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { count } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleBatchBoardClick = useCallback(() => {
|
|
||||||
dispatch(boardIdSelected('batch'));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const droppableData: AddToBatchDropData = {
|
|
||||||
id: 'batch-board',
|
|
||||||
actionType: 'ADD_TO_BATCH',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GenericBoard
|
|
||||||
board_id="batch"
|
|
||||||
droppableData={droppableData}
|
|
||||||
onClick={handleBatchBoardClick}
|
|
||||||
isSelected={isSelected}
|
|
||||||
icon={FaLayerGroup}
|
|
||||||
label="Batch"
|
|
||||||
badgeCount={count}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BatchBoard;
|
|
@ -15,10 +15,9 @@ import NoBoardBoard from './NoBoardBoard';
|
|||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ boards, gallery }) => {
|
({ gallery }) => {
|
||||||
const { searchText } = boards;
|
const { selectedBoardId, boardSearchText } = gallery;
|
||||||
const { selectedBoardId } = gallery;
|
return { selectedBoardId, boardSearchText };
|
||||||
return { selectedBoardId, searchText };
|
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
@ -29,11 +28,11 @@ type Props = {
|
|||||||
|
|
||||||
const BoardsList = (props: Props) => {
|
const BoardsList = (props: Props) => {
|
||||||
const { isOpen } = props;
|
const { isOpen } = props;
|
||||||
const { selectedBoardId, searchText } = useAppSelector(selector);
|
const { selectedBoardId, boardSearchText } = useAppSelector(selector);
|
||||||
const { data: boards } = useListAllBoardsQuery();
|
const { data: boards } = useListAllBoardsQuery();
|
||||||
const filteredBoards = searchText
|
const filteredBoards = boardSearchText
|
||||||
? boards?.filter((board) =>
|
? boards?.filter((board) =>
|
||||||
board.board_name.toLowerCase().includes(searchText.toLowerCase())
|
board.board_name.toLowerCase().includes(boardSearchText.toLowerCase())
|
||||||
)
|
)
|
||||||
: boards;
|
: boards;
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
@ -75,7 +74,7 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
<GridItem sx={{ p: 1.5 }}>
|
||||||
<NoBoardBoard isSelected={selectedBoardId === undefined} />
|
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
|
@ -9,7 +9,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
@ -21,27 +21,27 @@ import {
|
|||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ boards }) => {
|
({ gallery }) => {
|
||||||
const { searchText } = boards;
|
const { boardSearchText } = gallery;
|
||||||
return { searchText };
|
return { boardSearchText };
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const BoardsSearch = () => {
|
const BoardsSearch = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { searchText } = useAppSelector(selector);
|
const { boardSearchText } = useAppSelector(selector);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleBoardSearch = useCallback(
|
const handleBoardSearch = useCallback(
|
||||||
(searchTerm: string) => {
|
(searchTerm: string) => {
|
||||||
dispatch(setBoardSearchText(searchTerm));
|
dispatch(boardSearchTextChanged(searchTerm));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearBoardSearch = useCallback(() => {
|
const clearBoardSearch = useCallback(() => {
|
||||||
dispatch(setBoardSearchText(''));
|
dispatch(boardSearchTextChanged(''));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleKeydown = useCallback(
|
const handleKeydown = useCallback(
|
||||||
@ -74,11 +74,11 @@ const BoardsSearch = () => {
|
|||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder="Search Boards..."
|
placeholder="Search Boards..."
|
||||||
value={searchText}
|
value={boardSearchText}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
{searchText && searchText.length && (
|
{boardSearchText && boardSearchText.length && (
|
||||||
<InputRightElement>
|
<InputRightElement>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={clearBoardSearch}
|
onClick={clearBoardSearch}
|
||||||
|
@ -7,10 +7,11 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Image,
|
Image,
|
||||||
Text,
|
Text,
|
||||||
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
import { AddToBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
@ -22,7 +23,11 @@ import {
|
|||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FaUser } from 'react-icons/fa';
|
import { FaUser } from 'react-icons/fa';
|
||||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
import {
|
||||||
|
useGetBoardAssetsTotalQuery,
|
||||||
|
useGetBoardImagesTotalQuery,
|
||||||
|
useUpdateBoardMutation,
|
||||||
|
} from 'services/api/endpoints/boards';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
import AutoAddIcon from '../AutoAddIcon';
|
import AutoAddIcon from '../AutoAddIcon';
|
||||||
@ -67,6 +72,18 @@ const GalleryBoard = memo(
|
|||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
|
||||||
|
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
|
||||||
|
const tooltip = useMemo(() => {
|
||||||
|
if (!imagesTotal || !assetsTotal) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${imagesTotal} image${
|
||||||
|
imagesTotal > 1 ? 's' : ''
|
||||||
|
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`;
|
||||||
|
}, [assetsTotal, imagesTotal]);
|
||||||
|
|
||||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||||
board.cover_image_name ?? skipToken
|
board.cover_image_name ?? skipToken
|
||||||
);
|
);
|
||||||
@ -84,10 +101,10 @@ const GalleryBoard = memo(
|
|||||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||||
useUpdateBoardMutation();
|
useUpdateBoardMutation();
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = useMemo(
|
const droppableData: AddToBoardDropData = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
id: board_id,
|
id: board_id,
|
||||||
actionType: 'MOVE_BOARD',
|
actionType: 'ADD_TO_BOARD',
|
||||||
context: { boardId: board_id },
|
context: { boardId: board_id },
|
||||||
}),
|
}),
|
||||||
[board_id]
|
[board_id]
|
||||||
@ -148,6 +165,7 @@ const GalleryBoard = memo(
|
|||||||
setBoardToDelete={setBoardToDelete}
|
setBoardToDelete={setBoardToDelete}
|
||||||
>
|
>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
|
<Tooltip label={tooltip} openDelay={1000} hasArrow>
|
||||||
<Flex
|
<Flex
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleSelectBoard}
|
onClick={handleSelectBoard}
|
||||||
@ -278,6 +296,7 @@ const GalleryBoard = memo(
|
|||||||
dropLabel={<Text fontSize="md">Move</Text>}
|
dropLabel={<Text fontSize="md">Move</Text>}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</BoardContextMenu>
|
</BoardContextMenu>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Box, Flex, Image, Text } from '@chakra-ui/react';
|
import { Box, Flex, Image, Text } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
import { RemoveFromBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
@ -15,6 +15,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
|
|||||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
import AutoAddIcon from '../AutoAddIcon';
|
import AutoAddIcon from '../AutoAddIcon';
|
||||||
import BoardContextMenu from '../BoardContextMenu';
|
import BoardContextMenu from '../BoardContextMenu';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
@ -33,26 +34,27 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { autoAddBoardId, autoAssignBoardOnClick, isProcessing } =
|
const { autoAddBoardId, autoAssignBoardOnClick, isProcessing } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
const boardName = useBoardName(undefined);
|
const boardName = useBoardName('none');
|
||||||
const handleSelectBoard = useCallback(() => {
|
const handleSelectBoard = useCallback(() => {
|
||||||
dispatch(boardIdSelected(undefined));
|
dispatch(boardIdSelected('none'));
|
||||||
if (autoAssignBoardOnClick && !isProcessing) {
|
if (autoAssignBoardOnClick && !isProcessing) {
|
||||||
dispatch(autoAddBoardIdChanged(undefined));
|
dispatch(autoAddBoardIdChanged('none'));
|
||||||
}
|
}
|
||||||
}, [dispatch, autoAssignBoardOnClick, isProcessing]);
|
}, [dispatch, autoAssignBoardOnClick, isProcessing]);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const handleMouseOver = useCallback(() => {
|
const handleMouseOver = useCallback(() => {
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = useMemo(
|
const droppableData: RemoveFromBoardDropData = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
id: 'no_board',
|
id: 'no_board',
|
||||||
actionType: 'MOVE_BOARD',
|
actionType: 'REMOVE_FROM_BOARD',
|
||||||
context: { boardId: undefined },
|
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@ -72,7 +74,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
h: 'full',
|
h: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BoardContextMenu>
|
<BoardContextMenu board_id="none">
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Flex
|
<Flex
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -99,17 +101,6 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <Icon
|
|
||||||
boxSize={12}
|
|
||||||
as={FaBucket}
|
|
||||||
sx={{
|
|
||||||
opacity: 0.7,
|
|
||||||
color: 'base.500',
|
|
||||||
_dark: {
|
|
||||||
color: 'base.500',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
<Image
|
<Image
|
||||||
src={InvokeAILogoImage}
|
src={InvokeAILogoImage}
|
||||||
alt="invoke-ai-logo"
|
alt="invoke-ai-logo"
|
||||||
@ -125,19 +116,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* <Flex
|
{autoAddBoardId === 'none' && <AutoAddIcon />}
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
insetInlineEnd: 0,
|
|
||||||
top: 0,
|
|
||||||
p: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Badge variant="solid" sx={BASE_BADGE_STYLES}>
|
|
||||||
{totalImages}/{totalAssets}
|
|
||||||
</Badge>
|
|
||||||
</Flex> */}
|
|
||||||
{!autoAddBoardId && <AutoAddIcon />}
|
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -11,20 +11,20 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { ImageUsage } from 'app/contexts/AddImageToBoardContext';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import ImageUsageMessage from 'features/imageDeletion/components/ImageUsageMessage';
|
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
|
||||||
import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
|
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||||
|
import { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||||
import { some } from 'lodash-es';
|
import { some } from 'lodash-es';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useListAllImageNamesForBoardQuery } from 'services/api/endpoints/boards';
|
||||||
import {
|
import {
|
||||||
useDeleteBoardAndImagesMutation,
|
useDeleteBoardAndImagesMutation,
|
||||||
useDeleteBoardMutation,
|
useDeleteBoardMutation,
|
||||||
useListAllImageNamesForBoardQuery,
|
} from 'services/api/endpoints/images';
|
||||||
} from 'services/api/endpoints/boards';
|
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -32,7 +32,7 @@ type Props = {
|
|||||||
setBoardToDelete: (board?: BoardDTO) => void;
|
setBoardToDelete: (board?: BoardDTO) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteImageModal = (props: Props) => {
|
const DeleteBoardModal = (props: Props) => {
|
||||||
const { boardToDelete, setBoardToDelete } = props;
|
const { boardToDelete, setBoardToDelete } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canRestoreDeletedImagesFromBin = useAppSelector(
|
const canRestoreDeletedImagesFromBin = useAppSelector(
|
||||||
@ -49,13 +49,10 @@ const DeleteImageModal = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const imageUsageSummary: ImageUsage = {
|
const imageUsageSummary: ImageUsage = {
|
||||||
isInitialImage: some(allImageUsage, (usage) => usage.isInitialImage),
|
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
|
||||||
isCanvasImage: some(allImageUsage, (usage) => usage.isCanvasImage),
|
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
|
||||||
isNodesImage: some(allImageUsage, (usage) => usage.isNodesImage),
|
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
|
||||||
isControlNetImage: some(
|
isControlNetImage: some(allImageUsage, (i) => i.isControlNetImage),
|
||||||
allImageUsage,
|
|
||||||
(usage) => usage.isControlNetImage
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
return { imageUsageSummary };
|
return { imageUsageSummary };
|
||||||
}),
|
}),
|
||||||
@ -176,4 +173,4 @@ const DeleteImageModal = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(DeleteImageModal);
|
export default memo(DeleteBoardModal);
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import IAIButton from 'common/components/IAIButton';
|
|
||||||
|
|
||||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
|
||||||
import { memo, useContext, useRef, useState } from 'react';
|
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
|
||||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
|
||||||
|
|
||||||
const UpdateImageBoardModal = () => {
|
|
||||||
// const boards = useSelector(selectBoardsAll);
|
|
||||||
const { data: boards, isFetching } = useListAllBoardsQuery();
|
|
||||||
const { isOpen, onClose, handleAddToBoard, image } = useContext(
|
|
||||||
AddImageToBoardContext
|
|
||||||
);
|
|
||||||
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const currentBoard = boards?.find(
|
|
||||||
(board) => board.board_id === image?.board_id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog
|
|
||||||
isOpen={isOpen}
|
|
||||||
leastDestructiveRef={cancelRef}
|
|
||||||
onClose={onClose}
|
|
||||||
isCentered
|
|
||||||
>
|
|
||||||
<AlertDialogOverlay>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
|
||||||
{currentBoard ? 'Move Image to Board' : 'Add Image to Board'}
|
|
||||||
</AlertDialogHeader>
|
|
||||||
|
|
||||||
<AlertDialogBody>
|
|
||||||
<Box>
|
|
||||||
<Flex direction="column" gap={3}>
|
|
||||||
{currentBoard && (
|
|
||||||
<Text>
|
|
||||||
Moving this image from{' '}
|
|
||||||
<strong>{currentBoard.board_name}</strong> to
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{isFetching ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<IAIMantineSearchableSelect
|
|
||||||
placeholder="Select Board"
|
|
||||||
onChange={(v) => setSelectedBoard(v)}
|
|
||||||
value={selectedBoard}
|
|
||||||
data={(boards ?? []).map((board) => ({
|
|
||||||
label: board.board_name,
|
|
||||||
value: board.board_id,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
</AlertDialogBody>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<IAIButton onClick={onClose}>Cancel</IAIButton>
|
|
||||||
<IAIButton
|
|
||||||
isDisabled={!selectedBoard}
|
|
||||||
colorScheme="accent"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedBoard) {
|
|
||||||
handleAddToBoard(selectedBoard);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ml={3}
|
|
||||||
>
|
|
||||||
{currentBoard ? 'Move' : 'Add'}
|
|
||||||
</IAIButton>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogOverlay>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(UpdateImageBoardModal);
|
|
@ -9,16 +9,14 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
MenuList,
|
MenuList,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { DeleteImageButton } from 'features/imageDeletion/components/DeleteImageButton';
|
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
@ -109,13 +107,13 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(
|
const { currentData: imageDTO } = useGetImageDTOQuery(
|
||||||
lastSelectedImage ?? skipToken
|
lastSelectedImage?.image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentData: metadataData } = useGetImageMetadataQuery(
|
const { currentData: metadataData } = useGetImageMetadataQuery(
|
||||||
debounceState.isPending()
|
debounceState.isPending()
|
||||||
? skipToken
|
? skipToken
|
||||||
: debouncedMetadataQueryArg ?? skipToken
|
: debouncedMetadataQueryArg?.image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = metadataData?.metadata;
|
const metadata = metadataData?.metadata;
|
||||||
@ -173,7 +171,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(imageDTO));
|
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
|
@ -32,7 +32,7 @@ export const imagesSelector = createSelector(
|
|||||||
return {
|
return {
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
shouldHidePreview,
|
shouldHidePreview,
|
||||||
imageName: lastSelectedImage,
|
imageName: lastSelectedImage?.image_name,
|
||||||
progressImage,
|
progressImage,
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
shouldAntialiasProgressImage,
|
shouldAntialiasProgressImage,
|
||||||
@ -57,8 +57,6 @@ const CurrentImagePreview = () => {
|
|||||||
const {
|
const {
|
||||||
handlePrevImage,
|
handlePrevImage,
|
||||||
handleNextImage,
|
handleNextImage,
|
||||||
prevImageId,
|
|
||||||
nextImageId,
|
|
||||||
isOnLastImage,
|
isOnLastImage,
|
||||||
handleLoadMoreImages,
|
handleLoadMoreImages,
|
||||||
areMoreImagesAvailable,
|
areMoreImagesAvailable,
|
||||||
@ -70,7 +68,7 @@ const CurrentImagePreview = () => {
|
|||||||
() => {
|
() => {
|
||||||
handlePrevImage();
|
handlePrevImage();
|
||||||
},
|
},
|
||||||
[prevImageId]
|
[handlePrevImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -85,11 +83,11 @@ const CurrentImagePreview = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
nextImageId,
|
|
||||||
isOnLastImage,
|
isOnLastImage,
|
||||||
areMoreImagesAvailable,
|
areMoreImagesAvailable,
|
||||||
handleLoadMoreImages,
|
handleLoadMoreImages,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
handleNextImage,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -5,17 +5,19 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import IAIPopover from 'common/components/IAIPopover';
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import IAISwitch from 'common/components/IAISwitch';
|
||||||
import {
|
import {
|
||||||
autoAssignBoardOnClickChanged,
|
autoAssignBoardOnClickChanged,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
shouldAutoSwitchChanged,
|
shouldAutoSwitchChanged,
|
||||||
|
shouldShowDeleteButtonChanged,
|
||||||
} from 'features/gallery/store/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaWrench } from 'react-icons/fa';
|
import { FaWrench } from 'react-icons/fa';
|
||||||
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
||||||
|
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -24,12 +26,14 @@ const selector = createSelector(
|
|||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
shouldAutoSwitch,
|
shouldAutoSwitch,
|
||||||
autoAssignBoardOnClick,
|
autoAssignBoardOnClick,
|
||||||
|
shouldShowDeleteButton,
|
||||||
} = state.gallery;
|
} = state.gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
shouldAutoSwitch,
|
shouldAutoSwitch,
|
||||||
autoAssignBoardOnClick,
|
autoAssignBoardOnClick,
|
||||||
|
shouldShowDeleteButton,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -39,12 +43,37 @@ const GallerySettingsPopover = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { galleryImageMinimumWidth, shouldAutoSwitch, autoAssignBoardOnClick } =
|
const {
|
||||||
useAppSelector(selector);
|
galleryImageMinimumWidth,
|
||||||
|
shouldAutoSwitch,
|
||||||
|
autoAssignBoardOnClick,
|
||||||
|
shouldShowDeleteButton,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = useCallback(
|
||||||
|
(v: number) => {
|
||||||
dispatch(setGalleryImageMinimumWidth(v));
|
dispatch(setGalleryImageMinimumWidth(v));
|
||||||
};
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResetGalleryImageMinimumWidth = useCallback(() => {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(64));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleChangeAutoSwitch = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(shouldAutoSwitchChanged(e.target.checked));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeShowDeleteButton = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(shouldShowDeleteButtonChanged(e.target.checked));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
@ -57,7 +86,7 @@ const GallerySettingsPopover = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap={4}>
|
<Flex direction="column" gap={2}>
|
||||||
<IAISlider
|
<IAISlider
|
||||||
value={galleryImageMinimumWidth}
|
value={galleryImageMinimumWidth}
|
||||||
onChange={handleChangeGalleryImageMinimumWidth}
|
onChange={handleChangeGalleryImageMinimumWidth}
|
||||||
@ -66,14 +95,17 @@ const GallerySettingsPopover = () => {
|
|||||||
hideTooltip={true}
|
hideTooltip={true}
|
||||||
label={t('gallery.galleryImageSize')}
|
label={t('gallery.galleryImageSize')}
|
||||||
withReset
|
withReset
|
||||||
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
|
handleReset={handleResetGalleryImageMinimumWidth}
|
||||||
/>
|
/>
|
||||||
<IAISimpleCheckbox
|
<IAISwitch
|
||||||
label={t('gallery.autoSwitchNewImages')}
|
label={t('gallery.autoSwitchNewImages')}
|
||||||
isChecked={shouldAutoSwitch}
|
isChecked={shouldAutoSwitch}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={handleChangeAutoSwitch}
|
||||||
dispatch(shouldAutoSwitchChanged(e.target.checked))
|
/>
|
||||||
}
|
<IAISwitch
|
||||||
|
label="Show Delete Button"
|
||||||
|
isChecked={shouldShowDeleteButton}
|
||||||
|
onChange={handleChangeShowDeleteButton}
|
||||||
/>
|
/>
|
||||||
<IAISimpleCheckbox
|
<IAISimpleCheckbox
|
||||||
label={t('gallery.autoAssignBoardOnClick')}
|
label={t('gallery.autoAssignBoardOnClick')}
|
||||||
|
@ -4,28 +4,29 @@ import { MouseEvent, memo, useCallback } from 'react';
|
|||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
({ gallery }) => {
|
||||||
|
const selectionCount = gallery.selection.length;
|
||||||
|
|
||||||
|
return { selectionCount };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||||
// const selector = useMemo(
|
const { selectionCount } = useAppSelector(selector);
|
||||||
// () =>
|
|
||||||
// createSelector(
|
|
||||||
// [stateSelector],
|
|
||||||
// ({ gallery }) => {
|
|
||||||
// const selectionCount = gallery.selection.length;
|
|
||||||
|
|
||||||
// return { selectionCount };
|
|
||||||
// },
|
|
||||||
// defaultSelectorOptions
|
|
||||||
// ),
|
|
||||||
// []
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const { selectionCount } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -38,8 +39,24 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
bg: 'transparent',
|
bg: 'transparent',
|
||||||
_hover: { bg: 'transparent' },
|
_hover: { bg: 'transparent' },
|
||||||
}}
|
}}
|
||||||
renderMenu={() =>
|
renderMenu={() => {
|
||||||
imageDTO ? (
|
if (!imageDTO) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionCount > 1) {
|
||||||
|
return (
|
||||||
|
<MenuList
|
||||||
|
sx={{ visibility: 'visible !important' }}
|
||||||
|
motionProps={menuListMotionProps}
|
||||||
|
onContextMenu={skipEvent}
|
||||||
|
>
|
||||||
|
<MultipleSelectionMenuItems />
|
||||||
|
</MenuList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<MenuList
|
<MenuList
|
||||||
sx={{ visibility: 'visible !important' }}
|
sx={{ visibility: 'visible !important' }}
|
||||||
motionProps={menuListMotionProps}
|
motionProps={menuListMotionProps}
|
||||||
@ -47,8 +64,8 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
>
|
>
|
||||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||||
</MenuList>
|
</MenuList>
|
||||||
) : null
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
import { MenuItem } from '@chakra-ui/react';
|
import { MenuItem } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
imagesToChangeSelected,
|
||||||
|
isModalOpenChanged,
|
||||||
|
} from 'features/changeBoardModal/store/slice';
|
||||||
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FaFolder, FaFolderPlus, FaTrash } from 'react-icons/fa';
|
import { FaFolder, FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
const MultipleSelectionMenuItems = () => {
|
const MultipleSelectionMenuItems = () => {
|
||||||
const handleAddSelectionToBoard = useCallback(() => {
|
const dispatch = useAppDispatch();
|
||||||
// TODO: add selection to board
|
const selection = useAppSelector((state) => state.gallery.selection);
|
||||||
}, []);
|
|
||||||
|
const handleChangeBoard = useCallback(() => {
|
||||||
|
dispatch(imagesToChangeSelected(selection));
|
||||||
|
dispatch(isModalOpenChanged(true));
|
||||||
|
}, [dispatch, selection]);
|
||||||
|
|
||||||
const handleDeleteSelection = useCallback(() => {
|
const handleDeleteSelection = useCallback(() => {
|
||||||
// TODO: delete all selected images
|
dispatch(imagesToDeleteSelected(selection));
|
||||||
}, []);
|
}, [dispatch, selection]);
|
||||||
|
|
||||||
const handleAddSelectionToBatch = useCallback(() => {
|
|
||||||
// TODO: add selection to batch
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddSelectionToBoard}>
|
<MenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}>
|
||||||
Move Selection to Board
|
Change Board
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon={<FaFolderPlus />}
|
|
||||||
onClickCapture={handleAddSelectionToBatch}
|
|
||||||
>
|
|
||||||
Add Selection to Batch
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import { MenuItem } from '@chakra-ui/react';
|
import { MenuItem } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import {
|
import {
|
||||||
resizeAndScaleCanvas,
|
resizeAndScaleCanvas,
|
||||||
setInitialCanvasImage,
|
setInitialCanvasImage,
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice';
|
import {
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
imagesToChangeSelected,
|
||||||
|
isModalOpenChanged,
|
||||||
|
} from 'features/changeBoardModal/store/slice';
|
||||||
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
FaAsterisk,
|
FaAsterisk,
|
||||||
@ -29,13 +29,9 @@ import {
|
|||||||
FaShare,
|
FaShare,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import {
|
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||||
useGetImageMetadataQuery,
|
|
||||||
useRemoveImageFromBoardMutation,
|
|
||||||
} from 'services/api/endpoints/images';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
|
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
@ -45,32 +41,12 @@ type SingleSelectionMenuItemsProps = {
|
|||||||
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||||
const { imageDTO } = props;
|
const { imageDTO } = props;
|
||||||
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
({ gallery }) => {
|
|
||||||
const isInBatch = gallery.batchImageNames.includes(
|
|
||||||
imageDTO.image_name
|
|
||||||
);
|
|
||||||
|
|
||||||
return { isInBatch };
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[imageDTO.image_name]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isInBatch } = useAppSelector(selector);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
|
|
||||||
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
|
||||||
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
|
|
||||||
|
|
||||||
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
|
|
||||||
|
|
||||||
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
const [debouncedMetadataQueryArg, debounceState] = useDebounce(
|
||||||
imageDTO.image_name,
|
imageDTO.image_name,
|
||||||
@ -92,14 +68,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(imageDTO));
|
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
const [removeFromBoard] = useRemoveImageFromBoardMutation();
|
|
||||||
|
|
||||||
// Recall parameters handlers
|
// Recall parameters handlers
|
||||||
const handleRecallPrompt = useCallback(() => {
|
const handleRecallPrompt = useCallback(() => {
|
||||||
recallBothPrompts(
|
recallBothPrompts(
|
||||||
@ -144,20 +118,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
}, [metadata, recallAllParameters]);
|
}, [metadata, recallAllParameters]);
|
||||||
|
|
||||||
const handleAddToBoard = useCallback(() => {
|
const handleChangeBoard = useCallback(() => {
|
||||||
onClickAddToBoard(imageDTO);
|
dispatch(imagesToChangeSelected([imageDTO]));
|
||||||
}, [imageDTO, onClickAddToBoard]);
|
dispatch(isModalOpenChanged(true));
|
||||||
|
}, [dispatch, imageDTO]);
|
||||||
const handleRemoveFromBoard = useCallback(() => {
|
|
||||||
if (!imageDTO.board_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeFromBoard({ imageDTO });
|
|
||||||
}, [imageDTO, removeFromBoard]);
|
|
||||||
|
|
||||||
const handleAddToBatch = useCallback(() => {
|
|
||||||
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
|
||||||
}, [dispatch, imageDTO.image_name]);
|
|
||||||
|
|
||||||
const handleCopyImage = useCallback(() => {
|
const handleCopyImage = useCallback(() => {
|
||||||
copyImageToClipboard(imageDTO.image_url);
|
copyImageToClipboard(imageDTO.image_url);
|
||||||
@ -229,23 +193,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
{t('parameters.sendToUnifiedCanvas')}
|
{t('parameters.sendToUnifiedCanvas')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{isBatchEnabled && (
|
<MenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}>
|
||||||
<MenuItem
|
Change Board
|
||||||
icon={<FaFolder />}
|
|
||||||
isDisabled={isInBatch}
|
|
||||||
onClickCapture={handleAddToBatch}
|
|
||||||
>
|
|
||||||
Add to Batch
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
|
|
||||||
{imageDTO.board_id ? 'Change Board' : 'Add to Board'}
|
|
||||||
</MenuItem>
|
|
||||||
{imageDTO.board_id && (
|
|
||||||
<MenuItem icon={<FaFolder />} onClickCapture={handleRemoveFromBoard}>
|
|
||||||
Remove from Board
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
|
@ -20,16 +20,14 @@ import BoardsList from './Boards/BoardsList/BoardsList';
|
|||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GalleryPinButton from './GalleryPinButton';
|
import GalleryPinButton from './GalleryPinButton';
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import BatchImageGrid from './ImageGrid/BatchImageGrid';
|
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { selectedBoardId, galleryView } = state.gallery;
|
const { galleryView } = state.gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedBoardId,
|
|
||||||
galleryView,
|
galleryView,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -39,7 +37,7 @@ const selector = createSelector(
|
|||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
const galleryGridRef = useRef<HTMLDivElement>(null);
|
const galleryGridRef = useRef<HTMLDivElement>(null);
|
||||||
const { selectedBoardId, galleryView } = useAppSelector(selector);
|
const { galleryView } = useAppSelector(selector);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } =
|
||||||
useDisclosure();
|
useDisclosure();
|
||||||
@ -130,12 +128,7 @@ const ImageGalleryContent = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{selectedBoardId === 'batch' ? (
|
|
||||||
<BatchImageGrid />
|
|
||||||
) : (
|
|
||||||
<GalleryImageGrid />
|
<GalleryImageGrid />
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
|
||||||
import IAIErrorLoadingImageFallback from 'common/components/IAIErrorLoadingImageFallback';
|
|
||||||
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
|
||||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
|
||||||
import { imagesRemovedFromBatch } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
|
||||||
|
|
||||||
const makeSelector = (image_name: string) =>
|
|
||||||
createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
(state) => ({
|
|
||||||
selectionCount: state.gallery.selection.length,
|
|
||||||
selection: state.gallery.selection,
|
|
||||||
isSelected: state.gallery.selection.includes(image_name),
|
|
||||||
}),
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
type BatchImageProps = {
|
|
||||||
imageName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BatchImage = (props: BatchImageProps) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { imageName } = props;
|
|
||||||
const {
|
|
||||||
currentData: imageDTO,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
} = useGetImageDTOQuery(imageName);
|
|
||||||
const selector = useMemo(() => makeSelector(imageName), [imageName]);
|
|
||||||
|
|
||||||
const { isSelected, selectionCount, selection } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleClickRemove = useCallback(() => {
|
|
||||||
dispatch(imagesRemovedFromBatch([imageName]));
|
|
||||||
}, [dispatch, imageName]);
|
|
||||||
|
|
||||||
// const handleClick = useCallback(
|
|
||||||
// (e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
// if (e.shiftKey) {
|
|
||||||
// dispatch(imageRangeEndSelected(imageName));
|
|
||||||
// } else if (e.ctrlKey || e.metaKey) {
|
|
||||||
// dispatch(imageSelectionToggled(imageName));
|
|
||||||
// } else {
|
|
||||||
// dispatch(imageSelected(imageName));
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// [dispatch, imageName]
|
|
||||||
// );
|
|
||||||
|
|
||||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
|
||||||
if (selectionCount > 1) {
|
|
||||||
return {
|
|
||||||
id: 'batch',
|
|
||||||
payloadType: 'IMAGE_NAMES',
|
|
||||||
payload: { image_names: selection },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageDTO) {
|
|
||||||
return {
|
|
||||||
id: 'batch',
|
|
||||||
payloadType: 'IMAGE_DTO',
|
|
||||||
payload: { imageDTO },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [imageDTO, selection, selectionCount]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <IAIFillSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !imageDTO) {
|
|
||||||
return <IAIErrorLoadingImageFallback />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
|
||||||
<ImageContextMenu imageDTO={imageDTO}>
|
|
||||||
{(ref) => (
|
|
||||||
<Box
|
|
||||||
position="relative"
|
|
||||||
key={imageName}
|
|
||||||
userSelect="none"
|
|
||||||
ref={ref}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
aspectRatio: '1/1',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAIDndImage
|
|
||||||
// onClick={handleClick}
|
|
||||||
imageDTO={imageDTO}
|
|
||||||
draggableData={draggableData}
|
|
||||||
isSelected={isSelected}
|
|
||||||
minSize={0}
|
|
||||||
onClickReset={handleClickRemove}
|
|
||||||
isDropDisabled={true}
|
|
||||||
imageSx={{ w: 'full', h: 'full' }}
|
|
||||||
isUploadDisabled={true}
|
|
||||||
resetTooltip="Remove from batch"
|
|
||||||
withResetIcon
|
|
||||||
thumbnail
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ImageContextMenu>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BatchImage);
|
|
@ -1,87 +0,0 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
|
||||||
|
|
||||||
import { memo, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaImage } from 'react-icons/fa';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|
||||||
import { VirtuosoGrid } from 'react-virtuoso';
|
|
||||||
import BatchImage from './BatchImage';
|
|
||||||
import ItemContainer from './ImageGridItemContainer';
|
|
||||||
import ListContainer from './ImageGridListContainer';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
(state) => {
|
|
||||||
return {
|
|
||||||
imageNames: state.gallery.batchImageNames,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const BatchImageGrid = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const rootRef = useRef(null);
|
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
|
||||||
defer: true,
|
|
||||||
options: {
|
|
||||||
scrollbars: {
|
|
||||||
visibility: 'auto',
|
|
||||||
autoHide: 'leave',
|
|
||||||
autoHideDelay: 1300,
|
|
||||||
theme: 'os-theme-dark',
|
|
||||||
},
|
|
||||||
overflow: { x: 'hidden' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { imageNames } = useAppSelector(selector);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { current: root } = rootRef;
|
|
||||||
if (scroller && root) {
|
|
||||||
initialize({
|
|
||||||
target: root,
|
|
||||||
elements: {
|
|
||||||
viewport: scroller,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => osInstance()?.destroy();
|
|
||||||
}, [scroller, initialize, osInstance]);
|
|
||||||
|
|
||||||
if (imageNames.length) {
|
|
||||||
return (
|
|
||||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
|
||||||
<VirtuosoGrid
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
data={imageNames}
|
|
||||||
components={{
|
|
||||||
Item: ItemContainer,
|
|
||||||
List: ListContainer,
|
|
||||||
}}
|
|
||||||
scrollerRef={setScroller}
|
|
||||||
itemContent={(index, imageName) => (
|
|
||||||
<BatchImage key={imageName} imageName={imageName} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IAINoContentFallback
|
|
||||||
label={t('gallery.noImagesInGallery')}
|
|
||||||
icon={FaImage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BatchImageGrid);
|
|
@ -1,27 +1,18 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import {
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
ImageDTOsDraggableData,
|
||||||
import { stateSelector } from 'app/store/store';
|
ImageDraggableData,
|
||||||
|
TypesafeDraggableData,
|
||||||
|
} from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts';
|
||||||
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
|
import { FaTrash } from 'react-icons/fa';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
export const makeSelector = (image_name: string) =>
|
|
||||||
createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
({ gallery }) => ({
|
|
||||||
isSelected: gallery.selection.includes(image_name),
|
|
||||||
selectionCount: gallery.selection.length,
|
|
||||||
selection: gallery.selection,
|
|
||||||
}),
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
imageName: string;
|
imageName: string;
|
||||||
}
|
}
|
||||||
@ -30,22 +21,12 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { imageName } = props;
|
const { imageName } = props;
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
||||||
const localSelector = useMemo(() => makeSelector(imageName), [imageName]);
|
const shouldShowDeleteButton = useAppSelector(
|
||||||
|
(state) => state.gallery.shouldShowDeleteButton
|
||||||
|
);
|
||||||
|
|
||||||
const { isSelected, selectionCount, selection } =
|
const { handleClick, isSelected, selection, selectionCount } =
|
||||||
useAppSelector(localSelector);
|
useMultiselect(imageDTO);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
// disable multiselect for now
|
|
||||||
// if (e.shiftKey) {
|
|
||||||
// dispatch(imageRangeEndSelected(imageName));
|
|
||||||
// } else if (e.ctrlKey || e.metaKey) {
|
|
||||||
// dispatch(imageSelectionToggled(imageName));
|
|
||||||
// } else {
|
|
||||||
// dispatch(imageSelected(imageName));
|
|
||||||
// }
|
|
||||||
dispatch(imageSelected(imageName));
|
|
||||||
}, [dispatch, imageName]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -53,26 +34,28 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(imageDTO));
|
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||||
},
|
},
|
||||||
[dispatch, imageDTO]
|
[dispatch, imageDTO]
|
||||||
);
|
);
|
||||||
|
|
||||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||||
if (selectionCount > 1) {
|
if (selectionCount > 1) {
|
||||||
return {
|
const data: ImageDTOsDraggableData = {
|
||||||
id: 'gallery-image',
|
id: 'gallery-image',
|
||||||
payloadType: 'IMAGE_NAMES',
|
payloadType: 'IMAGE_DTOS',
|
||||||
payload: { image_names: selection },
|
payload: { imageDTOs: selection },
|
||||||
};
|
};
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageDTO) {
|
if (imageDTO) {
|
||||||
return {
|
const data: ImageDraggableData = {
|
||||||
id: 'gallery-image',
|
id: 'gallery-image',
|
||||||
payloadType: 'IMAGE_DTO',
|
payloadType: 'IMAGE_DTO',
|
||||||
payload: { imageDTO },
|
payload: { imageDTO },
|
||||||
};
|
};
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}, [imageDTO, selection, selectionCount]);
|
}, [imageDTO, selection, selectionCount]);
|
||||||
|
|
||||||
@ -103,9 +86,9 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
thumbnail={true}
|
thumbnail={true}
|
||||||
withHoverOverlay
|
withHoverOverlay
|
||||||
// resetIcon={<FaTrash />}
|
resetIcon={<FaTrash />}
|
||||||
// resetTooltip="Delete image"
|
resetTooltip="Delete image"
|
||||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
withResetIcon={shouldShowDeleteButton} // removed bc it's too easy to accidentally delete images
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { UnsafeImageMetadata } from 'services/api/endpoints/images';
|
import { UnsafeImageMetadata } from 'services/api/types';
|
||||||
import ImageMetadataItem from './ImageMetadataItem';
|
import ImageMetadataItem from './ImageMetadataItem';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
|
import { MouseEvent, useCallback, useMemo } from 'react';
|
||||||
|
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { selectionChanged } from '../store/gallerySlice';
|
||||||
|
import { imagesSelectors } from 'services/api/util';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[stateSelector, selectListImagesBaseQueryArgs],
|
||||||
|
({ gallery }, queryArgs) => {
|
||||||
|
const selection = gallery.selection;
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryArgs,
|
||||||
|
selection,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useMultiselect = (imageDTO?: ImageDTO) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { queryArgs, selection } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const { imageDTOs } = useListImagesQuery(queryArgs, {
|
||||||
|
selectFromResult: (result) => ({
|
||||||
|
imageDTOs: result.data ? imagesSelectors.selectAll(result.data) : [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.shiftKey) {
|
||||||
|
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 = imageDTOs.slice(start, end + 1);
|
||||||
|
dispatch(selectionChanged(uniq(selection.concat(imagesToSelect))));
|
||||||
|
}
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
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(imageDTO))));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(selectionChanged([imageDTO]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, imageDTO, imageDTOs, selection]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected = useMemo(
|
||||||
|
() =>
|
||||||
|
imageDTO
|
||||||
|
? selection.some((i) => i.image_name === imageDTO.image_name)
|
||||||
|
: false,
|
||||||
|
[imageDTO, selection]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectionCount = useMemo(() => selection.length, [selection.length]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selection,
|
||||||
|
selectionCount,
|
||||||
|
isSelected,
|
||||||
|
handleClick,
|
||||||
|
};
|
||||||
|
};
|
@ -4,14 +4,15 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
import { clamp, isEqual } from 'lodash-es';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import {
|
import {
|
||||||
ListImagesArgs,
|
|
||||||
imagesAdapter,
|
|
||||||
imagesApi,
|
imagesApi,
|
||||||
useLazyListImagesQuery,
|
useLazyListImagesQuery,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { selectListImagesBaseQueryArgs } from '../store/gallerySelectors';
|
import { selectListImagesBaseQueryArgs } from '../store/gallerySelectors';
|
||||||
import { IMAGE_LIMIT } from '../store/types';
|
import { IMAGE_LIMIT } from '../store/types';
|
||||||
|
import { ListImagesArgs } from 'services/api/types';
|
||||||
|
import { imagesAdapter } from 'services/api/util';
|
||||||
|
|
||||||
export const nextPrevImageButtonsSelector = createSelector(
|
export const nextPrevImageButtonsSelector = createSelector(
|
||||||
[stateSelector, selectListImagesBaseQueryArgs],
|
[stateSelector, selectListImagesBaseQueryArgs],
|
||||||
@ -19,12 +20,21 @@ export const nextPrevImageButtonsSelector = createSelector(
|
|||||||
const { data, status } =
|
const { data, status } =
|
||||||
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||||
|
|
||||||
|
const { data: total } =
|
||||||
|
state.gallery.galleryView === 'images'
|
||||||
|
? boardsApi.endpoints.getBoardImagesTotal.select(
|
||||||
|
baseQueryArgs.board_id ?? 'none'
|
||||||
|
)(state)
|
||||||
|
: boardsApi.endpoints.getBoardAssetsTotal.select(
|
||||||
|
baseQueryArgs.board_id ?? 'none'
|
||||||
|
)(state);
|
||||||
|
|
||||||
const lastSelectedImage =
|
const lastSelectedImage =
|
||||||
state.gallery.selection[state.gallery.selection.length - 1];
|
state.gallery.selection[state.gallery.selection.length - 1];
|
||||||
|
|
||||||
const isFetching = status === 'pending';
|
const isFetching = status === 'pending';
|
||||||
|
|
||||||
if (!data || !lastSelectedImage || data.total === 0) {
|
if (!data || !lastSelectedImage || total === 0) {
|
||||||
return {
|
return {
|
||||||
isFetching,
|
isFetching,
|
||||||
queryArgs: baseQueryArgs,
|
queryArgs: baseQueryArgs,
|
||||||
@ -44,30 +54,30 @@ export const nextPrevImageButtonsSelector = createSelector(
|
|||||||
const images = selectors.selectAll(data);
|
const images = selectors.selectAll(data);
|
||||||
|
|
||||||
const currentImageIndex = images.findIndex(
|
const currentImageIndex = images.findIndex(
|
||||||
(i) => i.image_name === lastSelectedImage
|
(i) => i.image_name === lastSelectedImage.image_name
|
||||||
);
|
);
|
||||||
const nextImageIndex = clamp(currentImageIndex + 1, 0, images.length - 1);
|
const nextImageIndex = clamp(currentImageIndex + 1, 0, images.length - 1);
|
||||||
|
|
||||||
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||||
|
|
||||||
const nextImageId = images[nextImageIndex]?.image_name;
|
const nextImageId = images[nextImageIndex]?.image_name;
|
||||||
const prevImageId = images[prevImageIndex]?.image_name;
|
const prevImageId = images[prevImageIndex]?.image_name;
|
||||||
|
|
||||||
const nextImage = selectors.selectById(data, nextImageId);
|
const nextImage = nextImageId
|
||||||
const prevImage = selectors.selectById(data, prevImageId);
|
? selectors.selectById(data, nextImageId)
|
||||||
|
: undefined;
|
||||||
|
const prevImage = prevImageId
|
||||||
|
? selectors.selectById(data, prevImageId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const imagesLength = images.length;
|
const imagesLength = images.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
loadedImagesCount: images.length,
|
||||||
isOnLastImage:
|
currentImageIndex,
|
||||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
areMoreImagesAvailable: (total ?? 0) > imagesLength,
|
||||||
areMoreImagesAvailable: (data?.total ?? 0) > imagesLength,
|
|
||||||
isFetching: status === 'pending',
|
isFetching: status === 'pending',
|
||||||
nextImage,
|
nextImage,
|
||||||
prevImage,
|
prevImage,
|
||||||
nextImageId,
|
|
||||||
prevImageId,
|
|
||||||
queryArgs,
|
queryArgs,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -82,22 +92,22 @@ export const useNextPrevImage = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOnFirstImage,
|
nextImage,
|
||||||
isOnLastImage,
|
prevImage,
|
||||||
nextImageId,
|
|
||||||
prevImageId,
|
|
||||||
areMoreImagesAvailable,
|
areMoreImagesAvailable,
|
||||||
isFetching,
|
isFetching,
|
||||||
queryArgs,
|
queryArgs,
|
||||||
|
loadedImagesCount,
|
||||||
|
currentImageIndex,
|
||||||
} = useAppSelector(nextPrevImageButtonsSelector);
|
} = useAppSelector(nextPrevImageButtonsSelector);
|
||||||
|
|
||||||
const handlePrevImage = useCallback(() => {
|
const handlePrevImage = useCallback(() => {
|
||||||
prevImageId && dispatch(imageSelected(prevImageId));
|
prevImage && dispatch(imageSelected(prevImage));
|
||||||
}, [dispatch, prevImageId]);
|
}, [dispatch, prevImage]);
|
||||||
|
|
||||||
const handleNextImage = useCallback(() => {
|
const handleNextImage = useCallback(() => {
|
||||||
nextImageId && dispatch(imageSelected(nextImageId));
|
nextImage && dispatch(imageSelected(nextImage));
|
||||||
}, [dispatch, nextImageId]);
|
}, [dispatch, nextImage]);
|
||||||
|
|
||||||
const [listImages] = useLazyListImagesQuery();
|
const [listImages] = useLazyListImagesQuery();
|
||||||
|
|
||||||
@ -108,10 +118,12 @@ export const useNextPrevImage = () => {
|
|||||||
return {
|
return {
|
||||||
handlePrevImage,
|
handlePrevImage,
|
||||||
handleNextImage,
|
handleNextImage,
|
||||||
isOnFirstImage,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
isOnLastImage,
|
isOnLastImage:
|
||||||
nextImageId,
|
currentImageIndex !== undefined &&
|
||||||
prevImageId,
|
currentImageIndex === loadedImagesCount - 1,
|
||||||
|
nextImage,
|
||||||
|
prevImage,
|
||||||
areMoreImagesAvailable,
|
areMoreImagesAvailable,
|
||||||
handleLoadMoreImages,
|
handleLoadMoreImages,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { ImageUsage } from 'app/contexts/AddImageToBoardContext';
|
import { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
export type RequestedBoardImagesDeletionArg = {
|
export type RequestedBoardImagesDeletionArg = {
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
type BoardsState = {
|
|
||||||
searchText: string;
|
|
||||||
updateBoardModalOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialBoardsState: BoardsState = {
|
|
||||||
updateBoardModalOpen: false,
|
|
||||||
searchText: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const boardsSlice = createSlice({
|
|
||||||
name: 'boards',
|
|
||||||
initialState: initialBoardsState,
|
|
||||||
reducers: {
|
|
||||||
setBoardSearchText: (state, action: PayloadAction<string>) => {
|
|
||||||
state.searchText = action.payload;
|
|
||||||
},
|
|
||||||
setUpdateBoardModalOpen: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.updateBoardModalOpen = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setBoardSearchText, setUpdateBoardModalOpen } =
|
|
||||||
boardsSlice.actions;
|
|
||||||
|
|
||||||
export default boardsSlice.reducer;
|
|
@ -1,7 +1,7 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { ListImagesArgs } from 'services/api/endpoints/images';
|
import { ListImagesArgs } from 'services/api/types';
|
||||||
import {
|
import {
|
||||||
ASSETS_CATEGORIES,
|
ASSETS_CATEGORIES,
|
||||||
IMAGE_CATEGORIES,
|
IMAGE_CATEGORIES,
|
||||||
@ -24,7 +24,7 @@ export const selectListImagesBaseQueryArgs = createSelector(
|
|||||||
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
|
||||||
|
|
||||||
const listImagesBaseQueryArgs: ListImagesArgs = {
|
const listImagesBaseQueryArgs: ListImagesArgs = {
|
||||||
board_id: selectedBoardId ?? 'none',
|
board_id: selectedBoardId,
|
||||||
categories,
|
categories,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
|
@ -1,66 +1,32 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import { uniq } from 'lodash-es';
|
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { BoardId, GalleryState, GalleryView } from './types';
|
import { BoardId, GalleryState, GalleryView } from './types';
|
||||||
|
|
||||||
export const initialGalleryState: GalleryState = {
|
export const initialGalleryState: GalleryState = {
|
||||||
selection: [],
|
selection: [],
|
||||||
shouldAutoSwitch: true,
|
shouldAutoSwitch: true,
|
||||||
autoAddBoardId: undefined,
|
|
||||||
autoAssignBoardOnClick: true,
|
autoAssignBoardOnClick: true,
|
||||||
|
autoAddBoardId: 'none',
|
||||||
galleryImageMinimumWidth: 96,
|
galleryImageMinimumWidth: 96,
|
||||||
selectedBoardId: undefined,
|
selectedBoardId: 'none',
|
||||||
galleryView: 'images',
|
galleryView: 'images',
|
||||||
batchImageNames: [],
|
shouldShowDeleteButton: false,
|
||||||
isBatchEnabled: false,
|
boardSearchText: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
reducers: {
|
reducers: {
|
||||||
imageRangeEndSelected: () => {
|
imageSelected: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
// imageRangeEndSelected: (state, action: PayloadAction<string>) => {
|
|
||||||
// const rangeEndImageName = action.payload;
|
|
||||||
// const lastSelectedImage = state.selection[state.selection.length - 1];
|
|
||||||
// const filteredImages = selectFilteredImagesLocal(state);
|
|
||||||
// const lastClickedIndex = filteredImages.findIndex(
|
|
||||||
// (n) => n.image_name === lastSelectedImage
|
|
||||||
// );
|
|
||||||
// const currentClickedIndex = filteredImages.findIndex(
|
|
||||||
// (n) => n.image_name === rangeEndImageName
|
|
||||||
// );
|
|
||||||
// if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
|
||||||
// // We have a valid range!
|
|
||||||
// const start = Math.min(lastClickedIndex, currentClickedIndex);
|
|
||||||
// const end = Math.max(lastClickedIndex, currentClickedIndex);
|
|
||||||
// const imagesToSelect = filteredImages
|
|
||||||
// .slice(start, end + 1)
|
|
||||||
// .map((i) => i.image_name);
|
|
||||||
// state.selection = uniq(state.selection.concat(imagesToSelect));
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
imageSelectionToggled: () => {
|
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
// imageSelectionToggled: (state, action: PayloadAction<string>) => {
|
|
||||||
// TODO: multiselect
|
|
||||||
// if (
|
|
||||||
// state.selection.includes(action.payload) &&
|
|
||||||
// state.selection.length > 1
|
|
||||||
// ) {
|
|
||||||
// state.selection = state.selection.filter(
|
|
||||||
// (imageName) => imageName !== action.payload
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// state.selection = uniq(state.selection.concat(action.payload));
|
|
||||||
// }
|
|
||||||
imageSelected: (state, action: PayloadAction<string | null>) => {
|
|
||||||
state.selection = action.payload ? [action.payload] : [];
|
state.selection = action.payload ? [action.payload] : [];
|
||||||
},
|
},
|
||||||
|
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||||
|
state.selection = action.payload;
|
||||||
|
},
|
||||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitch = action.payload;
|
state.shouldAutoSwitch = action.payload;
|
||||||
},
|
},
|
||||||
@ -74,53 +40,28 @@ export const gallerySlice = createSlice({
|
|||||||
state.selectedBoardId = action.payload;
|
state.selectedBoardId = action.payload;
|
||||||
state.galleryView = 'images';
|
state.galleryView = 'images';
|
||||||
},
|
},
|
||||||
isBatchEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
autoAddBoardIdChanged: (state, action: PayloadAction<BoardId>) => {
|
||||||
state.isBatchEnabled = action.payload;
|
|
||||||
},
|
|
||||||
imagesAddedToBatch: (state, action: PayloadAction<string[]>) => {
|
|
||||||
state.batchImageNames = uniq(
|
|
||||||
state.batchImageNames.concat(action.payload)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
imagesRemovedFromBatch: (state, action: PayloadAction<string[]>) => {
|
|
||||||
state.batchImageNames = state.batchImageNames.filter(
|
|
||||||
(imageName) => !action.payload.includes(imageName)
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSelection = state.selection.filter(
|
|
||||||
(imageName) => !action.payload.includes(imageName)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newSelection.length) {
|
|
||||||
state.selection = newSelection;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.selection = [state.batchImageNames[0]] ?? [];
|
|
||||||
},
|
|
||||||
batchReset: (state) => {
|
|
||||||
state.batchImageNames = [];
|
|
||||||
state.selection = [];
|
|
||||||
},
|
|
||||||
autoAddBoardIdChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<string | undefined>
|
|
||||||
) => {
|
|
||||||
state.autoAddBoardId = action.payload;
|
state.autoAddBoardId = action.payload;
|
||||||
},
|
},
|
||||||
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
galleryViewChanged: (state, action: PayloadAction<GalleryView>) => {
|
||||||
state.galleryView = action.payload;
|
state.galleryView = action.payload;
|
||||||
},
|
},
|
||||||
|
shouldShowDeleteButtonChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.shouldShowDeleteButton = action.payload;
|
||||||
|
},
|
||||||
|
boardSearchTextChanged: (state, action: PayloadAction<string>) => {
|
||||||
|
state.boardSearchText = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
const deletedBoardId = action.meta.arg.originalArgs;
|
const deletedBoardId = action.meta.arg.originalArgs;
|
||||||
if (deletedBoardId === state.selectedBoardId) {
|
if (deletedBoardId === state.selectedBoardId) {
|
||||||
state.selectedBoardId = undefined;
|
state.selectedBoardId = 'none';
|
||||||
state.galleryView = 'images';
|
state.galleryView = 'images';
|
||||||
}
|
}
|
||||||
if (deletedBoardId === state.autoAddBoardId) {
|
if (deletedBoardId === state.autoAddBoardId) {
|
||||||
state.autoAddBoardId = undefined;
|
state.autoAddBoardId = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.addMatcher(
|
builder.addMatcher(
|
||||||
@ -132,7 +73,7 @@ export const gallerySlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
||||||
state.autoAddBoardId = undefined;
|
state.autoAddBoardId = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -140,23 +81,21 @@ export const gallerySlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
imageRangeEndSelected,
|
|
||||||
imageSelectionToggled,
|
|
||||||
imageSelected,
|
imageSelected,
|
||||||
shouldAutoSwitchChanged,
|
shouldAutoSwitchChanged,
|
||||||
autoAssignBoardOnClickChanged,
|
autoAssignBoardOnClickChanged,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
boardIdSelected,
|
boardIdSelected,
|
||||||
isBatchEnabledChanged,
|
|
||||||
imagesAddedToBatch,
|
|
||||||
imagesRemovedFromBatch,
|
|
||||||
autoAddBoardIdChanged,
|
autoAddBoardIdChanged,
|
||||||
galleryViewChanged,
|
galleryViewChanged,
|
||||||
|
selectionChanged,
|
||||||
|
shouldShowDeleteButtonChanged,
|
||||||
|
boardSearchTextChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
imagesApi.endpoints.deleteBoard.matchFulfilled,
|
||||||
boardsApi.endpoints.deleteBoardAndImages.matchFulfilled
|
imagesApi.endpoints.deleteBoardAndImages.matchFulfilled
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ImageCategory } from 'services/api/types';
|
import { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
|
||||||
export const ASSETS_CATEGORIES: ImageCategory[] = [
|
export const ASSETS_CATEGORIES: ImageCategory[] = [
|
||||||
@ -11,17 +11,16 @@ export const INITIAL_IMAGE_LIMIT = 100;
|
|||||||
export const IMAGE_LIMIT = 20;
|
export const IMAGE_LIMIT = 20;
|
||||||
|
|
||||||
export type GalleryView = 'images' | 'assets';
|
export type GalleryView = 'images' | 'assets';
|
||||||
// export type BoardId = 'no_board' | (string & Record<never, never>);
|
export type BoardId = 'none' | (string & Record<never, never>);
|
||||||
export type BoardId = string | undefined;
|
|
||||||
|
|
||||||
export type GalleryState = {
|
export type GalleryState = {
|
||||||
selection: string[];
|
selection: ImageDTO[];
|
||||||
shouldAutoSwitch: boolean;
|
shouldAutoSwitch: boolean;
|
||||||
autoAddBoardId: string | undefined;
|
|
||||||
autoAssignBoardOnClick: boolean;
|
autoAssignBoardOnClick: boolean;
|
||||||
|
autoAddBoardId: BoardId;
|
||||||
galleryImageMinimumWidth: number;
|
galleryImageMinimumWidth: number;
|
||||||
selectedBoardId: BoardId;
|
selectedBoardId: BoardId;
|
||||||
galleryView: GalleryView;
|
galleryView: GalleryView;
|
||||||
batchImageNames: string[];
|
shouldShowDeleteButton: boolean;
|
||||||
isBatchEnabled: boolean;
|
boardSearchText: string;
|
||||||
};
|
};
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
|
|
||||||
type DeleteImageState = {
|
|
||||||
imageToDelete: ImageDTO | null;
|
|
||||||
isModalOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialDeleteImageState: DeleteImageState = {
|
|
||||||
imageToDelete: null,
|
|
||||||
isModalOpen: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageDeletion = createSlice({
|
|
||||||
name: 'imageDeletion',
|
|
||||||
initialState: initialDeleteImageState,
|
|
||||||
reducers: {
|
|
||||||
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.isModalOpen = action.payload;
|
|
||||||
},
|
|
||||||
imageToDeleteSelected: (state, action: PayloadAction<ImageDTO>) => {
|
|
||||||
state.imageToDelete = action.payload;
|
|
||||||
},
|
|
||||||
imageToDeleteCleared: (state) => {
|
|
||||||
state.imageToDelete = null;
|
|
||||||
state.isModalOpen = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
isModalOpenChanged,
|
|
||||||
imageToDeleteSelected,
|
|
||||||
imageToDeleteCleared,
|
|
||||||
} = imageDeletion.actions;
|
|
||||||
|
|
||||||
export default imageDeletion.reducer;
|
|
@ -1,6 +0,0 @@
|
|||||||
export type ImageUsage = {
|
|
||||||
isInitialImage: boolean;
|
|
||||||
isCanvasImage: boolean;
|
|
||||||
isNodesImage: boolean;
|
|
||||||
isControlNetImage: boolean;
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user