mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 09:28:02 -05:00
Compare commits
45 Commits
ryan/seaml
...
v3.6.0rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccd72349d | ||
|
|
30e12376d3 | ||
|
|
23c8a893e1 | ||
|
|
7d93329401 | ||
|
|
968fb655a4 | ||
|
|
80ec9f4131 | ||
|
|
f19def5f7b | ||
|
|
9e1dd8ac9c | ||
|
|
ebd68b7a6c | ||
|
|
68a231afea | ||
|
|
21ab650ac0 | ||
|
|
b501bd709f | ||
|
|
4082f25062 | ||
|
|
63d74b4ba6 | ||
|
|
da5907613b | ||
|
|
3a9201bd31 | ||
|
|
d6e2cb7cef | ||
|
|
0809e832d4 | ||
|
|
7269c9f02e | ||
|
|
d86d7e5c33 | ||
|
|
5d87578746 | ||
|
|
04aef021fc | ||
|
|
0fc08bb384 | ||
|
|
5779542084 | ||
|
|
ebda81e96e | ||
|
|
3fe332e85f | ||
|
|
3428ea1b3c | ||
|
|
6024fc7baf | ||
|
|
75c1c4ce5a | ||
|
|
ffa05a0bb3 | ||
|
|
a20e17330b | ||
|
|
4e83644433 | ||
|
|
604f0083f2 | ||
|
|
2a8a158823 | ||
|
|
f8c3db72e9 | ||
|
|
60815807f9 | ||
|
|
196fb0e014 | ||
|
|
eba668956d | ||
|
|
ee5ec023f4 | ||
|
|
d59661e0af | ||
|
|
f51e8eeae1 | ||
|
|
6e06935e75 | ||
|
|
f7f697849c | ||
|
|
8e17e29a5c | ||
|
|
12e9f17f7a |
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -1,5 +1,5 @@
|
||||
# continuous integration
|
||||
/.github/workflows/ @lstein @blessedcoolant @hipsterusername
|
||||
/.github/workflows/ @lstein @blessedcoolant @hipsterusername @ebr
|
||||
|
||||
# documentation
|
||||
/docs/ @lstein @blessedcoolant @hipsterusername @Millu
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# installation and configuration
|
||||
/pyproject.toml @lstein @blessedcoolant @hipsterusername
|
||||
/docker/ @lstein @blessedcoolant @hipsterusername
|
||||
/docker/ @lstein @blessedcoolant @hipsterusername @ebr
|
||||
/scripts/ @ebr @lstein @hipsterusername
|
||||
/installer/ @lstein @ebr @hipsterusername
|
||||
/invokeai/assets @lstein @ebr @hipsterusername
|
||||
@@ -26,9 +26,7 @@
|
||||
|
||||
# front ends
|
||||
/invokeai/frontend/CLI @lstein @hipsterusername
|
||||
/invokeai/frontend/install @lstein @ebr @hipsterusername
|
||||
/invokeai/frontend/install @lstein @ebr @hipsterusername
|
||||
/invokeai/frontend/merge @lstein @blessedcoolant @hipsterusername
|
||||
/invokeai/frontend/training @lstein @blessedcoolant @hipsterusername
|
||||
/invokeai/frontend/web @psychedelicious @blessedcoolant @maryhipp @hipsterusername
|
||||
|
||||
|
||||
|
||||
5
.github/workflows/build-container.yml
vendored
5
.github/workflows/build-container.yml
vendored
@@ -40,10 +40,14 @@ jobs:
|
||||
- name: Free up more disk space on the runner
|
||||
# https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
|
||||
run: |
|
||||
echo "----- Free space before cleanup"
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo swapoff /mnt/swapfile
|
||||
sudo rm -rf /mnt/swapfile
|
||||
echo "----- Free space after cleanup"
|
||||
df -h
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -91,6 +95,7 @@ jobs:
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build container
|
||||
timeout-minutes: 40
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
|
||||
@@ -59,7 +59,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
|
||||
# #### Build the Web UI ------------------------------------
|
||||
|
||||
FROM node:18-slim AS web-builder
|
||||
FROM node:20-slim AS web-builder
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -68,7 +68,7 @@ WORKDIR /build
|
||||
COPY invokeai/frontend/web/ ./
|
||||
RUN --mount=type=cache,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
RUN npx vite build
|
||||
|
||||
#### Runtime stage ---------------------------------------
|
||||
|
||||
|
||||
@@ -241,12 +241,12 @@ class InvokeAiInstance:
|
||||
pip[
|
||||
"install",
|
||||
"--require-virtualenv",
|
||||
"numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2
|
||||
"numpy==1.26.3", # choose versions that won't be uninstalled during phase 2
|
||||
"urllib3~=1.26.0",
|
||||
"requests~=2.28.0",
|
||||
"torch==2.1.2",
|
||||
"torchmetrics==0.11.4",
|
||||
"torchvision>=0.16.2",
|
||||
"torchvision==0.16.2",
|
||||
"--force-reinstall",
|
||||
"--find-links" if find_links is not None else None,
|
||||
find_links,
|
||||
|
||||
31
invokeai/backend/model_management/detect_baked_in_vae.py
Normal file
31
invokeai/backend/model_management/detect_baked_in_vae.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2024 Lincoln Stein and the InvokeAI Development Team
|
||||
"""
|
||||
This module exports the function has_baked_in_sdxl_vae().
|
||||
It returns True if an SDXL checkpoint model has the original SDXL 1.0 VAE,
|
||||
which doesn't work properly in fp16 mode.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from safetensors.torch import load_file
|
||||
|
||||
SDXL_1_0_VAE_HASH = "bc40b16c3a0fa4625abdfc01c04ffc21bf3cefa6af6c7768ec61eb1f1ac0da51"
|
||||
|
||||
|
||||
def has_baked_in_sdxl_vae(checkpoint_path: Path) -> bool:
|
||||
"""Return true if the checkpoint contains a custom (non SDXL-1.0) VAE."""
|
||||
hash = _vae_hash(checkpoint_path)
|
||||
return hash != SDXL_1_0_VAE_HASH
|
||||
|
||||
|
||||
def _vae_hash(checkpoint_path: Path) -> str:
|
||||
checkpoint = load_file(checkpoint_path, device="cpu")
|
||||
vae_keys = [x for x in checkpoint.keys() if x.startswith("first_stage_model.")]
|
||||
hash = hashlib.new("sha256")
|
||||
for key in vae_keys:
|
||||
value = checkpoint[key]
|
||||
hash.update(bytes(key, "UTF-8"))
|
||||
hash.update(bytes(str(value), "UTF-8"))
|
||||
|
||||
return hash.hexdigest()
|
||||
@@ -13,6 +13,7 @@ from safetensors.torch import load_file
|
||||
from transformers import CLIPTextModel, CLIPTokenizer
|
||||
|
||||
from invokeai.app.shared.models import FreeUConfig
|
||||
from invokeai.backend.model_management.model_load_optimizations import skip_torch_weight_init
|
||||
|
||||
from .models.lora import LoRAModel
|
||||
|
||||
@@ -211,8 +212,12 @@ class ModelPatcher:
|
||||
for i in range(ti_embedding.shape[0]):
|
||||
new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i))
|
||||
|
||||
# modify text_encoder
|
||||
text_encoder.resize_token_embeddings(init_tokens_count + new_tokens_added, pad_to_multiple_of)
|
||||
# Modify text_encoder.
|
||||
# resize_token_embeddings(...) constructs a new torch.nn.Embedding internally. Initializing the weights of
|
||||
# this embedding is slow and unnecessary, so we wrap this step in skip_torch_weight_init() to save some
|
||||
# time.
|
||||
with skip_torch_weight_init():
|
||||
text_encoder.resize_token_embeddings(init_tokens_count + new_tokens_added, pad_to_multiple_of)
|
||||
model_embeddings = text_encoder.get_input_embeddings()
|
||||
|
||||
for ti_name, ti in ti_list:
|
||||
|
||||
@@ -370,6 +370,8 @@ class LoRACheckpointProbe(CheckpointProbeBase):
|
||||
return BaseModelType.StableDiffusion1
|
||||
elif token_vector_length == 1024:
|
||||
return BaseModelType.StableDiffusion2
|
||||
elif token_vector_length == 1280:
|
||||
return BaseModelType.StableDiffusionXL # recognizes format at https://civitai.com/models/224641
|
||||
elif token_vector_length == 2048:
|
||||
return BaseModelType.StableDiffusionXL
|
||||
else:
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import json
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
from omegaconf import OmegaConf
|
||||
from pydantic import Field
|
||||
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
from invokeai.backend.model_management.detect_baked_in_vae import has_baked_in_sdxl_vae
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
from .base import (
|
||||
BaseModelType,
|
||||
DiffusersModel,
|
||||
@@ -116,14 +121,28 @@ class StableDiffusionXLModel(DiffusersModel):
|
||||
# The convert script adapted from the diffusers package uses
|
||||
# strings for the base model type. To avoid making too many
|
||||
# source code changes, we simply translate here
|
||||
if Path(output_path).exists():
|
||||
return output_path
|
||||
|
||||
if isinstance(config, cls.CheckpointConfig):
|
||||
from invokeai.backend.model_management.models.stable_diffusion import _convert_ckpt_and_cache
|
||||
|
||||
# Hack in VAE-fp16 fix - If model sdxl-vae-fp16-fix is installed,
|
||||
# then we bake it into the converted model unless there is already
|
||||
# a nonstandard VAE installed.
|
||||
kwargs = {}
|
||||
app_config = InvokeAIAppConfig.get_config()
|
||||
vae_path = app_config.models_path / "sdxl/vae/sdxl-vae-fp16-fix"
|
||||
if vae_path.exists() and not has_baked_in_sdxl_vae(Path(model_path)):
|
||||
InvokeAILogger.get_logger().warning("No baked-in VAE detected. Inserting sdxl-vae-fp16-fix.")
|
||||
kwargs["vae_path"] = vae_path
|
||||
|
||||
return _convert_ckpt_and_cache(
|
||||
version=base_model,
|
||||
model_config=config,
|
||||
output_path=output_path,
|
||||
use_safetensors=False, # corrupts sdxl models for some reason
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
return model_path
|
||||
|
||||
1
invokeai/frontend/web/.gitignore
vendored
1
invokeai/frontend/web/.gitignore
vendored
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.pnpm-store
|
||||
# We want to distribute the repo
|
||||
dist
|
||||
dist/**
|
||||
|
||||
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 272 B |
3
invokeai/frontend/web/favicon-outline.svg
Normal file
3
invokeai/frontend/web/favicon-outline.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.1951 10.6667H42V2H2V10.6667H14.8049L29.1951 33.3333H42V42H2V33.3333H14.8049" stroke="#E6FD13" stroke-width="2.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
@@ -8,8 +8,8 @@
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="mask-icon" href="/invoke-key-ylw-sm.svg" color="#E6FD13" sizes="any" />
|
||||
<link rel="icon" href="/invoke-key-char-on-ylw.svg" />
|
||||
<link rel="mask-icon" type="icon" href="favicon-outline.svg" color="#E6FD13" sizes="any" />
|
||||
<link rel="icon" type="icon" href="favicon-key.svg" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
@@ -73,10 +73,11 @@
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"compare-versions": "^6.1.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"framer-motion": "^10.16.16",
|
||||
"i18next": "^23.7.13",
|
||||
"framer-motion": "^10.17.9",
|
||||
"i18next": "^23.7.16",
|
||||
"i18next-http-backend": "^2.4.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"konva": "^9.3.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanostores": "^0.9.5",
|
||||
@@ -90,7 +91,7 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-hotkeys-hook": "4.4.1",
|
||||
"react-hotkeys-hook": "4.4.3",
|
||||
"react-i18next": "^14.0.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-konva": "^18.2.10",
|
||||
@@ -102,10 +103,10 @@
|
||||
"react-virtuoso": "^4.6.2",
|
||||
"reactflow": "^11.10.1",
|
||||
"redux-dynamic-middlewares": "^2.2.0",
|
||||
"redux-remember": "^5.0.1",
|
||||
"redux-remember": "^5.1.0",
|
||||
"roarr": "^7.21.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"socket.io-client": "^4.7.3",
|
||||
"type-fest": "^4.9.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"use-image": "^1.1.1",
|
||||
@@ -121,27 +122,27 @@
|
||||
"ts-toolbelt": "^9.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arthurgeron/eslint-plugin-react-usememo": "^2.2.2",
|
||||
"@arthurgeron/eslint-plugin-react-usememo": "^2.2.3",
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@storybook/addon-docs": "^7.6.6",
|
||||
"@storybook/addon-essentials": "^7.6.6",
|
||||
"@storybook/addon-interactions": "^7.6.6",
|
||||
"@storybook/addon-links": "^7.6.6",
|
||||
"@storybook/addon-storysource": "^7.6.6",
|
||||
"@storybook/blocks": "^7.6.6",
|
||||
"@storybook/manager-api": "^7.6.6",
|
||||
"@storybook/react": "^7.6.6",
|
||||
"@storybook/react-vite": "^7.6.6",
|
||||
"@storybook/test": "^7.6.6",
|
||||
"@storybook/theming": "^7.6.6",
|
||||
"@storybook/addon-docs": "^7.6.7",
|
||||
"@storybook/addon-essentials": "^7.6.7",
|
||||
"@storybook/addon-interactions": "^7.6.7",
|
||||
"@storybook/addon-links": "^7.6.7",
|
||||
"@storybook/addon-storysource": "^7.6.7",
|
||||
"@storybook/blocks": "^7.6.7",
|
||||
"@storybook/manager-api": "^7.6.7",
|
||||
"@storybook/react": "^7.6.7",
|
||||
"@storybook/react-vite": "^7.6.7",
|
||||
"@storybook/test": "^7.6.7",
|
||||
"@storybook/theming": "^7.6.7",
|
||||
"@types/dateformat": "^5.0.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/node": "^20.10.7",
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
@@ -159,10 +160,10 @@
|
||||
"openapi-typescript": "^6.7.3",
|
||||
"prettier": "^3.1.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"storybook": "^7.6.6",
|
||||
"storybook": "^7.6.7",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-css-injected-by-js": "^3.3.1",
|
||||
"vite-plugin-dts": "^3.7.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
|
||||
2218
invokeai/frontend/web/pnpm-lock.yaml
generated
2218
invokeai/frontend/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -121,7 +121,11 @@
|
||||
"unsaved": "несохраненный",
|
||||
"input": "Вход",
|
||||
"details": "Детали",
|
||||
"notInstalled": "Нет $t(common.installed)"
|
||||
"notInstalled": "Нет $t(common.installed)",
|
||||
"preferencesLabel": "Предпочтения",
|
||||
"or": "или",
|
||||
"advancedOptions": "Расширенные настройки",
|
||||
"free": "Свободно"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Генерации",
|
||||
@@ -365,7 +369,22 @@
|
||||
"desc": "Открывает меню добавления узла",
|
||||
"title": "Добавление узлов"
|
||||
},
|
||||
"nodesHotkeys": "Горячие клавиши узлов"
|
||||
"nodesHotkeys": "Горячие клавиши узлов",
|
||||
"cancelAndClear": {
|
||||
"desc": "Отмена текущего элемента очереди и очистка всех ожидающих элементов",
|
||||
"title": "Отменить и очистить"
|
||||
},
|
||||
"resetOptionsAndGallery": {
|
||||
"title": "Сброс настроек и галереи",
|
||||
"desc": "Сброс панелей галереи и настроек"
|
||||
},
|
||||
"searchHotkeys": "Поиск горячих клавиш",
|
||||
"noHotkeysFound": "Горячие клавиши не найдены",
|
||||
"toggleOptionsAndGallery": {
|
||||
"desc": "Открытие и закрытие панели опций и галереи",
|
||||
"title": "Переключить опции и галерею"
|
||||
},
|
||||
"clearSearch": "Очистить поиск"
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Менеджер моделей",
|
||||
@@ -1188,7 +1207,8 @@
|
||||
"handAndFace": "Руки и Лицо",
|
||||
"enableIPAdapter": "Включить IP Adapter",
|
||||
"maxFaces": "Макс Лица",
|
||||
"mlsdDescription": "Минималистичный детектор отрезков линии"
|
||||
"mlsdDescription": "Минималистичный детектор отрезков линии",
|
||||
"resizeSimple": "Изменить размер (простой)"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Авто добавление Доски",
|
||||
@@ -1540,7 +1560,8 @@
|
||||
"cancelBatchFailed": "Проблема с отменой пакета",
|
||||
"clearQueueAlertDialog2": "Вы уверены, что хотите очистить очередь?",
|
||||
"item": "Элемент",
|
||||
"graphFailedToQueue": "Не удалось поставить график в очередь"
|
||||
"graphFailedToQueue": "Не удалось поставить график в очередь",
|
||||
"openQueue": "Открыть очередь"
|
||||
},
|
||||
"sdxl": {
|
||||
"refinerStart": "Запуск перерисовщика",
|
||||
@@ -1635,9 +1656,38 @@
|
||||
"selectModel": "Выберите модель",
|
||||
"noRefinerModelsInstalled": "Модели SDXL Refiner не установлены",
|
||||
"noLoRAsInstalled": "Нет установленных LoRA",
|
||||
"selectLoRA": "Выберите LoRA"
|
||||
"selectLoRA": "Выберите LoRA",
|
||||
"noMainModelSelected": "Базовая модель не выбрана",
|
||||
"lora": "LoRA",
|
||||
"allLoRAsAdded": "Все LoRA добавлены",
|
||||
"defaultVAE": "Стандартное VAE",
|
||||
"incompatibleBaseModel": "Несовместимая базовая модель",
|
||||
"loraAlreadyAdded": "LoRA уже добавлена"
|
||||
},
|
||||
"app": {
|
||||
"storeNotInitialized": "Магазин не инициализирован"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
"infillTab": "Заполнение",
|
||||
"coherenceTab": "Согласованность",
|
||||
"title": "Композиция"
|
||||
},
|
||||
"control": {
|
||||
"controlAdaptersTab": "Адаптеры контроля",
|
||||
"ipTab": "Запросы изображений",
|
||||
"title": "Контроль"
|
||||
},
|
||||
"generation": {
|
||||
"title": "Генерация",
|
||||
"conceptsTab": "Концепты",
|
||||
"modelTab": "Модель"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Расширенные"
|
||||
},
|
||||
"image": {
|
||||
"title": "Изображение"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,11 @@
|
||||
"nextPage": "下一页",
|
||||
"saveAs": "保存为",
|
||||
"unsaved": "未保存",
|
||||
"ai": "ai"
|
||||
"ai": "ai",
|
||||
"preferencesLabel": "首选项",
|
||||
"or": "或",
|
||||
"advancedOptions": "高级选项",
|
||||
"free": "自由"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "生成的图像",
|
||||
@@ -164,18 +168,18 @@
|
||||
"starImage": "收藏图像"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"appHotkeys": "应用快捷键",
|
||||
"generalHotkeys": "一般快捷键",
|
||||
"galleryHotkeys": "图库快捷键",
|
||||
"unifiedCanvasHotkeys": "统一画布快捷键",
|
||||
"keyboardShortcuts": "快捷键",
|
||||
"appHotkeys": "应用",
|
||||
"generalHotkeys": "一般",
|
||||
"galleryHotkeys": "图库",
|
||||
"unifiedCanvasHotkeys": "统一画布",
|
||||
"invoke": {
|
||||
"title": "Invoke",
|
||||
"desc": "生成图像"
|
||||
},
|
||||
"cancel": {
|
||||
"title": "取消",
|
||||
"desc": "取消图像生成"
|
||||
"desc": "取消当前队列项目"
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "打开提示词框",
|
||||
@@ -361,11 +365,26 @@
|
||||
"title": "接受暂存图像",
|
||||
"desc": "接受当前暂存区中的图像"
|
||||
},
|
||||
"nodesHotkeys": "节点快捷键",
|
||||
"nodesHotkeys": "节点",
|
||||
"addNodes": {
|
||||
"title": "添加节点",
|
||||
"desc": "打开添加节点菜单"
|
||||
}
|
||||
},
|
||||
"cancelAndClear": {
|
||||
"desc": "取消当前队列项目并且清除所有待定项目",
|
||||
"title": "取消和清除"
|
||||
},
|
||||
"resetOptionsAndGallery": {
|
||||
"title": "重置选项和图库",
|
||||
"desc": "重置选项和图库面板"
|
||||
},
|
||||
"searchHotkeys": "检索快捷键",
|
||||
"noHotkeysFound": "未找到快捷键",
|
||||
"toggleOptionsAndGallery": {
|
||||
"desc": "打开和关闭选项和图库面板",
|
||||
"title": "开关选项和图库"
|
||||
},
|
||||
"clearSearch": "清除检索项"
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "模型管理器",
|
||||
@@ -563,8 +582,8 @@
|
||||
"info": "信息",
|
||||
"initialImage": "初始图像",
|
||||
"showOptionsPanel": "显示侧栏浮窗 (O 或 T)",
|
||||
"seamlessYAxis": "Y 轴",
|
||||
"seamlessXAxis": "X 轴",
|
||||
"seamlessYAxis": "无缝平铺 Y 轴",
|
||||
"seamlessXAxis": "无缝平铺 X 轴",
|
||||
"boundingBoxWidth": "边界框宽度",
|
||||
"boundingBoxHeight": "边界框高度",
|
||||
"denoisingStrength": "去噪强度",
|
||||
@@ -611,7 +630,7 @@
|
||||
"readyToInvoke": "准备调用",
|
||||
"noControlImageForControlAdapter": "有 #{{number}} 个 Control Adapter 缺失控制图像",
|
||||
"noModelForControlAdapter": "有 #{{number}} 个 Control Adapter 没有选择模型。",
|
||||
"incompatibleBaseModelForControlAdapter": "有 #{{number}} 个 Control Adapter 模型与主模型不匹配。"
|
||||
"incompatibleBaseModelForControlAdapter": "有 #{{number}} 个 Control Adapter 模型与主模型不兼容。"
|
||||
},
|
||||
"patchmatchDownScaleSize": "缩小",
|
||||
"coherenceSteps": "步数",
|
||||
@@ -642,7 +661,14 @@
|
||||
"unmasked": "取消遮罩",
|
||||
"cfgRescaleMultiplier": "CFG 重缩放倍数",
|
||||
"cfgRescale": "CFG 重缩放",
|
||||
"useSize": "使用尺寸"
|
||||
"useSize": "使用尺寸",
|
||||
"setToOptimalSize": "优化模型大小",
|
||||
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (可能过小)",
|
||||
"imageSize": "图像尺寸",
|
||||
"lockAspectRatio": "锁定纵横比",
|
||||
"swapDimensions": "交换尺寸",
|
||||
"aspect": "纵横",
|
||||
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (可能过大)"
|
||||
},
|
||||
"settings": {
|
||||
"models": "模型",
|
||||
@@ -1201,7 +1227,8 @@
|
||||
"openPose": "Openpose",
|
||||
"controlAdapter_other": "Control Adapters",
|
||||
"lineartAnime": "Lineart Anime",
|
||||
"canny": "Canny"
|
||||
"canny": "Canny",
|
||||
"resizeSimple": "缩放(简单)"
|
||||
},
|
||||
"queue": {
|
||||
"status": "状态",
|
||||
@@ -1248,7 +1275,7 @@
|
||||
"notReady": "无法排队",
|
||||
"batchFailedToQueue": "批次加入队列失败",
|
||||
"batchValues": "批次数",
|
||||
"queueCountPrediction": "添加 {{predicted}} 到队列",
|
||||
"queueCountPrediction": "{{promptsCount}} 提示词 × {{iterations}} 迭代次数 -> {{count}} 次生成",
|
||||
"batchQueued": "加入队列的批次",
|
||||
"queuedCount": "{{pending}} 待处理",
|
||||
"front": "前",
|
||||
@@ -1262,7 +1289,8 @@
|
||||
"queueMaxExceeded": "超出最大值 {{max_queue_size}},将跳过 {{skip}}",
|
||||
"graphFailedToQueue": "节点图加入队列失败",
|
||||
"batchFieldValues": "批处理值",
|
||||
"time": "时间"
|
||||
"time": "时间",
|
||||
"openQueue": "打开队列"
|
||||
},
|
||||
"sdxl": {
|
||||
"refinerStart": "Refiner 开始作用时机",
|
||||
@@ -1276,11 +1304,12 @@
|
||||
"denoisingStrength": "去噪强度",
|
||||
"refinermodel": "Refiner 模型",
|
||||
"posAestheticScore": "正向美学评分",
|
||||
"concatPromptStyle": "连接提示词 & 样式",
|
||||
"concatPromptStyle": "链接提示词 & 样式",
|
||||
"loading": "加载中...",
|
||||
"steps": "步数",
|
||||
"posStylePrompt": "正向样式提示词",
|
||||
"refiner": "Refiner"
|
||||
"refiner": "Refiner",
|
||||
"freePromptStyle": "手动输入样式提示词"
|
||||
},
|
||||
"metadata": {
|
||||
"positivePrompt": "正向提示词",
|
||||
@@ -1324,7 +1353,13 @@
|
||||
"noLoRAsInstalled": "无已安装的 LoRA",
|
||||
"esrganModel": "ESRGAN 模型",
|
||||
"addLora": "添加 LoRA",
|
||||
"noLoRAsLoaded": "无已加载的 LoRA"
|
||||
"noLoRAsLoaded": "无已加载的 LoRA",
|
||||
"noMainModelSelected": "未选择主模型",
|
||||
"lora": "LoRA",
|
||||
"allLoRAsAdded": "已添加所有 LoRA",
|
||||
"defaultVAE": "默认 VAE",
|
||||
"incompatibleBaseModel": "不兼容基础模型",
|
||||
"loraAlreadyAdded": "LoRA 已经被添加"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "自动添加面板",
|
||||
@@ -1368,7 +1403,9 @@
|
||||
"maxPrompts": "最大提示词数",
|
||||
"dynamicPrompts": "动态提示词",
|
||||
"promptsWithCount_other": "{{count}} 个提示词",
|
||||
"promptsPreview": "提示词预览"
|
||||
"promptsPreview": "提示词预览",
|
||||
"showDynamicPrompts": "显示动态提示词",
|
||||
"loading": "生成动态提示词中..."
|
||||
},
|
||||
"popovers": {
|
||||
"compositingMaskAdjustments": {
|
||||
@@ -1650,5 +1687,28 @@
|
||||
},
|
||||
"app": {
|
||||
"storeNotInitialized": "商店尚未初始化"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
"infillTab": "内补",
|
||||
"coherenceTab": "一致性层",
|
||||
"title": "合成"
|
||||
},
|
||||
"control": {
|
||||
"controlAdaptersTab": "Control Adapters",
|
||||
"ipTab": "图像提示",
|
||||
"title": "Control"
|
||||
},
|
||||
"generation": {
|
||||
"title": "生成",
|
||||
"conceptsTab": "概念",
|
||||
"modelTab": "模型"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级"
|
||||
},
|
||||
"image": {
|
||||
"title": "图像"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const useSocketIO = () => {
|
||||
}, [baseUrl]);
|
||||
|
||||
const socketOptions = useMemo(() => {
|
||||
const options: Parameters<typeof io>[0] = {
|
||||
const options: Partial<ManagerOptions & SocketOptions> = {
|
||||
timeout: 60000,
|
||||
path: '/ws/socket.io',
|
||||
autoConnect: false, // achtung! removing this breaks the dynamic middleware
|
||||
@@ -71,7 +71,7 @@ export const useSocketIO = () => {
|
||||
setEventListeners({ dispatch, socket });
|
||||
socket.connect();
|
||||
|
||||
if ($isDebugging.get()) {
|
||||
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
||||
window.$socketOptions = $socketOptions;
|
||||
console.log('Socket initialized', socket);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export const useSocketIO = () => {
|
||||
$isSocketInitialized.set(true);
|
||||
|
||||
return () => {
|
||||
if ($isDebugging.get()) {
|
||||
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
||||
window.$socketOptions = undefined;
|
||||
console.log('Socket teardown', socket);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { createLogWriter } from '@roarr/browser-log-writer';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { Logger, MessageSerializer } from 'roarr';
|
||||
import { ROARR, Roarr } from 'roarr';
|
||||
import { z } from 'zod';
|
||||
|
||||
const serializeMessage: MessageSerializer = (message) => {
|
||||
return JSON.stringify(message);
|
||||
};
|
||||
|
||||
ROARR.serializeMessage = serializeMessage;
|
||||
ROARR.write = createLogWriter();
|
||||
|
||||
export const BASE_CONTEXT = {};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
|
||||
import type { UseStore } from 'idb-keyval';
|
||||
import {
|
||||
clear,
|
||||
createStore as createIDBKeyValStore,
|
||||
get,
|
||||
set,
|
||||
} from 'idb-keyval';
|
||||
import { action, atom } from 'nanostores';
|
||||
import type { Driver } from 'redux-remember';
|
||||
|
||||
// Create a custom idb-keyval store (just needed to customize the name)
|
||||
export const $idbKeyValStore = atom<UseStore>(
|
||||
createIDBKeyValStore('invoke', 'invoke-store')
|
||||
);
|
||||
|
||||
export const clearIdbKeyValStore = action($idbKeyValStore, 'clear', (store) => {
|
||||
clear(store.get());
|
||||
});
|
||||
|
||||
// Create redux-remember driver, wrapping idb-keyval
|
||||
export const idbKeyValDriver: Driver = {
|
||||
getItem: (key) => {
|
||||
try {
|
||||
return get(key, $idbKeyValStore.get());
|
||||
} catch (originalError) {
|
||||
throw new StorageError({ key, originalError });
|
||||
}
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
try {
|
||||
return set(key, value, $idbKeyValStore.get());
|
||||
} catch (originalError) {
|
||||
throw new StorageError({ key, value, originalError });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { PersistError, RehydrateError } from 'redux-remember';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
export type StorageErrorArgs = {
|
||||
key: string;
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct
|
||||
value?: any;
|
||||
originalError?: unknown;
|
||||
};
|
||||
|
||||
export class StorageError extends Error {
|
||||
key: string;
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct
|
||||
value?: any;
|
||||
originalError?: Error;
|
||||
|
||||
constructor({ key, value, originalError }: StorageErrorArgs) {
|
||||
super(`Error setting ${key}`);
|
||||
this.name = 'StorageSetError';
|
||||
this.key = key;
|
||||
if (value !== undefined) {
|
||||
this.value = value;
|
||||
}
|
||||
if (originalError instanceof Error) {
|
||||
this.originalError = originalError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const errorHandler = (err: PersistError | RehydrateError) => {
|
||||
const log = logger('system');
|
||||
if (err instanceof PersistError) {
|
||||
log.error({ error: serializeError(err) }, 'Problem persisting state');
|
||||
} else if (err instanceof RehydrateError) {
|
||||
log.error({ error: serializeError(err) }, 'Problem rehydrating state');
|
||||
} else {
|
||||
log.error({ error: parseify(err) }, 'Problem in persistence layer');
|
||||
}
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||
import { controlAdaptersPersistDenylist } from 'features/controlAdapters/store/controlAdaptersPersistDenylist';
|
||||
import { dynamicPromptsPersistDenylist } from 'features/dynamicPrompts/store/dynamicPromptsPersistDenylist';
|
||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
||||
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
|
||||
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
|
||||
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
|
||||
import { omit } from 'lodash-es';
|
||||
import type { SerializeFunction } from 'redux-remember';
|
||||
|
||||
const serializationDenylist: {
|
||||
[key: string]: string[];
|
||||
} = {
|
||||
canvas: canvasPersistDenylist,
|
||||
gallery: galleryPersistDenylist,
|
||||
generation: generationPersistDenylist,
|
||||
nodes: nodesPersistDenylist,
|
||||
postprocessing: postprocessingPersistDenylist,
|
||||
system: systemPersistDenylist,
|
||||
ui: uiPersistDenylist,
|
||||
controlNet: controlAdaptersPersistDenylist,
|
||||
dynamicPrompts: dynamicPromptsPersistDenylist,
|
||||
};
|
||||
|
||||
export const serialize: SerializeFunction = (data, key) => {
|
||||
const result = omit(data, serializationDenylist[key] ?? []);
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||
import { initialControlAdapterState } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { initialDynamicPromptsState } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
||||
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
||||
import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice';
|
||||
import { initialSDXLState } from 'features/sdxl/store/sdxlSlice';
|
||||
import { initialConfigState } from 'features/system/store/configSlice';
|
||||
import { initialSystemState } from 'features/system/store/systemSlice';
|
||||
import { initialUIState } from 'features/ui/store/uiSlice';
|
||||
import { defaultsDeep } from 'lodash-es';
|
||||
import type { UnserializeFunction } from 'redux-remember';
|
||||
|
||||
const initialStates: {
|
||||
[key: string]: object; // TODO: type this properly
|
||||
} = {
|
||||
canvas: initialCanvasState,
|
||||
gallery: initialGalleryState,
|
||||
generation: initialGenerationState,
|
||||
nodes: initialNodesState,
|
||||
postprocessing: initialPostprocessingState,
|
||||
system: initialSystemState,
|
||||
config: initialConfigState,
|
||||
ui: initialUIState,
|
||||
controlAdapters: initialControlAdapterState,
|
||||
dynamicPrompts: initialDynamicPromptsState,
|
||||
sdxl: initialSDXLState,
|
||||
};
|
||||
|
||||
export const unserialize: UnserializeFunction = (data, key) => {
|
||||
const result = defaultsDeep(JSON.parse(data), initialStates[key]);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||
import { diff } from 'jsondiffpatch';
|
||||
|
||||
/**
|
||||
* Super simple logger middleware. Useful for debugging when the redux devtools are awkward.
|
||||
*/
|
||||
export const debugLoggerMiddleware: Middleware =
|
||||
(api: MiddlewareAPI) => (next) => (action) => {
|
||||
const originalState = api.getState();
|
||||
console.log('REDUX: dispatching', action);
|
||||
const result = next(action);
|
||||
const nextState = api.getState();
|
||||
console.log('REDUX: next state', nextState);
|
||||
console.log('REDUX: diff', diff(originalState, nextState));
|
||||
return result;
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { UnknownAction } from '@reduxjs/toolkit';
|
||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import type { Graph } from 'services/api/types';
|
||||
import { socketGeneratorProgress } from 'services/events/actions';
|
||||
|
||||
export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
if (isAnyGraphBuilt(action)) {
|
||||
@@ -30,5 +32,14 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
};
|
||||
}
|
||||
|
||||
if (socketGeneratorProgress.match(action)) {
|
||||
const sanitized = cloneDeep(action);
|
||||
if (sanitized.payload.data.progress_image) {
|
||||
sanitized.payload.data.progress_image.dataURL =
|
||||
'<Progress image omitted>';
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* This is a list of actions that should be excluded in the Redux DevTools.
|
||||
*/
|
||||
export const actionsDenylist = [
|
||||
export const actionsDenylist: string[] = [
|
||||
// very spammy canvas actions
|
||||
'canvas/setStageCoordinates',
|
||||
'canvas/setStageScale',
|
||||
'canvas/setBoundingBoxCoordinates',
|
||||
'canvas/setBoundingBoxDimensions',
|
||||
'canvas/addPointToCurrentLine',
|
||||
// 'canvas/setStageCoordinates',
|
||||
// 'canvas/setStageScale',
|
||||
// 'canvas/setBoundingBoxCoordinates',
|
||||
// 'canvas/setBoundingBoxDimensions',
|
||||
// 'canvas/addPointToCurrentLine',
|
||||
// bazillions during generation
|
||||
'socket/socketGeneratorProgress',
|
||||
'socket/appSocketGeneratorProgress',
|
||||
// 'socket/socketGeneratorProgress',
|
||||
// 'socket/appSocketGeneratorProgress',
|
||||
// this happens after every state change
|
||||
'@@REMEMBER_PERSISTED',
|
||||
// '@@REMEMBER_PERSISTED',
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
UnknownAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
|
||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||
@@ -69,7 +70,6 @@ import { addSessionRetrievalErrorEventListener } from './listeners/socketio/sock
|
||||
import { addSocketSubscribedEventListener as addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
|
||||
import { addSocketUnsubscribedEventListener as addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
|
||||
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
|
||||
import { addTabChangedListener } from './listeners/tabChanged';
|
||||
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
|
||||
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
||||
import { addWorkflowLoadRequestedListener } from './listeners/workflowLoadRequested';
|
||||
@@ -118,6 +118,9 @@ addImageToDeleteSelectedListener();
|
||||
addImagesStarredListener();
|
||||
addImagesUnstarredListener();
|
||||
|
||||
// Gallery
|
||||
addGalleryImageClickedListener();
|
||||
|
||||
// User Invoked
|
||||
addEnqueueRequestedCanvasListener();
|
||||
addEnqueueRequestedNodes();
|
||||
@@ -136,19 +139,7 @@ addCanvasMergedListener();
|
||||
addStagingAreaImageSavedListener();
|
||||
addCommitStagingAreaImageListener();
|
||||
|
||||
/**
|
||||
* Socket.IO Events - these handle SIO events directly and pass on internal application actions.
|
||||
* We don't handle SIO events in slices via `extraReducers` because some of these events shouldn't
|
||||
* actually be handled at all.
|
||||
*
|
||||
* For example, we don't want to respond to progress events for canceled sessions. To avoid
|
||||
* duplicating the logic to determine if an event should be responded to, we handle all of that
|
||||
* "is this session canceled?" logic in these listeners.
|
||||
*
|
||||
* The `socketGeneratorProgress` listener will then only dispatch the `appSocketGeneratorProgress`
|
||||
* action if it should be handled by the rest of the application. It is this `appSocketGeneratorProgress`
|
||||
* action that is handled by reducers in slices.
|
||||
*/
|
||||
// Socket.IO
|
||||
addGeneratorProgressListener();
|
||||
addGraphExecutionStateCompleteListener();
|
||||
addInvocationCompleteListener();
|
||||
@@ -196,8 +187,5 @@ addFirstListImagesListener();
|
||||
// Ad-hoc upscale workflwo
|
||||
addUpscaleRequestedListener();
|
||||
|
||||
// Tab Change
|
||||
addTabChangedListener();
|
||||
|
||||
// Dynamic prompts
|
||||
addDynamicPromptsListener();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { $logger } from 'app/logging/logger';
|
||||
import { canvasMerged } from 'features/canvas/store/actions';
|
||||
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
|
||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
|
||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@@ -30,7 +30,7 @@ export const addCanvasMergedListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
moduleLog.error('Problem getting canvas base layer');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
|
||||
import { buildWorkflowRight } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { BatchConfig } from 'services/api/types';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const addEnqueueRequestedNodes = () => {
|
||||
const { nodes, edges } = state.nodes;
|
||||
const workflow = state.workflow;
|
||||
const graph = buildNodesGraph(state.nodes);
|
||||
const builtWorkflow = buildWorkflowRight({
|
||||
const builtWorkflow = buildWorkflowWithValidation({
|
||||
nodes,
|
||||
edges,
|
||||
workflow,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const galleryImageClicked = createAction<{
|
||||
imageDTO: ImageDTO;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
}>('gallery/imageClicked');
|
||||
|
||||
/**
|
||||
* This listener handles the logic for selecting images in the gallery.
|
||||
*
|
||||
* Previously, this logic was in a `useCallback` with the whole gallery selection as a dependency. Every time
|
||||
* the selection changed, the callback got recreated and all images rerendered. This could easily block for
|
||||
* hundreds of ms, more for lower end devices.
|
||||
*
|
||||
* Moving this logic into a listener means we don't need to recalculate anything dynamically and the gallery
|
||||
* is much more responsive.
|
||||
*/
|
||||
|
||||
export const addGalleryImageClickedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: galleryImageClicked,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { imageDTO, shiftKey, ctrlKey, metaKey } = action.payload;
|
||||
const state = getState();
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data: listImagesData } =
|
||||
imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
|
||||
if (!listImagesData) {
|
||||
// Should never happen if we have clicked a gallery image
|
||||
return;
|
||||
}
|
||||
|
||||
const imageDTOs = imagesSelectors.selectAll(listImagesData);
|
||||
const selection = state.gallery.selection;
|
||||
|
||||
if (shiftKey) {
|
||||
const rangeEndImageName = imageDTO.image_name;
|
||||
const lastSelectedImage = selection[selection.length - 1]?.image_name;
|
||||
const lastClickedIndex = imageDTOs.findIndex(
|
||||
(n) => n.image_name === lastSelectedImage
|
||||
);
|
||||
const currentClickedIndex = imageDTOs.findIndex(
|
||||
(n) => n.image_name === rangeEndImageName
|
||||
);
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
// We have a valid range!
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
const imagesToSelect = imageDTOs.slice(start, end + 1);
|
||||
dispatch(selectionChanged(selection.concat(imagesToSelect)));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (
|
||||
selection.some((i) => i.image_name === imageDTO.image_name) &&
|
||||
selection.length > 1
|
||||
) {
|
||||
dispatch(
|
||||
selectionChanged(
|
||||
selection.filter((n) => n.image_name !== imageDTO.image_name)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(selectionChanged(selection.concat(imageDTO)));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectionChanged([imageDTO]));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
@@ -49,7 +49,7 @@ export const addRequestedSingleImageDeletionListener = () => {
|
||||
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
|
||||
const baseQueryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } =
|
||||
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||
|
||||
@@ -180,9 +180,9 @@ export const addRequestedMultipleImageDeletionListener = () => {
|
||||
imagesApi.endpoints.deleteImages.initiate({ imageDTOs })
|
||||
).unwrap();
|
||||
const state = getState();
|
||||
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } =
|
||||
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||
imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
|
||||
const newSelectedImageDTO = data
|
||||
? imagesSelectors.selectAll(data)[0]
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
} from 'features/dnd/types';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice';
|
||||
import {
|
||||
initialImageChanged,
|
||||
selectOptimalDimension,
|
||||
@@ -35,10 +34,10 @@ export const addImageDroppedListener = () => {
|
||||
|
||||
if (activeData.payloadType === 'IMAGE_DTO') {
|
||||
log.debug({ activeData, overData }, 'Image dropped');
|
||||
} else if (activeData.payloadType === 'IMAGE_DTOS') {
|
||||
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
|
||||
log.debug(
|
||||
{ activeData, overData },
|
||||
`Images (${activeData.payload.imageDTOs.length}) dropped`
|
||||
`Images (${getState().gallery.selection.length}) dropped`
|
||||
);
|
||||
} else if (activeData.payloadType === 'NODE_FIELD') {
|
||||
log.debug(
|
||||
@@ -49,19 +48,6 @@ export const addImageDroppedListener = () => {
|
||||
log.debug({ activeData, overData }, `Unknown payload dropped`);
|
||||
}
|
||||
|
||||
if (
|
||||
overData.actionType === 'ADD_FIELD_TO_LINEAR' &&
|
||||
activeData.payloadType === 'NODE_FIELD'
|
||||
) {
|
||||
const { nodeId, field } = activeData.payload;
|
||||
dispatch(
|
||||
workflowExposedFieldAdded({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on current image
|
||||
*/
|
||||
@@ -207,10 +193,9 @@ export const addImageDroppedListener = () => {
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'ADD_TO_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTOS' &&
|
||||
activeData.payload.imageDTOs
|
||||
activeData.payloadType === 'GALLERY_SELECTION'
|
||||
) {
|
||||
const { imageDTOs } = activeData.payload;
|
||||
const imageDTOs = getState().gallery.selection;
|
||||
const { boardId } = overData.context;
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImagesToBoard.initiate({
|
||||
@@ -226,10 +211,9 @@ export const addImageDroppedListener = () => {
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'REMOVE_FROM_BOARD' &&
|
||||
activeData.payloadType === 'IMAGE_DTOS' &&
|
||||
activeData.payload.imageDTOs
|
||||
activeData.payloadType === 'GALLERY_SELECTION'
|
||||
) {
|
||||
const { imageDTOs } = activeData.payload;
|
||||
const imageDTOs = getState().gallery.selection;
|
||||
dispatch(
|
||||
imagesApi.endpoints.removeImagesFromBoard.initiate({
|
||||
imageDTOs,
|
||||
|
||||
@@ -37,8 +37,10 @@ export const addModelSelectedListener = () => {
|
||||
const newModel = result.data;
|
||||
|
||||
const { base_model } = newModel;
|
||||
const didBaseModelChange =
|
||||
state.generation.model?.base_model !== base_model;
|
||||
|
||||
if (state.generation.model?.base_model !== base_model) {
|
||||
if (didBaseModelChange) {
|
||||
// we may need to reset some incompatible submodels
|
||||
let modelsCleared = 0;
|
||||
|
||||
@@ -81,7 +83,7 @@ export const addModelSelectedListener = () => {
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(modelChanged(newModel));
|
||||
dispatch(modelChanged(newModel, state.generation.model));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ export const addModelsLoadedListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(modelChanged(result.data));
|
||||
dispatch(modelChanged(result.data, currentModel));
|
||||
},
|
||||
});
|
||||
startAppListening({
|
||||
@@ -149,7 +149,7 @@ export const addModelsLoadedListener = () => {
|
||||
|
||||
if (!firstModel) {
|
||||
// No custom VAEs loaded at all; use the default
|
||||
dispatch(modelChanged(null));
|
||||
dispatch(vaeSelected(null));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export const addModelsLoadedListener = () => {
|
||||
const log = logger('models');
|
||||
log.info(
|
||||
{ models: action.payload.entities },
|
||||
`ControlNet models loaded (${action.payload.ids.length})`
|
||||
`T2I Adapter models loaded (${action.payload.ids.length})`
|
||||
);
|
||||
|
||||
selectAllT2IAdapters(getState().controlAdapters).forEach((ca) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||
import { setPositivePrompt } from 'features/parameters/store/generationSlice';
|
||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||
import { appSocketConnected } from 'services/events/actions';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
@@ -20,7 +20,7 @@ const matcher = isAnyOf(
|
||||
combinatorialToggled,
|
||||
maxPromptsChanged,
|
||||
maxPromptsReset,
|
||||
appSocketConnected
|
||||
socketConnected
|
||||
);
|
||||
|
||||
export const addDynamicPromptsListener = () => {
|
||||
|
||||
@@ -3,16 +3,16 @@ import { isInitializedChanged } from 'features/system/store/systemSlice';
|
||||
import { size } from 'lodash-es';
|
||||
import { api } from 'services/api';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import { appSocketConnected, socketConnected } from 'services/events/actions';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketConnectedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketConnected,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const log = logger('socketio');
|
||||
|
||||
log.debug('Connected');
|
||||
|
||||
const { nodeTemplates, config, system } = getState();
|
||||
@@ -29,9 +29,6 @@ export const addSocketConnectedEventListener = () => {
|
||||
} else {
|
||||
dispatch(isInitializedChanged(true));
|
||||
}
|
||||
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketConnected(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketDisconnected,
|
||||
socketDisconnected,
|
||||
} from 'services/events/actions';
|
||||
import { socketDisconnected } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketDisconnectedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketDisconnected,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: () => {
|
||||
log.debug('Disconnected');
|
||||
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketDisconnected(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketGeneratorProgress,
|
||||
socketGeneratorProgress,
|
||||
} from 'services/events/actions';
|
||||
import { socketGeneratorProgress } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addGeneratorProgressEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketGeneratorProgress,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
|
||||
effect: (action) => {
|
||||
log.trace(action.payload, `Generator progress`);
|
||||
|
||||
dispatch(appSocketGeneratorProgress(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketGraphExecutionStateComplete,
|
||||
socketGraphExecutionStateComplete,
|
||||
} from 'services/events/actions';
|
||||
import { socketGraphExecutionStateComplete } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addGraphExecutionStateCompleteEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketGraphExecutionStateComplete,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.debug(action.payload, 'Session complete');
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketGraphExecutionStateComplete(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,21 +15,19 @@ import {
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesAdapter } from 'services/api/util';
|
||||
import {
|
||||
appSocketInvocationComplete,
|
||||
socketInvocationComplete,
|
||||
} from 'services/events/actions';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
|
||||
const nodeTypeDenylist = ['load_image', 'image'];
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationCompleteEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationComplete,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('socketio');
|
||||
const { data } = action.payload;
|
||||
log.debug(
|
||||
{ data: parseify(data) },
|
||||
@@ -136,8 +134,6 @@ export const addInvocationCompleteEventListener = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketInvocationComplete(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketInvocationError,
|
||||
socketInvocationError,
|
||||
} from 'services/events/actions';
|
||||
import { socketInvocationError } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationErrorEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationError,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.error(
|
||||
action.payload,
|
||||
`Invocation error (${action.payload.data.node.type})`
|
||||
);
|
||||
dispatch(appSocketInvocationError(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketInvocationRetrievalError,
|
||||
socketInvocationRetrievalError,
|
||||
} from 'services/events/actions';
|
||||
import { socketInvocationRetrievalError } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationRetrievalErrorEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationRetrievalError,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.error(
|
||||
action.payload,
|
||||
`Invocation retrieval error (${action.payload.data.graph_execution_state_id})`
|
||||
);
|
||||
dispatch(appSocketInvocationRetrievalError(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketInvocationStarted,
|
||||
socketInvocationStarted,
|
||||
} from 'services/events/actions';
|
||||
import { socketInvocationStarted } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationStartedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationStarted,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
|
||||
effect: (action) => {
|
||||
log.debug(
|
||||
action.payload,
|
||||
`Invocation started (${action.payload.data.node.type})`
|
||||
);
|
||||
|
||||
dispatch(appSocketInvocationStarted(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketModelLoadCompleted,
|
||||
appSocketModelLoadStarted,
|
||||
socketModelLoadCompleted,
|
||||
socketModelLoadStarted,
|
||||
} from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addModelLoadEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketModelLoadStarted,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
const { base_model, model_name, model_type, submodel } =
|
||||
action.payload.data;
|
||||
|
||||
@@ -23,16 +22,12 @@ export const addModelLoadEventListener = () => {
|
||||
}
|
||||
|
||||
log.debug(action.payload, message);
|
||||
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketModelLoadStarted(action.payload));
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelLoadCompleted,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
const { base_model, model_name, model_type, submodel } =
|
||||
action.payload.data;
|
||||
|
||||
@@ -43,8 +38,6 @@ export const addModelLoadEventListener = () => {
|
||||
}
|
||||
|
||||
log.debug(action.payload, message);
|
||||
// pass along the socket event as an application action
|
||||
dispatch(appSocketModelLoadCompleted(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
||||
import {
|
||||
appSocketQueueItemStatusChanged,
|
||||
socketQueueItemStatusChanged,
|
||||
} from 'services/events/actions';
|
||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketQueueItemStatusChangedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketQueueItemStatusChanged,
|
||||
effect: async (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
|
||||
// we've got new status for the queue item, batch and queue
|
||||
const { queue_item, batch_status, queue_status } = action.payload.data;
|
||||
|
||||
@@ -73,9 +70,6 @@ export const addSocketQueueItemStatusChangedEventListener = () => {
|
||||
'InvocationCacheStatus',
|
||||
])
|
||||
);
|
||||
|
||||
// Pass the event along
|
||||
dispatch(appSocketQueueItemStatusChanged(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketSessionRetrievalError,
|
||||
socketSessionRetrievalError,
|
||||
} from 'services/events/actions';
|
||||
import { socketSessionRetrievalError } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSessionRetrievalErrorEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketSessionRetrievalError,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.error(
|
||||
action.payload,
|
||||
`Session retrieval error (${action.payload.data.graph_execution_state_id})`
|
||||
);
|
||||
dispatch(appSocketSessionRetrievalError(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketSubscribedSession,
|
||||
socketSubscribedSession,
|
||||
} from 'services/events/actions';
|
||||
import { socketSubscribedSession } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketSubscribedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketSubscribedSession,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.debug(action.payload, 'Subscribed');
|
||||
dispatch(appSocketSubscribedSession(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
appSocketUnsubscribedSession,
|
||||
socketUnsubscribedSession,
|
||||
} from 'services/events/actions';
|
||||
import { socketUnsubscribedSession } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketUnsubscribedEventListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: socketUnsubscribedSession,
|
||||
effect: (action, { dispatch }) => {
|
||||
const log = logger('socketio');
|
||||
effect: (action) => {
|
||||
log.debug(action.payload, 'Unsubscribed');
|
||||
dispatch(appSocketUnsubscribedSession(action.payload));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { modelChanged } from 'features/parameters/store/generationSlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { NON_REFINER_BASE_MODELS } from 'services/api/constants';
|
||||
import {
|
||||
mainModelsAdapterSelectors,
|
||||
modelsApi,
|
||||
} from 'services/api/endpoints/models';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const addTabChangedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: setActiveTab,
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
const activeTabName = action.payload;
|
||||
if (activeTabName === 'unifiedCanvas') {
|
||||
const currentBaseModel = getState().generation.model?.base_model;
|
||||
|
||||
if (
|
||||
currentBaseModel &&
|
||||
['sd-1', 'sd-2', 'sdxl'].includes(currentBaseModel)
|
||||
) {
|
||||
// if we're already on a valid model, no change needed
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// just grab fresh models
|
||||
const modelsRequest = dispatch(
|
||||
modelsApi.endpoints.getMainModels.initiate(NON_REFINER_BASE_MODELS)
|
||||
);
|
||||
const models = await modelsRequest.unwrap();
|
||||
// cancel this cache subscription
|
||||
modelsRequest.unsubscribe();
|
||||
|
||||
if (!models.ids.length) {
|
||||
// no valid canvas models
|
||||
dispatch(modelChanged(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// need to filter out all the invalid canvas models (currently refiner & any)
|
||||
const validCanvasModels = mainModelsAdapterSelectors
|
||||
.selectAll(models)
|
||||
.filter((model) =>
|
||||
['sd-1', 'sd-2', 'sdxl'].includes(model.base_model)
|
||||
);
|
||||
|
||||
const firstValidCanvasModel = validCanvasModels[0];
|
||||
|
||||
if (!firstValidCanvasModel) {
|
||||
// no valid canvas models
|
||||
dispatch(modelChanged(null));
|
||||
return;
|
||||
}
|
||||
|
||||
const { base_model, model_name, model_type } = firstValidCanvasModel;
|
||||
|
||||
dispatch(modelChanged({ base_model, model_name, model_type }));
|
||||
} catch {
|
||||
// network request failed, bail
|
||||
dispatch(modelChanged(null));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,40 +4,94 @@ import {
|
||||
combineReducers,
|
||||
configureStore,
|
||||
} from '@reduxjs/toolkit';
|
||||
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
|
||||
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
|
||||
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||
import canvasReducer, {
|
||||
initialCanvasState,
|
||||
migrateCanvasState,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import changeBoardModalReducer from 'features/changeBoardModal/store/slice';
|
||||
import controlAdaptersReducer from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { controlAdaptersPersistDenylist } from 'features/controlAdapters/store/controlAdaptersPersistDenylist';
|
||||
import controlAdaptersReducer, {
|
||||
initialControlAdaptersState,
|
||||
migrateControlAdaptersState,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import deleteImageModalReducer from 'features/deleteImageModal/store/slice';
|
||||
import dynamicPromptsReducer from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||
import hrfReducer from 'features/hrf/store/hrfSlice';
|
||||
import loraReducer from 'features/lora/store/loraSlice';
|
||||
import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice';
|
||||
import nodesReducer from 'features/nodes/store/nodesSlice';
|
||||
import { dynamicPromptsPersistDenylist } from 'features/dynamicPrompts/store/dynamicPromptsPersistDenylist';
|
||||
import dynamicPromptsReducer, {
|
||||
initialDynamicPromptsState,
|
||||
migrateDynamicPromptsState,
|
||||
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
||||
import galleryReducer, {
|
||||
initialGalleryState,
|
||||
migrateGalleryState,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import hrfReducer, {
|
||||
initialHRFState,
|
||||
migrateHRFState,
|
||||
} from 'features/hrf/store/hrfSlice';
|
||||
import loraReducer, {
|
||||
initialLoraState,
|
||||
migrateLoRAState,
|
||||
} from 'features/lora/store/loraSlice';
|
||||
import modelmanagerReducer, {
|
||||
initialModelManagerState,
|
||||
migrateModelManagerState,
|
||||
} from 'features/modelManager/store/modelManagerSlice';
|
||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||
import nodesReducer, {
|
||||
initialNodesState,
|
||||
migrateNodesState,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import nodeTemplatesReducer from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import workflowReducer from 'features/nodes/store/workflowSlice';
|
||||
import generationReducer from 'features/parameters/store/generationSlice';
|
||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
||||
import workflowReducer, {
|
||||
initialWorkflowState,
|
||||
migrateWorkflowState,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
||||
import generationReducer, {
|
||||
initialGenerationState,
|
||||
migrateGenerationState,
|
||||
} from 'features/parameters/store/generationSlice';
|
||||
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
|
||||
import postprocessingReducer, {
|
||||
initialPostprocessingState,
|
||||
migratePostprocessingState,
|
||||
} from 'features/parameters/store/postprocessingSlice';
|
||||
import queueReducer from 'features/queue/store/queueSlice';
|
||||
import sdxlReducer from 'features/sdxl/store/sdxlSlice';
|
||||
import sdxlReducer, {
|
||||
initialSDXLState,
|
||||
migrateSDXLState,
|
||||
} from 'features/sdxl/store/sdxlSlice';
|
||||
import configReducer from 'features/system/store/configSlice';
|
||||
import systemReducer from 'features/system/store/systemSlice';
|
||||
import uiReducer from 'features/ui/store/uiSlice';
|
||||
import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
|
||||
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
|
||||
import systemReducer, {
|
||||
initialSystemState,
|
||||
migrateSystemState,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
|
||||
import uiReducer, {
|
||||
initialUIState,
|
||||
migrateUIState,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { diff } from 'jsondiffpatch';
|
||||
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
|
||||
import dynamicMiddlewares from 'redux-dynamic-middlewares';
|
||||
import type { Driver } from 'redux-remember';
|
||||
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
|
||||
import { rememberEnhancer, rememberReducer } from 'redux-remember';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { api } from 'services/api';
|
||||
import { authToastMiddleware } from 'services/api/authToastMiddleware';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
import { STORAGE_PREFIX } from './constants';
|
||||
import { serialize } from './enhancers/reduxRemember/serialize';
|
||||
import { unserialize } from './enhancers/reduxRemember/unserialize';
|
||||
import { actionSanitizer } from './middleware/devtools/actionSanitizer';
|
||||
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
|
||||
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
|
||||
import { listenerMiddleware } from './middleware/listenerMiddleware';
|
||||
|
||||
const allReducers = {
|
||||
canvas: canvasReducer,
|
||||
gallery: galleryReducer,
|
||||
@@ -65,7 +119,7 @@ const rootReducer = combineReducers(allReducers);
|
||||
|
||||
const rememberedRootReducer = rememberReducer(rootReducer);
|
||||
|
||||
const rememberedKeys: (keyof typeof allReducers)[] = [
|
||||
const rememberedKeys = [
|
||||
'canvas',
|
||||
'gallery',
|
||||
'generation',
|
||||
@@ -80,15 +134,106 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
|
||||
'lora',
|
||||
'modelmanager',
|
||||
'hrf',
|
||||
];
|
||||
] satisfies (keyof typeof allReducers)[];
|
||||
|
||||
// Create a custom idb-keyval store (just needed to customize the name)
|
||||
export const idbKeyValStore = createIDBKeyValStore('invoke', 'invoke-store');
|
||||
type SliceConfig = {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
initialState: any;
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
migrate: (state: any) => any;
|
||||
};
|
||||
|
||||
// Create redux-remember driver, wrapping idb-keyval
|
||||
const idbKeyValDriver: Driver = {
|
||||
getItem: (key) => get(key, idbKeyValStore),
|
||||
setItem: (key, value) => set(key, value, idbKeyValStore),
|
||||
const sliceConfigs: {
|
||||
[key in (typeof rememberedKeys)[number]]: SliceConfig;
|
||||
} = {
|
||||
canvas: { initialState: initialCanvasState, migrate: migrateCanvasState },
|
||||
gallery: { initialState: initialGalleryState, migrate: migrateGalleryState },
|
||||
generation: {
|
||||
initialState: initialGenerationState,
|
||||
migrate: migrateGenerationState,
|
||||
},
|
||||
nodes: { initialState: initialNodesState, migrate: migrateNodesState },
|
||||
postprocessing: {
|
||||
initialState: initialPostprocessingState,
|
||||
migrate: migratePostprocessingState,
|
||||
},
|
||||
system: { initialState: initialSystemState, migrate: migrateSystemState },
|
||||
workflow: {
|
||||
initialState: initialWorkflowState,
|
||||
migrate: migrateWorkflowState,
|
||||
},
|
||||
ui: { initialState: initialUIState, migrate: migrateUIState },
|
||||
controlAdapters: {
|
||||
initialState: initialControlAdaptersState,
|
||||
migrate: migrateControlAdaptersState,
|
||||
},
|
||||
dynamicPrompts: {
|
||||
initialState: initialDynamicPromptsState,
|
||||
migrate: migrateDynamicPromptsState,
|
||||
},
|
||||
sdxl: { initialState: initialSDXLState, migrate: migrateSDXLState },
|
||||
lora: { initialState: initialLoraState, migrate: migrateLoRAState },
|
||||
modelmanager: {
|
||||
initialState: initialModelManagerState,
|
||||
migrate: migrateModelManagerState,
|
||||
},
|
||||
hrf: { initialState: initialHRFState, migrate: migrateHRFState },
|
||||
};
|
||||
|
||||
const unserialize: UnserializeFunction = (data, key) => {
|
||||
const log = logger('system');
|
||||
const config = sliceConfigs[key as keyof typeof sliceConfigs];
|
||||
if (!config) {
|
||||
throw new Error(`No unserialize config for slice "${key}"`);
|
||||
}
|
||||
try {
|
||||
const { initialState, migrate } = config;
|
||||
const parsed = JSON.parse(data);
|
||||
// strip out old keys
|
||||
const stripped = pick(parsed, keys(initialState));
|
||||
// run (additive) migrations
|
||||
const migrated = migrate(stripped);
|
||||
// merge in initial state as default values, covering any missing keys
|
||||
const transformed = defaultsDeep(migrated, initialState);
|
||||
|
||||
log.debug(
|
||||
{
|
||||
persistedData: parsed,
|
||||
rehydratedData: transformed,
|
||||
diff: diff(parsed, transformed) as JsonObject, // this is always serializable
|
||||
},
|
||||
`Rehydrated slice "${key}"`
|
||||
);
|
||||
return transformed;
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
{ error: serializeError(err) },
|
||||
`Error rehydrating slice "${key}", falling back to default initial state`
|
||||
);
|
||||
return config.initialState;
|
||||
}
|
||||
};
|
||||
|
||||
const serializationDenylist: {
|
||||
[key in (typeof rememberedKeys)[number]]?: string[];
|
||||
} = {
|
||||
canvas: canvasPersistDenylist,
|
||||
gallery: galleryPersistDenylist,
|
||||
generation: generationPersistDenylist,
|
||||
nodes: nodesPersistDenylist,
|
||||
postprocessing: postprocessingPersistDenylist,
|
||||
system: systemPersistDenylist,
|
||||
ui: uiPersistDenylist,
|
||||
controlAdapters: controlAdaptersPersistDenylist,
|
||||
dynamicPrompts: dynamicPromptsPersistDenylist,
|
||||
};
|
||||
|
||||
export const serialize: SerializeFunction = (data, key) => {
|
||||
const result = omit(
|
||||
data,
|
||||
serializationDenylist[key as keyof typeof serializationDenylist] ?? []
|
||||
);
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
|
||||
export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
@@ -114,6 +259,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
prefix: uniqueStoreKey
|
||||
? `${STORAGE_PREFIX}${uniqueStoreKey}-`
|
||||
: STORAGE_PREFIX,
|
||||
errorHandler,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -124,21 +270,9 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
stateSanitizer,
|
||||
trace: true,
|
||||
predicate: (state, action) => {
|
||||
// TODO: hook up to the log level param in system slice
|
||||
// manually type state, cannot type the arg
|
||||
// const typedState = state as ReturnType<typeof rootReducer>;
|
||||
|
||||
// TODO: doing this breaks the rtk query devtools, commenting out for now
|
||||
// if (action.type.startsWith('api/')) {
|
||||
// // don't log api actions, with manual cache updates they are extremely noisy
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (actionsDenylist.includes(action.type)) {
|
||||
// don't log other noisy actions
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,6 +43,16 @@ export type SDFeature =
|
||||
| 'vae'
|
||||
| 'hrf';
|
||||
|
||||
export type NumericalParameterConfig = {
|
||||
initial: number;
|
||||
sliderMin: number;
|
||||
sliderMax: number;
|
||||
numberInputMin: number;
|
||||
numberInputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration options for the InvokeAI UI.
|
||||
* Distinct from system settings which may be changed inside the app.
|
||||
@@ -66,69 +76,32 @@ export type AppConfig = {
|
||||
defaultModel?: string;
|
||||
disabledControlNetModels: string[];
|
||||
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[];
|
||||
iterations: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
width: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
height: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
steps: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
guidance: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
img2imgStrength: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
hrfStrength: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
fineStep: number;
|
||||
coarseStep: number;
|
||||
};
|
||||
// Core parameters
|
||||
iterations: NumericalParameterConfig;
|
||||
width: NumericalParameterConfig; // initial value comes from model
|
||||
height: NumericalParameterConfig; // initial value comes from model
|
||||
steps: NumericalParameterConfig;
|
||||
guidance: NumericalParameterConfig;
|
||||
cfgRescaleMultiplier: NumericalParameterConfig;
|
||||
img2imgStrength: NumericalParameterConfig;
|
||||
// Canvas
|
||||
boundingBoxHeight: NumericalParameterConfig; // initial value comes from model
|
||||
boundingBoxWidth: NumericalParameterConfig; // initial value comes from model
|
||||
scaledBoundingBoxHeight: NumericalParameterConfig; // initial value comes from model
|
||||
scaledBoundingBoxWidth: NumericalParameterConfig; // initial value comes from model
|
||||
canvasCoherenceStrength: NumericalParameterConfig;
|
||||
canvasCoherenceSteps: NumericalParameterConfig;
|
||||
infillTileSize: NumericalParameterConfig;
|
||||
infillPatchmatchDownscaleSize: NumericalParameterConfig;
|
||||
// Misc advanced
|
||||
clipSkip: NumericalParameterConfig; // slider and input max are ignored for this, because the values depend on the model
|
||||
maskBlur: NumericalParameterConfig;
|
||||
hrfStrength: NumericalParameterConfig;
|
||||
dynamicPrompts: {
|
||||
maxPrompts: {
|
||||
initial: number;
|
||||
min: number;
|
||||
sliderMax: number;
|
||||
inputMax: number;
|
||||
};
|
||||
maxPrompts: NumericalParameterConfig;
|
||||
};
|
||||
ca: {
|
||||
weight: NumericalParameterConfig;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,16 +1,5 @@
|
||||
/**
|
||||
* This is a copy-paste of https://github.com/lukasbach/chakra-ui-contextmenu with a small change.
|
||||
*
|
||||
* The reactflow background element somehow prevents the chakra `useOutsideClick()` hook from working.
|
||||
* With a menu open, clicking on the reactflow background element doesn't close the menu.
|
||||
*
|
||||
* Reactflow does provide an `onPaneClick` to handle clicks on the background element, but it is not
|
||||
* straightforward to programatically close the menu.
|
||||
*
|
||||
* As a (hopefully temporary) workaround, we will use a dirty hack:
|
||||
* - create `globalContextMenuCloseTrigger: number` in `ui` slice
|
||||
* - increment it in `onPaneClick` (and wherever else we want to close the menu)
|
||||
* - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes
|
||||
* Adapted from https://github.com/lukasbach/chakra-ui-contextmenu
|
||||
*/
|
||||
import type {
|
||||
ChakraProps,
|
||||
@@ -18,9 +7,9 @@ import type {
|
||||
MenuProps,
|
||||
PortalProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { Portal, useEventListener } from '@chakra-ui/react';
|
||||
import { Portal, useDisclosure, useEventListener } from '@chakra-ui/react';
|
||||
import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper';
|
||||
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
|
||||
import { useGlobalMenuClose } from 'common/hooks/useGlobalMenuClose';
|
||||
import { typedMemo } from 'common/util/typedMemo';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -34,94 +23,89 @@ export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> {
|
||||
|
||||
export const InvContextMenu = typedMemo(
|
||||
<T extends HTMLElement = HTMLElement>(props: InvContextMenuProps<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [isDeferredOpen, setIsDeferredOpen] = useState(false);
|
||||
const [position, setPosition] = useState<[number, number]>([0, 0]);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [position, setPosition] = useState([-1, -1]);
|
||||
const targetRef = useRef<T>(null);
|
||||
const lastPositionRef = useRef([-1, -1]);
|
||||
const timeoutRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
setIsRendered(true);
|
||||
setTimeout(() => {
|
||||
setIsDeferredOpen(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setIsDeferredOpen(false);
|
||||
const timeout = setTimeout(() => {
|
||||
setIsRendered(isOpen);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isOpen]);
|
||||
useGlobalMenuClose(onClose);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setIsDeferredOpen(false);
|
||||
setIsRendered(false);
|
||||
}, []);
|
||||
const onContextMenu = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (e.shiftKey) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
targetRef.current?.contains(e.target as HTMLElement) ||
|
||||
e.target === targetRef.current
|
||||
) {
|
||||
// clear pending delayed open
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
e.preventDefault();
|
||||
if (
|
||||
lastPositionRef.current[0] !== e.pageX ||
|
||||
lastPositionRef.current[1] !== e.pageY
|
||||
) {
|
||||
// if the mouse moved, we need to close, wait for animation and reopen the menu at the new position
|
||||
onClose();
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
onOpen();
|
||||
setPosition([e.pageX, e.pageY]);
|
||||
}, 100);
|
||||
} else {
|
||||
// else we can just open the menu at the current position
|
||||
onOpen();
|
||||
setPosition([e.pageX, e.pageY]);
|
||||
}
|
||||
}
|
||||
lastPositionRef.current = [e.pageX, e.pageY];
|
||||
},
|
||||
[onClose, onOpen]
|
||||
);
|
||||
|
||||
// This is the change from the original chakra-ui-contextmenu
|
||||
// Close all menus when the globalContextMenuCloseTrigger changes
|
||||
useGlobalMenuCloseTrigger(onClose);
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEventListener('contextmenu', (e) => {
|
||||
if (
|
||||
targetRef.current?.contains(e.target as HTMLElement) ||
|
||||
e.target === targetRef.current
|
||||
) {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
setPosition([e.pageX, e.pageY]);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
const onCloseHandler = useCallback(() => {
|
||||
props.menuProps?.onClose?.();
|
||||
setIsOpen(false);
|
||||
}, [props.menuProps]);
|
||||
useEventListener('contextmenu', onContextMenu);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.children(targetRef)}
|
||||
{isRendered && (
|
||||
<Portal {...props.portalProps}>
|
||||
<InvMenu
|
||||
isLazy
|
||||
isOpen={isDeferredOpen}
|
||||
gutter={0}
|
||||
onClose={onCloseHandler}
|
||||
placement="auto-end"
|
||||
{...props.menuProps}
|
||||
>
|
||||
<InvMenuButton
|
||||
aria-hidden={true}
|
||||
w={1}
|
||||
h={1}
|
||||
position="absolute"
|
||||
left={position[0]}
|
||||
top={position[1]}
|
||||
cursor="default"
|
||||
bg="transparent"
|
||||
size="sm"
|
||||
_hover={_hover}
|
||||
{...props.menuButtonProps}
|
||||
/>
|
||||
{props.renderMenu()}
|
||||
</InvMenu>
|
||||
</Portal>
|
||||
)}
|
||||
<Portal {...props.portalProps}>
|
||||
<InvMenu
|
||||
isLazy
|
||||
isOpen={isOpen}
|
||||
gutter={0}
|
||||
placement="auto-end"
|
||||
onClose={onClose}
|
||||
{...props.menuProps}
|
||||
>
|
||||
<InvMenuButton
|
||||
aria-hidden={true}
|
||||
w={1}
|
||||
h={1}
|
||||
position="absolute"
|
||||
left={position[0]}
|
||||
top={position[1]}
|
||||
cursor="default"
|
||||
bg="transparent"
|
||||
size="sm"
|
||||
_hover={_hover}
|
||||
pointerEvents="none"
|
||||
{...props.menuButtonProps}
|
||||
/>
|
||||
{props.renderMenu()}
|
||||
</InvMenu>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const _hover: ChakraProps['_hover'] = { bg: 'transparent' };
|
||||
|
||||
Object.assign(InvContextMenu, {
|
||||
displayName: 'InvContextMenu',
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ export const InvControl = memo(
|
||||
orientation={orientation}
|
||||
isDisabled={isDisabled}
|
||||
{...formControlProps}
|
||||
{...ctx.controlProps}
|
||||
>
|
||||
<Flex className="invcontrol-label-wrapper">
|
||||
{label && (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { FormLabelProps } from '@chakra-ui/react';
|
||||
import type { FormControlProps, FormLabelProps } from '@chakra-ui/react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo } from 'react';
|
||||
|
||||
export type InvControlGroupProps = {
|
||||
labelProps?: FormLabelProps;
|
||||
controlProps?: FormControlProps;
|
||||
isDisabled?: boolean;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
MenuList as ChakraMenuList,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
import { skipMouseEvent } from 'common/util/skipMouseEvent';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { menuListMotionProps } from './constants';
|
||||
@@ -16,6 +17,7 @@ export const InvMenuList = memo(
|
||||
<ChakraMenuList
|
||||
ref={ref}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipMouseEvent}
|
||||
{...props}
|
||||
/>
|
||||
</Portal>
|
||||
|
||||
@@ -42,9 +42,9 @@ const line = definePartsStyle(() => ({
|
||||
px: 4,
|
||||
py: 1,
|
||||
fontSize: 'sm',
|
||||
color: 'base.400',
|
||||
color: 'base.200',
|
||||
_selected: {
|
||||
color: 'blue.400',
|
||||
color: 'blue.200',
|
||||
},
|
||||
},
|
||||
tabpanel: {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { idbKeyValStore } from 'app/store/store';
|
||||
import { clear } from 'idb-keyval';
|
||||
import { clearIdbKeyValStore } from 'app/store/enhancers/reduxRemember/driver';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useClearStorage = () => {
|
||||
const clearStorage = useCallback(() => {
|
||||
clear(idbKeyValStore);
|
||||
clearIdbKeyValStore();
|
||||
localStorage.clear();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ export const useGlobalHotkeys = () => {
|
||||
{
|
||||
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
|
||||
);
|
||||
@@ -74,7 +73,6 @@ export const useGlobalHotkeys = () => {
|
||||
{
|
||||
enabled: () => !isDisabledClearQueue && !isLoadingClearQueue,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ const $onCloseCallbacks = atom<CB[]>([]);
|
||||
* This hook provides a way to close all menus by calling `onCloseGlobal()`. Menus that want to be closed
|
||||
* in this way should register themselves by passing a callback to `useGlobalMenuCloseTrigger()`.
|
||||
*/
|
||||
export const useGlobalMenuCloseTrigger = (onClose?: CB) => {
|
||||
export const useGlobalMenuClose = (onClose?: CB) => {
|
||||
useEffect(() => {
|
||||
if (!onClose) {
|
||||
return;
|
||||
@@ -1,27 +1,33 @@
|
||||
// https://stackoverflow.com/a/73731908
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export function useSingleAndDoubleClick(
|
||||
handleSingleClick: () => void,
|
||||
handleDoubleClick: () => void,
|
||||
delay = 250
|
||||
) {
|
||||
export type UseSingleAndDoubleClickOptions = {
|
||||
onSingleClick: () => void;
|
||||
onDoubleClick: () => void;
|
||||
latency?: number;
|
||||
};
|
||||
|
||||
export function useSingleAndDoubleClick({
|
||||
onSingleClick,
|
||||
onDoubleClick,
|
||||
latency = 250,
|
||||
}: UseSingleAndDoubleClickOptions): () => void {
|
||||
const [click, setClick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (click === 1) {
|
||||
handleSingleClick();
|
||||
onSingleClick();
|
||||
}
|
||||
setClick(0);
|
||||
}, delay);
|
||||
}, latency);
|
||||
|
||||
if (click === 2) {
|
||||
handleDoubleClick();
|
||||
onDoubleClick();
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [click, handleSingleClick, handleDoubleClick, delay]);
|
||||
}, [click, onDoubleClick, latency, onSingleClick]);
|
||||
|
||||
const onClick = useCallback(() => setClick((prev) => prev + 1), []);
|
||||
|
||||
|
||||
8
invokeai/frontend/web/src/common/util/skipMouseEvent.ts
Normal file
8
invokeai/frontend/web/src/common/util/skipMouseEvent.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
/**
|
||||
* Prevents the default behavior of the event.
|
||||
*/
|
||||
export const skipMouseEvent = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
const ClearCanvasHistoryButtonModal = () => {
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
@@ -23,7 +23,7 @@ const ClearCanvasHistoryButtonModal = () => {
|
||||
<InvButton
|
||||
onClick={onOpen}
|
||||
size="sm"
|
||||
leftIcon={<FaTrash />}
|
||||
leftIcon={<PiTrashSimpleFill />}
|
||||
isDisabled={isStaging}
|
||||
>
|
||||
{t('unifiedCanvas.clearCanvasHistory')}
|
||||
|
||||
@@ -10,20 +10,19 @@ import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut';
|
||||
import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp';
|
||||
import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom';
|
||||
import {
|
||||
$canvasBaseLayer,
|
||||
$canvasStage,
|
||||
$isModifyingBoundingBox,
|
||||
$isMouseOverBoundingBox,
|
||||
$isMovingStage,
|
||||
$isTransformingBoundingBox,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import {
|
||||
canvasResized,
|
||||
selectCanvasSlice,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
setCanvasBaseLayer,
|
||||
setCanvasStage,
|
||||
} from 'features/canvas/util/konvaInstanceProvider';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
@@ -61,7 +60,6 @@ const IAICanvas = () => {
|
||||
);
|
||||
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
|
||||
const stageScale = useAppSelector((s) => s.canvas.stageScale);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const shouldShowIntermediates = useAppSelector(
|
||||
(s) => s.canvas.shouldShowIntermediates
|
||||
);
|
||||
@@ -78,10 +76,11 @@ const IAICanvas = () => {
|
||||
const isMovingStage = useStore($isMovingStage);
|
||||
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
|
||||
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
|
||||
const tool = useStore($tool);
|
||||
useCanvasHotkeys();
|
||||
const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
|
||||
setCanvasStage(el as Konva.Stage);
|
||||
stageRef.current = el;
|
||||
const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => {
|
||||
$canvasStage.set(stageElement);
|
||||
stageRef.current = stageElement;
|
||||
}, []);
|
||||
const stageCursor = useMemo(() => {
|
||||
if (tool === 'move' || isStaging) {
|
||||
@@ -104,10 +103,14 @@ const IAICanvas = () => {
|
||||
shouldRestrictStrokesToBox,
|
||||
tool,
|
||||
]);
|
||||
const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => {
|
||||
setCanvasBaseLayer(el as Konva.Layer);
|
||||
canvasBaseLayerRef.current = el;
|
||||
}, []);
|
||||
|
||||
const canvasBaseLayerRefCallback = useCallback(
|
||||
(layerElement: Konva.Layer) => {
|
||||
$canvasBaseLayer.set(layerElement);
|
||||
canvasBaseLayerRef.current = layerElement;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@ import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaArrowLeft,
|
||||
FaArrowRight,
|
||||
FaCheck,
|
||||
FaEye,
|
||||
FaEyeSlash,
|
||||
FaSave,
|
||||
FaTimes,
|
||||
} from 'react-icons/fa';
|
||||
PiArrowLeftBold,
|
||||
PiArrowRightBold,
|
||||
PiCheckBold,
|
||||
PiEyeBold,
|
||||
PiEyeSlashBold,
|
||||
PiFloppyDiskBold,
|
||||
PiXBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
@@ -140,7 +140,7 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
<InvIconButton
|
||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
icon={<FaArrowLeft />}
|
||||
icon={<PiArrowLeftBold />}
|
||||
onClick={handlePrevImage}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!shouldShowStagingImage}
|
||||
@@ -154,7 +154,7 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
<InvIconButton
|
||||
tooltip={`${t('unifiedCanvas.next')} (Right)`}
|
||||
aria-label={`${t('unifiedCanvas.next')} (Right)`}
|
||||
icon={<FaArrowRight />}
|
||||
icon={<PiArrowRightBold />}
|
||||
onClick={handleNextImage}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!shouldShowStagingImage}
|
||||
@@ -164,7 +164,7 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
<InvIconButton
|
||||
tooltip={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
icon={<FaCheck />}
|
||||
icon={<PiCheckBold />}
|
||||
onClick={handleAccept}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
@@ -180,7 +180,7 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
: t('unifiedCanvas.showResultsOff')
|
||||
}
|
||||
data-alert={!shouldShowStagingImage}
|
||||
icon={shouldShowStagingImage ? <FaEye /> : <FaEyeSlash />}
|
||||
icon={shouldShowStagingImage ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||
onClick={handleToggleShouldShowStagingImage}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
@@ -188,14 +188,14 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
isDisabled={!imageDTO || !imageDTO.is_intermediate}
|
||||
icon={<FaSave />}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={handleSaveToGallery}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
<InvIconButton
|
||||
tooltip={t('unifiedCanvas.discardAll')}
|
||||
aria-label={t('unifiedCanvas.discardAll')}
|
||||
icon={<FaTimes />}
|
||||
icon={<PiXBold />}
|
||||
onClick={handleDiscardStagingArea}
|
||||
colorScheme="error"
|
||||
fontSize={20}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
$cursorPosition,
|
||||
$isMovingBoundingBox,
|
||||
$isTransformingBoundingBox,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
@@ -89,7 +90,7 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
|
||||
const maskColorString = useAppSelector((s) =>
|
||||
rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })
|
||||
);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const tool = useStore($tool);
|
||||
const layer = useAppSelector((s) => s.canvas.layer);
|
||||
const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale);
|
||||
const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale);
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import {
|
||||
$isDrawing,
|
||||
$isMouseOverBoundingBox,
|
||||
$isMouseOverBoundingBoxOutline,
|
||||
$isMovingBoundingBox,
|
||||
$isTransformingBoundingBox,
|
||||
setIsMouseOverBoundingBox,
|
||||
setIsMovingBoundingBox,
|
||||
setIsTransformingBoundingBox,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import {
|
||||
aspectRatioChanged,
|
||||
@@ -30,7 +30,7 @@ import type Konva from 'konva';
|
||||
import type { GroupConfig } from 'konva/lib/Group';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Group, Rect, Transformer } from 'react-konva';
|
||||
|
||||
@@ -49,18 +49,19 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
);
|
||||
const stageScale = useAppSelector((s) => s.canvas.stageScale);
|
||||
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale);
|
||||
const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio);
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
const transformerRef = useRef<Konva.Transformer>(null);
|
||||
const shapeRef = useRef<Konva.Rect>(null);
|
||||
const shift = useStore($shift);
|
||||
const tool = useStore($tool);
|
||||
const isDrawing = useStore($isDrawing);
|
||||
const isMovingBoundingBox = useStore($isMovingBoundingBox);
|
||||
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
|
||||
const [isMouseOverBoundingBoxOutline, setIsMouseOverBoundingBoxOutline] =
|
||||
useState(false);
|
||||
const isMouseOverBoundingBoxOutline = useStore(
|
||||
$isMouseOverBoundingBoxOutline
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !shapeRef.current) {
|
||||
@@ -228,43 +229,43 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
);
|
||||
|
||||
const handleStartedTransforming = useCallback(() => {
|
||||
setIsTransformingBoundingBox(true);
|
||||
$isTransformingBoundingBox.set(true);
|
||||
}, []);
|
||||
|
||||
const handleEndedTransforming = useCallback(() => {
|
||||
setIsTransformingBoundingBox(false);
|
||||
setIsMovingBoundingBox(false);
|
||||
setIsMouseOverBoundingBox(false);
|
||||
setIsMouseOverBoundingBoxOutline(false);
|
||||
$isTransformingBoundingBox.set(false);
|
||||
$isMovingBoundingBox.set(false);
|
||||
$isMouseOverBoundingBox.set(false);
|
||||
$isMouseOverBoundingBoxOutline.set(false);
|
||||
}, []);
|
||||
|
||||
const handleStartedMoving = useCallback(() => {
|
||||
setIsMovingBoundingBox(true);
|
||||
$isMovingBoundingBox.set(true);
|
||||
}, []);
|
||||
|
||||
const handleEndedModifying = useCallback(() => {
|
||||
setIsTransformingBoundingBox(false);
|
||||
setIsMovingBoundingBox(false);
|
||||
setIsMouseOverBoundingBox(false);
|
||||
setIsMouseOverBoundingBoxOutline(false);
|
||||
$isTransformingBoundingBox.set(false);
|
||||
$isMovingBoundingBox.set(false);
|
||||
$isMouseOverBoundingBox.set(false);
|
||||
$isMouseOverBoundingBoxOutline.set(false);
|
||||
}, []);
|
||||
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsMouseOverBoundingBoxOutline(true);
|
||||
$isMouseOverBoundingBoxOutline.set(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseOut = useCallback(() => {
|
||||
!isTransformingBoundingBox &&
|
||||
!isMovingBoundingBox &&
|
||||
setIsMouseOverBoundingBoxOutline(false);
|
||||
$isMouseOverBoundingBoxOutline.set(false);
|
||||
}, [isMovingBoundingBox, isTransformingBoundingBox]);
|
||||
|
||||
const handleMouseEnterBoundingBox = useCallback(() => {
|
||||
setIsMouseOverBoundingBox(true);
|
||||
$isMouseOverBoundingBox.set(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeaveBoundingBox = useCallback(() => {
|
||||
setIsMouseOverBoundingBox(false);
|
||||
$isMouseOverBoundingBox.set(false);
|
||||
}, []);
|
||||
|
||||
const stroke = useMemo(() => {
|
||||
|
||||
@@ -25,7 +25,11 @@ import { memo, useCallback } from 'react';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaMask, FaSave, FaTrash } from 'react-icons/fa';
|
||||
import {
|
||||
PiExcludeBold,
|
||||
PiFloppyDiskBackFill,
|
||||
PiTrashSimpleFill,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
const IAICanvasMaskOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -110,7 +114,7 @@ const IAICanvasMaskOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={t('unifiedCanvas.maskingOptions')}
|
||||
tooltip={t('unifiedCanvas.maskingOptions')}
|
||||
icon={<FaMask />}
|
||||
icon={<PiExcludeBold />}
|
||||
isChecked={layer === 'mask'}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
@@ -136,12 +140,16 @@ const IAICanvasMaskOptions = () => {
|
||||
onChange={handleChangeMaskColor}
|
||||
/>
|
||||
</Box>
|
||||
<InvButton size="sm" leftIcon={<FaSave />} onClick={handleSaveMask}>
|
||||
<InvButton
|
||||
size="sm"
|
||||
leftIcon={<PiFloppyDiskBackFill />}
|
||||
onClick={handleSaveMask}
|
||||
>
|
||||
{t('unifiedCanvas.saveMask')}
|
||||
</InvButton>
|
||||
<InvButton
|
||||
size="sm"
|
||||
leftIcon={<FaTrash />}
|
||||
leftIcon={<PiTrashSimpleFill />}
|
||||
onClick={handleClearMask}
|
||||
>
|
||||
{t('unifiedCanvas.clearMask')}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaRedo } from 'react-icons/fa';
|
||||
import { PiArrowClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
const IAICanvasRedoButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -34,7 +34,7 @@ const IAICanvasRedoButton = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.redo')} (Ctrl+Shift+Z)`}
|
||||
tooltip={`${t('unifiedCanvas.redo')} (Ctrl+Shift+Z)`}
|
||||
icon={<FaRedo />}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
onClick={handleRedo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaWrench } from 'react-icons/fa';
|
||||
import { PiGearSixBold } from 'react-icons/pi';
|
||||
|
||||
const IAICanvasSettingsButtonPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -114,7 +114,7 @@ const IAICanvasSettingsButtonPopover = () => {
|
||||
<InvIconButton
|
||||
tooltip={t('unifiedCanvas.canvasSettings')}
|
||||
aria-label={t('unifiedCanvas.canvasSettings')}
|
||||
icon={<FaWrench />}
|
||||
icon={<PiGearSixBold />}
|
||||
/>
|
||||
</InvPopoverTrigger>
|
||||
<InvPopoverContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
|
||||
@@ -10,14 +11,16 @@ import {
|
||||
InvPopoverTrigger,
|
||||
} from 'common/components/InvPopover/wrapper';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { resetToolInteractionState } from 'features/canvas/store/canvasNanostore';
|
||||
import {
|
||||
$tool,
|
||||
resetToolInteractionState,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import {
|
||||
addEraseRect,
|
||||
addFillRect,
|
||||
setBrushColor,
|
||||
setBrushSize,
|
||||
setTool,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { InvIconButton, InvPopover } from 'index';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -26,17 +29,19 @@ import type { RgbaColor } from 'react-colorful';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaEraser,
|
||||
FaEyeDropper,
|
||||
FaFillDrip,
|
||||
FaPaintBrush,
|
||||
FaSlidersH,
|
||||
FaTimes,
|
||||
} from 'react-icons/fa';
|
||||
PiEraserBold,
|
||||
PiEyedropperBold,
|
||||
PiPaintBrushBold,
|
||||
PiPaintBucketBold,
|
||||
PiSlidersHorizontalBold,
|
||||
PiXBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
const marks = [1, 25, 50, 75, 100];
|
||||
|
||||
const IAICanvasToolChooserOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const tool = useStore($tool);
|
||||
const brushColor = useAppSelector((s) => s.canvas.brushColor);
|
||||
const brushSize = useAppSelector((s) => s.canvas.brushSize);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
@@ -163,17 +168,17 @@ const IAICanvasToolChooserOptions = () => {
|
||||
);
|
||||
|
||||
const handleSelectBrushTool = useCallback(() => {
|
||||
dispatch(setTool('brush'));
|
||||
$tool.set('brush');
|
||||
resetToolInteractionState();
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
const handleSelectEraserTool = useCallback(() => {
|
||||
dispatch(setTool('eraser'));
|
||||
$tool.set('eraser');
|
||||
resetToolInteractionState();
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
const handleSelectColorPickerTool = useCallback(() => {
|
||||
dispatch(setTool('colorPicker'));
|
||||
$tool.set('colorPicker');
|
||||
resetToolInteractionState();
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
const handleFillRect = useCallback(() => {
|
||||
dispatch(addFillRect());
|
||||
}, [dispatch]);
|
||||
@@ -198,7 +203,7 @@ const IAICanvasToolChooserOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.brush')} (B)`}
|
||||
tooltip={`${t('unifiedCanvas.brush')} (B)`}
|
||||
icon={<FaPaintBrush />}
|
||||
icon={<PiPaintBrushBold />}
|
||||
isChecked={tool === 'brush' && !isStaging}
|
||||
onClick={handleSelectBrushTool}
|
||||
isDisabled={isStaging}
|
||||
@@ -206,7 +211,7 @@ const IAICanvasToolChooserOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
|
||||
tooltip={`${t('unifiedCanvas.eraser')} (E)`}
|
||||
icon={<FaEraser />}
|
||||
icon={<PiEraserBold />}
|
||||
isChecked={tool === 'eraser' && !isStaging}
|
||||
isDisabled={isStaging}
|
||||
onClick={handleSelectEraserTool}
|
||||
@@ -214,21 +219,21 @@ const IAICanvasToolChooserOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.fillBoundingBox')} (Shift+F)`}
|
||||
tooltip={`${t('unifiedCanvas.fillBoundingBox')} (Shift+F)`}
|
||||
icon={<FaFillDrip />}
|
||||
icon={<PiPaintBucketBold />}
|
||||
isDisabled={isStaging}
|
||||
onClick={handleFillRect}
|
||||
/>
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
|
||||
tooltip={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
|
||||
icon={<FaTimes />}
|
||||
icon={<PiXBold />}
|
||||
isDisabled={isStaging}
|
||||
onClick={handleEraseBoundingBox}
|
||||
/>
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.colorPicker')} (C)`}
|
||||
tooltip={`${t('unifiedCanvas.colorPicker')} (C)`}
|
||||
icon={<FaEyeDropper />}
|
||||
icon={<PiEyedropperBold />}
|
||||
isChecked={tool === 'colorPicker' && !isStaging}
|
||||
isDisabled={isStaging}
|
||||
onClick={handleSelectColorPickerTool}
|
||||
@@ -238,7 +243,7 @@ const IAICanvasToolChooserOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={t('unifiedCanvas.brushOptions')}
|
||||
tooltip={t('unifiedCanvas.brushOptions')}
|
||||
icon={<FaSlidersH />}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
/>
|
||||
</InvPopoverTrigger>
|
||||
<InvPopoverContent>
|
||||
@@ -281,5 +286,3 @@ const IAICanvasToolChooserOptions = () => {
|
||||
};
|
||||
|
||||
export default memo(IAICanvasToolChooserOptions);
|
||||
|
||||
const marks = [1, 25, 50, 75, 100];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
@@ -14,31 +15,30 @@ import {
|
||||
canvasMerged,
|
||||
canvasSavedToGallery,
|
||||
} from 'features/canvas/store/actions';
|
||||
import { $canvasBaseLayer, $tool } from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import {
|
||||
resetCanvas,
|
||||
resetCanvasView,
|
||||
setIsMaskEnabled,
|
||||
setLayer,
|
||||
setTool,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
||||
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import { InvIconButton } from 'index';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaArrowsAlt,
|
||||
FaCopy,
|
||||
FaCrosshairs,
|
||||
FaDownload,
|
||||
FaLayerGroup,
|
||||
FaSave,
|
||||
FaTrash,
|
||||
FaUpload,
|
||||
} from 'react-icons/fa';
|
||||
PiCopyBold,
|
||||
PiCrosshairSimpleBold,
|
||||
PiDownloadSimpleBold,
|
||||
PiFloppyDiskBold,
|
||||
PiHandGrabbingBold,
|
||||
PiStackBold,
|
||||
PiTrashSimpleBold,
|
||||
PiUploadSimpleBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
import IAICanvasMaskOptions from './IAICanvasMaskOptions';
|
||||
import IAICanvasRedoButton from './IAICanvasRedoButton';
|
||||
@@ -50,9 +50,8 @@ const IAICanvasToolbar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
|
||||
const layer = useAppSelector((s) => s.canvas.layer);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const tool = useStore($tool);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const { t } = useTranslation();
|
||||
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||
|
||||
@@ -81,7 +80,7 @@ const IAICanvasToolbar = () => {
|
||||
enabled: () => true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[canvasBaseLayer]
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -93,7 +92,7 @@ const IAICanvasToolbar = () => {
|
||||
enabled: () => !isStaging,
|
||||
preventDefault: true,
|
||||
},
|
||||
[canvasBaseLayer]
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -105,7 +104,7 @@ const IAICanvasToolbar = () => {
|
||||
enabled: () => !isStaging,
|
||||
preventDefault: true,
|
||||
},
|
||||
[canvasBaseLayer]
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -117,7 +116,7 @@ const IAICanvasToolbar = () => {
|
||||
enabled: () => !isStaging && isClipboardAPIAvailable,
|
||||
preventDefault: true,
|
||||
},
|
||||
[canvasBaseLayer, isClipboardAPIAvailable]
|
||||
[isClipboardAPIAvailable]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -129,33 +128,42 @@ const IAICanvasToolbar = () => {
|
||||
enabled: () => !isStaging,
|
||||
preventDefault: true,
|
||||
},
|
||||
[canvasBaseLayer]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSelectMoveTool = useCallback(() => {
|
||||
dispatch(setTool('move'));
|
||||
}, [dispatch]);
|
||||
$tool.set('move');
|
||||
}, []);
|
||||
|
||||
const handleClickResetCanvasView = useSingleAndDoubleClick(
|
||||
() => handleResetCanvasView(false),
|
||||
() => handleResetCanvasView(true)
|
||||
const handleResetCanvasView = useCallback(
|
||||
(shouldScaleTo1 = false) => {
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
const clientRect = canvasBaseLayer.getClientRect({
|
||||
skipTransform: true,
|
||||
});
|
||||
dispatch(
|
||||
resetCanvasView({
|
||||
contentRect: clientRect,
|
||||
shouldScaleTo1,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const onSingleClick = useCallback(() => {
|
||||
handleResetCanvasView(false);
|
||||
}, [handleResetCanvasView]);
|
||||
const onDoubleClick = useCallback(() => {
|
||||
handleResetCanvasView(true);
|
||||
}, [handleResetCanvasView]);
|
||||
|
||||
const handleResetCanvasView = (shouldScaleTo1 = false) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
const clientRect = canvasBaseLayer.getClientRect({
|
||||
skipTransform: true,
|
||||
});
|
||||
dispatch(
|
||||
resetCanvasView({
|
||||
contentRect: clientRect,
|
||||
shouldScaleTo1,
|
||||
})
|
||||
);
|
||||
};
|
||||
const handleClickResetCanvasView = useSingleAndDoubleClick({
|
||||
onSingleClick,
|
||||
onDoubleClick,
|
||||
});
|
||||
|
||||
const handleResetCanvas = useCallback(() => {
|
||||
dispatch(resetCanvas());
|
||||
@@ -217,14 +225,14 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
||||
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
||||
icon={<FaArrowsAlt />}
|
||||
icon={<PiHandGrabbingBold />}
|
||||
isChecked={tool === 'move' || isStaging}
|
||||
onClick={handleSelectMoveTool}
|
||||
/>
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
||||
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
||||
icon={<FaCrosshairs />}
|
||||
icon={<PiCrosshairSimpleBold />}
|
||||
onClick={handleClickResetCanvasView}
|
||||
/>
|
||||
</InvButtonGroup>
|
||||
@@ -233,14 +241,14 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||
icon={<FaLayerGroup />}
|
||||
icon={<PiStackBold />}
|
||||
onClick={handleMergeVisible}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||
icon={<FaSave />}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={handleSaveToGallery}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
@@ -248,7 +256,7 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||
icon={<FaCopy />}
|
||||
icon={<PiCopyBold />}
|
||||
onClick={handleCopyImageToClipboard}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
@@ -256,7 +264,7 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||
icon={<FaDownload />}
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
onClick={handleDownloadAsImage}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
@@ -270,7 +278,7 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('common.upload')}`}
|
||||
tooltip={`${t('common.upload')}`}
|
||||
icon={<FaUpload />}
|
||||
icon={<PiUploadSimpleBold />}
|
||||
isDisabled={isStaging}
|
||||
{...getUploadButtonProps()}
|
||||
/>
|
||||
@@ -278,7 +286,7 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={handleResetCanvas}
|
||||
colorScheme="error"
|
||||
isDisabled={isStaging}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaUndo } from 'react-icons/fa';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
const IAICanvasUndoButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -33,7 +33,7 @@ const IAICanvasUndoButton = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('unifiedCanvas.undo')} (Ctrl+Z)`}
|
||||
tooltip={`${t('unifiedCanvas.undo')} (Ctrl+Z)`}
|
||||
icon={<FaUndo />}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
onClick={handleUndo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
$isMovingBoundingBox,
|
||||
setIsMovingStage,
|
||||
$isMovingStage,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { setStageCoordinates } from 'features/canvas/store/canvasSlice';
|
||||
@@ -12,18 +12,19 @@ import { useCallback } from 'react';
|
||||
const useCanvasDrag = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const isMovingBoundingBox = useStore($isMovingBoundingBox);
|
||||
const handleDragStart = useCallback(() => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
if (
|
||||
!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsMovingStage(true);
|
||||
}, [isMovingBoundingBox, isStaging, tool]);
|
||||
$isMovingStage.set(true);
|
||||
}, [isStaging]);
|
||||
|
||||
const handleDragMove = useCallback(
|
||||
(e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
const tool = $tool.get();
|
||||
if (!((tool === 'move' || isStaging) && !$isMovingBoundingBox.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,15 +32,17 @@ const useCanvasDrag = () => {
|
||||
|
||||
dispatch(setStageCoordinates(newCoordinates));
|
||||
},
|
||||
[dispatch, isMovingBoundingBox, isStaging, tool]
|
||||
[dispatch, isStaging]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
|
||||
if (
|
||||
!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsMovingStage(false);
|
||||
}, [isMovingBoundingBox, isStaging, tool]);
|
||||
$isMovingStage.set(false);
|
||||
}, [isStaging]);
|
||||
|
||||
return {
|
||||
handleDragStart,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
$canvasStage,
|
||||
$tool,
|
||||
$toolStash,
|
||||
resetCanvasInteractionState,
|
||||
resetToolInteractionState,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
@@ -9,12 +12,9 @@ import {
|
||||
setIsMaskEnabled,
|
||||
setShouldShowBoundingBox,
|
||||
setShouldSnapToGrid,
|
||||
setTool,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import type { CanvasTool } from 'features/canvas/store/canvasTypes';
|
||||
import { getCanvasStage } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const useInpaintingCanvasHotkeys = () => {
|
||||
@@ -23,12 +23,9 @@ const useInpaintingCanvasHotkeys = () => {
|
||||
const shouldShowBoundingBox = useAppSelector(
|
||||
(s) => s.canvas.shouldShowBoundingBox
|
||||
);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
|
||||
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
|
||||
const previousToolRef = useRef<CanvasTool | null>(null);
|
||||
const canvasStage = getCanvasStage();
|
||||
|
||||
// Beta Keys
|
||||
const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]);
|
||||
@@ -96,37 +93,47 @@ const useInpaintingCanvasHotkeys = () => {
|
||||
[activeTabName, shouldShowBoundingBox]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
['space'],
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.repeat) {
|
||||
return;
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === ' ' && !e.repeat) {
|
||||
console.log('spaceeee');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
canvasStage?.container().focus();
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.repeat || e.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
if ($toolStash.get() || $tool.get() === 'move') {
|
||||
return;
|
||||
}
|
||||
$canvasStage.get()?.container().focus();
|
||||
$toolStash.set($tool.get());
|
||||
$tool.set('move');
|
||||
resetToolInteractionState();
|
||||
}, []);
|
||||
const onKeyUp = useCallback((e: KeyboardEvent) => {
|
||||
if (e.repeat || e.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
if (!$toolStash.get() || $tool.get() !== 'move') {
|
||||
return;
|
||||
}
|
||||
$canvasStage.get()?.container().focus();
|
||||
$tool.set($toolStash.get() ?? 'move');
|
||||
$toolStash.set(null);
|
||||
}, []);
|
||||
|
||||
if (tool !== 'move') {
|
||||
previousToolRef.current = tool;
|
||||
dispatch(setTool('move'));
|
||||
resetToolInteractionState();
|
||||
}
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
if (
|
||||
tool === 'move' &&
|
||||
previousToolRef.current &&
|
||||
previousToolRef.current !== 'move'
|
||||
) {
|
||||
dispatch(setTool(previousToolRef.current));
|
||||
previousToolRef.current = 'move';
|
||||
}
|
||||
},
|
||||
{
|
||||
keyup: true,
|
||||
keydown: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[tool, previousToolRef]
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
};
|
||||
}, [onKeyDown, onKeyUp]);
|
||||
};
|
||||
|
||||
export default useInpaintingCanvasHotkeys;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
setIsDrawing,
|
||||
setIsMovingStage,
|
||||
$isDrawing,
|
||||
$isMovingStage,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { addLine } from 'features/canvas/store/canvasSlice';
|
||||
@@ -15,7 +16,6 @@ import useColorPicker from './useColorUnderCursor';
|
||||
|
||||
const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
const { commitColorUnderCursor } = useColorPicker();
|
||||
|
||||
@@ -26,9 +26,10 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
}
|
||||
|
||||
stageRef.current.container().focus();
|
||||
const tool = $tool.get();
|
||||
|
||||
if (tool === 'move' || isStaging) {
|
||||
setIsMovingStage(true);
|
||||
$isMovingStage.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,12 +46,17 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
|
||||
e.evt.preventDefault();
|
||||
|
||||
setIsDrawing(true);
|
||||
$isDrawing.set(true);
|
||||
|
||||
// Add a new line starting from the current cursor position.
|
||||
dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y]));
|
||||
dispatch(
|
||||
addLine({
|
||||
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
||||
tool,
|
||||
})
|
||||
);
|
||||
},
|
||||
[stageRef, tool, isStaging, dispatch, commitColorUnderCursor]
|
||||
[stageRef, isStaging, dispatch, commitColorUnderCursor]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
$cursorPosition,
|
||||
$isDrawing,
|
||||
setCursorPosition,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
|
||||
@@ -20,8 +20,6 @@ const useCanvasMouseMove = (
|
||||
lastCursorPositionRef: MutableRefObject<Vector2d>
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isDrawing = useStore($isDrawing);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
const { updateColorUnderCursor } = useColorPicker();
|
||||
|
||||
@@ -36,16 +34,17 @@ const useCanvasMouseMove = (
|
||||
return;
|
||||
}
|
||||
|
||||
setCursorPosition(scaledCursorPosition);
|
||||
$cursorPosition.set(scaledCursorPosition);
|
||||
|
||||
lastCursorPositionRef.current = scaledCursorPosition;
|
||||
const tool = $tool.get();
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
updateColorUnderCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDrawing || tool === 'move' || isStaging) {
|
||||
if (!$isDrawing.get() || tool === 'move' || isStaging) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,11 +55,9 @@ const useCanvasMouseMove = (
|
||||
}, [
|
||||
didMouseMoveRef,
|
||||
dispatch,
|
||||
isDrawing,
|
||||
isStaging,
|
||||
lastCursorPositionRef,
|
||||
stageRef,
|
||||
tool,
|
||||
updateColorUnderCursor,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
$isDrawing,
|
||||
setIsDrawing,
|
||||
setIsMovingStage,
|
||||
$isMovingStage,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
|
||||
@@ -18,12 +18,11 @@ const useCanvasMouseUp = (
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isDrawing = useStore($isDrawing);
|
||||
const tool = useAppSelector((s) => s.canvas.tool);
|
||||
const isStaging = useAppSelector(isStagingSelector);
|
||||
|
||||
return useCallback(() => {
|
||||
if (tool === 'move' || isStaging) {
|
||||
setIsMovingStage(false);
|
||||
if ($tool.get() === 'move' || isStaging) {
|
||||
$isMovingStage.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,8 +45,8 @@ const useCanvasMouseUp = (
|
||||
} else {
|
||||
didMouseMoveRef.current = false;
|
||||
}
|
||||
setIsDrawing(false);
|
||||
}, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef, tool]);
|
||||
$isDrawing.set(false);
|
||||
}, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef]);
|
||||
};
|
||||
|
||||
export default useCanvasMouseUp;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
$canvasBaseLayer,
|
||||
$canvasStage,
|
||||
$tool,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import {
|
||||
commitColorPickerColor,
|
||||
setColorPickerColor,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
getCanvasBaseLayer,
|
||||
getCanvasStage,
|
||||
} from 'features/canvas/util/konvaInstanceProvider';
|
||||
import Konva from 'konva';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useColorPicker = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const stage = getCanvasStage();
|
||||
|
||||
const updateColorUnderCursor = useCallback(() => {
|
||||
const stage = $canvasStage.get();
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
if (!stage || !canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
@@ -47,10 +48,11 @@ const useColorPicker = () => {
|
||||
}
|
||||
|
||||
dispatch(setColorPickerColor({ r, g, b, a }));
|
||||
}, [canvasBaseLayer, dispatch, stage]);
|
||||
}, [dispatch]);
|
||||
|
||||
const commitColorUnderCursor = useCallback(() => {
|
||||
dispatch(commitColorPickerColor());
|
||||
$tool.set('brush');
|
||||
}, [dispatch]);
|
||||
|
||||
return { updateColorUnderCursor, commitColorUnderCursor };
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { CanvasTool } from 'features/canvas/store/canvasTypes';
|
||||
import type Konva from 'konva';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { atom, computed } from 'nanostores';
|
||||
|
||||
export const $cursorPosition = atom<Vector2d | null>(null);
|
||||
export const $tool = atom<CanvasTool>('move');
|
||||
export const $toolStash = atom<CanvasTool | null>(null);
|
||||
export const $isDrawing = atom<boolean>(false);
|
||||
export const $isMouseOverBoundingBox = atom<boolean>(false);
|
||||
export const $isMoveBoundingBoxKeyHeld = atom<boolean>(false);
|
||||
@@ -9,6 +13,7 @@ export const $isMoveStageKeyHeld = atom<boolean>(false);
|
||||
export const $isMovingBoundingBox = atom<boolean>(false);
|
||||
export const $isMovingStage = atom<boolean>(false);
|
||||
export const $isTransformingBoundingBox = atom<boolean>(false);
|
||||
export const $isMouseOverBoundingBoxOutline = atom<boolean>(false);
|
||||
export const $isModifyingBoundingBox = computed(
|
||||
[$isTransformingBoundingBox, $isMovingBoundingBox],
|
||||
(isTransformingBoundingBox, isMovingBoundingBox) =>
|
||||
@@ -25,49 +30,15 @@ export const resetCanvasInteractionState = () => {
|
||||
$isMovingStage.set(false);
|
||||
};
|
||||
|
||||
export const setCursorPosition = (cursorPosition: Vector2d | null) => {
|
||||
$cursorPosition.set(cursorPosition);
|
||||
};
|
||||
|
||||
export const setIsDrawing = (isDrawing: boolean) => {
|
||||
$isDrawing.set(isDrawing);
|
||||
};
|
||||
|
||||
export const setIsMouseOverBoundingBox = (isMouseOverBoundingBox: boolean) => {
|
||||
$isMouseOverBoundingBox.set(isMouseOverBoundingBox);
|
||||
};
|
||||
|
||||
export const setIsMoveBoundingBoxKeyHeld = (
|
||||
isMoveBoundingBoxKeyHeld: boolean
|
||||
) => {
|
||||
$isMoveBoundingBoxKeyHeld.set(isMoveBoundingBoxKeyHeld);
|
||||
};
|
||||
|
||||
export const setIsMoveStageKeyHeld = (isMoveStageKeyHeld: boolean) => {
|
||||
$isMoveStageKeyHeld.set(isMoveStageKeyHeld);
|
||||
};
|
||||
|
||||
export const setIsMovingBoundingBox = (isMovingBoundingBox: boolean) => {
|
||||
$isMovingBoundingBox.set(isMovingBoundingBox);
|
||||
};
|
||||
|
||||
export const setIsMovingStage = (isMovingStage: boolean) => {
|
||||
$isMovingStage.set(isMovingStage);
|
||||
};
|
||||
|
||||
export const setIsTransformingBoundingBox = (
|
||||
isTransformingBoundingBox: boolean
|
||||
) => {
|
||||
$isTransformingBoundingBox.set(isTransformingBoundingBox);
|
||||
};
|
||||
|
||||
export const resetToolInteractionState = () => {
|
||||
setIsTransformingBoundingBox(false);
|
||||
setIsMouseOverBoundingBox(false);
|
||||
setIsMovingBoundingBox(false);
|
||||
setIsMovingStage(false);
|
||||
$isTransformingBoundingBox.set(false);
|
||||
$isMouseOverBoundingBox.set(false);
|
||||
$isMovingBoundingBox.set(false);
|
||||
$isMovingStage.set(false);
|
||||
};
|
||||
|
||||
export const setCanvasInteractionStateMouseOut = () => {
|
||||
setCursorPosition(null);
|
||||
$cursorPosition.set(null);
|
||||
};
|
||||
export const $canvasBaseLayer = atom<Konva.Layer | null>(null);
|
||||
export const $canvasStage = atom<Konva.Stage | null>(null);
|
||||
|
||||
@@ -10,7 +10,7 @@ import calculateScale from 'features/canvas/util/calculateScale';
|
||||
import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
|
||||
import floorCoordinates from 'features/canvas/util/floorCoordinates';
|
||||
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
||||
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
|
||||
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import { modelChanged } from 'features/parameters/store/generationSlice';
|
||||
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
|
||||
@@ -23,7 +23,7 @@ import { clamp, cloneDeep } from 'lodash-es';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { appSocketQueueItemStatusChanged } from 'services/events/actions';
|
||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||
|
||||
import type {
|
||||
BoundingBoxScaleMethod,
|
||||
@@ -53,10 +53,11 @@ export const initialLayerState: CanvasLayerState = {
|
||||
};
|
||||
|
||||
export const initialCanvasState: CanvasState = {
|
||||
_version: 1,
|
||||
boundingBoxCoordinates: { x: 0, y: 0 },
|
||||
boundingBoxDimensions: { width: 512, height: 512 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
|
||||
boundingBoxScaleMethod: 'none',
|
||||
boundingBoxScaleMethod: 'auto',
|
||||
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||
brushSize: 50,
|
||||
colorPickerColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||
@@ -84,7 +85,6 @@ export const initialCanvasState: CanvasState = {
|
||||
stageCoordinates: { x: 0, y: 0 },
|
||||
stageDimensions: { width: 0, height: 0 },
|
||||
stageScale: 1,
|
||||
tool: 'brush',
|
||||
batchIds: [],
|
||||
aspectRatio: {
|
||||
id: '1:1',
|
||||
@@ -99,13 +99,10 @@ const setBoundingBoxDimensionsReducer = (
|
||||
optimalDimension: number
|
||||
) => {
|
||||
const boundingBoxDimensions = payload;
|
||||
const newDimensions = roundDimensionsToMultiple(
|
||||
{
|
||||
...state.boundingBoxDimensions,
|
||||
...boundingBoxDimensions,
|
||||
},
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
);
|
||||
const newDimensions = {
|
||||
...state.boundingBoxDimensions,
|
||||
...boundingBoxDimensions,
|
||||
};
|
||||
state.boundingBoxDimensions = newDimensions;
|
||||
if (state.boundingBoxScaleMethod === 'auto') {
|
||||
const scaledDimensions = getScaledBoundingBoxDimensions(
|
||||
@@ -120,18 +117,9 @@ export const canvasSlice = createSlice({
|
||||
name: 'canvas',
|
||||
initialState: initialCanvasState,
|
||||
reducers: {
|
||||
setTool: (state, action: PayloadAction<CanvasTool>) => {
|
||||
state.tool = action.payload;
|
||||
},
|
||||
setLayer: (state, action: PayloadAction<CanvasLayer>) => {
|
||||
state.layer = action.payload;
|
||||
},
|
||||
toggleTool: (state) => {
|
||||
const currentTool = state.tool;
|
||||
if (currentTool !== 'move') {
|
||||
state.tool = currentTool === 'brush' ? 'eraser' : 'brush';
|
||||
}
|
||||
},
|
||||
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
|
||||
state.maskColor = action.payload;
|
||||
},
|
||||
@@ -377,9 +365,13 @@ export const canvasSlice = createSlice({
|
||||
|
||||
state.futureLayerStates = [];
|
||||
},
|
||||
addLine: (state, action: PayloadAction<number[]>) => {
|
||||
const { tool, layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
|
||||
addLine: (
|
||||
state,
|
||||
action: PayloadAction<{ points: number[]; tool: CanvasTool }>
|
||||
) => {
|
||||
const { layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
|
||||
state;
|
||||
const { points, tool } = action.payload;
|
||||
|
||||
if (tool === 'move' || tool === 'colorPicker') {
|
||||
return;
|
||||
@@ -402,7 +394,7 @@ export const canvasSlice = createSlice({
|
||||
layer,
|
||||
tool,
|
||||
strokeWidth: newStrokeWidth,
|
||||
points: action.payload,
|
||||
points,
|
||||
...newColor,
|
||||
};
|
||||
|
||||
@@ -470,10 +462,31 @@ export const canvasSlice = createSlice({
|
||||
},
|
||||
resetCanvas: (state) => {
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
|
||||
state.layerState = cloneDeep(initialLayerState);
|
||||
state.futureLayerStates = [];
|
||||
state.batchIds = [];
|
||||
state.boundingBoxCoordinates = {
|
||||
...initialCanvasState.boundingBoxCoordinates,
|
||||
};
|
||||
state.boundingBoxDimensions = {
|
||||
...initialCanvasState.boundingBoxDimensions,
|
||||
};
|
||||
state.stageScale = calculateScale(
|
||||
state.stageDimensions.width,
|
||||
state.stageDimensions.height,
|
||||
state.boundingBoxDimensions.width,
|
||||
state.boundingBoxDimensions.height,
|
||||
STAGE_PADDING_PERCENTAGE
|
||||
);
|
||||
state.stageCoordinates = calculateCoordinates(
|
||||
state.stageDimensions.width,
|
||||
state.stageDimensions.height,
|
||||
0,
|
||||
0,
|
||||
state.boundingBoxDimensions.width,
|
||||
state.boundingBoxDimensions.height,
|
||||
1
|
||||
);
|
||||
},
|
||||
canvasResized: (
|
||||
state,
|
||||
@@ -496,32 +509,28 @@ export const canvasSlice = createSlice({
|
||||
stageDimensions: { width: stageWidth, height: stageHeight },
|
||||
} = state;
|
||||
|
||||
const { x, y, width, height } = contentRect;
|
||||
const newScale = shouldScaleTo1
|
||||
? 1
|
||||
: calculateScale(
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
contentRect.width || state.boundingBoxDimensions.width,
|
||||
contentRect.height || state.boundingBoxDimensions.height,
|
||||
STAGE_PADDING_PERCENTAGE
|
||||
);
|
||||
|
||||
if (width !== 0 && height !== 0) {
|
||||
const newScale = shouldScaleTo1
|
||||
? 1
|
||||
: calculateScale(
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
width,
|
||||
height,
|
||||
STAGE_PADDING_PERCENTAGE
|
||||
);
|
||||
const newCoordinates = calculateCoordinates(
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
contentRect.x || state.boundingBoxCoordinates.x,
|
||||
contentRect.y || state.boundingBoxCoordinates.y,
|
||||
contentRect.width || state.boundingBoxDimensions.width,
|
||||
contentRect.height || state.boundingBoxDimensions.height,
|
||||
newScale
|
||||
);
|
||||
|
||||
const newCoordinates = calculateCoordinates(
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
newScale
|
||||
);
|
||||
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = newCoordinates;
|
||||
}
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = newCoordinates;
|
||||
},
|
||||
nextStagingAreaImage: (state) => {
|
||||
if (!state.layerState.stagingArea.images.length) {
|
||||
@@ -665,7 +674,6 @@ export const canvasSlice = createSlice({
|
||||
...state.colorPickerColor,
|
||||
a: state.brushColor.a,
|
||||
};
|
||||
state.tool = 'brush';
|
||||
},
|
||||
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
|
||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
||||
@@ -680,6 +688,12 @@ export const canvasSlice = createSlice({
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(modelChanged, (state, action) => {
|
||||
if (
|
||||
action.meta.previousModel?.base_model === action.payload?.base_model
|
||||
) {
|
||||
// The base model hasn't changed, we don't need to optimize the size
|
||||
return;
|
||||
}
|
||||
const optimalDimension = getOptimalDimension(action.payload);
|
||||
const { width, height } = state.boundingBoxDimensions;
|
||||
if (getIsSizeOptimal(width, height, optimalDimension)) {
|
||||
@@ -695,7 +709,7 @@ export const canvasSlice = createSlice({
|
||||
);
|
||||
});
|
||||
|
||||
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
|
||||
builder.addCase(socketQueueItemStatusChanged, (state, action) => {
|
||||
const batch_status = action.payload.data.batch_status;
|
||||
if (!state.batchIds.includes(batch_status.batch_id)) {
|
||||
return;
|
||||
@@ -766,9 +780,7 @@ export const {
|
||||
setShouldSnapToGrid,
|
||||
setStageCoordinates,
|
||||
setStageScale,
|
||||
setTool,
|
||||
toggleShouldLockBoundingBox,
|
||||
toggleTool,
|
||||
undo,
|
||||
setScaledBoundingBoxDimensions,
|
||||
setShouldRestrictStrokesToBox,
|
||||
@@ -784,3 +796,12 @@ export const {
|
||||
export default canvasSlice.reducer;
|
||||
|
||||
export const selectCanvasSlice = (state: RootState) => state.canvas;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateCanvasState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
state.aspectRatio = initialAspectRatioState;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -117,6 +117,7 @@ export const isCanvasAnyLine = (
|
||||
): obj is CanvasMaskLine | CanvasBaseLine => obj.kind === 'line';
|
||||
|
||||
export interface CanvasState {
|
||||
_version: 1;
|
||||
boundingBoxCoordinates: Vector2d;
|
||||
boundingBoxDimensions: Dimensions;
|
||||
boundingBoxPreviewFill: RgbaColor;
|
||||
@@ -148,7 +149,6 @@ export interface CanvasState {
|
||||
stageCoordinates: Vector2d;
|
||||
stageDimensions: Dimensions;
|
||||
stageScale: number;
|
||||
tool: CanvasTool;
|
||||
generationMode?: GenerationMode;
|
||||
batchIds: string[];
|
||||
aspectRatio: AspectRatioState;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
|
||||
|
||||
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ export const getBaseLayerBlob = async (
|
||||
state: RootState,
|
||||
alwaysUseBoundingBox: boolean = false
|
||||
) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
throw new Error('Problem getting base layer blob');
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import {
|
||||
$canvasBaseLayer,
|
||||
$canvasStage,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import type {
|
||||
CanvasLayerState,
|
||||
Dimensions,
|
||||
} from 'features/canvas/store/canvasTypes';
|
||||
import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
||||
import { konvaNodeToImageData } from 'features/canvas/util/konvaNodeToImageData';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
|
||||
import createMaskStage from './createMaskStage';
|
||||
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
import { konvaNodeToImageData } from './konvaNodeToImageData';
|
||||
|
||||
/**
|
||||
* Gets Blob and ImageData objects for the base and mask layers
|
||||
@@ -23,8 +26,8 @@ export const getCanvasData = async (
|
||||
) => {
|
||||
const log = logger('canvas');
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const canvasStage = getCanvasStage();
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
const canvasStage = $canvasStage.get();
|
||||
|
||||
if (!canvasBaseLayer || !canvasStage) {
|
||||
log.error('Unable to find canvas / stage');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
|
||||
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
/**
|
||||
* Gets the canvas base layer blob, without bounding box
|
||||
*/
|
||||
export const getFullBaseLayerBlob = async () => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type Konva from 'konva';
|
||||
|
||||
let canvasBaseLayer: Konva.Layer | null = null;
|
||||
let canvasStage: Konva.Stage | null = null;
|
||||
|
||||
export const setCanvasBaseLayer = (layer: Konva.Layer) => {
|
||||
canvasBaseLayer = layer;
|
||||
};
|
||||
|
||||
export const getCanvasBaseLayer = () => canvasBaseLayer;
|
||||
|
||||
export const setCanvasStage = (stage: Konva.Stage) => {
|
||||
canvasStage = stage;
|
||||
};
|
||||
|
||||
export const getCanvasStage = () => canvasStage;
|
||||
@@ -16,7 +16,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCopy, FaTrash } from 'react-icons/fa';
|
||||
import { PiCopyBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import ControlAdapterImagePreview from './ControlAdapterImagePreview';
|
||||
@@ -106,7 +106,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
|
||||
tooltip={t('controlnet.duplicate')}
|
||||
aria-label={t('controlnet.duplicate')}
|
||||
onClick={handleDuplicate}
|
||||
icon={<FaCopy />}
|
||||
icon={<PiCopyBold />}
|
||||
/>
|
||||
<InvIconButton
|
||||
size="sm"
|
||||
@@ -114,7 +114,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
|
||||
aria-label={t('controlnet.delete')}
|
||||
colorScheme="error"
|
||||
onClick={handleDelete}
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
<InvIconButton
|
||||
size="sm"
|
||||
@@ -132,8 +132,6 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
|
||||
variant="ghost"
|
||||
icon={
|
||||
<ChevronUpIcon
|
||||
boxSize={4}
|
||||
color="base.300"
|
||||
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="normal"
|
||||
@@ -143,7 +141,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
|
||||
</Flex>
|
||||
|
||||
<Flex w="full" flexDir="column" gap={4}>
|
||||
<Flex gap={4} w="full" alignItems="center">
|
||||
<Flex gap={8} w="full" alignItems="center">
|
||||
<Flex flexDir="column" gap={2} h={32} w="full">
|
||||
<ParamControlAdapterWeight id={id} />
|
||||
<ParamControlAdapterBeginEnd id={id} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from 'features/canvas/store/actions';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImage, FaMask } from 'react-icons/fa';
|
||||
import { PiExcludeBold, PiImageSquareBold } from 'react-icons/pi';
|
||||
|
||||
type ControlNetCanvasImageImportsProps = {
|
||||
id: string;
|
||||
@@ -29,17 +29,17 @@ const ControlNetCanvasImageImports = (
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<Flex gap={4}>
|
||||
<InvIconButton
|
||||
size="sm"
|
||||
icon={<FaImage />}
|
||||
icon={<PiImageSquareBold />}
|
||||
tooltip={t('controlnet.importImageFromCanvas')}
|
||||
aria-label={t('controlnet.importImageFromCanvas')}
|
||||
onClick={handleImportImageFromCanvas}
|
||||
/>
|
||||
<InvIconButton
|
||||
size="sm"
|
||||
icon={<FaMask />}
|
||||
icon={<PiExcludeBold />}
|
||||
tooltip={t('controlnet.importMaskFromCanvas')}
|
||||
aria-label={t('controlnet.importMaskFromCanvas')}
|
||||
onClick={handleImportMaskFromCanvas}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvControlGroup } from 'common/components/InvControl/InvControlGroup';
|
||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||
@@ -17,10 +17,22 @@ type ParamControlAdapterWeightProps = {
|
||||
const formatValue = (v: number) => v.toFixed(2);
|
||||
|
||||
const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isEnabled = useControlAdapterIsEnabled(id);
|
||||
const weight = useControlAdapterWeight(id);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
|
||||
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
|
||||
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
|
||||
const numberInputMin = useAppSelector(
|
||||
(s) => s.config.sd.ca.weight.numberInputMin
|
||||
);
|
||||
const numberInputMax = useAppSelector(
|
||||
(s) => s.config.sd.ca.weight.numberInputMax
|
||||
);
|
||||
const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
|
||||
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
|
||||
|
||||
const onChange = useCallback(
|
||||
(weight: number) => {
|
||||
dispatch(controlAdapterWeightChanged({ id, weight }));
|
||||
@@ -43,23 +55,23 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => {
|
||||
<InvSlider
|
||||
value={weight}
|
||||
onChange={onChange}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
fineStep={0.01}
|
||||
defaultValue={initial}
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
marks={marks}
|
||||
formatValue={formatValue}
|
||||
/>
|
||||
<InvNumberInput
|
||||
value={weight}
|
||||
onChange={onChange}
|
||||
min={-1}
|
||||
max={2}
|
||||
step={0.05}
|
||||
fineStep={0.01}
|
||||
min={numberInputMin}
|
||||
max={numberInputMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
maxW={20}
|
||||
defaultValue={1}
|
||||
defaultValue={initial}
|
||||
/>
|
||||
</InvControl>
|
||||
</InvControlGroup>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ParameterT2IAdapterModel,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { cloneDeep, merge, uniq } from 'lodash-es';
|
||||
import { appSocketInvocationError } from 'services/events/actions';
|
||||
import { socketInvocationError } from 'services/events/actions';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { controlAdapterImageProcessed } from './actions';
|
||||
@@ -51,10 +51,12 @@ export const {
|
||||
selectTotal: selectControlAdapterTotal,
|
||||
} = caAdapterSelectors;
|
||||
|
||||
export const initialControlAdapterState: ControlAdaptersState =
|
||||
export const initialControlAdaptersState: ControlAdaptersState =
|
||||
caAdapter.getInitialState<{
|
||||
_version: 1;
|
||||
pendingControlImages: string[];
|
||||
}>({
|
||||
_version: 1,
|
||||
pendingControlImages: [],
|
||||
});
|
||||
|
||||
@@ -96,7 +98,7 @@ export const selectValidT2IAdapters = (controlAdapters: ControlAdaptersState) =>
|
||||
|
||||
export const controlAdaptersSlice = createSlice({
|
||||
name: 'controlAdapters',
|
||||
initialState: initialControlAdapterState,
|
||||
initialState: initialControlAdaptersState,
|
||||
reducers: {
|
||||
controlAdapterAdded: {
|
||||
reducer: (
|
||||
@@ -267,31 +269,29 @@ export const controlAdaptersSlice = createSlice({
|
||||
|
||||
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = {
|
||||
id,
|
||||
changes: { model },
|
||||
changes: { model, shouldAutoConfig: true },
|
||||
};
|
||||
|
||||
update.changes.processedControlImage = null;
|
||||
|
||||
if (cn.shouldAutoConfig) {
|
||||
let processorType: ControlAdapterProcessorType | undefined = undefined;
|
||||
let processorType: ControlAdapterProcessorType | undefined = undefined;
|
||||
|
||||
for (const modelSubstring in CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS) {
|
||||
if (model.model_name.includes(modelSubstring)) {
|
||||
processorType =
|
||||
CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS[modelSubstring];
|
||||
break;
|
||||
}
|
||||
for (const modelSubstring in CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS) {
|
||||
if (model.model_name.includes(modelSubstring)) {
|
||||
processorType =
|
||||
CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS[modelSubstring];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (processorType) {
|
||||
update.changes.processorType = processorType;
|
||||
update.changes.processorNode = CONTROLNET_PROCESSORS[processorType]
|
||||
.default as RequiredControlAdapterProcessorNode;
|
||||
} else {
|
||||
update.changes.processorType = 'none';
|
||||
update.changes.processorNode = CONTROLNET_PROCESSORS.none
|
||||
.default as RequiredControlAdapterProcessorNode;
|
||||
}
|
||||
if (processorType) {
|
||||
update.changes.processorType = processorType;
|
||||
update.changes.processorNode = CONTROLNET_PROCESSORS[processorType]
|
||||
.default as RequiredControlAdapterProcessorNode;
|
||||
} else {
|
||||
update.changes.processorType = 'none';
|
||||
update.changes.processorNode = CONTROLNET_PROCESSORS.none
|
||||
.default as RequiredControlAdapterProcessorNode;
|
||||
}
|
||||
|
||||
caAdapter.updateOne(state, update);
|
||||
@@ -435,7 +435,7 @@ export const controlAdaptersSlice = createSlice({
|
||||
caAdapter.updateOne(state, update);
|
||||
},
|
||||
controlAdaptersReset: () => {
|
||||
return cloneDeep(initialControlAdapterState);
|
||||
return cloneDeep(initialControlAdaptersState);
|
||||
},
|
||||
pendingControlImagesCleared: (state) => {
|
||||
state.pendingControlImages = [];
|
||||
@@ -454,7 +454,7 @@ export const controlAdaptersSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(appSocketInvocationError, (state) => {
|
||||
builder.addCase(socketInvocationError, (state) => {
|
||||
state.pendingControlImages = [];
|
||||
});
|
||||
},
|
||||
@@ -493,3 +493,11 @@ export const isAnyControlAdapterAdded = isAnyOf(
|
||||
|
||||
export const selectControlAdaptersSlice = (state: RootState) =>
|
||||
state.controlAdapters;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateControlAdaptersState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
type DeleteImageButtonProps = Omit<InvIconButtonProps, 'aria-label'> & {
|
||||
onClick: () => void;
|
||||
@@ -17,7 +17,7 @@ export const DeleteImageButton = memo((props: DeleteImageButtonProps) => {
|
||||
return (
|
||||
<InvIconButton
|
||||
onClick={onClick}
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
tooltip={`${t('gallery.deleteImage')} (Del)`}
|
||||
aria-label={`${t('gallery.deleteImage')} (Del)`}
|
||||
isDisabled={isDisabled || !isConnected}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { Box, Flex, Heading, Image } from '@chakra-ui/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvText } from 'common/components/InvText/wrapper';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import { memo } from 'react';
|
||||
@@ -34,6 +35,7 @@ const multiImageStyles: ChakraProps['sx'] = {
|
||||
|
||||
const DragPreview = (props: OverlayDragImageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const selectionCount = useAppSelector((s) => s.gallery.selection.length);
|
||||
if (!props.dragData) {
|
||||
return null;
|
||||
}
|
||||
@@ -79,10 +81,10 @@ const DragPreview = (props: OverlayDragImageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.dragData.payloadType === 'IMAGE_DTOS') {
|
||||
if (props.dragData.payloadType === 'GALLERY_SELECTION') {
|
||||
return (
|
||||
<Flex sx={multiImageStyles}>
|
||||
<Heading>{props.dragData.payload.imageDTOs.length}</Heading>
|
||||
<Heading>{selectionCount}</Heading>
|
||||
<Heading size="sm">{t('parameters.images')}</Heading>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
useDroppable as useOriginalDroppable,
|
||||
UseDroppableArguments,
|
||||
} from '@dnd-kit/core';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import type {
|
||||
FieldInputInstance,
|
||||
FieldInputTemplate,
|
||||
@@ -51,15 +52,6 @@ export type NodesImageDropData = BaseDropData & {
|
||||
};
|
||||
};
|
||||
|
||||
export type NodesMultiImageDropData = BaseDropData & {
|
||||
actionType: 'SET_MULTI_NODES_IMAGE';
|
||||
context: { nodeId: string; fieldName: string };
|
||||
};
|
||||
|
||||
export type AddToBatchDropData = BaseDropData & {
|
||||
actionType: 'ADD_TO_BATCH';
|
||||
};
|
||||
|
||||
export type AddToBoardDropData = BaseDropData & {
|
||||
actionType: 'ADD_TO_BOARD';
|
||||
context: { boardId: string };
|
||||
@@ -69,21 +61,14 @@ export type RemoveFromBoardDropData = BaseDropData & {
|
||||
actionType: 'REMOVE_FROM_BOARD';
|
||||
};
|
||||
|
||||
export type AddFieldToLinearViewDropData = BaseDropData & {
|
||||
actionType: 'ADD_FIELD_TO_LINEAR';
|
||||
};
|
||||
|
||||
export type TypesafeDroppableData =
|
||||
| CurrentImageDropData
|
||||
| InitialImageDropData
|
||||
| ControlAdapterDropData
|
||||
| CanvasInitialImageDropData
|
||||
| NodesImageDropData
|
||||
| AddToBatchDropData
|
||||
| NodesMultiImageDropData
|
||||
| AddToBoardDropData
|
||||
| RemoveFromBoardDropData
|
||||
| AddFieldToLinearViewDropData;
|
||||
| RemoveFromBoardDropData;
|
||||
|
||||
type BaseDragData = {
|
||||
id: string;
|
||||
@@ -103,15 +88,15 @@ export type ImageDraggableData = BaseDragData & {
|
||||
payload: { imageDTO: ImageDTO };
|
||||
};
|
||||
|
||||
export type ImageDTOsDraggableData = BaseDragData & {
|
||||
payloadType: 'IMAGE_DTOS';
|
||||
payload: { imageDTOs: ImageDTO[] };
|
||||
export type GallerySelectionDraggableData = BaseDragData & {
|
||||
payloadType: 'GALLERY_SELECTION';
|
||||
payload: { boardId: BoardId };
|
||||
};
|
||||
|
||||
export type TypesafeDraggableData =
|
||||
| NodeFieldDraggableData
|
||||
| ImageDraggableData
|
||||
| ImageDTOsDraggableData;
|
||||
| GallerySelectionDraggableData;
|
||||
|
||||
export interface UseDroppableTypesafeArguments
|
||||
extends Omit<UseDroppableArguments, 'data'> {
|
||||
|
||||
@@ -16,8 +16,6 @@ export const isValidDrop = (
|
||||
}
|
||||
|
||||
switch (actionType) {
|
||||
case 'ADD_FIELD_TO_LINEAR':
|
||||
return payloadType === 'NODE_FIELD';
|
||||
case 'SET_CURRENT_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_INITIAL_IMAGE':
|
||||
@@ -28,15 +26,13 @@ export const isValidDrop = (
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_MULTI_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||
case 'ADD_TO_BATCH':
|
||||
return payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||
case 'ADD_TO_BOARD': {
|
||||
// If the board is the same, don't allow the drop
|
||||
|
||||
// Check the payload types
|
||||
const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||
const isPayloadValid = ['IMAGE_DTO', 'GALLERY_SELECTION'].includes(
|
||||
payloadType
|
||||
);
|
||||
if (!isPayloadValid) {
|
||||
return false;
|
||||
}
|
||||
@@ -50,12 +46,10 @@ export const isValidDrop = (
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
|
||||
if (payloadType === 'IMAGE_DTOS') {
|
||||
if (payloadType === 'GALLERY_SELECTION') {
|
||||
// Assume all images are on the same board - this is true for the moment
|
||||
const { imageDTOs } = active.data.current.payload;
|
||||
const currentBoard = imageDTOs[0]?.board_id ?? 'none';
|
||||
const currentBoard = active.data.current.payload.boardId;
|
||||
const destinationBoard = overData.context.boardId;
|
||||
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
|
||||
@@ -65,7 +59,9 @@ export const isValidDrop = (
|
||||
// If the board is the same, don't allow the drop
|
||||
|
||||
// Check the payload types
|
||||
const isPayloadValid = payloadType === 'IMAGE_DTO' || 'IMAGE_DTOS';
|
||||
const isPayloadValid = ['IMAGE_DTO', 'GALLERY_SELECTION'].includes(
|
||||
payloadType
|
||||
);
|
||||
if (!isPayloadValid) {
|
||||
return false;
|
||||
}
|
||||
@@ -78,11 +74,8 @@ export const isValidDrop = (
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
if (payloadType === 'IMAGE_DTOS') {
|
||||
// Assume all images are on the same board - this is true for the moment
|
||||
const { imageDTOs } = active.data.current.payload;
|
||||
const currentBoard = imageDTOs[0]?.board_id ?? 'none';
|
||||
|
||||
if (payloadType === 'GALLERY_SELECTION') {
|
||||
const currentBoard = active.data.current.payload.boardId;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,17 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ParamDynamicPromptsMaxPrompts = () => {
|
||||
const maxPrompts = useAppSelector((s) => s.dynamicPrompts.maxPrompts);
|
||||
const min = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.min);
|
||||
const sliderMin = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.sliderMin
|
||||
);
|
||||
const sliderMax = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.sliderMax
|
||||
);
|
||||
const inputMax = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.inputMax
|
||||
const numberInputMin = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.numberInputMin
|
||||
);
|
||||
const numberInputMax = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.numberInputMax
|
||||
);
|
||||
const initial = useAppSelector(
|
||||
(s) => s.config.sd.dynamicPrompts.maxPrompts.initial
|
||||
@@ -36,14 +41,15 @@ const ParamDynamicPromptsMaxPrompts = () => {
|
||||
renderInfoPopoverInPortal={false}
|
||||
>
|
||||
<InvSlider
|
||||
min={min}
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
value={maxPrompts}
|
||||
defaultValue={initial}
|
||||
onChange={handleChange}
|
||||
marks
|
||||
withNumberInput
|
||||
numberInputMax={inputMax}
|
||||
numberInputMin={numberInputMin}
|
||||
numberInputMax={numberInputMax}
|
||||
/>
|
||||
</InvControl>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,9 @@ export const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']);
|
||||
export type SeedBehaviour = z.infer<typeof zSeedBehaviour>;
|
||||
export const isSeedBehaviour = (v: unknown): v is SeedBehaviour =>
|
||||
zSeedBehaviour.safeParse(v).success;
|
||||
|
||||
export interface DynamicPromptsState {
|
||||
_version: 1;
|
||||
maxPrompts: number;
|
||||
combinatorial: boolean;
|
||||
prompts: string[];
|
||||
@@ -18,6 +20,7 @@ export interface DynamicPromptsState {
|
||||
}
|
||||
|
||||
export const initialDynamicPromptsState: DynamicPromptsState = {
|
||||
_version: 1,
|
||||
maxPrompts: 100,
|
||||
combinatorial: true,
|
||||
prompts: [],
|
||||
@@ -78,3 +81,11 @@ export default dynamicPromptsSlice.reducer;
|
||||
|
||||
export const selectDynamicPromptsSlice = (state: RootState) =>
|
||||
state.dynamicPrompts;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateDynamicPromptsState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSelect } from 'common/components/InvSelect/InvSelect';
|
||||
import { InvSelectFallback } from 'common/components/InvSelect/InvSelectFallback';
|
||||
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
|
||||
import type { EmbeddingSelectProps } from 'features/embedding/types';
|
||||
import { t } from 'i18next';
|
||||
@@ -47,23 +46,16 @@ export const EmbeddingSelect = memo(
|
||||
onChange: _onChange,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <InvSelectFallback label={t('common.loading')} />;
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return <InvSelectFallback label={t('embedding.noEmbeddingsLoaded')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InvControl isDisabled={!options.length}>
|
||||
<InvControl>
|
||||
<InvSelect
|
||||
placeholder={t('embedding.addEmbedding')}
|
||||
placeholder={
|
||||
isLoading ? t('common.loading') : t('embedding.addEmbedding')
|
||||
}
|
||||
defaultMenuIsOpen
|
||||
autoFocus
|
||||
value={null}
|
||||
options={options}
|
||||
isDisabled={!options.length}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
onChange={onChange}
|
||||
onMenuClose={onClose}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaPlus } from 'react-icons/fa';
|
||||
@@ -90,13 +89,9 @@ const BoardContextMenu = ({
|
||||
}
|
||||
}, [t, board_id, bulkDownload, dispatch]);
|
||||
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const renderMenuFunc = useCallback(
|
||||
() => (
|
||||
<InvMenuList visibility="visible" onContextMenu={skipEvent}>
|
||||
<InvMenuList visibility="visible">
|
||||
<InvMenuGroup title={boardName}>
|
||||
<InvMenuItem
|
||||
icon={<FaPlus />}
|
||||
@@ -131,7 +126,6 @@ const BoardContextMenu = ({
|
||||
isBulkDownloadEnabled,
|
||||
isSelectedForAutoAdd,
|
||||
setBoardToDelete,
|
||||
skipEvent,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
|
||||
|
||||
const AddBoardButton = () => {
|
||||
@@ -14,7 +14,7 @@ const AddBoardButton = () => {
|
||||
|
||||
return (
|
||||
<InvIconButton
|
||||
icon={<FaPlus />}
|
||||
icon={<PiPlusBold />}
|
||||
isLoading={isLoading}
|
||||
tooltip={t('boards.addBoard')}
|
||||
aria-label={t('boards.addBoard')}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user