mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 00:08:00 -05:00
Compare commits
25 Commits
ryan/seaml
...
v3.6.0rc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
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',
|
||||
];
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 |
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,13 +26,13 @@ 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 IAICanvasToolChooserOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -198,7 +198,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 +206,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 +214,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 +238,7 @@ const IAICanvasToolChooserOptions = () => {
|
||||
<InvIconButton
|
||||
aria-label={t('unifiedCanvas.brushOptions')}
|
||||
tooltip={t('unifiedCanvas.brushOptions')}
|
||||
icon={<FaSlidersH />}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
/>
|
||||
</InvPopoverTrigger>
|
||||
<InvPopoverContent>
|
||||
|
||||
@@ -30,15 +30,15 @@ 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';
|
||||
@@ -217,14 +217,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 +233,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 +248,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 +256,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 +270,7 @@ const IAICanvasToolbar = () => {
|
||||
<InvIconButton
|
||||
aria-label={`${t('common.upload')}`}
|
||||
tooltip={`${t('common.upload')}`}
|
||||
icon={<FaUpload />}
|
||||
icon={<PiUploadSimpleBold />}
|
||||
isDisabled={isStaging}
|
||||
{...getUploadButtonProps()}
|
||||
/>
|
||||
@@ -278,7 +278,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}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 +24,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 +54,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 },
|
||||
@@ -695,7 +697,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;
|
||||
@@ -784,3 +786,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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
@@ -6,6 +5,7 @@ import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
const BoardsSearch = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -66,7 +66,7 @@ const BoardsSearch = () => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={t('boards.clearSearch')}
|
||||
icon={<CloseIcon boxSize={3} />}
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import InvokeAILogoImage from 'assets/images/logo.png';
|
||||
import InvokeLogoSVG from 'assets/images/invoke-key-wht-lrg.svg';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { InvText } from 'common/components/InvText/wrapper';
|
||||
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
|
||||
@@ -101,10 +101,10 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
alignItems="center"
|
||||
>
|
||||
<Image
|
||||
src={InvokeAILogoImage}
|
||||
src={InvokeLogoSVG}
|
||||
alt="invoke-ai-logo"
|
||||
opacity={0.4}
|
||||
filter="grayscale(1)"
|
||||
opacity={0.7}
|
||||
mixBlendMode="overlay"
|
||||
mt={-6}
|
||||
w={16}
|
||||
h={16}
|
||||
|
||||
@@ -151,7 +151,7 @@ const DeleteBoardModal = (props: Props) => {
|
||||
</Flex>
|
||||
</InvAlertDialogBody>
|
||||
<InvAlertDialogFooter>
|
||||
<Flex w="full" gap={2} justifyContent="space-between">
|
||||
<Flex w="full" gap={2} justifyContent="end">
|
||||
<InvButton ref={cancelRef} onClick={handleClose}>
|
||||
{t('boards.cancel')}
|
||||
</InvButton>
|
||||
|
||||
@@ -29,14 +29,15 @@ import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaAsterisk,
|
||||
FaCode,
|
||||
FaHourglassHalf,
|
||||
FaQuoteRight,
|
||||
FaRulerVertical,
|
||||
FaSeedling,
|
||||
} from 'react-icons/fa';
|
||||
import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6';
|
||||
PiAsteriskBold,
|
||||
PiDotsThreeOutlineFill,
|
||||
PiFlowArrowBold,
|
||||
PiHourglassHighBold,
|
||||
PiInfoBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
@@ -210,7 +211,7 @@ const CurrentImageButtons = () => {
|
||||
aria-label={t('parameters.imageActions')}
|
||||
tooltip={t('parameters.imageActions')}
|
||||
isDisabled={!imageDTO}
|
||||
icon={<FaEllipsis />}
|
||||
icon={<PiDotsThreeOutlineFill />}
|
||||
/>
|
||||
<InvMenuList>
|
||||
{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}
|
||||
@@ -220,7 +221,7 @@ const CurrentImageButtons = () => {
|
||||
|
||||
<InvButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<InvIconButton
|
||||
icon={<FaCircleNodes />}
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!imageDTO?.has_workflow}
|
||||
@@ -229,7 +230,7 @@ const CurrentImageButtons = () => {
|
||||
/>
|
||||
<InvIconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<FaQuoteRight />}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!metadata?.positive_prompt}
|
||||
@@ -237,7 +238,7 @@ const CurrentImageButtons = () => {
|
||||
/>
|
||||
<InvIconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<FaSeedling />}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={metadata?.seed === null || metadata?.seed === undefined}
|
||||
@@ -245,7 +246,7 @@ const CurrentImageButtons = () => {
|
||||
/>
|
||||
<InvIconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<FaRulerVertical />}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
isDisabled={
|
||||
@@ -258,7 +259,7 @@ const CurrentImageButtons = () => {
|
||||
/>
|
||||
<InvIconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<FaAsterisk />}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!metadata}
|
||||
@@ -274,7 +275,7 @@ const CurrentImageButtons = () => {
|
||||
|
||||
<InvButtonGroup>
|
||||
<InvIconButton
|
||||
icon={<FaCode />}
|
||||
icon={<PiInfoBold />}
|
||||
tooltip={`${t('parameters.info')} (I)`}
|
||||
aria-label={`${t('parameters.info')} (I)`}
|
||||
isChecked={shouldShowImageDetails}
|
||||
@@ -286,7 +287,7 @@ const CurrentImageButtons = () => {
|
||||
<InvIconButton
|
||||
aria-label={t('settings.displayInProgress')}
|
||||
tooltip={t('settings.displayInProgress')}
|
||||
icon={<FaHourglassHalf />}
|
||||
icon={<PiHourglassHighBold />}
|
||||
isChecked={shouldShowProgressInViewer}
|
||||
onClick={handleClickProgressImagesToggle}
|
||||
/>
|
||||
|
||||
@@ -11,13 +11,11 @@ import type {
|
||||
import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
@@ -39,43 +37,6 @@ const CurrentImagePreview = () => {
|
||||
(s) => s.ui.shouldShowProgressInViewer
|
||||
);
|
||||
|
||||
const {
|
||||
handlePrevImage,
|
||||
handleNextImage,
|
||||
isOnLastImage,
|
||||
handleLoadMoreImages,
|
||||
areMoreImagesAvailable,
|
||||
isFetching,
|
||||
} = useNextPrevImage();
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
handlePrevImage();
|
||||
},
|
||||
[handlePrevImage]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
|
||||
handleLoadMoreImages();
|
||||
return;
|
||||
}
|
||||
if (!isOnLastImage) {
|
||||
handleNextImage();
|
||||
}
|
||||
},
|
||||
[
|
||||
isOnLastImage,
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
isFetching,
|
||||
handleNextImage,
|
||||
]
|
||||
);
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
|
||||
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaWrench } from 'react-icons/fa';
|
||||
import { RiSettings4Fill } from 'react-icons/ri';
|
||||
|
||||
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
||||
|
||||
@@ -61,7 +61,7 @@ const GallerySettingsPopover = () => {
|
||||
tooltip={t('gallery.gallerySettings')}
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
size="sm"
|
||||
icon={<FaWrench />}
|
||||
icon={<RiSettings4Fill />}
|
||||
/>
|
||||
</InvPopoverTrigger>
|
||||
<InvPopoverContent>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvMenuItem } from 'common/components/InvMenu/InvMenuItem';
|
||||
import { InvMenuDivider } from 'common/components/InvMenu/wrapper';
|
||||
import {
|
||||
imagesToChangeSelected,
|
||||
isModalOpenChanged,
|
||||
@@ -11,8 +12,13 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaFolder, FaTrash } from 'react-icons/fa';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
import {
|
||||
PiDownloadSimpleBold,
|
||||
PiFoldersBold,
|
||||
PiStarBold,
|
||||
PiStarFill,
|
||||
PiTrashSimpleBold,
|
||||
} from 'react-icons/pi';
|
||||
import {
|
||||
useBulkDownloadImagesMutation,
|
||||
useStarImagesMutation,
|
||||
@@ -90,7 +96,7 @@ const MultipleSelectionMenuItems = () => {
|
||||
<>
|
||||
{areAllStarred && (
|
||||
<InvMenuItem
|
||||
icon={customStarUi ? customStarUi.on.icon : <MdStarBorder />}
|
||||
icon={customStarUi ? customStarUi.on.icon : <PiStarBold />}
|
||||
onClickCapture={handleUnstarSelection}
|
||||
>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
@@ -98,23 +104,27 @@ const MultipleSelectionMenuItems = () => {
|
||||
)}
|
||||
{(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && (
|
||||
<InvMenuItem
|
||||
icon={customStarUi ? customStarUi.on.icon : <MdStar />}
|
||||
icon={customStarUi ? customStarUi.on.icon : <PiStarFill />}
|
||||
onClickCapture={handleStarSelection}
|
||||
>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</InvMenuItem>
|
||||
)}
|
||||
{isBulkDownloadEnabled && (
|
||||
<InvMenuItem icon={<FaDownload />} onClickCapture={handleBulkDownload}>
|
||||
<InvMenuItem
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
onClickCapture={handleBulkDownload}
|
||||
>
|
||||
{t('gallery.downloadSelection')}
|
||||
</InvMenuItem>
|
||||
)}
|
||||
<InvMenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}>
|
||||
<InvMenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
|
||||
{t('boards.changeBoard')}
|
||||
</InvMenuItem>
|
||||
<InvMenuDivider />
|
||||
<InvMenuItem
|
||||
color="error.300"
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClickCapture={handleDeleteSelection}
|
||||
>
|
||||
{t('gallery.deleteSelection')}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAppToaster } from 'app/components/Toaster';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvMenuItem } from 'common/components/InvMenu/InvMenuItem';
|
||||
import { InvMenuDivider } from 'common/components/InvMenu/wrapper';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
@@ -25,18 +26,18 @@ import { memo, useCallback } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaAsterisk,
|
||||
FaCopy,
|
||||
FaDownload,
|
||||
FaExternalLinkAlt,
|
||||
FaFolder,
|
||||
FaQuoteRight,
|
||||
FaSeedling,
|
||||
FaShare,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa';
|
||||
import { FaCircleNodes } from 'react-icons/fa6';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
PiAsteriskBold,
|
||||
PiCopyBold,
|
||||
PiDownloadSimpleBold,
|
||||
PiFlowArrowBold,
|
||||
PiFoldersBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiShareFatBold,
|
||||
PiStarBold,
|
||||
PiStarFill,
|
||||
PiTrashSimpleBold,
|
||||
} from 'react-icons/pi';
|
||||
import {
|
||||
useStarImagesMutation,
|
||||
useUnstarImagesMutation,
|
||||
@@ -155,12 +156,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
as="a"
|
||||
href={imageDTO.image_url}
|
||||
target="_blank"
|
||||
icon={<FaExternalLinkAlt />}
|
||||
icon={<PiShareFatBold />}
|
||||
>
|
||||
{t('common.openInNewTab')}
|
||||
</InvMenuItem>
|
||||
{isClipboardAPIAvailable && (
|
||||
<InvMenuItem icon={<FaCopy />} onClickCapture={handleCopyImage}>
|
||||
<InvMenuItem icon={<PiCopyBold />} onClickCapture={handleCopyImage}>
|
||||
{t('parameters.copyImage')}
|
||||
</InvMenuItem>
|
||||
)}
|
||||
@@ -169,17 +170,18 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
download={true}
|
||||
href={imageDTO.image_url}
|
||||
target="_blank"
|
||||
icon={<FaDownload />}
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
w="100%"
|
||||
>
|
||||
{t('parameters.downloadImage')}
|
||||
</InvMenuItem>
|
||||
<InvMenuDivider />
|
||||
<InvMenuItem
|
||||
icon={
|
||||
getAndLoadEmbeddedWorkflowResult.isLoading ? (
|
||||
<SpinnerIcon />
|
||||
) : (
|
||||
<FaCircleNodes />
|
||||
<PiFlowArrowBold />
|
||||
)
|
||||
}
|
||||
onClickCapture={handleLoadWorkflow}
|
||||
@@ -188,7 +190,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
{t('nodes.loadWorkflow')}
|
||||
</InvMenuItem>
|
||||
<InvMenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <FaQuoteRight />}
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
|
||||
onClickCapture={handleRecallPrompt}
|
||||
isDisabled={
|
||||
isLoadingMetadata ||
|
||||
@@ -199,21 +201,22 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
{t('parameters.usePrompt')}
|
||||
</InvMenuItem>
|
||||
<InvMenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <FaSeedling />}
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
|
||||
onClickCapture={handleRecallSeed}
|
||||
isDisabled={isLoadingMetadata || metadata?.seed === undefined}
|
||||
>
|
||||
{t('parameters.useSeed')}
|
||||
</InvMenuItem>
|
||||
<InvMenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <FaAsterisk />}
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
|
||||
onClickCapture={handleUseAllParameters}
|
||||
isDisabled={isLoadingMetadata || !metadata}
|
||||
>
|
||||
{t('parameters.useAll')}
|
||||
</InvMenuItem>
|
||||
<InvMenuDivider />
|
||||
<InvMenuItem
|
||||
icon={<FaShare />}
|
||||
icon={<PiShareFatBold />}
|
||||
onClickCapture={handleSendToImageToImage}
|
||||
id="send-to-img2img"
|
||||
>
|
||||
@@ -221,34 +224,36 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
</InvMenuItem>
|
||||
{isCanvasEnabled && (
|
||||
<InvMenuItem
|
||||
icon={<FaShare />}
|
||||
icon={<PiShareFatBold />}
|
||||
onClickCapture={handleSendToCanvas}
|
||||
id="send-to-canvas"
|
||||
>
|
||||
{t('parameters.sendToUnifiedCanvas')}
|
||||
</InvMenuItem>
|
||||
)}
|
||||
<InvMenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}>
|
||||
<InvMenuDivider />
|
||||
<InvMenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
|
||||
{t('boards.changeBoard')}
|
||||
</InvMenuItem>
|
||||
{imageDTO.starred ? (
|
||||
<InvMenuItem
|
||||
icon={customStarUi ? customStarUi.off.icon : <MdStar />}
|
||||
icon={customStarUi ? customStarUi.off.icon : <PiStarFill />}
|
||||
onClickCapture={handleUnstarImage}
|
||||
>
|
||||
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
|
||||
</InvMenuItem>
|
||||
) : (
|
||||
<InvMenuItem
|
||||
icon={customStarUi ? customStarUi.on.icon : <MdStarBorder />}
|
||||
icon={customStarUi ? customStarUi.on.icon : <PiStarBold />}
|
||||
onClickCapture={handleStarImage}
|
||||
>
|
||||
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
|
||||
</InvMenuItem>
|
||||
)}
|
||||
<InvMenuDivider />
|
||||
<InvMenuItem
|
||||
color="error.300"
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClickCapture={handleDelete}
|
||||
>
|
||||
{t('gallery.deleteImage')}
|
||||
|
||||
@@ -15,7 +15,8 @@ import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup'
|
||||
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaImages, FaServer } from 'react-icons/fa';
|
||||
import { PiImagesBold } from 'react-icons/pi';
|
||||
import { RiServerLine } from 'react-icons/ri';
|
||||
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
@@ -83,7 +84,7 @@ const ImageGalleryContent = () => {
|
||||
isChecked={galleryView === 'images'}
|
||||
onClick={handleClickImages}
|
||||
w="full"
|
||||
leftIcon={<FaImages />}
|
||||
leftIcon={<PiImagesBold size="16px" />}
|
||||
data-testid="images-tab"
|
||||
>
|
||||
{t('parameters.images')}
|
||||
@@ -94,7 +95,7 @@ const ImageGalleryContent = () => {
|
||||
isChecked={galleryView === 'assets'}
|
||||
onClick={handleClickAssets}
|
||||
w="full"
|
||||
leftIcon={<FaServer />}
|
||||
leftIcon={<RiServerLine size="16px" />}
|
||||
data-testid="assets-tab"
|
||||
>
|
||||
{t('gallery.assets')}
|
||||
|
||||
@@ -13,14 +13,13 @@ import type {
|
||||
ImageDTOsDraggableData,
|
||||
TypesafeDraggableData,
|
||||
} from 'features/dnd/types';
|
||||
import type { VirtuosoGalleryContext } from 'features/gallery/components/ImageGrid/types';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||
import { useScrollToVisible } from 'features/gallery/hooks/useScrollToVisible';
|
||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
import { PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi';
|
||||
import {
|
||||
useGetImageDTOQuery,
|
||||
useStarImagesMutation,
|
||||
@@ -35,12 +34,11 @@ const imageIconStyleOverrides: SystemStyleObject = {
|
||||
interface HoverableImageProps {
|
||||
imageName: string;
|
||||
index: number;
|
||||
virtuosoContext: VirtuosoGalleryContext;
|
||||
}
|
||||
|
||||
const GalleryImage = (props: HoverableImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { imageName, virtuosoContext } = props;
|
||||
const { imageName } = props;
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
|
||||
const shift = useStore($shift);
|
||||
const { t } = useTranslation();
|
||||
@@ -50,11 +48,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
|
||||
const customStarUi = useStore($customStarUI);
|
||||
|
||||
const imageContainerRef = useScrollToVisible(
|
||||
const imageContainerRef = useScrollIntoView(
|
||||
isSelected,
|
||||
props.index,
|
||||
selectionCount,
|
||||
virtuosoContext
|
||||
selectionCount
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@@ -114,10 +111,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
|
||||
const starIcon = useMemo(() => {
|
||||
if (imageDTO?.starred) {
|
||||
return customStarUi ? customStarUi.on.icon : <MdStar size="20" />;
|
||||
return customStarUi ? customStarUi.on.icon : <PiStarFill size="20" />;
|
||||
}
|
||||
if (!imageDTO?.starred && isHovered) {
|
||||
return customStarUi ? customStarUi.off.icon : <MdStarBorder size="20" />;
|
||||
return customStarUi ? customStarUi.off.icon : <PiStarBold size="20" />;
|
||||
}
|
||||
}, [imageDTO?.starred, isHovered, customStarUi]);
|
||||
|
||||
@@ -131,12 +128,22 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
return '';
|
||||
}, [imageDTO?.starred, customStarUi]);
|
||||
|
||||
const dataTestId = useMemo(
|
||||
() => getGalleryImageDataTestId(imageDTO?.image_name),
|
||||
[imageDTO?.image_name]
|
||||
);
|
||||
|
||||
if (!imageDTO) {
|
||||
return <IAIFillSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" data-testid={`image-${imageDTO.image_name}`}>
|
||||
<Box
|
||||
w="full"
|
||||
h="full"
|
||||
className="gallerygrid-image"
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<Flex
|
||||
ref={imageContainerRef}
|
||||
userSelect="none"
|
||||
@@ -169,7 +176,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
||||
{isHovered && shift && (
|
||||
<IAIDndImageIcon
|
||||
onClick={handleDelete}
|
||||
icon={<FaTrash />}
|
||||
icon={<PiTrashSimpleFill size="16px" />}
|
||||
tooltip={t('gallery.deleteImage')}
|
||||
styleOverrides={imageIconStyleOverrides}
|
||||
/>
|
||||
|
||||
@@ -4,13 +4,12 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { InvButton } from 'common/components/InvButton/InvButton';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import type { VirtuosoGalleryContext } from 'features/gallery/components/ImageGrid/types';
|
||||
import { $useNextPrevImageState } from 'features/gallery/hooks/useNextPrevImage';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { IMAGE_LIMIT } from 'features/gallery/store/types';
|
||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamationCircle, FaImage } from 'react-icons/fa';
|
||||
import type {
|
||||
@@ -20,10 +19,6 @@ import type {
|
||||
VirtuosoGridHandle,
|
||||
} from 'react-virtuoso';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import {
|
||||
useLazyListImagesQuery,
|
||||
useListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
|
||||
|
||||
import GalleryImage from './GalleryImage';
|
||||
@@ -46,56 +41,24 @@ const GalleryImageGrid = () => {
|
||||
);
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const { currentViewTotal } = useBoardTotal(selectedBoardId);
|
||||
const queryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||
|
||||
const virtuosoRangeRef = useRef<ListRange | null>(null);
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoGridHandle>(null);
|
||||
|
||||
const { currentData, isFetching, isSuccess, isError } =
|
||||
useListImagesQuery(queryArgs);
|
||||
|
||||
const [listImages] = useLazyListImagesQuery();
|
||||
|
||||
const areMoreAvailable = useMemo(() => {
|
||||
if (!currentData || !currentViewTotal) {
|
||||
return false;
|
||||
}
|
||||
return currentData.ids.length < currentViewTotal;
|
||||
}, [currentData, currentViewTotal]);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
if (!areMoreAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
listImages({
|
||||
...queryArgs,
|
||||
offset: currentData?.ids.length ?? 0,
|
||||
limit: IMAGE_LIMIT,
|
||||
});
|
||||
}, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
|
||||
|
||||
const virtuosoContext = useMemo<VirtuosoGalleryContext>(() => {
|
||||
return {
|
||||
virtuosoRef,
|
||||
rootRef,
|
||||
virtuosoRangeRef,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const itemContentFunc: ItemContent<EntityId, VirtuosoGalleryContext> =
|
||||
useCallback(
|
||||
(index, imageName, virtuosoContext) => (
|
||||
<GalleryImage
|
||||
key={imageName}
|
||||
index={index}
|
||||
imageName={imageName as string}
|
||||
virtuosoContext={virtuosoContext}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
const {
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
queryResult: { currentData, isFetching, isSuccess, isError },
|
||||
} = useGalleryImages();
|
||||
useGalleryHotkeys();
|
||||
const itemContentFunc: ItemContent<EntityId, void> = useCallback(
|
||||
(index, imageName) => (
|
||||
<GalleryImage
|
||||
key={imageName}
|
||||
index={index}
|
||||
imageName={imageName as string}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize the gallery's custom scrollbar
|
||||
@@ -116,8 +79,10 @@ const GalleryImageGrid = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
$useNextPrevImageState.setKey('virtuosoRef', virtuosoRef);
|
||||
$useNextPrevImageState.setKey('virtuosoRangeRef', virtuosoRangeRef);
|
||||
virtuosoGridRefs.set({ rootRef, virtuosoRangeRef, virtuosoRef });
|
||||
return () => {
|
||||
virtuosoGridRefs.set({});
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!currentData) {
|
||||
@@ -142,7 +107,7 @@ const GalleryImageGrid = () => {
|
||||
if (isSuccess && currentData) {
|
||||
return (
|
||||
<>
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%" id="gallery-grid">
|
||||
<VirtuosoGrid
|
||||
style={virtuosoStyles}
|
||||
data={currentData.ids}
|
||||
@@ -152,13 +117,12 @@ const GalleryImageGrid = () => {
|
||||
itemContent={itemContentFunc}
|
||||
ref={virtuosoRef}
|
||||
rangeChanged={onRangeChanged}
|
||||
context={virtuosoContext}
|
||||
overscan={10}
|
||||
/>
|
||||
</Box>
|
||||
<InvButton
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreAvailable}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
isLoading={isFetching}
|
||||
loadingText={t('gallery.loading')}
|
||||
flexShrink={0}
|
||||
|
||||
@@ -3,13 +3,15 @@ import { Box, forwardRef } from '@chakra-ui/react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const imageItemContainerTestId = 'image-item-container';
|
||||
|
||||
type ItemContainerProps = PropsWithChildren & FlexProps;
|
||||
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
|
||||
<Box
|
||||
className="item-container"
|
||||
ref={ref}
|
||||
p={1.5}
|
||||
data-testid="image-item-container"
|
||||
data-testid={imageItemContainerTestId}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const imageListContainerTestId = 'image-list-container';
|
||||
|
||||
type ListContainerProps = PropsWithChildren & FlexProps;
|
||||
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
const galleryImageMinimumWidth = useAppSelector(
|
||||
@@ -16,7 +18,7 @@ const ListContainer = forwardRef((props: ListContainerProps, ref) => {
|
||||
className="list-container"
|
||||
ref={ref}
|
||||
gridTemplateColumns={`repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr))`}
|
||||
data-testid="image-list-container"
|
||||
data-testid={imageListContainerTestId}
|
||||
>
|
||||
{props.children}
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const getGalleryImageDataTestId = (imageName?: string) =>
|
||||
`gallery-image-${imageName}`;
|
||||
@@ -1,8 +1,11 @@
|
||||
import { atom } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import type { ListRange, VirtuosoGridHandle } from 'react-virtuoso';
|
||||
|
||||
export type VirtuosoGalleryContext = {
|
||||
virtuosoRef: RefObject<VirtuosoGridHandle>;
|
||||
rootRef: RefObject<HTMLDivElement>;
|
||||
virtuosoRangeRef: RefObject<ListRange>;
|
||||
export type VirtuosoGridRefs = {
|
||||
virtuosoRef?: RefObject<VirtuosoGridHandle>;
|
||||
rootRef?: RefObject<HTMLDivElement>;
|
||||
virtuosoRangeRef?: RefObject<ListRange>;
|
||||
};
|
||||
|
||||
export const virtuosoGridRefs = atom<VirtuosoGridRefs>({});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { Box, Flex, Spinner } from '@chakra-ui/react';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
@@ -14,15 +15,14 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
|
||||
const NextPrevImageButtons = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { handleLeftImage, handleRightImage, isOnFirstImage, isOnLastImage } =
|
||||
useGalleryNavigation();
|
||||
|
||||
const {
|
||||
handlePrevImage,
|
||||
handleNextImage,
|
||||
isOnFirstImage,
|
||||
isOnLastImage,
|
||||
handleLoadMoreImages,
|
||||
areMoreImagesAvailable,
|
||||
isFetching,
|
||||
} = useNextPrevImage();
|
||||
handleLoadMoreImages,
|
||||
queryResult: { isFetching },
|
||||
} = useGalleryImages();
|
||||
|
||||
return (
|
||||
<Box pos="relative" h="full" w="full">
|
||||
@@ -37,7 +37,7 @@ const NextPrevImageButtons = () => {
|
||||
aria-label={t('accessibility.previousImage')}
|
||||
icon={<FaAngleLeft size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={handlePrevImage}
|
||||
onClick={handleLeftImage}
|
||||
boxSize={16}
|
||||
sx={nextPrevButtonStyles}
|
||||
/>
|
||||
@@ -54,7 +54,7 @@ const NextPrevImageButtons = () => {
|
||||
aria-label={t('accessibility.nextImage')}
|
||||
icon={<FaAngleRight size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={handleNextImage}
|
||||
onClick={handleRightImage}
|
||||
boxSize={16}
|
||||
sx={nextPrevButtonStyles}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
/**
|
||||
* Registers gallery hotkeys. This hook is a singleton.
|
||||
*/
|
||||
export const useGalleryHotkeys = () => {
|
||||
const {
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
queryResult: { isFetching },
|
||||
} = useGalleryImages();
|
||||
|
||||
const {
|
||||
handleLeftImage,
|
||||
handleRightImage,
|
||||
handleUpImage,
|
||||
handleDownImage,
|
||||
isOnLastImage,
|
||||
areImagesBelowCurrent,
|
||||
} = useGalleryNavigation();
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
handleLeftImage();
|
||||
},
|
||||
[handleLeftImage]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
|
||||
handleLoadMoreImages();
|
||||
return;
|
||||
}
|
||||
if (!isOnLastImage) {
|
||||
handleRightImage();
|
||||
}
|
||||
},
|
||||
[
|
||||
isOnLastImage,
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
isFetching,
|
||||
handleRightImage,
|
||||
]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'up',
|
||||
() => {
|
||||
handleUpImage();
|
||||
},
|
||||
{ preventDefault: true },
|
||||
[handleUpImage]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'down',
|
||||
() => {
|
||||
if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) {
|
||||
handleLoadMoreImages();
|
||||
return;
|
||||
}
|
||||
handleDownImage();
|
||||
},
|
||||
{ preventDefault: true },
|
||||
[
|
||||
areImagesBelowCurrent,
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
isFetching,
|
||||
handleDownImage,
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { IMAGE_LIMIT } from 'features/gallery/store/types';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
} from 'services/api/endpoints/boards';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
import type { ListImagesArgs } from 'services/api/types';
|
||||
|
||||
export type UseGalleryImagesReturn = {
|
||||
handleLoadMoreImages: () => void;
|
||||
areMoreImagesAvailable: boolean;
|
||||
queryResult: ReturnType<typeof useListImagesQuery>;
|
||||
};
|
||||
|
||||
// The gallery is a singleton but multiple components need access to its query data.
|
||||
// If we don't define the query args outside of the hook, then each component will
|
||||
// have its own query args and trigger multiple requests. We use an atom to store
|
||||
// the query args outside of the hook so that all consumers use the same query args.
|
||||
const $queryArgs = atom<ListImagesArgs | null>(null);
|
||||
|
||||
/**
|
||||
* Provides access to the gallery images and a way to imperatively fetch more.
|
||||
*
|
||||
* This hook is a singleton.
|
||||
*/
|
||||
export const useGalleryImages = (): UseGalleryImagesReturn => {
|
||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||
const baseQueryArgs = useAppSelector(selectListImagesBaseQueryArgs);
|
||||
const queryArgs = useStore($queryArgs);
|
||||
const queryResult = useListImagesQuery(queryArgs ?? baseQueryArgs);
|
||||
const boardId = useMemo(
|
||||
() => baseQueryArgs.board_id ?? 'none',
|
||||
[baseQueryArgs.board_id]
|
||||
);
|
||||
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(boardId);
|
||||
const { data: imagesTotal } = useGetBoardImagesTotalQuery(boardId);
|
||||
const currentViewTotal = useMemo(
|
||||
() => (galleryView === 'images' ? imagesTotal?.total : assetsTotal?.total),
|
||||
[assetsTotal?.total, galleryView, imagesTotal?.total]
|
||||
);
|
||||
const loadedImagesCount = useMemo(
|
||||
() => queryResult.data?.ids.length ?? 0,
|
||||
[queryResult.data?.ids.length]
|
||||
);
|
||||
const areMoreImagesAvailable = useMemo(() => {
|
||||
if (!currentViewTotal || !queryResult.data) {
|
||||
return false;
|
||||
}
|
||||
return queryResult.data.ids.length < currentViewTotal;
|
||||
}, [queryResult.data, currentViewTotal]);
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
// To load more images, we update the query args with an offset and limit.
|
||||
const _queryArgs: ListImagesArgs = loadedImagesCount
|
||||
? {
|
||||
...baseQueryArgs,
|
||||
offset: loadedImagesCount,
|
||||
limit: IMAGE_LIMIT,
|
||||
}
|
||||
: baseQueryArgs;
|
||||
$queryArgs.set(_queryArgs);
|
||||
}, [baseQueryArgs, loadedImagesCount]);
|
||||
|
||||
return {
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
queryResult,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { imageItemContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridItemContainer';
|
||||
import { imageListContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridListContainer';
|
||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { getIsVisible } from 'features/gallery/util/getIsVisible';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
/**
|
||||
* This hook is used to navigate the gallery using the arrow keys.
|
||||
*
|
||||
* The gallery is rendered as a grid. In order to navigate the grid,
|
||||
* we need to know how many images are in each row and whether or not
|
||||
* an image is visible in the gallery.
|
||||
*
|
||||
* We use direct DOM query selectors to check if an image is visible
|
||||
* to avoid having to track a ref for each image.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the number of images per row in the gallery by grabbing their DOM elements.
|
||||
*/
|
||||
const getImagesPerRow = (): number => {
|
||||
const widthOfGalleryImage =
|
||||
document
|
||||
.querySelector(`[data-testid="${imageItemContainerTestId}"]`)
|
||||
?.getBoundingClientRect().width ?? 1;
|
||||
|
||||
const widthOfGalleryGrid =
|
||||
document
|
||||
.querySelector(`[data-testid="${imageListContainerTestId}"]`)
|
||||
?.getBoundingClientRect().width ?? 0;
|
||||
|
||||
const imagesPerRow = Math.round(widthOfGalleryGrid / widthOfGalleryImage);
|
||||
|
||||
return imagesPerRow;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls to the image with the given name.
|
||||
* If the image is not fully visible, it will not be scrolled to.
|
||||
* @param imageName The image name to scroll to.
|
||||
* @param index The index of the image in the gallery.
|
||||
*/
|
||||
const scrollToImage = (imageName: string, index: number) => {
|
||||
const virtuosoContext = virtuosoGridRefs.get();
|
||||
const range = virtuosoContext.virtuosoRangeRef?.current;
|
||||
const root = virtuosoContext.rootRef?.current;
|
||||
const virtuoso = virtuosoContext.virtuosoRef?.current;
|
||||
|
||||
if (!range || !virtuoso || !root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageElement = document.querySelector(
|
||||
`[data-testid="${getGalleryImageDataTestId(imageName)}"]`
|
||||
);
|
||||
const itemRect = imageElement?.getBoundingClientRect();
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
if (!itemRect || !getIsVisible(itemRect, rootRect)) {
|
||||
virtuoso.scrollToIndex({
|
||||
index,
|
||||
align: getScrollToIndexAlign(index, range),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Utilities to get the image to the left, right, up, or down of the current image.
|
||||
|
||||
const getLeftImage = (images: ImageDTO[], currentIndex: number) => {
|
||||
const index = clamp(currentIndex - 1, 0, images.length - 1);
|
||||
const image = images[index];
|
||||
return { index, image };
|
||||
};
|
||||
|
||||
const getRightImage = (images: ImageDTO[], currentIndex: number) => {
|
||||
const index = clamp(currentIndex + 1, 0, images.length - 1);
|
||||
const image = images[index];
|
||||
return { index, image };
|
||||
};
|
||||
|
||||
const getUpImage = (images: ImageDTO[], currentIndex: number) => {
|
||||
const imagesPerRow = getImagesPerRow();
|
||||
// If we are on the first row, we want to stay on the first row, not go to first image
|
||||
const isOnFirstRow = currentIndex < imagesPerRow;
|
||||
const index = isOnFirstRow
|
||||
? currentIndex
|
||||
: clamp(currentIndex - imagesPerRow, 0, images.length - 1);
|
||||
const image = images[index];
|
||||
return { index, image };
|
||||
};
|
||||
|
||||
const getDownImage = (images: ImageDTO[], currentIndex: number) => {
|
||||
const imagesPerRow = getImagesPerRow();
|
||||
// If there are no images below the current image, we want to stay where we are
|
||||
const areImagesBelow = currentIndex < images.length - imagesPerRow;
|
||||
const index = areImagesBelow
|
||||
? clamp(currentIndex + imagesPerRow, 0, images.length - 1)
|
||||
: currentIndex;
|
||||
const image = images[index];
|
||||
return { index, image };
|
||||
};
|
||||
|
||||
const getImageFuncs = {
|
||||
left: getLeftImage,
|
||||
right: getRightImage,
|
||||
up: getUpImage,
|
||||
down: getDownImage,
|
||||
};
|
||||
|
||||
export type UseGalleryNavigationReturn = {
|
||||
handleLeftImage: () => void;
|
||||
handleRightImage: () => void;
|
||||
handleUpImage: () => void;
|
||||
handleDownImage: () => void;
|
||||
isOnFirstImage: boolean;
|
||||
isOnLastImage: boolean;
|
||||
areImagesBelowCurrent: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides access to the gallery navigation via arrow keys.
|
||||
* Also provides information about the current image's position in the gallery,
|
||||
* useful for determining whether to load more images or display navigatin
|
||||
* buttons.
|
||||
*/
|
||||
export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
||||
const dispatch = useAppDispatch();
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const {
|
||||
queryResult: { data },
|
||||
} = useGalleryImages();
|
||||
const loadedImagesCount = useMemo(
|
||||
() => data?.ids.length ?? 0,
|
||||
[data?.ids.length]
|
||||
);
|
||||
const lastSelectedImageIndex = useMemo(() => {
|
||||
if (!data || !lastSelectedImage) {
|
||||
return 0;
|
||||
}
|
||||
return imagesSelectors
|
||||
.selectAll(data)
|
||||
.findIndex((i) => i.image_name === lastSelectedImage.image_name);
|
||||
}, [lastSelectedImage, data]);
|
||||
|
||||
const handleNavigation = useCallback(
|
||||
(direction: 'left' | 'right' | 'up' | 'down') => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { index, image } = getImageFuncs[direction](
|
||||
imagesSelectors.selectAll(data),
|
||||
lastSelectedImageIndex
|
||||
);
|
||||
if (!image || index === lastSelectedImageIndex) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageSelected(image));
|
||||
scrollToImage(image.image_name, index);
|
||||
},
|
||||
[dispatch, lastSelectedImageIndex, data]
|
||||
);
|
||||
|
||||
const isOnFirstImage = useMemo(
|
||||
() => lastSelectedImageIndex === 0,
|
||||
[lastSelectedImageIndex]
|
||||
);
|
||||
|
||||
const isOnLastImage = useMemo(
|
||||
() => lastSelectedImageIndex === loadedImagesCount - 1,
|
||||
[lastSelectedImageIndex, loadedImagesCount]
|
||||
);
|
||||
|
||||
const areImagesBelowCurrent = useMemo(() => {
|
||||
const imagesPerRow = getImagesPerRow();
|
||||
return lastSelectedImageIndex + imagesPerRow < loadedImagesCount;
|
||||
}, [lastSelectedImageIndex, loadedImagesCount]);
|
||||
|
||||
const handleLeftImage = useCallback(() => {
|
||||
handleNavigation('left');
|
||||
}, [handleNavigation]);
|
||||
|
||||
const handleRightImage = useCallback(() => {
|
||||
handleNavigation('right');
|
||||
}, [handleNavigation]);
|
||||
|
||||
const handleUpImage = useCallback(() => {
|
||||
handleNavigation('up');
|
||||
}, [handleNavigation]);
|
||||
|
||||
const handleDownImage = useCallback(() => {
|
||||
handleNavigation('down');
|
||||
}, [handleNavigation]);
|
||||
|
||||
return {
|
||||
handleLeftImage,
|
||||
handleRightImage,
|
||||
handleUpImage,
|
||||
handleDownImage,
|
||||
isOnFirstImage,
|
||||
isOnLastImage,
|
||||
areImagesBelowCurrent,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import {
|
||||
selectGallerySlice,
|
||||
selectionChanged,
|
||||
@@ -8,29 +8,24 @@ import {
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[selectGallerySlice, selectListImagesBaseQueryArgs],
|
||||
(gallery, queryArgs) => {
|
||||
return {
|
||||
queryArgs,
|
||||
selection: gallery.selection,
|
||||
};
|
||||
}
|
||||
const selectGallerySelection = createMemoizedSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.selection
|
||||
);
|
||||
|
||||
const EMPTY_ARRAY: ImageDTO[] = [];
|
||||
|
||||
export const useMultiselect = (imageDTO?: ImageDTO) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { queryArgs, selection } = useAppSelector(selector);
|
||||
|
||||
const { imageDTOs } = useListImagesQuery(queryArgs, {
|
||||
selectFromResult: (result) => ({
|
||||
imageDTOs: result.data ? imagesSelectors.selectAll(result.data) : [],
|
||||
}),
|
||||
});
|
||||
const selection = useAppSelector(selectGallerySelection);
|
||||
const { data } = useGalleryImages().queryResult;
|
||||
const imageDTOs = useMemo(
|
||||
() => (data ? imagesSelectors.selectAll(data) : EMPTY_ARRAY),
|
||||
[data]
|
||||
);
|
||||
|
||||
const isMultiSelectEnabled = useFeatureStatus('multiselect').isFeatureEnabled;
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_LIMIT } from 'features/gallery/store/types';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { map } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ListRange, VirtuosoGridHandle } from 'react-virtuoso';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import {
|
||||
imagesApi,
|
||||
useLazyListImagesQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import type { ListImagesArgs } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
export type UseNextPrevImageState = {
|
||||
virtuosoRef: RefObject<VirtuosoGridHandle> | undefined;
|
||||
virtuosoRangeRef: RefObject<ListRange> | undefined;
|
||||
};
|
||||
|
||||
export const $useNextPrevImageState = map<UseNextPrevImageState>({
|
||||
virtuosoRef: undefined,
|
||||
virtuosoRangeRef: undefined,
|
||||
});
|
||||
|
||||
export const nextPrevImageButtonsSelector = createMemoizedSelector(
|
||||
[(state: RootState) => state, selectListImagesBaseQueryArgs],
|
||||
(state, baseQueryArgs) => {
|
||||
const { data, status } =
|
||||
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||
|
||||
const { data: totalsData } =
|
||||
state.gallery.galleryView === 'images'
|
||||
? boardsApi.endpoints.getBoardImagesTotal.select(
|
||||
baseQueryArgs.board_id ?? 'none'
|
||||
)(state)
|
||||
: boardsApi.endpoints.getBoardAssetsTotal.select(
|
||||
baseQueryArgs.board_id ?? 'none'
|
||||
)(state);
|
||||
|
||||
const lastSelectedImage =
|
||||
state.gallery.selection[state.gallery.selection.length - 1];
|
||||
|
||||
const isFetching = status === 'pending';
|
||||
|
||||
if (!data || !lastSelectedImage || totalsData?.total === 0) {
|
||||
return {
|
||||
isFetching,
|
||||
queryArgs: baseQueryArgs,
|
||||
isOnFirstImage: true,
|
||||
isOnLastImage: true,
|
||||
};
|
||||
}
|
||||
|
||||
const queryArgs: ListImagesArgs = {
|
||||
...baseQueryArgs,
|
||||
offset: data.ids.length,
|
||||
limit: IMAGE_LIMIT,
|
||||
};
|
||||
|
||||
const images = imagesSelectors.selectAll(data);
|
||||
|
||||
const currentImageIndex = images.findIndex(
|
||||
(i) => i.image_name === lastSelectedImage.image_name
|
||||
);
|
||||
const nextImageIndex = clamp(currentImageIndex + 1, 0, images.length - 1);
|
||||
const prevImageIndex = clamp(currentImageIndex - 1, 0, images.length - 1);
|
||||
|
||||
const nextImageId = images[nextImageIndex]?.image_name;
|
||||
const prevImageId = images[prevImageIndex]?.image_name;
|
||||
|
||||
const nextImage = nextImageId
|
||||
? imagesSelectors.selectById(data, nextImageId)
|
||||
: undefined;
|
||||
const prevImage = prevImageId
|
||||
? imagesSelectors.selectById(data, prevImageId)
|
||||
: undefined;
|
||||
|
||||
const imagesLength = images.length;
|
||||
|
||||
return {
|
||||
loadedImagesCount: images.length,
|
||||
currentImageIndex,
|
||||
areMoreImagesAvailable: (totalsData?.total ?? 0) > imagesLength,
|
||||
isFetching: status === 'pending',
|
||||
nextImage,
|
||||
prevImage,
|
||||
nextImageIndex,
|
||||
prevImageIndex,
|
||||
queryArgs,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const useNextPrevImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const {
|
||||
nextImage,
|
||||
nextImageIndex,
|
||||
prevImage,
|
||||
prevImageIndex,
|
||||
areMoreImagesAvailable,
|
||||
isFetching,
|
||||
queryArgs,
|
||||
loadedImagesCount,
|
||||
currentImageIndex,
|
||||
} = useAppSelector(nextPrevImageButtonsSelector);
|
||||
|
||||
const handlePrevImage = useCallback(() => {
|
||||
prevImage && dispatch(imageSelected(prevImage));
|
||||
const range = $useNextPrevImageState.get().virtuosoRangeRef?.current;
|
||||
const virtuoso = $useNextPrevImageState.get().virtuosoRef?.current;
|
||||
if (!range || !virtuoso) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevImageIndex !== undefined &&
|
||||
(prevImageIndex < range.startIndex || prevImageIndex > range.endIndex)
|
||||
) {
|
||||
virtuoso.scrollToIndex({
|
||||
index: prevImageIndex,
|
||||
behavior: 'smooth',
|
||||
align: getScrollToIndexAlign(prevImageIndex, range),
|
||||
});
|
||||
}
|
||||
}, [dispatch, prevImage, prevImageIndex]);
|
||||
|
||||
const handleNextImage = useCallback(() => {
|
||||
nextImage && dispatch(imageSelected(nextImage));
|
||||
const range = $useNextPrevImageState.get().virtuosoRangeRef?.current;
|
||||
const virtuoso = $useNextPrevImageState.get().virtuosoRef?.current;
|
||||
if (!range || !virtuoso) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
nextImageIndex !== undefined &&
|
||||
(nextImageIndex < range.startIndex || nextImageIndex > range.endIndex)
|
||||
) {
|
||||
virtuoso.scrollToIndex({
|
||||
index: nextImageIndex,
|
||||
behavior: 'smooth',
|
||||
align: getScrollToIndexAlign(nextImageIndex, range),
|
||||
});
|
||||
}
|
||||
}, [dispatch, nextImage, nextImageIndex]);
|
||||
|
||||
const [listImages] = useLazyListImagesQuery();
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
listImages(queryArgs);
|
||||
}, [listImages, queryArgs]);
|
||||
|
||||
return {
|
||||
handlePrevImage,
|
||||
handleNextImage,
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
currentImageIndex !== undefined &&
|
||||
currentImageIndex === loadedImagesCount - 1,
|
||||
nextImage,
|
||||
prevImage,
|
||||
areMoreImagesAvailable,
|
||||
handleLoadMoreImages,
|
||||
isFetching,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||
import { getIsVisible } from 'features/gallery/util/getIsVisible';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Scrolls an image into view when it is selected. This is necessary because
|
||||
* the image grid is virtualized, so the image may not be visible when it is
|
||||
* selected.
|
||||
*
|
||||
* Also handles when an image is selected programmatically - for example, when
|
||||
* auto-switching the new gallery images.
|
||||
*
|
||||
* @param isSelected Whether the image is selected.
|
||||
* @param index The index of the image in the gallery.
|
||||
* @param selectionCount The number of images selected.
|
||||
* @returns
|
||||
*/
|
||||
export const useScrollIntoView = (
|
||||
isSelected: boolean,
|
||||
index: number,
|
||||
selectionCount: number
|
||||
) => {
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSelected || selectionCount !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const virtuosoContext = virtuosoGridRefs.get();
|
||||
const range = virtuosoContext.virtuosoRangeRef?.current;
|
||||
const root = virtuosoContext.rootRef?.current;
|
||||
const virtuoso = virtuosoContext.virtuosoRef?.current;
|
||||
|
||||
if (!range || !virtuoso || !root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemRect = imageContainerRef.current?.getBoundingClientRect();
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
|
||||
if (!itemRect || !getIsVisible(itemRect, rootRect)) {
|
||||
virtuoso.scrollToIndex({
|
||||
index,
|
||||
align: getScrollToIndexAlign(index, range),
|
||||
});
|
||||
}
|
||||
}, [isSelected, index, selectionCount]);
|
||||
|
||||
return imageContainerRef;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { VirtuosoGalleryContext } from 'features/gallery/components/ImageGrid/types';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useScrollToVisible = (
|
||||
isSelected: boolean,
|
||||
index: number,
|
||||
selectionCount: number,
|
||||
virtuosoContext: VirtuosoGalleryContext
|
||||
) => {
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isSelected ||
|
||||
selectionCount !== 1 ||
|
||||
!virtuosoContext.rootRef.current ||
|
||||
!virtuosoContext.virtuosoRef.current ||
|
||||
!virtuosoContext.virtuosoRangeRef.current ||
|
||||
!imageContainerRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemRect = imageContainerRef.current.getBoundingClientRect();
|
||||
const rootRect = virtuosoContext.rootRef.current.getBoundingClientRect();
|
||||
const itemIsVisible =
|
||||
itemRect.top >= rootRect.top &&
|
||||
itemRect.bottom <= rootRect.bottom &&
|
||||
itemRect.left >= rootRect.left &&
|
||||
itemRect.right <= rootRect.right;
|
||||
|
||||
if (!itemIsVisible) {
|
||||
virtuosoContext.virtuosoRef.current.scrollToIndex({
|
||||
index,
|
||||
behavior: 'smooth',
|
||||
align: getScrollToIndexAlign(
|
||||
index,
|
||||
virtuosoContext.virtuosoRangeRef.current
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [isSelected, index, selectionCount, virtuosoContext]);
|
||||
|
||||
return imageContainerRef;
|
||||
};
|
||||
@@ -106,3 +106,11 @@ const isAnyBoardDeleted = isAnyOf(
|
||||
);
|
||||
|
||||
export const selectGallerySlice = (state: RootState) => state.gallery;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateGalleryState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Gets whether the item is visible in the root element.
|
||||
*/
|
||||
|
||||
export const getIsVisible = (itemRect: DOMRect, rootRect: DOMRect) => {
|
||||
return (
|
||||
itemRect.top >= rootRect.top &&
|
||||
itemRect.bottom <= rootRect.bottom &&
|
||||
itemRect.left >= rootRect.left &&
|
||||
itemRect.right <= rootRect.right
|
||||
);
|
||||
};
|
||||
@@ -8,8 +8,14 @@ import { useTranslation } from 'react-i18next';
|
||||
const ParamHrfStrength = () => {
|
||||
const hrfStrength = useAppSelector((s) => s.hrf.hrfStrength);
|
||||
const initial = useAppSelector((s) => s.config.sd.hrfStrength.initial);
|
||||
const min = useAppSelector((s) => s.config.sd.hrfStrength.min);
|
||||
const sliderMin = useAppSelector((s) => s.config.sd.hrfStrength.sliderMin);
|
||||
const sliderMax = useAppSelector((s) => s.config.sd.hrfStrength.sliderMax);
|
||||
const numberInputMin = useAppSelector(
|
||||
(s) => s.config.sd.hrfStrength.numberInputMin
|
||||
);
|
||||
const numberInputMax = useAppSelector(
|
||||
(s) => s.config.sd.hrfStrength.numberInputMax
|
||||
);
|
||||
const coarseStep = useAppSelector((s) => s.config.sd.hrfStrength.coarseStep);
|
||||
const fineStep = useAppSelector((s) => s.config.sd.hrfStrength.fineStep);
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -25,7 +31,7 @@ const ParamHrfStrength = () => {
|
||||
return (
|
||||
<InvControl label={t('parameters.denoisingStrength')}>
|
||||
<InvSlider
|
||||
min={min}
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
@@ -34,6 +40,8 @@ const ParamHrfStrength = () => {
|
||||
onChange={onChange}
|
||||
marks
|
||||
withNumberInput
|
||||
numberInputMin={numberInputMin}
|
||||
numberInputMax={numberInputMax}
|
||||
/>
|
||||
</InvControl>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,14 @@ import type {
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
|
||||
export interface HRFState {
|
||||
_version: 1;
|
||||
hrfEnabled: boolean;
|
||||
hrfStrength: ParameterStrength;
|
||||
hrfMethod: ParameterHRFMethod;
|
||||
}
|
||||
|
||||
export const initialHRFState: HRFState = {
|
||||
_version: 1,
|
||||
hrfStrength: 0.45,
|
||||
hrfEnabled: false,
|
||||
hrfMethod: 'ESRGAN',
|
||||
@@ -41,3 +43,11 @@ export const { setHrfEnabled, setHrfStrength, setHrfMethod } = hrfSlice.actions;
|
||||
export default hrfSlice.reducer;
|
||||
|
||||
export const selectHrfSlice = (state: RootState) => state.hrf;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateHRFState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
InvCardBody,
|
||||
InvCardHeader,
|
||||
} from 'common/components/InvCard/wrapper';
|
||||
import { InvLabel } from 'common/components/InvControl/InvLabel';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { InvText } from 'common/components/InvText/wrapper';
|
||||
import type { LoRA } from 'features/lora/store/loraSlice';
|
||||
import { loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -35,9 +35,9 @@ export const LoRACard = memo((props: LoRACardProps) => {
|
||||
return (
|
||||
<InvCard variant="lora">
|
||||
<InvCardHeader>
|
||||
<InvText noOfLines={1} wordBreak="break-all">
|
||||
<InvLabel noOfLines={1} wordBreak="break-all">
|
||||
{lora.model_name}
|
||||
</InvText>
|
||||
</InvLabel>
|
||||
<InvIconButton
|
||||
aria-label="Remove LoRA"
|
||||
variant="ghost"
|
||||
|
||||
@@ -14,16 +14,18 @@ export const defaultLoRAConfig = {
|
||||
};
|
||||
|
||||
export type LoraState = {
|
||||
_version: 1;
|
||||
loras: Record<string, LoRA>;
|
||||
};
|
||||
|
||||
export const intialLoraState: LoraState = {
|
||||
export const initialLoraState: LoraState = {
|
||||
_version: 1,
|
||||
loras: {},
|
||||
};
|
||||
|
||||
export const loraSlice = createSlice({
|
||||
name: 'lora',
|
||||
initialState: intialLoraState,
|
||||
initialState: initialLoraState,
|
||||
reducers: {
|
||||
loraAdded: (state, action: PayloadAction<LoRAModelConfigEntity>) => {
|
||||
const { model_name, id, base_model } = action.payload;
|
||||
@@ -77,3 +79,11 @@ export const {
|
||||
export default loraSlice.reducer;
|
||||
|
||||
export const selectLoraSlice = (state: RootState) => state.lora;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateLoRAState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { InvButtonProps } from 'common/components/InvButton/types';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSync } from 'react-icons/fa';
|
||||
import { PiArrowsClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
import { useSyncModels } from './useSyncModels';
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SyncModelsButton = memo(
|
||||
<InvButton
|
||||
isLoading={isLoading}
|
||||
onClick={syncModels}
|
||||
leftIcon={<FaSync />}
|
||||
leftIcon={<PiArrowsClockwiseBold />}
|
||||
minW="max-content"
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSync } from 'react-icons/fa';
|
||||
import { PiArrowsClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
import { useSyncModels } from './useSyncModels';
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SyncModelsIconButton = memo(
|
||||
|
||||
return (
|
||||
<InvIconButton
|
||||
icon={<FaSync />}
|
||||
icon={<PiArrowsClockwiseBold />}
|
||||
tooltip={t('modelManager.syncModels')}
|
||||
aria-label={t('modelManager.syncModels')}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -3,11 +3,13 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
|
||||
type ModelManagerState = {
|
||||
_version: 1;
|
||||
searchFolder: string | null;
|
||||
advancedAddScanModel: string | null;
|
||||
};
|
||||
|
||||
const initialModelManagerState: ModelManagerState = {
|
||||
export const initialModelManagerState: ModelManagerState = {
|
||||
_version: 1,
|
||||
searchFolder: null,
|
||||
advancedAddScanModel: null,
|
||||
};
|
||||
@@ -31,3 +33,11 @@ export const { setSearchFolder, setAdvancedAddScanModel } =
|
||||
export default modelManagerSlice.reducer;
|
||||
|
||||
export const selectModelManagerSlice = (state: RootState) => state.modelmanager;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const migrateModelManagerState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { Badge, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InvButton } from 'common/components/InvButton/InvButton';
|
||||
@@ -11,6 +10,7 @@ import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import type {
|
||||
LoRAModelConfigEntity,
|
||||
MainModelConfigEntity,
|
||||
@@ -109,7 +109,7 @@ const ModelListItem = (props: ModelListItemProps) => {
|
||||
</Flex>
|
||||
<InvIconButton
|
||||
onClick={onOpen}
|
||||
icon={<DeleteIcon />}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
aria-label={t('modelManager.deleteConfig')}
|
||||
colorScheme="error"
|
||||
/>
|
||||
|
||||
@@ -155,8 +155,9 @@ const AddNodePopover = () => {
|
||||
return;
|
||||
}
|
||||
addNode(v.value);
|
||||
dispatch(addNodePopoverClosed());
|
||||
},
|
||||
[addNode]
|
||||
[addNode, dispatch]
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
@@ -235,6 +236,7 @@ const AddNodePopover = () => {
|
||||
onMenuClose={onClose}
|
||||
onKeyDown={onKeyDown}
|
||||
inputRef={inputRef}
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
</InvPopoverBody>
|
||||
</InvPopoverContent>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { InvControlGroup } from 'common/components/InvControl/InvControlGroup';
|
||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
@@ -11,6 +13,8 @@ type Props = {
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
const props: ChakraProps = { w: 'unset' };
|
||||
|
||||
const InvocationNodeFooter = ({ nodeId }: Props) => {
|
||||
const hasImageOutput = useHasImageOutput(nodeId);
|
||||
const isCacheEnabled = useFeatureStatus('invocationCache').isFeatureEnabled;
|
||||
@@ -20,13 +24,16 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
|
||||
layerStyle="nodeFooter"
|
||||
w="full"
|
||||
borderBottomRadius="base"
|
||||
gap={4}
|
||||
px={2}
|
||||
py={0}
|
||||
h={8}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{isCacheEnabled && <UseCacheCheckbox nodeId={nodeId} />}
|
||||
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
|
||||
<InvControlGroup controlProps={props} labelProps={props}>
|
||||
{isCacheEnabled && <UseCacheCheckbox nodeId={nodeId} />}
|
||||
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
|
||||
</InvControlGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaExpand,
|
||||
// FaInfo,
|
||||
FaMapMarkerAlt,
|
||||
} from 'react-icons/fa';
|
||||
import { FaMagnifyingGlassMinus, FaMagnifyingGlassPlus } from 'react-icons/fa6';
|
||||
PiFrameCornersBold,
|
||||
PiMagnifyingGlassMinusBold,
|
||||
PiMagnifyingGlassPlusBold,
|
||||
PiMapPinBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
const ViewportControls = () => {
|
||||
@@ -52,19 +52,19 @@ const ViewportControls = () => {
|
||||
tooltip={t('nodes.zoomInNodes')}
|
||||
aria-label={t('nodes.zoomInNodes')}
|
||||
onClick={handleClickedZoomIn}
|
||||
icon={<FaMagnifyingGlassPlus />}
|
||||
icon={<PiMagnifyingGlassPlusBold />}
|
||||
/>
|
||||
<InvIconButton
|
||||
tooltip={t('nodes.zoomOutNodes')}
|
||||
aria-label={t('nodes.zoomOutNodes')}
|
||||
onClick={handleClickedZoomOut}
|
||||
icon={<FaMagnifyingGlassMinus />}
|
||||
icon={<PiMagnifyingGlassMinusBold />}
|
||||
/>
|
||||
<InvIconButton
|
||||
tooltip={t('nodes.fitViewportNodes')}
|
||||
aria-label={t('nodes.fitViewportNodes')}
|
||||
onClick={handleClickedFitView}
|
||||
icon={<FaExpand />}
|
||||
icon={<PiFrameCornersBold />}
|
||||
/>
|
||||
{/* <InvTooltip
|
||||
label={
|
||||
@@ -93,7 +93,7 @@ const ViewportControls = () => {
|
||||
}
|
||||
isChecked={shouldShowMinimapPanel}
|
||||
onClick={handleClickedToggleMiniMapPanel}
|
||||
icon={<FaMapMarkerAlt />}
|
||||
icon={<PiMapPinBold />}
|
||||
/>
|
||||
</InvButtonGroup>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
const AddNodeButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -16,7 +16,7 @@ const AddNodeButton = () => {
|
||||
<InvIconButton
|
||||
tooltip={t('nodes.addNodeToolTip')}
|
||||
aria-label={t('nodes.addNode')}
|
||||
icon={<FaPlus />}
|
||||
icon={<PiPlusBold />}
|
||||
onClick={handleOpenAddNodePopover}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InvButton } from 'common/components/InvButton/InvButton';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSyncAlt } from 'react-icons/fa';
|
||||
import { PiArrowsClockwiseBold } from 'react-icons/pi';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
|
||||
const ReloadNodeTemplatesButton = () => {
|
||||
@@ -15,7 +15,7 @@ const ReloadNodeTemplatesButton = () => {
|
||||
|
||||
return (
|
||||
<InvButton
|
||||
leftIcon={<FaSyncAlt />}
|
||||
leftIcon={<PiArrowsClockwiseBold />}
|
||||
tooltip={t('nodes.reloadNodeTemplates')}
|
||||
aria-label={t('nodes.reloadNodeTemplates')}
|
||||
onClick={handleReloadSchema}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user