mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
48 Commits
v4.2.9.dev
...
v4.2.9.dev
Author | SHA1 | Date | |
---|---|---|---|
987dce801c | |||
f0f6813979 | |||
28b86d3917 | |||
38e25c45bf | |||
978a869a70 | |||
2acc5c8317 | |||
3fe4b770b8 | |||
c800f84c50 | |||
a64dd129f1 | |||
cd4cc56add | |||
c19aa0389a | |||
fdb125b294 | |||
686856d111 | |||
fec61ce1ff | |||
bd5780de4d | |||
db4782a632 | |||
c4b2099c4f | |||
ec745f663d | |||
4f47577076 | |||
6ee8e13632 | |||
d469b1771e | |||
e79f9782ab | |||
ded72e0362 | |||
37ed567aad | |||
9692a5f996 | |||
5db7b48cd8 | |||
ea014c66ac | |||
25918c28aa | |||
0c60469401 | |||
f1aa50f447 | |||
a413b261f0 | |||
4a1a6639f6 | |||
201c370ca1 | |||
d070c7c726 | |||
e38e20a992 | |||
39a94ec70e | |||
c7bfae2d1e | |||
e7944c427d | |||
48ed4e120d | |||
a5b038a1b1 | |||
dc752c98b0 | |||
85a47cc6fe | |||
6450f42cfa | |||
3876f71ff4 | |||
cf819e8eab | |||
2217fb8485 | |||
43652e830a | |||
a3417bf81d |
@ -20,7 +20,6 @@ from typing import (
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
import semver
|
||||
@ -80,7 +79,7 @@ class UIConfigBase(BaseModel):
|
||||
version: str = Field(
|
||||
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
|
||||
)
|
||||
node_pack: Optional[str] = Field(default=None, description="Whether or not this is a custom node")
|
||||
node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes")
|
||||
classification: Classification = Field(default=Classification.Stable, description="The node's classification")
|
||||
|
||||
model_config = ConfigDict(
|
||||
@ -230,18 +229,16 @@ class BaseInvocation(ABC, BaseModel):
|
||||
@staticmethod
|
||||
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
|
||||
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
|
||||
uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None))
|
||||
if uiconfig is not None:
|
||||
if uiconfig.title is not None:
|
||||
schema["title"] = uiconfig.title
|
||||
if uiconfig.tags is not None:
|
||||
schema["tags"] = uiconfig.tags
|
||||
if uiconfig.category is not None:
|
||||
schema["category"] = uiconfig.category
|
||||
if uiconfig.node_pack is not None:
|
||||
schema["node_pack"] = uiconfig.node_pack
|
||||
schema["classification"] = uiconfig.classification
|
||||
schema["version"] = uiconfig.version
|
||||
if title := model_class.UIConfig.title:
|
||||
schema["title"] = title
|
||||
if tags := model_class.UIConfig.tags:
|
||||
schema["tags"] = tags
|
||||
if category := model_class.UIConfig.category:
|
||||
schema["category"] = category
|
||||
if node_pack := model_class.UIConfig.node_pack:
|
||||
schema["node_pack"] = node_pack
|
||||
schema["classification"] = model_class.UIConfig.classification
|
||||
schema["version"] = model_class.UIConfig.version
|
||||
if "required" not in schema or not isinstance(schema["required"], list):
|
||||
schema["required"] = []
|
||||
schema["class"] = "invocation"
|
||||
@ -312,7 +309,7 @@ class BaseInvocation(ABC, BaseModel):
|
||||
json_schema_extra={"field_kind": FieldKind.NodeAttribute},
|
||||
)
|
||||
|
||||
UIConfig: ClassVar[Type[UIConfigBase]]
|
||||
UIConfig: ClassVar[UIConfigBase]
|
||||
|
||||
model_config = ConfigDict(
|
||||
protected_namespaces=(),
|
||||
@ -441,30 +438,25 @@ def invocation(
|
||||
validate_fields(cls.model_fields, invocation_type)
|
||||
|
||||
# Add OpenAPI schema extras
|
||||
uiconfig_name = cls.__qualname__ + ".UIConfig"
|
||||
if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name:
|
||||
cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {})
|
||||
cls.UIConfig.title = title
|
||||
cls.UIConfig.tags = tags
|
||||
cls.UIConfig.category = category
|
||||
cls.UIConfig.classification = classification
|
||||
|
||||
# Grab the node pack's name from the module name, if it's a custom node
|
||||
is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations"
|
||||
if is_custom_node:
|
||||
cls.UIConfig.node_pack = cls.__module__.split(".")[0]
|
||||
else:
|
||||
cls.UIConfig.node_pack = None
|
||||
uiconfig: dict[str, Any] = {}
|
||||
uiconfig["title"] = title
|
||||
uiconfig["tags"] = tags
|
||||
uiconfig["category"] = category
|
||||
uiconfig["classification"] = classification
|
||||
# The node pack is the module name - will be "invokeai" for built-in nodes
|
||||
uiconfig["node_pack"] = cls.__module__.split(".")[0]
|
||||
|
||||
if version is not None:
|
||||
try:
|
||||
semver.Version.parse(version)
|
||||
except ValueError as e:
|
||||
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
|
||||
cls.UIConfig.version = version
|
||||
uiconfig["version"] = version
|
||||
else:
|
||||
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
|
||||
cls.UIConfig.version = "1.0.0"
|
||||
uiconfig["version"] = "1.0.0"
|
||||
|
||||
cls.UIConfig = UIConfigBase(**uiconfig)
|
||||
|
||||
if use_cache is not None:
|
||||
cls.model_fields["use_cache"].default = use_cache
|
||||
|
@ -88,7 +88,8 @@ class QueueItemEventBase(QueueEventBase):
|
||||
|
||||
item_id: int = Field(description="The ID of the queue item")
|
||||
batch_id: str = Field(description="The ID of the queue batch")
|
||||
origin: str | None = Field(default=None, description="The origin of the batch")
|
||||
origin: str | None = Field(default=None, description="The origin of the queue item")
|
||||
destination: str | None = Field(default=None, description="The destination of the queue item")
|
||||
|
||||
|
||||
class InvocationEventBase(QueueItemEventBase):
|
||||
@ -114,6 +115,7 @@ class InvocationStartedEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@ -148,6 +150,7 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@ -186,6 +189,7 @@ class InvocationCompleteEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@ -219,6 +223,7 @@ class InvocationErrorEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@ -257,6 +262,7 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
status=queue_item.status,
|
||||
error_type=queue_item.error_type,
|
||||
|
@ -77,7 +77,14 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
|
||||
|
||||
class Batch(BaseModel):
|
||||
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
|
||||
origin: str | None = Field(default=None, description="The origin of this batch.")
|
||||
origin: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
|
||||
)
|
||||
destination: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
|
||||
)
|
||||
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
|
||||
graph: Graph = Field(description="The graph to initialize the session with")
|
||||
workflow: Optional[WorkflowWithoutID] = Field(
|
||||
@ -196,7 +203,14 @@ class SessionQueueItemWithoutGraph(BaseModel):
|
||||
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
|
||||
priority: int = Field(default=0, description="The priority of this queue item")
|
||||
batch_id: str = Field(description="The ID of the batch associated with this queue item")
|
||||
origin: str | None = Field(default=None, description="The origin of this queue item. ")
|
||||
origin: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
|
||||
)
|
||||
destination: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
|
||||
)
|
||||
session_id: str = Field(
|
||||
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
|
||||
)
|
||||
@ -297,6 +311,7 @@ class BatchStatus(BaseModel):
|
||||
queue_id: str = Field(..., description="The ID of the queue")
|
||||
batch_id: str = Field(..., description="The ID of the batch")
|
||||
origin: str | None = Field(..., description="The origin of the batch")
|
||||
destination: str | None = Field(..., description="The destination of the batch")
|
||||
pending: int = Field(..., description="Number of queue items with status 'pending'")
|
||||
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
|
||||
completed: int = Field(..., description="Number of queue items with status 'complete'")
|
||||
@ -443,6 +458,7 @@ class SessionQueueValueToInsert(NamedTuple):
|
||||
priority: int # priority
|
||||
workflow: Optional[str] # workflow json
|
||||
origin: str | None
|
||||
destination: str | None
|
||||
|
||||
|
||||
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
|
||||
@ -464,6 +480,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
|
||||
priority, # priority
|
||||
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
|
||||
batch.origin, # origin
|
||||
batch.destination, # destination
|
||||
)
|
||||
)
|
||||
return values_to_insert
|
||||
|
@ -128,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
|
||||
self.__cursor.executemany(
|
||||
"""--sql
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values_to_insert,
|
||||
)
|
||||
@ -579,7 +579,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
session_id,
|
||||
batch_id,
|
||||
queue_id,
|
||||
origin
|
||||
origin,
|
||||
destination
|
||||
FROM session_queue
|
||||
WHERE queue_id = ?
|
||||
"""
|
||||
@ -659,7 +660,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
self.__lock.acquire()
|
||||
self.__cursor.execute(
|
||||
"""--sql
|
||||
SELECT status, count(*), origin
|
||||
SELECT status, count(*), origin, destination
|
||||
FROM session_queue
|
||||
WHERE
|
||||
queue_id = ?
|
||||
@ -672,6 +673,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
total = sum(row[1] for row in result)
|
||||
counts: dict[str, int] = {row[0]: row[1] for row in result}
|
||||
origin = result[0]["origin"] if result else None
|
||||
destination = result[0]["destination"] if result else None
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
@ -681,6 +683,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
return BatchStatus(
|
||||
batch_id=batch_id,
|
||||
origin=origin,
|
||||
destination=destination,
|
||||
queue_id=queue_id,
|
||||
pending=counts.get("pending", 0),
|
||||
in_progress=counts.get("in_progress", 0),
|
||||
|
@ -10,9 +10,11 @@ class Migration15Callback:
|
||||
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
|
||||
"""
|
||||
- Adds `origin` column to the session queue table.
|
||||
- Adds `destination` column to the session queue table.
|
||||
"""
|
||||
|
||||
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
|
||||
cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;")
|
||||
|
||||
|
||||
def build_migration_15() -> Migration:
|
||||
@ -21,6 +23,7 @@ def build_migration_15() -> Migration:
|
||||
|
||||
This migration does the following:
|
||||
- Adds `origin` column to the session queue table.
|
||||
- Adds `destination` column to the session queue table.
|
||||
"""
|
||||
migration_15 = Migration(
|
||||
from_version=14,
|
||||
|
@ -64,6 +64,7 @@
|
||||
"@roarr/browser-log-writer": "^1.3.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chakra-react-select": "^4.9.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dateformat": "^5.0.3",
|
||||
"fracturedjsonjs": "^4.0.2",
|
||||
@ -92,7 +93,6 @@
|
||||
"react-icons": "^5.2.1",
|
||||
"react-redux": "9.1.2",
|
||||
"react-resizable-panels": "^2.0.23",
|
||||
"react-select": "5.8.0",
|
||||
"react-use": "^17.5.1",
|
||||
"react-virtuoso": "^4.9.0",
|
||||
"reactflow": "^11.11.4",
|
||||
|
329
invokeai/frontend/web/pnpm-lock.yaml
generated
329
invokeai/frontend/web/pnpm-lock.yaml
generated
@ -41,6 +41,9 @@ dependencies:
|
||||
chakra-react-select:
|
||||
specifier: ^4.9.1
|
||||
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
cmdk:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
@ -125,9 +128,6 @@ dependencies:
|
||||
react-resizable-panels:
|
||||
specifier: ^2.0.23
|
||||
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
|
||||
react-select:
|
||||
specifier: 5.8.0
|
||||
version: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
react-use:
|
||||
specifier: ^17.5.1
|
||||
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
|
||||
@ -2053,7 +2053,7 @@ packages:
|
||||
dependencies:
|
||||
'@chakra-ui/dom-utils': 2.1.0
|
||||
react: 18.3.1
|
||||
react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1)
|
||||
react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
@ -3784,6 +3784,288 @@ packages:
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
dev: false
|
||||
|
||||
/@radix-ui/primitive@1.0.1:
|
||||
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.4
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
|
||||
peerDependencies:
|
||||
@ -5210,7 +5492,6 @@ packages:
|
||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
dev: true
|
||||
|
||||
/@types/react-transition-group@4.4.10:
|
||||
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
|
||||
@ -6268,6 +6549,21 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
dependencies:
|
||||
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
dev: false
|
||||
|
||||
/color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
@ -9712,8 +10008,8 @@ packages:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
dev: false
|
||||
|
||||
/react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==}
|
||||
/react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
@ -9867,6 +10163,25 @@ packages:
|
||||
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
|
||||
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
|
||||
tslib: 2.7.0
|
||||
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
|
||||
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
|
||||
peerDependencies:
|
||||
|
@ -164,10 +164,10 @@
|
||||
"alpha": "Alpha",
|
||||
"selected": "Selected",
|
||||
"tab": "Tab",
|
||||
"viewing": "Viewing",
|
||||
"viewingDesc": "Review images in a large gallery view",
|
||||
"editing": "Editing",
|
||||
"editingDesc": "Edit on the Control Layers canvas",
|
||||
"view": "View",
|
||||
"viewDesc": "Review images in a large gallery view",
|
||||
"edit": "Edit",
|
||||
"editDesc": "Edit on the Canvas",
|
||||
"comparing": "Comparing",
|
||||
"comparingDesc": "Comparing two images",
|
||||
"enabled": "Enabled",
|
||||
@ -328,9 +328,13 @@
|
||||
"completedIn": "Completed in",
|
||||
"batch": "Batch",
|
||||
"origin": "Origin",
|
||||
"originCanvas": "Canvas",
|
||||
"originWorkflows": "Workflows",
|
||||
"originOther": "Other",
|
||||
"destination": "Destination",
|
||||
"upscaling": "Upscaling",
|
||||
"canvas": "Canvas",
|
||||
"generation": "Generation",
|
||||
"workflows": "Workflows",
|
||||
"other": "Other",
|
||||
"gallery": "Gallery",
|
||||
"batchFieldValues": "Batch Field Values",
|
||||
"item": "Item",
|
||||
"session": "Session",
|
||||
@ -1675,32 +1679,44 @@
|
||||
"deletePrompt": "Delete Prompt",
|
||||
"resetRegion": "Reset Region",
|
||||
"debugLayers": "Debug Layers",
|
||||
"showHUD": "Show HUD",
|
||||
"rectangle": "Rectangle",
|
||||
"maskFill": "Mask Fill",
|
||||
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
|
||||
"addControlLayer": "Add $t(controlLayers.controlLayer)",
|
||||
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
|
||||
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
|
||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||
"raster": "Raster",
|
||||
"rasterLayer_one": "Raster Layer",
|
||||
"controlLayer_one": "Control Layer",
|
||||
"inpaintMask_one": "Inpaint Mask",
|
||||
"regionalGuidance_one": "Regional Guidance",
|
||||
"ipAdapter_one": "IP Adapter",
|
||||
"rasterLayer_other": "Raster Layers",
|
||||
"controlLayer_other": "Control Layers",
|
||||
"inpaintMask_other": "Inpaint Masks",
|
||||
"regionalGuidance_other": "Regional Guidance",
|
||||
"ipAdapter_other": "IP Adapters",
|
||||
"rasterLayer": "Raster Layer",
|
||||
"controlLayer": "Control Layer",
|
||||
"inpaintMask": "Inpaint Mask",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"sendToGallery": "Send To Gallery",
|
||||
"sendToGalleryDesc": "Generations will be sent to the gallery.",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
"sendToCanvasDesc": "Generations will be staged onto the canvas.",
|
||||
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
|
||||
"ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
|
||||
"rasterLayer_withCount_other": "Raster Layers",
|
||||
"controlLayer_withCount_other": "Control Layers",
|
||||
"inpaintMask_withCount_other": "Inpaint Masks",
|
||||
"regionalGuidance_withCount_other": "Regional Guidance",
|
||||
"ipAdapter_withCount_other": "IP Adapters",
|
||||
"opacity": "Opacity",
|
||||
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
||||
"controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
|
||||
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
||||
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
||||
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
|
||||
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
||||
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
||||
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
|
||||
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
|
||||
@ -1734,7 +1750,10 @@
|
||||
"unlocked": "Unlocked",
|
||||
"deleteSelected": "Delete Selected",
|
||||
"deleteAll": "Delete All",
|
||||
"flipHorizontal": "Flip Horizontal",
|
||||
"flipVertical": "Flip Vertical",
|
||||
"fill": {
|
||||
"fillColor": "Fill Color",
|
||||
"fillStyle": "Fill Style",
|
||||
"solid": "Solid",
|
||||
"grid": "Grid",
|
||||
|
@ -16,6 +16,8 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||
import SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import { AppContent } from 'features/ui/components/AppContent';
|
||||
@ -121,6 +123,8 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
||||
<StylePresetModal />
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<PreselectedImage selectedImage={selectedImage} />
|
||||
<SettingsModal />
|
||||
<RefreshAfterResetModal />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -68,7 +68,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
|
||||
objects: [imageObject],
|
||||
};
|
||||
|
||||
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
api.dispatch(rasterLayerAdded({ overrides, isSelected: false }));
|
||||
api.dispatch(sessionStagingAreaReset());
|
||||
},
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
let didStartStaging = false;
|
||||
|
||||
if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') {
|
||||
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
|
||||
dispatch(sessionStartedStaging());
|
||||
didStartStaging = true;
|
||||
}
|
||||
@ -70,7 +70,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
const { g, noise, posCond } = buildGraphResult.value;
|
||||
|
||||
const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond));
|
||||
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
|
||||
|
||||
const prepareBatchResult = withResult(() =>
|
||||
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
||||
);
|
||||
|
||||
if (isErr(prepareBatchResult)) {
|
||||
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
||||
|
@ -32,6 +32,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
||||
workflow: builtWorkflow,
|
||||
runs: state.params.iterations,
|
||||
origin: 'workflows',
|
||||
destination: 'gallery',
|
||||
},
|
||||
prepend: action.payload.prepend,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
||||
|
||||
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
||||
|
||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
|
||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
|
||||
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||
|
104
invokeai/frontend/web/src/common/components/IconSwitch.tsx
Normal file
104
invokeai/frontend/web/src/common/components/IconSwitch.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
type IconSwitchProps = {
|
||||
isChecked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
iconChecked: ReactElement;
|
||||
tooltipChecked?: ReactNode;
|
||||
iconUnchecked: ReactElement;
|
||||
tooltipUnchecked?: ReactNode;
|
||||
ariaLabel: string;
|
||||
};
|
||||
|
||||
const getSx = (padding: string | number): SystemStyleObject => ({
|
||||
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
|
||||
'&[data-checked="true"]': {
|
||||
left: `calc(100% - ${padding})`,
|
||||
transform: 'translateX(-100%)',
|
||||
},
|
||||
'&[data-checked="false"]': {
|
||||
left: padding,
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
});
|
||||
|
||||
export const IconSwitch = memo(
|
||||
({
|
||||
isChecked,
|
||||
onChange,
|
||||
iconChecked,
|
||||
tooltipChecked,
|
||||
iconUnchecked,
|
||||
tooltipUnchecked,
|
||||
ariaLabel,
|
||||
}: IconSwitchProps) => {
|
||||
const onUncheck = useCallback(() => {
|
||||
onChange(false);
|
||||
}, [onChange]);
|
||||
const onCheck = useCallback(() => {
|
||||
onChange(true);
|
||||
}, [onChange]);
|
||||
|
||||
const gap = useToken('space', 1.5);
|
||||
const sx = useMemo(() => getSx(gap), [gap]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
h="full"
|
||||
p={gap}
|
||||
gap={gap}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
bg="invokeBlue.400"
|
||||
w={12}
|
||||
top={gap}
|
||||
bottom={gap}
|
||||
data-checked={isChecked}
|
||||
sx={sx}
|
||||
/>
|
||||
<Tooltip hasArrow label={tooltipUnchecked}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
fontSize={16}
|
||||
icon={iconUnchecked}
|
||||
onClick={onUncheck}
|
||||
variant={!isChecked ? 'solid' : 'ghost'}
|
||||
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
|
||||
aria-label={ariaLabel}
|
||||
data-checked={!isChecked}
|
||||
w={12}
|
||||
alignSelf="stretch"
|
||||
h="auto"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label={tooltipChecked}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
fontSize={16}
|
||||
icon={iconChecked}
|
||||
onClick={onCheck}
|
||||
variant={isChecked ? 'solid' : 'ghost'}
|
||||
colorScheme={isChecked ? 'invokeBlue' : 'base'}
|
||||
aria-label={ariaLabel}
|
||||
data-checked={isChecked}
|
||||
w={12}
|
||||
alignSelf="stretch"
|
||||
h="auto"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IconSwitch.displayName = 'IconSwitch';
|
@ -1,7 +1,7 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
|
@ -83,6 +83,7 @@ const ChangeBoardModal = () => {
|
||||
acceptCallback={handleChangeBoard}
|
||||
acceptButtonText={t('boards.move')}
|
||||
cancelButtonText={t('boards.cancel')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Text>
|
||||
|
@ -31,22 +31,22 @@ export const CanvasAddEntityButtons = memo(() => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<ButtonGroup orientation="vertical" isAttached={false}>
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center">
|
||||
<ButtonGroup position="relative" orientation="vertical" isAttached={false} top="20%">
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask', { count: 1 })}
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance', { count: 1 })}
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer', { count: 1 })}
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer', { count: 1 })}
|
||||
{t('controlLayers.controlLayer')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.ipAdapter', { count: 1 })}
|
||||
{t('controlLayers.ipAdapter')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
@ -33,19 +33,19 @@ export const CanvasEntityListMenuItems = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask', { count: 1 })}
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance', { count: 1 })}
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer', { count: 1 })}
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer', { count: 1 })}
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.ipAdapter', { count: 1 })}
|
||||
{t('controlLayers.ipAdapter')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
|
@ -57,7 +57,7 @@ const marks = [
|
||||
mapOpacityToSliderValue(1),
|
||||
];
|
||||
|
||||
const sliderDefaultValue = mapOpacityToSliderValue(100);
|
||||
const sliderDefaultValue = mapOpacityToSliderValue(1);
|
||||
|
||||
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||
|
||||
@ -134,7 +134,11 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<FormControl w="min-content" gap={2}>
|
||||
<FormControl
|
||||
w="min-content"
|
||||
gap={2}
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
>
|
||||
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||
<PopoverAnchor>
|
||||
<NumberInput
|
||||
@ -152,7 +156,6 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
onKeyDown={onKeyDown}
|
||||
clampValueOnBlur={false}
|
||||
variant="outline"
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
>
|
||||
<NumberInputField paddingInlineEnd={7} />
|
||||
<PopoverTrigger>
|
||||
@ -164,6 +167,7 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
position="absolute"
|
||||
insetInlineEnd={0}
|
||||
h="full"
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</NumberInput>
|
||||
@ -181,6 +185,7 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
marks={marks}
|
||||
formatValue={formatSliderValue}
|
||||
alwaysShowMarks
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
|
||||
|
||||
export const CanvasModeSwitcher = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector(selectCanvasMode);
|
||||
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
|
||||
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);
|
||||
|
||||
return (
|
||||
<ButtonGroup variant="outline">
|
||||
<Button onClick={onClickGenerate} colorScheme={mode === 'generate' ? 'invokeBlue' : 'base'}>
|
||||
{t('controlLayers.generateMode')}
|
||||
</Button>
|
||||
<Button onClick={onClickCompose} colorScheme={mode === 'compose' ? 'invokeBlue' : 'base'}>
|
||||
{t('controlLayers.composeMode')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasModeSwitcher.displayName = 'CanvasModeSwitcher';
|
@ -1,21 +1,37 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { Box, ContextMenu, Divider, Flex, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
|
||||
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const CanvasPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const renderMenu = useCallback(
|
||||
() => (
|
||||
<MenuList>
|
||||
<CanvasEntityListMenuItems />
|
||||
</MenuList>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListActionBar />
|
||||
<Divider py={0} />
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} stopImmediatePropagation stopPropagation>
|
||||
{(ref) => (
|
||||
<Box ref={ref} w="full" h="full">
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Box>
|
||||
)}
|
||||
</ContextMenu>
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipSendToGallery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
|
||||
|
||||
const TooltipSendToCanvas = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
||||
|
||||
export const CanvasSendToToggle = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isComposing = useAppSelector(selectIsComposing);
|
||||
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
dispatch(sessionSendToCanvasChanged(isChecked));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={isComposing}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiImageBold />}
|
||||
tooltipUnchecked={<TooltipSendToGallery />}
|
||||
iconChecked={<PiPaintBrushBold />}
|
||||
tooltipChecked={<TooltipSendToCanvas />}
|
||||
ariaLabel="Toggle canvas mode"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSendToToggle.displayName = 'CanvasSendToToggle';
|
@ -1,8 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
@ -29,8 +28,7 @@ export const ControlLayer = memo(({ id }: Props) => {
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<ControlLayerBadges />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
<CanvasEntitySettingsWrapper>
|
||||
<ControlLayerControlAdapter />
|
||||
|
@ -19,8 +19,6 @@ export const CanvasEditor = memo(() => {
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
layerStyle="first"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
@ -32,14 +30,14 @@ export const CanvasEditor = memo(() => {
|
||||
>
|
||||
<ControlLayersToolbar />
|
||||
<StageComponent />
|
||||
<Flex position="absolute" bottom={16} gap={2} align="center" justify="center">
|
||||
<Flex position="absolute" bottom={8} gap={2} align="center" justify="center">
|
||||
<CanvasManagerProviderGate>
|
||||
<StagingAreaIsStagingGate>
|
||||
<StagingAreaToolbar />
|
||||
</StagingAreaIsStagingGate>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<Flex position="absolute" bottom={16}>
|
||||
<Flex position="absolute" bottom={8}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
<Transform />
|
||||
|
@ -1,19 +1,20 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher';
|
||||
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
||||
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ControlLayersToolbar = memo(() => {
|
||||
useCanvasUndoRedo();
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
@ -26,10 +27,8 @@ export const ControlLayersToolbar = memo(() => {
|
||||
<CanvasResetViewButton />
|
||||
<Spacer />
|
||||
<ToolFillColorPicker />
|
||||
<CanvasModeSwitcher />
|
||||
<UndoRedoButtonGroup />
|
||||
<CanvasSettingsPopover />
|
||||
<ViewerToggleMenu />
|
||||
<ViewerToggle />
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
|
@ -15,18 +15,6 @@ export const Filter = memo(() => {
|
||||
const isFiltering = useStore(canvasManager.filter.$isFiltering);
|
||||
const isProcessing = useStore(canvasManager.filter.$isProcessing);
|
||||
|
||||
const previewFilter = useCallback(() => {
|
||||
canvasManager.filter.previewFilter();
|
||||
}, [canvasManager.filter]);
|
||||
|
||||
const applyFilter = useCallback(() => {
|
||||
canvasManager.filter.applyFilter();
|
||||
}, [canvasManager.filter]);
|
||||
|
||||
const cancelFilter = useCallback(() => {
|
||||
canvasManager.filter.cancelFilter();
|
||||
}, [canvasManager.filter]);
|
||||
|
||||
const onChangeFilterConfig = useCallback(
|
||||
(filterConfig: FilterConfig) => {
|
||||
canvasManager.filter.$config.set(filterConfig);
|
||||
@ -65,24 +53,27 @@ export const Filter = memo(() => {
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={previewFilter}
|
||||
onClick={canvasManager.filter.previewFilter}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.preview')}
|
||||
>
|
||||
{t('controlLayers.filter.preview')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={applyFilter}
|
||||
onClick={canvasManager.filter.applyFilter}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={cancelFilter}
|
||||
onClick={canvasManager.filter.cancelFilter}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
|
@ -1,53 +1,27 @@
|
||||
import { Box, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Grid, GridItem, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { round } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
|
||||
|
||||
export const HeadsUpDisplay = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const stageAttrs = useStore(canvasManager.stateApi.$stageAttrs);
|
||||
const cursorPos = useStore(canvasManager.stateApi.$lastCursorPos);
|
||||
const isDrawing = useStore(canvasManager.stateApi.$isDrawing);
|
||||
const isMouseDown = useStore(canvasManager.stateApi.$isMouseDown);
|
||||
const lastMouseDownPos = useStore(canvasManager.stateApi.$lastMouseDownPos);
|
||||
const lastAddedPoint = useStore(canvasManager.stateApi.$lastAddedPoint);
|
||||
const bbox = useAppSelector(selectBbox);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
|
||||
<HUDItem label="Zoom" value={`${round(stageAttrs.scale * 100, 2)}%`} />
|
||||
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
|
||||
<HUDItem
|
||||
label="Stage Size"
|
||||
value={`${round(stageAttrs.width / stageAttrs.scale, 2)}×${round(stageAttrs.height / stageAttrs.scale, 2)} px`}
|
||||
/>
|
||||
<HUDItem label="BBox Size" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
|
||||
<HUDItem label="BBox Position" value={`${bbox.rect.x}, ${bbox.rect.y}`} />
|
||||
<HUDItem label="BBox Width % 8" value={round(bbox.rect.width % 8, 2)} />
|
||||
<HUDItem label="BBox Height % 8" value={round(bbox.rect.height % 8, 2)} />
|
||||
<HUDItem label="BBox X % 8" value={round(bbox.rect.x % 8, 2)} />
|
||||
<HUDItem label="BBox Y % 8" value={round(bbox.rect.y % 8, 2)} />
|
||||
<HUDItem
|
||||
label="Cursor Position"
|
||||
value={cursorPos ? `${round(cursorPos.x, 2)}, ${round(cursorPos.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
<HUDItem label="Is Drawing" value={isDrawing ? 'True' : 'False'} />
|
||||
<HUDItem label="Is Mouse Down" value={isMouseDown ? 'True' : 'False'} />
|
||||
<HUDItem
|
||||
label="Last Mouse Down Pos"
|
||||
value={lastMouseDownPos ? `${round(lastMouseDownPos.x, 2)}, ${round(lastMouseDownPos.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
<HUDItem
|
||||
label="Last Added Point"
|
||||
value={lastAddedPoint ? `${round(lastAddedPoint.x, 2)}, ${round(lastAddedPoint.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
</Flex>
|
||||
<Grid
|
||||
bg="base.900"
|
||||
borderBottomEndRadius="base"
|
||||
p={2}
|
||||
gap={2}
|
||||
borderRadius="base"
|
||||
templateColumns="auto auto"
|
||||
opacity={0.6}
|
||||
>
|
||||
<HUDItem label="BBox" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
|
||||
<HUDItem label="Scaled BBox" value={`${bbox.scaledSize.width}×${bbox.scaledSize.height} px`} />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
@ -55,12 +29,14 @@ HeadsUpDisplay.displayName = 'HeadsUpDisplay';
|
||||
|
||||
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
|
||||
return (
|
||||
<Box display="inline-block" lineHeight={1}>
|
||||
<Text as="span">{label}: </Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{value}
|
||||
</Text>
|
||||
</Box>
|
||||
<>
|
||||
<GridItem>
|
||||
<Text textAlign="end">{label}: </Text>
|
||||
</GridItem>
|
||||
<GridItem fontWeight="semibold">
|
||||
<Text>{value}</Text>
|
||||
</GridItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
@ -18,10 +18,10 @@ export const IPAdapter = memo(({ id }: Props) => {
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
<CanvasEntityContainer>
|
||||
<CanvasEntityHeader ps={4}>
|
||||
<CanvasEntityHeader ps={4} py={5}>
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
<IPAdapterSettings />
|
||||
</CanvasEntityContainer>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
@ -25,8 +24,7 @@ export const InpaintMask = memo(({ id }: Props) => {
|
||||
<CanvasEntityPreviewImage />
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
</CanvasEntityContainer>
|
||||
</EntityMaskAdapterGate>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
@ -25,8 +24,7 @@ export const RasterLayer = memo(({ id }: Props) => {
|
||||
<CanvasEntityPreviewImage />
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
</CanvasEntityContainer>
|
||||
</EntityLayerAdapterGate>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
||||
@ -28,8 +27,7 @@ export const RegionalGuidance = memo(({ id }: Props) => {
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<RegionalGuidanceBadges />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
</CanvasEntityHeader>
|
||||
<RegionalGuidanceSettings />
|
||||
</CanvasEntityContainer>
|
||||
|
@ -12,10 +12,13 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
|
||||
return (
|
||||
<Tooltip label={t('controlLayers.deletePrompt')}>
|
||||
<IconButton
|
||||
variant="promptOverlay"
|
||||
variant="link"
|
||||
aria-label={t('controlLayers.deletePrompt')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={onDelete}
|
||||
flexGrow={0}
|
||||
size="sm"
|
||||
p={0}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
import { usePrompt } from 'features/prompt/usePrompt';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const _focusVisible: SystemStyleObject = {
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
export const RegionalGuidanceNegativePrompt = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const selectPrompt = useMemo(
|
||||
@ -49,14 +53,25 @@ export const RegionalGuidanceNegativePrompt = memo(() => {
|
||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
variant="darkFilled"
|
||||
paddingRight={30}
|
||||
variant="outline"
|
||||
paddingInlineStart={2}
|
||||
paddingInlineEnd={8}
|
||||
fontSize="sm"
|
||||
zIndex="0 !important"
|
||||
_focusVisible={_focusVisible}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<Flex
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
position="absolute"
|
||||
insetBlockStart={2}
|
||||
insetInlineEnd={0}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
</Flex>
|
||||
</Box>
|
||||
</PromptPopover>
|
||||
);
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
import { usePrompt } from 'features/prompt/usePrompt';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const _focusVisible: SystemStyleObject = {
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
export const RegionalGuidancePositivePrompt = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const selectPrompt = useMemo(
|
||||
@ -49,14 +53,25 @@ export const RegionalGuidancePositivePrompt = memo(() => {
|
||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
variant="darkFilled"
|
||||
paddingRight={30}
|
||||
variant="outline"
|
||||
paddingInlineStart={2}
|
||||
paddingInlineEnd={8}
|
||||
minH={28}
|
||||
zIndex="0 !important"
|
||||
_focusVisible={_focusVisible}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<Flex
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
position="absolute"
|
||||
insetBlockStart={2}
|
||||
insetInlineEnd={0}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
</Flex>
|
||||
</Box>
|
||||
</PromptPopover>
|
||||
);
|
||||
|
@ -35,7 +35,7 @@ export const RegionalGuidanceSettings = memo(() => {
|
||||
{flags.hasPositivePrompt && (
|
||||
<>
|
||||
<RegionalGuidancePositivePrompt />
|
||||
{(flags.hasNegativePrompt || flags.hasIPAdapters) && <Divider />}
|
||||
{!flags.hasNegativePrompt && flags.hasIPAdapters && <Divider />}
|
||||
</>
|
||||
)}
|
||||
{flags.hasNegativePrompt && (
|
||||
|
@ -18,6 +18,7 @@ import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/compo
|
||||
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
||||
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
|
||||
import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton';
|
||||
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiSettings4Fill } from 'react-icons/ri';
|
||||
@ -27,7 +28,7 @@ export const CanvasSettingsPopover = memo(() => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
|
||||
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} variant="ghost" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
@ -37,6 +38,7 @@ export const CanvasSettingsPopover = memo(() => {
|
||||
<CanvasSettingsInvertScrollCheckbox />
|
||||
<CanvasSettingsClipToBboxCheckbox />
|
||||
<CanvasSettingsDynamicGridSwitch />
|
||||
<CanvasSettingsShowHUDSwitch />
|
||||
<CanvasSettingsResetButton />
|
||||
<DebugSettings />
|
||||
</Flex>
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSettingsSlice, settingsShowHUDToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
|
||||
export const CanvasSettingsShowHUDSwitch = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
const onChange = useCallback(() => {
|
||||
dispatch(settingsShowHUDToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel m={0} flexGrow={1}>
|
||||
{t('controlLayers.showHUD')}
|
||||
</FormLabel>
|
||||
<Switch size="sm" isChecked={showHUD} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSettingsShowHUDSwitch.displayName = 'CanvasSettingsShowHUDSwitch';
|
@ -8,20 +8,18 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const showHud = false;
|
||||
|
||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||
Konva.showWarnings = false;
|
||||
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null) => {
|
||||
const store = useAppStore();
|
||||
const socket = useStore($socket);
|
||||
const dpr = useDevicePixelRatio({ round: false });
|
||||
@ -42,28 +40,25 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
||||
const manager = new CanvasManager(stage, container, store, socket);
|
||||
manager.initialize();
|
||||
return manager.destroy;
|
||||
}, [asPreview, container, socket, stage, store]);
|
||||
}, [container, socket, stage, store]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Konva.pixelRatio = dpr;
|
||||
}, [dpr]);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
asPreview?: boolean;
|
||||
};
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
|
||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
export const StageComponent = memo(() => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const [stage] = useState(
|
||||
() =>
|
||||
new Konva.Stage({
|
||||
id: uuidv4(),
|
||||
id: getPrefixedId('konva_stage'),
|
||||
container: document.createElement('div'),
|
||||
listening: !asPreview,
|
||||
})
|
||||
);
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
@ -72,7 +67,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
setContainer(el);
|
||||
}, []);
|
||||
|
||||
useStageRenderer(stage, container, asPreview);
|
||||
useStageRenderer(stage, container);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@ -82,10 +77,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" bg={dynamicGrid ? 'base.850' : 'base.900'}>
|
||||
<Flex position="relative" w="full" h="full" bg={dynamicGrid ? 'base.850' : 'base.900'} borderRadius="base">
|
||||
{!dynamicGrid && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
bgImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||
top={0}
|
||||
right={0}
|
||||
@ -102,15 +98,12 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
left={0}
|
||||
ref={containerRef}
|
||||
borderRadius="base"
|
||||
border={1}
|
||||
borderStyle="solid"
|
||||
borderColor="base.700"
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
{!asPreview && (
|
||||
<Flex position="absolute" top={0} insetInlineStart={0} pointerEvents="none">
|
||||
{showHud && <HeadsUpDisplay />}
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<HeadsUpDisplay />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
@ -98,9 +98,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive]
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@ -108,9 +108,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive]
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@ -118,9 +118,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onAccept,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive]
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
const counterText = useMemo(() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
@ -23,17 +23,13 @@ export const ToolFillColorPicker = memo(() => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
as="button"
|
||||
aria-label={t('controlLayers.brushColor')}
|
||||
borderRadius="full"
|
||||
borderWidth={1}
|
||||
bg={rgbaColorToString(fill)}
|
||||
w={8}
|
||||
h={8}
|
||||
cursor="pointer"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Flex role="button" aria-label={t('controlLayers.fill.fillColor')} tabIndex={-1} w={8} h={8}>
|
||||
<Tooltip label={t('controlLayers.fill.fillColor')}>
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Box borderRadius="full" w={6} h={6} borderWidth={1} bg={rgbaColorToString(fill)} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody minH={64}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
@ -6,9 +6,9 @@ import {
|
||||
useEntityIdentifierContext,
|
||||
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const TransformBox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@ -16,14 +16,6 @@ const TransformBox = memo(() => {
|
||||
const adapter = useEntityAdapter(entityIdentifier);
|
||||
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||
|
||||
const applyTransform = useCallback(() => {
|
||||
adapter.transformer.applyTransform();
|
||||
}, [adapter.transformer]);
|
||||
|
||||
const cancelFilter = useCallback(() => {
|
||||
adapter.transformer.stopTransform();
|
||||
}, [adapter.transformer]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
bg="base.800"
|
||||
@ -40,16 +32,33 @@ const TransformBox = memo(() => {
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.tool.transform')}
|
||||
</Heading>
|
||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.transformer.resetTransform}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('accessibility.reset')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={applyTransform}
|
||||
onClick={adapter.transformer.applyTransform}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('common.apply')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('common.apply')}
|
||||
</Button>
|
||||
<Button leftIcon={<PiXBold />} onClick={cancelFilter} isLoading={isProcessing} loadingText={t('common.cancel')}>
|
||||
<Button
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.transformer.stopTransform}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('common.cancel')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
ipaAdded,
|
||||
rasterLayerAdded,
|
||||
rgAdded,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
type: CanvasEntityIdentifier['type'];
|
||||
};
|
||||
|
||||
export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
switch (type) {
|
||||
case 'inpaint_mask':
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'raster_layer':
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'control_layer':
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
dispatch(ipaAdded({ isSelected: true }));
|
||||
break;
|
||||
}
|
||||
}, [dispatch, type]);
|
||||
|
||||
const label = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'inpaint_mask':
|
||||
return t('controlLayers.addInpaintMask');
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.addRegionalGuidance');
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.addRasterLayer');
|
||||
case 'control_layer':
|
||||
return t('controlLayers.addControlLayer');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.addIPAdapter');
|
||||
}
|
||||
}, [type, t]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={label}
|
||||
tooltip={label}
|
||||
variant="link"
|
||||
icon={<PiPlusBold />}
|
||||
onClick={onClick}
|
||||
alignSelf="stretch"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityAddOfTypeButton.displayName = 'CanvasEntityAddOfTypeButton';
|
@ -0,0 +1,31 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityDeleteButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityDeleted({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={t('common.delete')}
|
||||
tooltip={t('common.delete')}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiTrashSimpleFill />}
|
||||
onClick={onClick}
|
||||
colorScheme="error"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton';
|
@ -1,34 +1,29 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
||||
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCheckBold } from 'react-icons/pi';
|
||||
import { PiCircleBold, PiCircleFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityEnabledToggle = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const isEnabled = useEntityIsEnabled(entityIdentifier);
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityIsEnabledToggled({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
const isHovered = useBoolean(false);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="sm"
|
||||
onMouseOver={isHovered.setTrue}
|
||||
onMouseOut={isHovered.setFalse}
|
||||
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||
variant="ghost"
|
||||
icon={isEnabled || isHovered.isTrue ? <PiCheckBold /> : undefined}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={isEnabled ? <PiCircleFill /> : <PiCircleBold />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
|
||||
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
|
||||
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
@ -53,6 +54,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
||||
</Text>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
</Flex>
|
||||
<Collapse in={collapse.isTrue}>
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const CanvasEntityHeaderCommonActions = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
|
||||
return (
|
||||
<Flex alignSelf="stretch">
|
||||
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityDeleteButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityHeaderCommonActions.displayName = 'CanvasEntityHeaderCommonActions';
|
@ -1,34 +1,29 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLockSimpleFill } from 'react-icons/pi';
|
||||
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityIsLockedToggle = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityIsLockedToggled({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
const isHovered = useBoolean(false);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="sm"
|
||||
onMouseOver={isHovered.setTrue}
|
||||
onMouseOut={isHovered.setFalse}
|
||||
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||
variant="ghost"
|
||||
icon={isLocked || isHovered.isTrue ? <PiLockSimpleFill /> : undefined}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
@ -11,7 +11,10 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
const ChakraCanvas = chakra.canvas;
|
||||
|
||||
const PADDING = 4;
|
||||
const PADDING = 2;
|
||||
|
||||
const CONTAINER_WIDTH = 36; // this is size 12 in our theme - need it in px for the canvas
|
||||
const CONTAINER_WIDTH_PX = `${CONTAINER_WIDTH}px`;
|
||||
|
||||
export const CanvasEntityPreviewImage = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
@ -31,11 +34,10 @@ export const CanvasEntityPreviewImage = memo(() => {
|
||||
[entityIdentifier]
|
||||
);
|
||||
const maskColor = useSelector(selectMaskColor);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const cache = useStore(adapter.renderer.$canvasCache);
|
||||
useEffect(() => {
|
||||
if (!cache || !canvasRef.current || !containerRef.current) {
|
||||
if (!cache || !canvasRef.current) {
|
||||
return;
|
||||
}
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
@ -49,7 +51,7 @@ export const CanvasEntityPreviewImage = memo(() => {
|
||||
canvasRef.current.width = rect.width;
|
||||
canvasRef.current.height = rect.height;
|
||||
|
||||
const scale = containerRef.current.offsetWidth / rect.width;
|
||||
const scale = CONTAINER_WIDTH / rect.width;
|
||||
|
||||
const sx = rect.x;
|
||||
const sy = rect.y;
|
||||
@ -72,11 +74,10 @@ export const CanvasEntityPreviewImage = memo(() => {
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
ref={containerRef}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w={12}
|
||||
h={12}
|
||||
w={CONTAINER_WIDTH_PX}
|
||||
h={CONTAINER_WIDTH_PX}
|
||||
borderRadius="sm"
|
||||
borderWidth={1}
|
||||
bg="base.900"
|
||||
|
@ -1,16 +1,14 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const UndoRedoButtonGroup = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
export const useCanvasUndoRedo = () => {
|
||||
useAssertSingleton('useCanvasUndoRedo');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const mayUndo = useAppSelector(selectCanvasMayUndo);
|
||||
@ -27,25 +25,4 @@ export const UndoRedoButtonGroup = memo(() => {
|
||||
mayRedo,
|
||||
handleRedo,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
aria-label={t('unifiedCanvas.undo')}
|
||||
tooltip={t('unifiedCanvas.undo')}
|
||||
onClick={handleUndo}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
isDisabled={!mayUndo}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={t('unifiedCanvas.redo')}
|
||||
tooltip={t('unifiedCanvas.redo')}
|
||||
onClick={handleRedo}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
isDisabled={!mayRedo}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
UndoRedoButtonGroup.displayName = 'UndoRedoButtonGroup';
|
||||
};
|
@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
|
||||
const parts: string[] = [];
|
||||
if (entityIdentifier.type === 'inpaint_mask') {
|
||||
parts.push(t('controlLayers.inpaintMask', { count: 1 }));
|
||||
parts.push(t('controlLayers.inpaintMask'));
|
||||
} else if (entityIdentifier.type === 'control_layer') {
|
||||
parts.push(t('controlLayers.controlLayer', { count: 1 }));
|
||||
parts.push(t('controlLayers.controlLayer'));
|
||||
} else if (entityIdentifier.type === 'raster_layer') {
|
||||
parts.push(t('controlLayers.rasterLayer', { count: 1 }));
|
||||
parts.push(t('controlLayers.rasterLayer'));
|
||||
} else if (entityIdentifier.type === 'ip_adapter') {
|
||||
parts.push(t('common.ipAdapter', { count: 1 }));
|
||||
parts.push(t('common.ipAdapter'));
|
||||
} else if (entityIdentifier.type === 'regional_guidance') {
|
||||
parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
|
||||
parts.push(t('controlLayers.regionalGuidance'));
|
||||
} else {
|
||||
assert(false, 'Unexpected entity type');
|
||||
}
|
||||
|
@ -8,15 +8,15 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin
|
||||
const typeString = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'control_layer':
|
||||
return t('controlLayers.controlLayer', { count: 0 });
|
||||
return t('controlLayers.controlLayer');
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.rasterLayer', { count: 0 });
|
||||
return t('controlLayers.rasterLayer');
|
||||
case 'inpaint_mask':
|
||||
return t('controlLayers.inpaintMask', { count: 0 });
|
||||
return t('controlLayers.inpaintMask');
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.regionalGuidance', { count: 0 });
|
||||
return t('controlLayers.regionalGuidance');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.ipAdapter', { count: 0 });
|
||||
return t('controlLayers.ipAdapter');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -112,12 +112,11 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC {
|
||||
this.renderer.updateOpacity(opacity);
|
||||
}
|
||||
|
||||
if (state.type === 'control_layer' && this.state.type === 'control_layer') {
|
||||
if (this.isFirstRender || state.withTransparencyEffect !== this.state.withTransparencyEffect) {
|
||||
if (state.type === 'control_layer' && prevState.type === 'control_layer') {
|
||||
if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) {
|
||||
this.renderer.updateTransparencyEffect(state.withTransparencyEffect);
|
||||
}
|
||||
}
|
||||
// this.transformer.syncInteractionState();
|
||||
|
||||
if (this.isFirstRender) {
|
||||
this.transformer.updateBbox();
|
||||
|
@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
||||
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
|
||||
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
|
||||
// updated attributes to the object group, propagating the transformation on down.
|
||||
this.parent.renderer.konva.objectGroup.setAttrs({
|
||||
x: this.konva.proxyRect.x(),
|
||||
y: this.konva.proxyRect.y(),
|
||||
scaleX: this.konva.proxyRect.scaleX(),
|
||||
scaleY: this.konva.proxyRect.scaleY(),
|
||||
rotation: this.konva.proxyRect.rotation(),
|
||||
});
|
||||
this.syncObjectGroupWithProxyRect();
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transformend', () => {
|
||||
@ -395,6 +389,54 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
||||
this.parent.konva.layer.add(this.konva.transformer);
|
||||
}
|
||||
|
||||
// TODO(psyche): These don't work when the entity is rotated, need to do some math to offset the flip after rotation
|
||||
// flipHorizontal = () => {
|
||||
// if (!this.isTransforming || this.$isProcessing.get()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Flipping horizontally = flipping across the vertical axis:
|
||||
// // - Flip by negating the x scale
|
||||
// // - Restore position by translating the rect rightwards by the width of the rect
|
||||
// const x = this.konva.proxyRect.x();
|
||||
// const width = this.konva.proxyRect.width();
|
||||
// const scaleX = this.konva.proxyRect.scaleX();
|
||||
// this.konva.proxyRect.setAttrs({
|
||||
// scaleX: -scaleX,
|
||||
// x: x + width * scaleX,
|
||||
// });
|
||||
|
||||
// this.syncObjectGroupWithProxyRect();
|
||||
// };
|
||||
|
||||
// flipVertical = () => {
|
||||
// if (!this.isTransforming || this.$isProcessing.get()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Flipping vertically = flipping across the horizontal axis:
|
||||
// // - Flip by negating the y scale
|
||||
// // - Restore position by translating the rect downwards by the height of the rect
|
||||
// const y = this.konva.proxyRect.y();
|
||||
// const height = this.konva.proxyRect.height();
|
||||
// const scaleY = this.konva.proxyRect.scaleY();
|
||||
// this.konva.proxyRect.setAttrs({
|
||||
// scaleY: -scaleY,
|
||||
// y: y + height * scaleY,
|
||||
// });
|
||||
// this.syncObjectGroupWithProxyRect();
|
||||
// };
|
||||
|
||||
syncObjectGroupWithProxyRect = () => {
|
||||
this.parent.renderer.konva.objectGroup.setAttrs({
|
||||
x: this.konva.proxyRect.x(),
|
||||
y: this.konva.proxyRect.y(),
|
||||
scaleX: this.konva.proxyRect.scaleX(),
|
||||
scaleY: this.konva.proxyRect.scaleY(),
|
||||
rotation: this.konva.proxyRect.rotation(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the transformer's visual components to match the parent entity's position and bounding box.
|
||||
* @param position The position of the parent entity
|
||||
@ -510,6 +552,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
||||
this.stopTransform();
|
||||
};
|
||||
|
||||
resetTransform = () => {
|
||||
this.resetScale();
|
||||
this.updatePosition();
|
||||
this.updateBbox();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its
|
||||
* original state.
|
||||
@ -520,12 +568,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
||||
this.isTransforming = false;
|
||||
this.setInteractionMode('off');
|
||||
|
||||
// Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or
|
||||
// Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or
|
||||
// canceled a transformation. In either case, the scale should be reset.
|
||||
this.resetScale();
|
||||
|
||||
this.updatePosition();
|
||||
this.updateBbox();
|
||||
this.resetTransform();
|
||||
this.syncInteractionState();
|
||||
this.manager.stateApi.$transformingEntity.set(null);
|
||||
this.$isProcessing.set(false);
|
||||
|
@ -250,12 +250,10 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.konva.colorPicker.group.visible(tool === 'colorPicker');
|
||||
};
|
||||
|
||||
render = () => {
|
||||
syncCursorStyle = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||
const tool = this.manager.stateApi.$tool.get();
|
||||
|
||||
@ -294,17 +292,10 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
// Non-drawable layers don't have tools
|
||||
stage.container.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
stage.setIsDraggable(tool === 'view');
|
||||
|
||||
if (!cursorPos || renderedEntityCount === 0) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
this.konva.group.visible(false);
|
||||
} else {
|
||||
this.konva.group.visible(true);
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
renderBrushTool = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
@ -332,7 +323,10 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
};
|
||||
|
||||
renderEraserTool = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
@ -359,7 +353,10 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
};
|
||||
|
||||
renderColorPicker = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
|
||||
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
|
||||
@ -397,12 +394,8 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
|
||||
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
|
||||
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
|
||||
const innerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS
|
||||
);
|
||||
const outerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS
|
||||
);
|
||||
const innerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS);
|
||||
const outerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS);
|
||||
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
@ -435,6 +428,38 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const tool = this.manager.stateApi.$tool.get();
|
||||
|
||||
const isDrawable =
|
||||
!!selectedEntity &&
|
||||
selectedEntity.state.isEnabled &&
|
||||
!selectedEntity.state.isLocked &&
|
||||
isDrawableEntity(selectedEntity.state);
|
||||
|
||||
this.syncCursorStyle();
|
||||
|
||||
stage.setIsDraggable(tool === 'view');
|
||||
|
||||
if (!cursorPos || renderedEntityCount === 0) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
this.konva.group.visible(false);
|
||||
} else {
|
||||
this.konva.group.visible(true);
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
this.renderBrushTool(cursorPos);
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
this.renderEraserTool(cursorPos);
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
this.renderColorPicker(cursorPos);
|
||||
}
|
||||
|
||||
this.setToolVisibility(tool, isDrawable);
|
||||
@ -864,6 +889,10 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.manager.stateApi.$spaceKey.set(true);
|
||||
this.manager.stateApi.$lastCursorPos.set(null);
|
||||
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||
} else if (e.key === 'Alt') {
|
||||
// Select the color picker on alt key down
|
||||
this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get());
|
||||
this.manager.stateApi.$tool.set('colorPicker');
|
||||
}
|
||||
};
|
||||
|
||||
@ -880,6 +909,11 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
|
||||
this.manager.stateApi.$toolBuffer.set(null);
|
||||
this.manager.stateApi.$spaceKey.set(false);
|
||||
} else if (e.key === 'Alt') {
|
||||
// Revert the tool to the previous tool on alt key up
|
||||
const toolBuffer = this.manager.stateApi.$toolBuffer.get();
|
||||
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
|
||||
this.manager.stateApi.$toolBuffer.set(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
|
||||
export type CanvasSessionState = {
|
||||
mode: SessionMode;
|
||||
sendToCanvas: boolean;
|
||||
isStaging: boolean;
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
};
|
||||
|
||||
const initialState: CanvasSessionState = {
|
||||
mode: 'generate',
|
||||
sendToCanvas: false,
|
||||
isStaging: false,
|
||||
stagedImages: [],
|
||||
selectedStagedImageIndex: 0,
|
||||
@ -27,6 +27,7 @@ export const canvasSessionSlice = createSlice({
|
||||
},
|
||||
sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
||||
const { stagingAreaImage } = action.payload;
|
||||
state.isStaging = true;
|
||||
state.stagedImages.push(stagingAreaImage);
|
||||
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
||||
},
|
||||
@ -50,9 +51,8 @@ export const canvasSessionSlice = createSlice({
|
||||
state.stagedImages = [];
|
||||
state.selectedStagedImageIndex = 0;
|
||||
},
|
||||
sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => {
|
||||
const { mode } = action.payload;
|
||||
state.mode = mode;
|
||||
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.sendToCanvas = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -64,7 +64,7 @@ export const {
|
||||
sessionStagingAreaReset,
|
||||
sessionNextStagedImageSelected,
|
||||
sessionPrevStagedImageSelected,
|
||||
sessionModeChanged,
|
||||
sessionSendToCanvasChanged,
|
||||
} = canvasSessionSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@ -85,3 +85,7 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
|
||||
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
|
||||
|
||||
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
|
||||
export const selectIsComposing = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
(canvasSession) => canvasSession.sendToCanvas
|
||||
);
|
||||
|
@ -35,10 +35,14 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsAutoSaveToggled: (state) => {
|
||||
state.autoSave = !state.autoSave;
|
||||
},
|
||||
settingsShowHUDToggled: (state) => {
|
||||
state.showHUD = !state.showHUD;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions;
|
||||
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } =
|
||||
canvasSettingsSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
|
@ -1045,10 +1045,7 @@ export const canvasSlice = createSlice({
|
||||
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
|
||||
state.bbox.rect.width = bboxDims.width;
|
||||
state.bbox.rect.height = bboxDims.height;
|
||||
|
||||
if (state.bbox.scaleMethod === 'auto') {
|
||||
state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension);
|
||||
}
|
||||
syncScaledSize(state);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -317,7 +317,7 @@ export const selectScheduler = createParamsSelector((params) => params.scheduler
|
||||
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
|
||||
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
|
||||
export const selectSeed = createParamsSelector((params) => params.seed);
|
||||
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldConcatPrompts);
|
||||
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldRandomizeSeed);
|
||||
export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision);
|
||||
export const selectIterations = createParamsSelector((params) => params.iterations);
|
||||
export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise);
|
||||
|
@ -685,8 +685,6 @@ export type StagingAreaImage = {
|
||||
offsetY: number;
|
||||
};
|
||||
|
||||
export type SessionMode = 'generate' | 'compose';
|
||||
|
||||
export type CanvasState = {
|
||||
_version: 3;
|
||||
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
||||
|
@ -81,6 +81,7 @@ const DeleteImageModal = () => {
|
||||
cancelButtonText={t('boards.cancel')}
|
||||
acceptButtonText={t('controlnet.delete')}
|
||||
acceptCallback={handleDelete}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<ImageUsageMessage imageUsage={imageUsageSummary} />
|
||||
|
@ -20,7 +20,7 @@ export const DynamicPromptsModal = memo(() => {
|
||||
const { isOpen, onClose } = useDynamicPromptsModal();
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered useInert={false}>
|
||||
<ModalOverlay />
|
||||
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
|
||||
<ModalHeader>{t('dynamicPrompts.dynamicPrompts')}</ModalHeader>
|
||||
|
@ -59,7 +59,7 @@ const GalleryPanelContent = () => {
|
||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" pt={2} tabIndex={-1}>
|
||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||
<Flex alignItems="center" gap={0}>
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
|
@ -21,7 +21,7 @@ export const ImageViewer = memo(() => {
|
||||
<Flex
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
layerStyle="first"
|
||||
layerStyle="body"
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
@ -29,7 +29,6 @@ export const ImageViewer = memo(() => {
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
p={2}
|
||||
rowGap={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
|
@ -1,69 +1,61 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Icon,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
|
||||
export const ViewerToggleMenu = () => {
|
||||
const TooltipEdit = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.edit')}</Text>
|
||||
<Text fontWeight="normal">{t('common.editDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipEdit.displayName = 'TooltipEdit';
|
||||
|
||||
const TooltipView = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.view')}</Text>
|
||||
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipView.displayName = 'TooltipView';
|
||||
|
||||
export const ViewerToggle = memo(() => {
|
||||
const imageViewer = useImageViewer();
|
||||
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
imageViewer.onClose();
|
||||
} else {
|
||||
imageViewer.onOpen();
|
||||
}
|
||||
},
|
||||
[imageViewer]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" data-testid="toggle-viewer-menu-button" pointerEvents="auto">
|
||||
<Flex gap={3} w="full" alignItems="center">
|
||||
{imageViewer.isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
|
||||
<Text fontSize="md">{imageViewer.isOpen ? t('common.viewing') : t('common.editing')}</Text>
|
||||
<Icon as={PiCaretDownBold} />
|
||||
</Flex>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={2} pointerEvents="auto">
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<Flex flexDir="column">
|
||||
<Button onClick={imageViewer.onOpen} variant="ghost" h="auto" w="auto" p={2}>
|
||||
<Flex gap={2} w="full">
|
||||
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'visible' : 'hidden'} />
|
||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||
<Text fontWeight="semibold" color="base.100">
|
||||
{t('common.viewing')}
|
||||
</Text>
|
||||
<Text fontWeight="normal" color="base.300">
|
||||
{t('common.viewingDesc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button onClick={imageViewer.onClose} variant="ghost" h="auto" w="auto" p={2}>
|
||||
<Flex gap={2} w="full">
|
||||
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'hidden' : 'visible'} />
|
||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||
<Text fontWeight="semibold" color="base.100">
|
||||
{t('common.editing')}
|
||||
</Text>
|
||||
<Text fontWeight="normal" color="base.300">
|
||||
{t('common.editingDesc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<IconSwitch
|
||||
isChecked={!imageViewer.isOpen}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiEyeBold />}
|
||||
tooltipUnchecked={<TooltipView />}
|
||||
iconChecked={<PiPencilBold />}
|
||||
tooltipChecked={<TooltipEdit />}
|
||||
ariaLabel="Toggle viewer"
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ViewerToggle.displayName = 'ViewerToggle';
|
||||
|
@ -7,7 +7,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||
import { ViewerToggle } from './ViewerToggleMenu';
|
||||
|
||||
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
@ -31,7 +31,7 @@ export const ViewerToolbar = memo(() => {
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto">
|
||||
{showToggle && <ViewerToggleMenu />}
|
||||
{showToggle && <ViewerToggle />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
@ -123,6 +123,7 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
|
||||
title={t('modelManager.deleteModel')}
|
||||
acceptCallback={handleModelDelete}
|
||||
acceptButtonText={t('modelManager.delete')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex rowGap={4} flexDirection="column">
|
||||
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>
|
||||
|
@ -67,6 +67,7 @@ export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => {
|
||||
acceptButtonText={`${t('modelManager.convert')}`}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex flexDirection="column" rowGap={4}>
|
||||
<Text fontSize="md">{t('modelManager.convertToDiffusersHelpText1')}</Text>
|
||||
|
@ -2,6 +2,7 @@ import 'reactflow/dist/style.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||
@ -10,7 +11,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { MdDeviceHub } from 'react-icons/md';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||
import { Flow } from './flow/Flow';
|
||||
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
||||
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||
@ -31,7 +31,7 @@ const NodeEditor = () => {
|
||||
{data && (
|
||||
<>
|
||||
<Flow />
|
||||
<AddNodePopover />
|
||||
<AddNodeCmdk />
|
||||
<TopPanel />
|
||||
<BottomLeftPanel />
|
||||
<MinimapPanel />
|
||||
|
@ -0,0 +1,420 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
Spacer,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
||||
import {
|
||||
$cursorPos,
|
||||
$edgePendingUpdate,
|
||||
$pendingConnection,
|
||||
$templates,
|
||||
edgesChanged,
|
||||
nodesChanged,
|
||||
useAddNodeCmdk,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
|
||||
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
||||
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memoize } from 'lodash-es';
|
||||
import { computed } from 'nanostores';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFlaskBold, PiHammerBold } from 'react-icons/pi';
|
||||
import type { EdgeChange, NodeChange } from 'reactflow';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
const useThrottle = <T,>(value: T, limit: number) => {
|
||||
const [throttledValue, setThrottledValue] = useState(value);
|
||||
const lastRan = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(
|
||||
function () {
|
||||
if (Date.now() - lastRan.current >= limit) {
|
||||
setThrottledValue(value);
|
||||
lastRan.current = Date.now();
|
||||
}
|
||||
},
|
||||
limit - (Date.now() - lastRan.current)
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, limit]);
|
||||
|
||||
return throttledValue;
|
||||
};
|
||||
|
||||
const useAddNode = () => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const buildInvocation = useBuildNode();
|
||||
const templates = useStore($templates);
|
||||
const pendingConnection = useStore($pendingConnection);
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: string): void => {
|
||||
const node = buildInvocation(nodeType);
|
||||
if (!node) {
|
||||
const errorMessage = t('nodes.unknownNode', {
|
||||
nodeType: nodeType,
|
||||
});
|
||||
toast({
|
||||
status: 'error',
|
||||
title: errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a cozy spot for the node
|
||||
const cursorPos = $cursorPos.get();
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
|
||||
node.selected = true;
|
||||
|
||||
// Deselect all other nodes and edges
|
||||
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
nodes.forEach(({ id, selected }) => {
|
||||
if (selected) {
|
||||
nodeChanges.push({ type: 'select', id, selected: false });
|
||||
}
|
||||
});
|
||||
edges.forEach(({ id, selected }) => {
|
||||
if (selected) {
|
||||
edgeChanges.push({ type: 'select', id, selected: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Onwards!
|
||||
if (nodeChanges.length > 0) {
|
||||
store.dispatch(nodesChanged(nodeChanges));
|
||||
}
|
||||
if (edgeChanges.length > 0) {
|
||||
store.dispatch(edgesChanged(edgeChanges));
|
||||
}
|
||||
|
||||
// Auto-connect an edge if we just added a node and have a pending connection
|
||||
if (pendingConnection && isInvocationNode(node)) {
|
||||
const edgePendingUpdate = $edgePendingUpdate.get();
|
||||
const { handleType } = pendingConnection;
|
||||
|
||||
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
|
||||
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
|
||||
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
|
||||
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
|
||||
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
const connection = getFirstValidConnection(
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
nodes,
|
||||
edges,
|
||||
templates,
|
||||
edgePendingUpdate
|
||||
);
|
||||
if (connection) {
|
||||
const newEdge = connectionToEdge(connection);
|
||||
store.dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
|
||||
}
|
||||
}
|
||||
},
|
||||
[buildInvocation, pendingConnection, store, t, templates]
|
||||
);
|
||||
|
||||
return addNode;
|
||||
};
|
||||
|
||||
const cmdkRootSx: SystemStyleObject = {
|
||||
'[cmdk-root]': {
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
},
|
||||
'[cmdk-list]': {
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
},
|
||||
};
|
||||
|
||||
export const AddNodeCmdk = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const addNodeCmdk = useAddNodeCmdk();
|
||||
const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const addNode = useAddNode();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const throttledSearchTerm = useThrottle(searchTerm, 100);
|
||||
|
||||
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { enabled: tab === 'workflows', preventDefault: true }, [tab]);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
addNodeCmdk.setFalse();
|
||||
setSearchTerm('');
|
||||
$pendingConnection.set(null);
|
||||
}, [addNodeCmdk]);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(value: string) => {
|
||||
addNode(value);
|
||||
onClose();
|
||||
},
|
||||
[addNode, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={addNodeCmdkIsOpen}
|
||||
onClose={onClose}
|
||||
useInert={false}
|
||||
initialFocusRef={inputRef}
|
||||
size="xl"
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent h="512" maxH="70%">
|
||||
<ModalBody p={2} h="full" sx={cmdkRootSx}>
|
||||
<CommandRoot loop shouldFilter={false}>
|
||||
<Flex flexDir="column" h="full" gap={2}>
|
||||
<Input ref={inputRef} value={searchTerm} onChange={onChange} placeholder={t('nodes.nodeSearch')} />
|
||||
<Box w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<CommandEmpty>
|
||||
<IAINoContentFallback
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
icon={null}
|
||||
label="No matching items"
|
||||
/>
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<NodeCommandList searchTerm={throttledSearchTerm} onSelect={onSelect} />
|
||||
</CommandList>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
</Flex>
|
||||
</CommandRoot>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
AddNodeCmdk.displayName = 'AddNodeCmdk';
|
||||
|
||||
const cmdkItemSx: SystemStyleObject = {
|
||||
'&[data-selected="true"]': {
|
||||
bg: 'base.700',
|
||||
},
|
||||
};
|
||||
|
||||
type NodeCommandItemData = {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
classification: S['Classification'];
|
||||
nodePack: string;
|
||||
};
|
||||
|
||||
const $templatesArray = computed($templates, (templates) => Object.values(templates));
|
||||
|
||||
const createRegex = memoize(
|
||||
(inputValue: string) =>
|
||||
new RegExp(
|
||||
inputValue
|
||||
.trim()
|
||||
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
||||
.split(' ')
|
||||
.join('.*'),
|
||||
'gi'
|
||||
)
|
||||
);
|
||||
|
||||
// Filterable items are a subset of Invocation template - we also want to filter for notes or current image node,
|
||||
// so we are using a less specific type instead of `InvocationTemplate`
|
||||
type FilterableItem = {
|
||||
type: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
classification: S['Classification'];
|
||||
nodePack: string;
|
||||
};
|
||||
|
||||
const filter = memoize(
|
||||
(item: FilterableItem, searchTerm: string) => {
|
||||
const regex = createRegex(searchTerm);
|
||||
|
||||
if (!searchTerm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.title.includes(searchTerm) || regex.test(item.title)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.type.includes(searchTerm) || regex.test(item.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.description.includes(searchTerm) || regex.test(item.description)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.nodePack.includes(searchTerm) || regex.test(item.nodePack)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.classification.includes(searchTerm) || regex.test(item.classification)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const tag of item.tags) {
|
||||
if (tag.includes(searchTerm) || regex.test(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
(item: FilterableItem, searchTerm: string) => `${item.type}-${searchTerm}`
|
||||
);
|
||||
|
||||
const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; onSelect: (value: string) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const templatesArray = useStore($templatesArray);
|
||||
const pendingConnection = useStore($pendingConnection);
|
||||
const currentImageFilterItem = useMemo<FilterableItem>(
|
||||
() => ({
|
||||
type: 'current_image',
|
||||
title: t('nodes.currentImage'),
|
||||
description: t('nodes.currentImageDescription'),
|
||||
tags: ['progress', 'image', 'current'],
|
||||
classification: 'stable',
|
||||
nodePack: 'invokeai',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
const notesFilterItem = useMemo<FilterableItem>(
|
||||
() => ({
|
||||
type: 'notes',
|
||||
title: t('nodes.notes'),
|
||||
description: t('nodes.notesDescription'),
|
||||
tags: ['notes'],
|
||||
classification: 'stable',
|
||||
nodePack: 'invokeai',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const items = useMemo<NodeCommandItemData[]>(() => {
|
||||
// If we have a connection in progress, we need to filter the node choices
|
||||
const _items: NodeCommandItemData[] = [];
|
||||
|
||||
if (!pendingConnection) {
|
||||
for (const template of templatesArray) {
|
||||
if (filter(template, searchTerm)) {
|
||||
_items.push({
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
classification: template.classification,
|
||||
nodePack: template.nodePack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of [currentImageFilterItem, notesFilterItem]) {
|
||||
if (filter(item, searchTerm)) {
|
||||
_items.push({
|
||||
label: item.title,
|
||||
value: item.type,
|
||||
description: item.description,
|
||||
classification: item.classification,
|
||||
nodePack: item.nodePack,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const template of templatesArray) {
|
||||
if (filter(template, searchTerm)) {
|
||||
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
|
||||
|
||||
for (const field of Object.values(candidateFields)) {
|
||||
const sourceType =
|
||||
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
const targetType =
|
||||
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
|
||||
if (validateConnectionTypes(sourceType, targetType)) {
|
||||
_items.push({
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
classification: template.classification,
|
||||
nodePack: template.nodePack,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _items;
|
||||
}, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<CommandItem key={item.value} value={item.value} onSelect={onSelect} asChild>
|
||||
<Flex role="button" flexDir="column" sx={cmdkItemSx} py={1} px={2} borderRadius="base">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
{item.classification === 'beta' && <Icon boxSize={4} color="invokeYellow.300" as={PiHammerBold} />}
|
||||
{item.classification === 'prototype' && <Icon boxSize={4} color="invokeRed.300" as={PiFlaskBold} />}
|
||||
<Text fontWeight="semibold">{item.label}</Text>
|
||||
<Spacer />
|
||||
<Text variant="subtext" fontWeight="semibold">
|
||||
{item.nodePack}
|
||||
</Text>
|
||||
</Flex>
|
||||
{item.description && <Text color="base.200">{item.description}</Text>}
|
||||
</Flex>
|
||||
</CommandItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
NodeCommandList.displayName = 'CommandListItems';
|
@ -1,267 +0,0 @@
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
|
||||
import type { SelectInstance } from 'chakra-react-select';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
||||
import {
|
||||
$cursorPos,
|
||||
$edgePendingUpdate,
|
||||
$isAddNodePopoverOpen,
|
||||
$pendingConnection,
|
||||
$templates,
|
||||
closeAddNodePopover,
|
||||
edgesChanged,
|
||||
nodesChanged,
|
||||
openAddNodePopover,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
|
||||
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
||||
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
||||
import type { AnyNode } from 'features/nodes/types/invocation';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { filter, map, memoize, some } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
|
||||
import type { EdgeChange, NodeChange } from 'reactflow';
|
||||
|
||||
const createRegex = memoize(
|
||||
(inputValue: string) =>
|
||||
new RegExp(
|
||||
inputValue
|
||||
.trim()
|
||||
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
||||
.split(' ')
|
||||
.join('.*'),
|
||||
'gi'
|
||||
)
|
||||
);
|
||||
|
||||
const filterOption = memoize((option: FilterOptionOption<ComboboxOption>, inputValue: string) => {
|
||||
if (!inputValue) {
|
||||
return true;
|
||||
}
|
||||
const regex = createRegex(inputValue);
|
||||
return (
|
||||
regex.test(option.label) ||
|
||||
regex.test(option.data.description ?? '') ||
|
||||
(option.data.tags ?? []).some((tag) => regex.test(tag))
|
||||
);
|
||||
});
|
||||
|
||||
const AddNodePopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const buildInvocation = useBuildNode();
|
||||
const { t } = useTranslation();
|
||||
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const templates = useStore($templates);
|
||||
const pendingConnection = useStore($pendingConnection);
|
||||
const isOpen = useStore($isAddNodePopoverOpen);
|
||||
const store = useAppStore();
|
||||
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
// If we have a connection in progress, we need to filter the node choices
|
||||
const templatesArray = map(templates);
|
||||
if (!pendingConnection) {
|
||||
return templatesArray;
|
||||
}
|
||||
|
||||
return filter(templates, (template) => {
|
||||
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
|
||||
return some(candidateFields, (field) => {
|
||||
const sourceType =
|
||||
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
const targetType =
|
||||
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
|
||||
return validateConnectionTypes(sourceType, targetType);
|
||||
});
|
||||
});
|
||||
}, [templates, pendingConnection]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const _options: ComboboxOption[] = map(filteredTemplates, (template) => {
|
||||
return {
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
tags: template.tags,
|
||||
};
|
||||
});
|
||||
|
||||
//We only want these nodes if we're not filtered
|
||||
if (!pendingConnection) {
|
||||
_options.push({
|
||||
label: t('nodes.currentImage'),
|
||||
value: 'current_image',
|
||||
description: t('nodes.currentImageDescription'),
|
||||
tags: ['progress'],
|
||||
});
|
||||
|
||||
_options.push({
|
||||
label: t('nodes.notes'),
|
||||
value: 'notes',
|
||||
description: t('nodes.notesDescription'),
|
||||
tags: ['notes'],
|
||||
});
|
||||
}
|
||||
|
||||
_options.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return _options;
|
||||
}, [filteredTemplates, pendingConnection, t]);
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: string): AnyNode | null => {
|
||||
const node = buildInvocation(nodeType);
|
||||
if (!node) {
|
||||
const errorMessage = t('nodes.unknownNode', {
|
||||
nodeType: nodeType,
|
||||
});
|
||||
toast({
|
||||
status: 'error',
|
||||
title: errorMessage,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find a cozy spot for the node
|
||||
const cursorPos = $cursorPos.get();
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
|
||||
node.selected = true;
|
||||
|
||||
// Deselect all other nodes and edges
|
||||
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
nodes.forEach(({ id, selected }) => {
|
||||
if (selected) {
|
||||
nodeChanges.push({ type: 'select', id, selected: false });
|
||||
}
|
||||
});
|
||||
edges.forEach(({ id, selected }) => {
|
||||
if (selected) {
|
||||
edgeChanges.push({ type: 'select', id, selected: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Onwards!
|
||||
if (nodeChanges.length > 0) {
|
||||
dispatch(nodesChanged(nodeChanges));
|
||||
}
|
||||
if (edgeChanges.length > 0) {
|
||||
dispatch(edgesChanged(edgeChanges));
|
||||
}
|
||||
return node;
|
||||
},
|
||||
[buildInvocation, store, dispatch, t]
|
||||
);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
const node = addNode(v.value);
|
||||
|
||||
// Auto-connect an edge if we just added a node and have a pending connection
|
||||
if (pendingConnection && isInvocationNode(node)) {
|
||||
const edgePendingUpdate = $edgePendingUpdate.get();
|
||||
const { handleType } = pendingConnection;
|
||||
|
||||
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
|
||||
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
|
||||
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
|
||||
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
|
||||
|
||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||
const connection = getFirstValidConnection(
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
nodes,
|
||||
edges,
|
||||
templates,
|
||||
edgePendingUpdate
|
||||
);
|
||||
if (connection) {
|
||||
const newEdge = connectionToEdge(connection);
|
||||
dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
|
||||
}
|
||||
}
|
||||
|
||||
closeAddNodePopover();
|
||||
},
|
||||
[addNode, dispatch, pendingConnection, store, templates]
|
||||
);
|
||||
|
||||
const handleHotkeyOpen: HotkeyCallback = useCallback((e) => {
|
||||
if (!$isAddNodePopoverOpen.get()) {
|
||||
e.preventDefault();
|
||||
openAddNodePopover();
|
||||
flushSync(() => {
|
||||
selectRef.current?.inputRef?.focus();
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
|
||||
|
||||
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
onClose={closeAddNodePopover}
|
||||
placement="bottom"
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
closeOnBlur={true}
|
||||
returnFocusOnClose={true}
|
||||
initialFocusRef={inputRef}
|
||||
isLazy
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
p={0}
|
||||
top={-1}
|
||||
shadow="dark-lg"
|
||||
borderColor="invokeBlue.400"
|
||||
borderWidth="2px"
|
||||
borderStyle="solid"
|
||||
>
|
||||
<PopoverBody w="32rem" p={0}>
|
||||
<Combobox
|
||||
menuIsOpen={isOpen}
|
||||
selectRef={selectRef}
|
||||
value={null}
|
||||
placeholder={t('nodes.nodeSearch')}
|
||||
options={options}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
filterOption={filterOption}
|
||||
onChange={onChange}
|
||||
onMenuClose={closeAddNodePopover}
|
||||
inputRef={inputRef}
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AddNodePopover);
|
@ -8,10 +8,10 @@ import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||
import {
|
||||
$addNodeCmdk,
|
||||
$cursorPos,
|
||||
$didUpdateEdge,
|
||||
$edgePendingUpdate,
|
||||
$isAddNodePopoverOpen,
|
||||
$lastEdgeUpdateMouseEvent,
|
||||
$pendingConnection,
|
||||
$viewport,
|
||||
@ -281,7 +281,7 @@ export const Flow = memo(() => {
|
||||
const onEscapeHotkey = useCallback(() => {
|
||||
if (!$edgePendingUpdate.get()) {
|
||||
$pendingConnection.set(null);
|
||||
$isAddNodePopoverOpen.set(false);
|
||||
$addNodeCmdk.set(false);
|
||||
cancelConnection();
|
||||
}
|
||||
}, [cancelConnection]);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { openAddNodePopover } from 'features/nodes/store/nodesSlice';
|
||||
import { useAddNodeCmdk } from 'features/nodes/store/nodesSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
const AddNodeButton = () => {
|
||||
const addNodeCmdk = useAddNodeCmdk();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -12,7 +13,7 @@ const AddNodeButton = () => {
|
||||
tooltip={t('nodes.addNodeToolTip')}
|
||||
aria-label={t('nodes.addNode')}
|
||||
icon={<PiPlusBold />}
|
||||
onClick={openAddNodePopover}
|
||||
onClick={addNodeCmdk.setTrue}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
);
|
||||
|
@ -47,6 +47,7 @@ const ClearFlowButton = () => {
|
||||
onClose={onClose}
|
||||
title={t('nodes.clearWorkflow')}
|
||||
acceptCallback={handleNewWorkflow}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text>{t('nodes.clearWorkflowDesc')}</Text>
|
||||
|
@ -102,7 +102,7 @@ const WorkflowEditorSettings = ({ children }: Props) => {
|
||||
<>
|
||||
{children({ onOpen })}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered useInert={false}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
|
||||
|
@ -3,7 +3,6 @@ import 'reactflow/dist/style.css';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
@ -34,7 +33,6 @@ const NodeEditorPanelGroup = () => {
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<QueueControls />
|
||||
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
|
||||
<Flex justifyContent="space-between" alignItems="center" gap="4">
|
||||
<WorkflowLibraryButton />
|
||||
|
@ -2,9 +2,9 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import {
|
||||
$addNodeCmdk,
|
||||
$didUpdateEdge,
|
||||
$edgePendingUpdate,
|
||||
$isAddNodePopoverOpen,
|
||||
$pendingConnection,
|
||||
$templates,
|
||||
edgesChanged,
|
||||
@ -107,7 +107,7 @@ export const useConnection = () => {
|
||||
$pendingConnection.set(null);
|
||||
} else {
|
||||
// The mouse is not over a node - we should open the add node popover
|
||||
$isAddNodePopoverOpen.set(true);
|
||||
$addNodeCmdk.set(true);
|
||||
}
|
||||
}, [store, templates, updateNodeInternals]);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import type {
|
||||
@ -431,14 +432,8 @@ export const $didUpdateEdge = atom(false);
|
||||
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
|
||||
|
||||
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||
export const $isAddNodePopoverOpen = atom(false);
|
||||
export const closeAddNodePopover = () => {
|
||||
$isAddNodePopoverOpen.set(false);
|
||||
$pendingConnection.set(null);
|
||||
};
|
||||
export const openAddNodePopover = () => {
|
||||
$isAddNodePopoverOpen.set(true);
|
||||
};
|
||||
export const $addNodeCmdk = atom(false);
|
||||
export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrateNodesState = (state: any): any => {
|
||||
|
@ -163,6 +163,7 @@ export const collect: InvocationTemplate = {
|
||||
},
|
||||
},
|
||||
useCache: true,
|
||||
nodePack: 'invokeai',
|
||||
classification: 'stable',
|
||||
};
|
||||
|
||||
@ -480,6 +481,7 @@ const iterate: InvocationTemplate = {
|
||||
},
|
||||
},
|
||||
useCache: true,
|
||||
nodePack: 'invokeai',
|
||||
classification: 'stable',
|
||||
};
|
||||
|
||||
@ -1152,6 +1154,7 @@ export const schema = {
|
||||
type: 'object',
|
||||
required: ['type', 'id'],
|
||||
title: 'CollectInvocation',
|
||||
node_pack: 'invokeai',
|
||||
description: 'Collects values into a collection',
|
||||
classification: 'stable',
|
||||
version: '1.0.0',
|
||||
@ -1513,6 +1516,7 @@ export const schema = {
|
||||
title: 'IterateInvocation',
|
||||
description: 'Iterates over a list of items',
|
||||
classification: 'stable',
|
||||
node_pack: 'invokeai',
|
||||
version: '1.1.0',
|
||||
output: {
|
||||
$ref: '#/components/schemas/IterateInvocationOutput',
|
||||
|
@ -16,7 +16,7 @@ const zInvocationTemplate = z.object({
|
||||
outputType: z.string().min(1),
|
||||
version: zSemVer,
|
||||
useCache: z.boolean(),
|
||||
nodePack: z.string().min(1).nullish(),
|
||||
nodePack: z.string().min(1).default('invokeai'),
|
||||
classification: zClassification,
|
||||
});
|
||||
export type InvocationTemplate = z.infer<typeof zInvocationTemplate>;
|
||||
@ -26,7 +26,7 @@ export type InvocationTemplate = z.infer<typeof zInvocationTemplate>;
|
||||
export const zInvocationNodeData = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
version: zSemVer,
|
||||
nodePack: z.string().min(1).nullish(),
|
||||
nodePack: z.string().min(1).default('invokeai'),
|
||||
label: z.string(),
|
||||
notes: z.string(),
|
||||
type: z.string().trim().min(1),
|
||||
|
@ -10,7 +10,9 @@ export const prepareLinearUIBatch = (
|
||||
g: Graph,
|
||||
prepend: boolean,
|
||||
noise: Invocation<'noise'>,
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>,
|
||||
origin: 'generation' | 'workflows' | 'upscaling',
|
||||
destination: 'canvas' | 'gallery'
|
||||
): BatchConfig => {
|
||||
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
|
||||
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
||||
@ -103,7 +105,8 @@ export const prepareLinearUIBatch = (
|
||||
graph: g.getGraph(),
|
||||
runs: 1,
|
||||
data,
|
||||
origin: 'canvas',
|
||||
origin,
|
||||
destination,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -29,7 +29,7 @@ export const addInpaint = async (
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
const { mode } = canvasSession;
|
||||
const { sendToCanvas: isComposing } = canvasSession;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
@ -99,7 +99,7 @@ export const addInpaint = async (
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (mode === 'generate') {
|
||||
if (!isComposing) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ export const addInpaint = async (
|
||||
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (mode === 'generate') {
|
||||
if (!isComposing) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const addOutpaint = async (
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
const { mode } = canvasSession;
|
||||
const { sendToCanvas: isComposing } = canvasSession;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
@ -123,7 +123,7 @@ export const addOutpaint = async (
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (mode === 'generate') {
|
||||
if (!isComposing) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ export const addOutpaint = async (
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (mode === 'generate') {
|
||||
if (!isComposing) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ export const buildSD1Graph = async (
|
||||
canvasOutput = addWatermarker(g, canvasOutput);
|
||||
}
|
||||
|
||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
||||
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||
|
||||
g.updateNode(canvasOutput, {
|
||||
id: getPrefixedId('canvas_output'),
|
||||
|
@ -285,7 +285,7 @@ export const buildSDXLGraph = async (
|
||||
canvasOutput = addWatermarker(g, canvasOutput);
|
||||
}
|
||||
|
||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
||||
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||
|
||||
g.updateNode(canvasOutput, {
|
||||
id: getPrefixedId('canvas_output'),
|
||||
|
@ -38,6 +38,7 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe
|
||||
isOpen: true,
|
||||
isIntermediate: type === 'save_image' ? false : true,
|
||||
useCache: template.useCache,
|
||||
nodePack: template.nodePack,
|
||||
inputs,
|
||||
},
|
||||
};
|
||||
|
@ -85,6 +85,7 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor
|
||||
isOpen: true,
|
||||
isIntermediate: node.is_intermediate ?? false,
|
||||
useCache: node.use_cache ?? true,
|
||||
nodePack: template.nodePack,
|
||||
inputs,
|
||||
},
|
||||
});
|
||||
|
@ -28,6 +28,7 @@ describe('validateWorkflow', () => {
|
||||
isOpen: true,
|
||||
isIntermediate: true,
|
||||
useCache: true,
|
||||
nodePack: 'invokeai',
|
||||
inputs: {
|
||||
model: {
|
||||
name: 'model',
|
||||
@ -56,6 +57,7 @@ describe('validateWorkflow', () => {
|
||||
isOpen: true,
|
||||
isIntermediate: true,
|
||||
useCache: true,
|
||||
nodePack: 'invokeai',
|
||||
inputs: {
|
||||
board: {
|
||||
name: 'board',
|
||||
|
@ -1,27 +1,26 @@
|
||||
import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
|
||||
|
||||
type Props = ButtonProps;
|
||||
|
||||
const ClearQueueButton = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const { isLoading, isDisabled } = useClearQueue();
|
||||
const clearQueue = useClearQueue();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
isLoading={clearQueue.isLoading}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
leftIcon={<PiTrashSimpleFill />}
|
||||
colorScheme="error"
|
||||
onClick={dialogState.setTrue}
|
||||
onClick={clearQueue.openDialog}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
>
|
||||
|
@ -1,27 +1,77 @@
|
||||
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const $boolean = atom(false);
|
||||
export const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean);
|
||||
const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean);
|
||||
|
||||
export const useClearQueue = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const dialog = useClearQueueConfirmationAlertDialog();
|
||||
const isOpen = useStore(dialog.$boolean);
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const isConnected = useStore($isConnected);
|
||||
const [trigger, { isLoading }] = useClearQueueMutation({
|
||||
fixedCacheKey: 'clearQueue',
|
||||
});
|
||||
|
||||
const clearQueue = useCallback(async () => {
|
||||
if (!queueStatus?.queue.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await trigger().unwrap();
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_SUCCEEDED',
|
||||
title: t('queue.clearSucceeded'),
|
||||
status: 'success',
|
||||
});
|
||||
dispatch(listCursorChanged(undefined));
|
||||
dispatch(listPriorityChanged(undefined));
|
||||
} catch {
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_FAILED',
|
||||
title: t('queue.clearFailed'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [queueStatus?.queue.total, trigger, dispatch, t]);
|
||||
|
||||
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
|
||||
|
||||
return {
|
||||
clearQueue,
|
||||
isOpen,
|
||||
openDialog: dialog.setTrue,
|
||||
closeDialog: dialog.setFalse,
|
||||
isLoading,
|
||||
queueStatus,
|
||||
isDisabled,
|
||||
};
|
||||
};
|
||||
|
||||
export const ClearQueueConfirmationsAlertDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const isOpen = useStore(dialogState.$boolean);
|
||||
const { clearQueue } = useClearQueue();
|
||||
const clearQueue = useClearQueue();
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={dialogState.setFalse}
|
||||
isOpen={clearQueue.isOpen}
|
||||
onClose={clearQueue.closeDialog}
|
||||
title={t('queue.clearTooltip')}
|
||||
acceptCallback={clearQueue}
|
||||
acceptCallback={clearQueue.clearQueue}
|
||||
acceptButtonText={t('queue.clear')}
|
||||
useInert={false}
|
||||
>
|
||||
<Text>{t('queue.clearQueueAlertDialog')}</Text>
|
||||
<br />
|
||||
|
@ -1,67 +1,40 @@
|
||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
|
||||
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
|
||||
|
||||
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
export const ClearQueueIconButton = memo((_) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const { isLoading, isDisabled } = useClearQueue();
|
||||
const clearQueue = useClearQueue();
|
||||
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.clear')}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
icon={<PiTrashSimpleBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={dialogState.setTrue}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton';
|
||||
|
||||
const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.cancel')}
|
||||
tooltip={t('queue.cancelTooltip')}
|
||||
icon={<PiXBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={cancelQueueItem}
|
||||
data-testid={t('queue.cancel')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton';
|
||||
|
||||
export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
// Show the single item clear button when shift is pressed
|
||||
// Otherwise show the clear queue button
|
||||
const shift = useShiftModifier();
|
||||
|
||||
if (shift) {
|
||||
return <ClearAllQueueIconButton {...props} />;
|
||||
}
|
||||
|
||||
return <ClearSingleQueueItemIconButton {...props} />;
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="lg"
|
||||
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
|
||||
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
|
||||
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
|
||||
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
|
||||
colorScheme="error"
|
||||
onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
|
||||
data-testid={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
/>
|
||||
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||
<QueueCountBadge targetRef={ref} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
|
||||
|
@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => {
|
||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||
|
||||
return (
|
||||
<Flex pos="relative" flexGrow={1} minW="240px">
|
||||
<Flex pos="relative" w="192px">
|
||||
<QueueIterationsNumberInput />
|
||||
<QueueButtonTooltip>
|
||||
<Button
|
||||
@ -25,7 +25,6 @@ export const InvokeQueueBackButton = memo(() => {
|
||||
isDisabled={isDisabled}
|
||||
rightIcon={<RiSparkling2Fill />}
|
||||
variant="solid"
|
||||
zIndex={1}
|
||||
colorScheme="invokeYellow"
|
||||
size="lg"
|
||||
w="calc(100% - 60px)"
|
||||
|
@ -1,101 +0,0 @@
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
useDisclosure,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
export const QueueActionsMenuButton = memo(() => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
}),
|
||||
});
|
||||
const { isLoading: isLoadingClearQueue, isDisabled: isDisabledClearQueue } = useClearQueue();
|
||||
const {
|
||||
resumeProcessor,
|
||||
isLoading: isLoadingResumeProcessor,
|
||||
isDisabled: isDisabledResumeProcessor,
|
||||
} = useResumeProcessor();
|
||||
const {
|
||||
pauseProcessor,
|
||||
isLoading: isLoadingPauseProcessor,
|
||||
isDisabled: isDisabledPauseProcessor,
|
||||
} = usePauseProcessor();
|
||||
const openQueue = useCallback(() => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose} placement="bottom-end">
|
||||
<MenuButton as={IconButton} aria-label="Queue Actions Menu" icon={<RiListCheck />} />
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiTrashSimpleBold size="16px" />}
|
||||
onClick={dialogState.setTrue}
|
||||
isLoading={isLoadingClearQueue}
|
||||
isDisabled={isDisabledClearQueue}
|
||||
>
|
||||
{t('queue.clearTooltip')}
|
||||
</MenuItem>
|
||||
{isResumeEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPlayFill size="14px" />}
|
||||
onClick={resumeProcessor}
|
||||
isLoading={isLoadingResumeProcessor}
|
||||
isDisabled={isDisabledResumeProcessor}
|
||||
>
|
||||
{t('queue.resumeTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isPauseEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPauseFill size="14px" />}
|
||||
onClick={pauseProcessor}
|
||||
isLoading={isLoadingPauseProcessor}
|
||||
isDisabled={isDisabledPauseProcessor}
|
||||
>
|
||||
{t('queue.pauseTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<RiPlayList2Fill />} onClick={openQueue}>
|
||||
{t('queue.openQueue')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{queueSize > 0 && (
|
||||
<Badge pos="absolute" insetInlineStart={-3} insetBlockStart={-1.5} colorScheme="invokeYellow" zIndex="docked">
|
||||
{queueSize}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
QueueActionsMenuButton.displayName = 'QueueActionsMenuButton';
|
@ -1,24 +1,27 @@
|
||||
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||
import { QueueActionsMenuButton } from './QueueActionsMenuButton';
|
||||
|
||||
const QueueControls = () => {
|
||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
return (
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} pt={2} flexDir="column">
|
||||
<ButtonGroup size="lg" isAttached={false}>
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<Flex gap={2}>
|
||||
{isPrependEnabled && <QueueFrontButton />}
|
||||
<InvokeQueueBackButton />
|
||||
<Spacer />
|
||||
<QueueActionsMenuButton />
|
||||
{tab === 'generation' && <CanvasSendToToggle />}
|
||||
<ClearQueueIconButton />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<ProgressBar />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { Badge, Portal } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
type Props = {
|
||||
targetRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
|
||||
const isParametersPanelOpen = useStore($isParametersPanelOpen);
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = targetRef.current;
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
if (!$isParametersPanelOpen.get()) {
|
||||
return;
|
||||
}
|
||||
const { x, y } = target.getBoundingClientRect();
|
||||
setBadgePos({ x: `${x - 7}px`, y: `${y - 5}px` });
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(cb);
|
||||
resizeObserver.observe(parent);
|
||||
cb();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [targetRef]);
|
||||
|
||||
if (queueSize === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!badgePos) {
|
||||
return null;
|
||||
}
|
||||
if (!isParametersPanelOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Badge
|
||||
pos="absolute"
|
||||
insetInlineStart={badgePos.x}
|
||||
insetBlockStart={badgePos.y}
|
||||
colorScheme="invokeYellow"
|
||||
zIndex="docked"
|
||||
shadow="dark-lg"
|
||||
userSelect="none"
|
||||
>
|
||||
{queueSize}
|
||||
</Badge>
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
QueueCountBadge.displayName = 'QueueCountBadge';
|
@ -1,6 +1,7 @@
|
||||
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
|
||||
@ -52,6 +53,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
|
||||
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
|
||||
const originText = useOriginText(item.origin);
|
||||
const destinationText = useDestinationText(item.destination);
|
||||
|
||||
const icon = useMemo(() => <PiXBold />, []);
|
||||
return (
|
||||
@ -76,6 +78,11 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
{originText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.destination} flexShrink={0}>
|
||||
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" alignItems="center">
|
||||
{destinationText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
|
||||
{executionTime || '-'}
|
||||
</Flex>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
@ -17,7 +18,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { session_id, batch_id, item_id, origin } = queueItemDTO;
|
||||
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
|
||||
const { t } = useTranslation();
|
||||
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
|
||||
|
||||
@ -26,6 +27,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { data: queueItem } = useGetQueueItemQuery(item_id);
|
||||
|
||||
const originText = useOriginText(origin);
|
||||
const destinationText = useDestinationText(destination);
|
||||
|
||||
const statusAndTiming = useMemo(() => {
|
||||
if (!queueItem) {
|
||||
@ -54,6 +56,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
>
|
||||
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
|
||||
<QueueItemData label={t('queue.origin')} data={originText} />
|
||||
<QueueItemData label={t('queue.destination')} data={destinationText} />
|
||||
<QueueItemData label={t('queue.item')} data={item_id} />
|
||||
<QueueItemData label={t('queue.batch')} data={batch_id} />
|
||||
<QueueItemData label={t('queue.session')} data={session_id} />
|
||||
|
@ -25,6 +25,9 @@ const QueueListHeader = () => {
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.origin')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.destination} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.destination')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.time')}</Text>
|
||||
</Flex>
|
||||
|
@ -4,7 +4,8 @@ export const COLUMN_WIDTHS = {
|
||||
statusDot: 2,
|
||||
time: '4rem',
|
||||
origin: '5rem',
|
||||
destination: '6rem',
|
||||
batchId: '5rem',
|
||||
fieldValues: 'auto',
|
||||
actions: 'auto',
|
||||
};
|
||||
} as const;
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
|
||||
export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (destination === 'canvas') {
|
||||
return t('queue.canvas');
|
||||
}
|
||||
|
||||
if (destination === 'gallery') {
|
||||
return t('queue.gallery');
|
||||
}
|
||||
|
||||
return t('queue.other');
|
||||
};
|
@ -4,13 +4,13 @@ import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (origin === 'canvas') {
|
||||
return t('queue.originCanvas');
|
||||
if (origin === 'generation') {
|
||||
return t('queue.generation');
|
||||
}
|
||||
|
||||
if (origin === 'workflows') {
|
||||
return t('queue.originWorkflows');
|
||||
return t('queue.workflows');
|
||||
}
|
||||
|
||||
return t('queue.originOther');
|
||||
return t('queue.other');
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user