Merge branch 'main' into lstein/global-configuration
@ -98,7 +98,8 @@ class CompelInvocation(BaseInvocation):
|
||||
|
||||
# TODO: support legacy blend?
|
||||
|
||||
prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string(prompt_str)
|
||||
conjunction = Compel.parse_prompt_string(prompt_str)
|
||||
prompt: Union[FlattenedPrompt, Blend] = conjunction.prompts[0]
|
||||
|
||||
if context.services.configuration.log_tokenization:
|
||||
log_tokenization_for_prompt_object(prompt, tokenizer)
|
||||
|
@ -5,7 +5,12 @@ from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
import numpy as np
|
||||
|
||||
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig
|
||||
from .baseinvocation import (
|
||||
BaseInvocation,
|
||||
BaseInvocationOutput,
|
||||
InvocationContext,
|
||||
InvocationConfig,
|
||||
)
|
||||
|
||||
|
||||
class MathInvocationConfig(BaseModel):
|
||||
@ -22,19 +27,21 @@ class MathInvocationConfig(BaseModel):
|
||||
|
||||
class IntOutput(BaseInvocationOutput):
|
||||
"""An integer output"""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["int_output"] = "int_output"
|
||||
a: int = Field(default=None, description="The output integer")
|
||||
#fmt: on
|
||||
# fmt: on
|
||||
|
||||
|
||||
class AddInvocation(BaseInvocation, MathInvocationConfig):
|
||||
"""Adds two numbers"""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["add"] = "add"
|
||||
a: int = Field(default=0, description="The first number")
|
||||
b: int = Field(default=0, description="The second number")
|
||||
#fmt: on
|
||||
# fmt: on
|
||||
|
||||
def invoke(self, context: InvocationContext) -> IntOutput:
|
||||
return IntOutput(a=self.a + self.b)
|
||||
@ -42,11 +49,12 @@ class AddInvocation(BaseInvocation, MathInvocationConfig):
|
||||
|
||||
class SubtractInvocation(BaseInvocation, MathInvocationConfig):
|
||||
"""Subtracts two numbers"""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["sub"] = "sub"
|
||||
a: int = Field(default=0, description="The first number")
|
||||
b: int = Field(default=0, description="The second number")
|
||||
#fmt: on
|
||||
# fmt: on
|
||||
|
||||
def invoke(self, context: InvocationContext) -> IntOutput:
|
||||
return IntOutput(a=self.a - self.b)
|
||||
@ -54,11 +62,12 @@ class SubtractInvocation(BaseInvocation, MathInvocationConfig):
|
||||
|
||||
class MultiplyInvocation(BaseInvocation, MathInvocationConfig):
|
||||
"""Multiplies two numbers"""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["mul"] = "mul"
|
||||
a: int = Field(default=0, description="The first number")
|
||||
b: int = Field(default=0, description="The second number")
|
||||
#fmt: on
|
||||
# fmt: on
|
||||
|
||||
def invoke(self, context: InvocationContext) -> IntOutput:
|
||||
return IntOutput(a=self.a * self.b)
|
||||
@ -66,11 +75,12 @@ class MultiplyInvocation(BaseInvocation, MathInvocationConfig):
|
||||
|
||||
class DivideInvocation(BaseInvocation, MathInvocationConfig):
|
||||
"""Divides two numbers"""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["div"] = "div"
|
||||
a: int = Field(default=0, description="The first number")
|
||||
b: int = Field(default=0, description="The second number")
|
||||
#fmt: on
|
||||
# fmt: on
|
||||
|
||||
def invoke(self, context: InvocationContext) -> IntOutput:
|
||||
return IntOutput(a=int(self.a / self.b))
|
||||
@ -78,8 +88,13 @@ class DivideInvocation(BaseInvocation, MathInvocationConfig):
|
||||
|
||||
class RandomIntInvocation(BaseInvocation):
|
||||
"""Outputs a single random integer."""
|
||||
#fmt: off
|
||||
|
||||
# fmt: off
|
||||
type: Literal["rand_int"] = "rand_int"
|
||||
#fmt: on
|
||||
low: int = Field(default=0, description="The inclusive low value")
|
||||
high: int = Field(
|
||||
default=np.iinfo(np.int32).max, description="The exclusive high value"
|
||||
)
|
||||
# fmt: on
|
||||
def invoke(self, context: InvocationContext) -> IntOutput:
|
||||
return IntOutput(a=np.random.randint(0, np.iinfo(np.int32).max))
|
||||
return IntOutput(a=np.random.randint(self.low, self.high))
|
||||
|
@ -16,6 +16,7 @@ from compel.prompt_parser import (
|
||||
FlattenedPrompt,
|
||||
Fragment,
|
||||
PromptParser,
|
||||
Conjunction,
|
||||
)
|
||||
|
||||
import invokeai.backend.util.logging as logger
|
||||
@ -26,58 +27,48 @@ from ..util import torch_dtype
|
||||
|
||||
config = get_invokeai_config()
|
||||
|
||||
def get_uc_and_c_and_ec(
|
||||
prompt_string, model, log_tokens=False, skip_normalize_legacy_blend=False
|
||||
):
|
||||
def get_uc_and_c_and_ec(prompt_string,
|
||||
model: InvokeAIDiffuserComponent,
|
||||
log_tokens=False, skip_normalize_legacy_blend=False):
|
||||
# lazy-load any deferred textual inversions.
|
||||
# this might take a couple of seconds the first time a textual inversion is used.
|
||||
model.textual_inversion_manager.create_deferred_token_ids_for_any_trigger_terms(
|
||||
prompt_string
|
||||
)
|
||||
model.textual_inversion_manager.create_deferred_token_ids_for_any_trigger_terms(prompt_string)
|
||||
|
||||
tokenizer = model.tokenizer
|
||||
compel = Compel(
|
||||
tokenizer=tokenizer,
|
||||
text_encoder=model.text_encoder,
|
||||
textual_inversion_manager=model.textual_inversion_manager,
|
||||
dtype_for_device_getter=torch_dtype,
|
||||
truncate_long_prompts=False
|
||||
)
|
||||
compel = Compel(tokenizer=model.tokenizer,
|
||||
text_encoder=model.text_encoder,
|
||||
textual_inversion_manager=model.textual_inversion_manager,
|
||||
dtype_for_device_getter=torch_dtype,
|
||||
truncate_long_prompts=False,
|
||||
)
|
||||
|
||||
# get rid of any newline characters
|
||||
prompt_string = prompt_string.replace("\n", " ")
|
||||
(
|
||||
positive_prompt_string,
|
||||
negative_prompt_string,
|
||||
) = split_prompt_to_positive_and_negative(prompt_string)
|
||||
legacy_blend = try_parse_legacy_blend(
|
||||
positive_prompt_string, skip_normalize_legacy_blend
|
||||
)
|
||||
positive_prompt: Union[FlattenedPrompt, Blend]
|
||||
if legacy_blend is not None:
|
||||
positive_prompt = legacy_blend
|
||||
else:
|
||||
positive_prompt = Compel.parse_prompt_string(positive_prompt_string)
|
||||
negative_prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string(
|
||||
negative_prompt_string
|
||||
)
|
||||
positive_prompt_string, negative_prompt_string = split_prompt_to_positive_and_negative(prompt_string)
|
||||
|
||||
legacy_blend = try_parse_legacy_blend(positive_prompt_string, skip_normalize_legacy_blend)
|
||||
positive_conjunction: Conjunction
|
||||
if legacy_blend is not None:
|
||||
positive_conjunction = legacy_blend
|
||||
else:
|
||||
positive_conjunction = Compel.parse_prompt_string(positive_prompt_string)
|
||||
positive_prompt = positive_conjunction.prompts[0]
|
||||
|
||||
negative_conjunction = Compel.parse_prompt_string(negative_prompt_string)
|
||||
negative_prompt: FlattenedPrompt | Blend = negative_conjunction.prompts[0]
|
||||
|
||||
tokens_count = get_max_token_count(model.tokenizer, positive_prompt)
|
||||
if log_tokens or config.log_tokenization:
|
||||
log_tokenization(positive_prompt, negative_prompt, tokenizer=tokenizer)
|
||||
log_tokenization(positive_prompt, negative_prompt, tokenizer=model.tokenizer)
|
||||
|
||||
c, options = compel.build_conditioning_tensor_for_prompt_object(positive_prompt)
|
||||
uc, _ = compel.build_conditioning_tensor_for_prompt_object(negative_prompt)
|
||||
[c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc])
|
||||
|
||||
tokens_count = get_max_token_count(tokenizer, positive_prompt)
|
||||
|
||||
ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(
|
||||
tokens_count_including_eos_bos=tokens_count,
|
||||
cross_attention_control_args=options.get("cross_attention_control", None),
|
||||
)
|
||||
ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(tokens_count_including_eos_bos=tokens_count,
|
||||
cross_attention_control_args=options.get(
|
||||
'cross_attention_control', None))
|
||||
return uc, c, ec
|
||||
|
||||
|
||||
def get_prompt_structure(
|
||||
prompt_string, skip_normalize_legacy_blend: bool = False
|
||||
) -> (Union[FlattenedPrompt, Blend], FlattenedPrompt):
|
||||
@ -88,18 +79,17 @@ def get_prompt_structure(
|
||||
legacy_blend = try_parse_legacy_blend(
|
||||
positive_prompt_string, skip_normalize_legacy_blend
|
||||
)
|
||||
positive_prompt: Union[FlattenedPrompt, Blend]
|
||||
positive_prompt: Conjunction
|
||||
if legacy_blend is not None:
|
||||
positive_prompt = legacy_blend
|
||||
positive_conjunction = legacy_blend
|
||||
else:
|
||||
positive_prompt = Compel.parse_prompt_string(positive_prompt_string)
|
||||
negative_prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string(
|
||||
negative_prompt_string
|
||||
)
|
||||
positive_conjunction = Compel.parse_prompt_string(positive_prompt_string)
|
||||
positive_prompt = positive_conjunction.prompts[0]
|
||||
negative_conjunction = Compel.parse_prompt_string(negative_prompt_string)
|
||||
negative_prompt: FlattenedPrompt|Blend = negative_conjunction.prompts[0]
|
||||
|
||||
return positive_prompt, negative_prompt
|
||||
|
||||
|
||||
def get_max_token_count(
|
||||
tokenizer, prompt: Union[FlattenedPrompt, Blend], truncate_if_too_long=False
|
||||
) -> int:
|
||||
@ -246,22 +236,21 @@ def log_tokenization_for_text(text, tokenizer, display_label=None, truncate_if_t
|
||||
logger.info(f"[TOKENLOG] Tokens Discarded ({totalTokens - usedTokens}):")
|
||||
logger.debug(f"{discarded}\x1b[0m")
|
||||
|
||||
|
||||
def try_parse_legacy_blend(text: str, skip_normalize: bool = False) -> Optional[Blend]:
|
||||
def try_parse_legacy_blend(text: str, skip_normalize: bool = False) -> Optional[Conjunction]:
|
||||
weighted_subprompts = split_weighted_subprompts(text, skip_normalize=skip_normalize)
|
||||
if len(weighted_subprompts) <= 1:
|
||||
return None
|
||||
strings = [x[0] for x in weighted_subprompts]
|
||||
weights = [x[1] for x in weighted_subprompts]
|
||||
|
||||
pp = PromptParser()
|
||||
parsed_conjunctions = [pp.parse_conjunction(x) for x in strings]
|
||||
flattened_prompts = [x.prompts[0] for x in parsed_conjunctions]
|
||||
|
||||
return Blend(
|
||||
prompts=flattened_prompts, weights=weights, normalize_weights=not skip_normalize
|
||||
)
|
||||
|
||||
flattened_prompts = []
|
||||
weights = []
|
||||
for i, x in enumerate(parsed_conjunctions):
|
||||
if len(x.prompts)>0:
|
||||
flattened_prompts.append(x.prompts[0])
|
||||
weights.append(weighted_subprompts[i][1])
|
||||
return Conjunction([Blend(prompts=flattened_prompts, weights=weights, normalize_weights=not skip_normalize)])
|
||||
|
||||
def split_weighted_subprompts(text, skip_normalize=False) -> list:
|
||||
"""
|
||||
|
@ -548,8 +548,9 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
|
||||
additional_guidance = []
|
||||
extra_conditioning_info = conditioning_data.extra
|
||||
with self.invokeai_diffuser.custom_attention_context(
|
||||
extra_conditioning_info=extra_conditioning_info,
|
||||
step_count=len(self.scheduler.timesteps),
|
||||
self.invokeai_diffuser.model,
|
||||
extra_conditioning_info=extra_conditioning_info,
|
||||
step_count=len(self.scheduler.timesteps),
|
||||
):
|
||||
yield PipelineIntermediateState(
|
||||
run_id=run_id,
|
||||
|
@ -10,6 +10,7 @@ import diffusers
|
||||
import psutil
|
||||
import torch
|
||||
from compel.cross_attention_control import Arguments
|
||||
from diffusers.models.unet_2d_condition import UNet2DConditionModel
|
||||
from diffusers.models.attention_processor import AttentionProcessor
|
||||
from torch import nn
|
||||
|
||||
@ -352,8 +353,7 @@ def restore_default_cross_attention(
|
||||
else:
|
||||
remove_attention_function(model)
|
||||
|
||||
|
||||
def override_cross_attention(model, context: Context, is_running_diffusers=False):
|
||||
def setup_cross_attention_control_attention_processors(unet: UNet2DConditionModel, context: Context):
|
||||
"""
|
||||
Inject attention parameters and functions into the passed in model to enable cross attention editing.
|
||||
|
||||
@ -372,37 +372,22 @@ def override_cross_attention(model, context: Context, is_running_diffusers=False
|
||||
indices = torch.arange(max_length, dtype=torch.long)
|
||||
for name, a0, a1, b0, b1 in context.arguments.edit_opcodes:
|
||||
if b0 < max_length:
|
||||
if name == "equal": # or (name == "replace" and a1 - a0 == b1 - b0):
|
||||
if name == "equal":# or (name == "replace" and a1 - a0 == b1 - b0):
|
||||
# these tokens have not been edited
|
||||
indices[b0:b1] = indices_target[a0:a1]
|
||||
mask[b0:b1] = 1
|
||||
|
||||
context.cross_attention_mask = mask.to(device)
|
||||
context.cross_attention_index_map = indices.to(device)
|
||||
if is_running_diffusers:
|
||||
unet = model
|
||||
old_attn_processors = unet.attn_processors
|
||||
if torch.backends.mps.is_available():
|
||||
# see note in StableDiffusionGeneratorPipeline.__init__ about borked slicing on MPS
|
||||
unet.set_attn_processor(SwapCrossAttnProcessor())
|
||||
else:
|
||||
# try to re-use an existing slice size
|
||||
default_slice_size = 4
|
||||
slice_size = next(
|
||||
(
|
||||
p.slice_size
|
||||
for p in old_attn_processors.values()
|
||||
if type(p) is SlicedAttnProcessor
|
||||
),
|
||||
default_slice_size,
|
||||
)
|
||||
unet.set_attn_processor(SlicedSwapCrossAttnProcesser(slice_size=slice_size))
|
||||
return old_attn_processors
|
||||
old_attn_processors = unet.attn_processors
|
||||
if torch.backends.mps.is_available():
|
||||
# see note in StableDiffusionGeneratorPipeline.__init__ about borked slicing on MPS
|
||||
unet.set_attn_processor(SwapCrossAttnProcessor())
|
||||
else:
|
||||
context.register_cross_attention_modules(model)
|
||||
inject_attention_function(model, context)
|
||||
return None
|
||||
|
||||
# try to re-use an existing slice size
|
||||
default_slice_size = 4
|
||||
slice_size = next((p.slice_size for p in old_attn_processors.values() if type(p) is SlicedAttnProcessor), default_slice_size)
|
||||
unet.set_attn_processor(SlicedSwapCrossAttnProcesser(slice_size=slice_size))
|
||||
|
||||
def get_cross_attention_modules(
|
||||
model, which: CrossAttentionType
|
||||
|
@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from diffusers import UNet2DConditionModel
|
||||
from diffusers.models.attention_processor import AttentionProcessor
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
@ -17,8 +18,8 @@ from .cross_attention_control import (
|
||||
CrossAttentionType,
|
||||
SwapCrossAttnContext,
|
||||
get_cross_attention_modules,
|
||||
override_cross_attention,
|
||||
restore_default_cross_attention,
|
||||
setup_cross_attention_control_attention_processors,
|
||||
)
|
||||
from .cross_attention_map_saving import AttentionMapSaver
|
||||
|
||||
@ -80,24 +81,35 @@ class InvokeAIDiffuserComponent:
|
||||
self.cross_attention_control_context = None
|
||||
self.sequential_guidance = config.sequential_guidance
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def custom_attention_context(
|
||||
self, extra_conditioning_info: Optional[ExtraConditioningInfo], step_count: int
|
||||
cls,
|
||||
unet: UNet2DConditionModel, # note: also may futz with the text encoder depending on requested LoRAs
|
||||
extra_conditioning_info: Optional[ExtraConditioningInfo],
|
||||
step_count: int
|
||||
):
|
||||
do_swap = (
|
||||
extra_conditioning_info is not None
|
||||
and extra_conditioning_info.wants_cross_attention_control
|
||||
)
|
||||
old_attn_processor = None
|
||||
if do_swap:
|
||||
old_attn_processor = self.override_cross_attention(
|
||||
extra_conditioning_info, step_count=step_count
|
||||
)
|
||||
old_attn_processors = None
|
||||
if extra_conditioning_info and (
|
||||
extra_conditioning_info.wants_cross_attention_control
|
||||
):
|
||||
old_attn_processors = unet.attn_processors
|
||||
# Load lora conditions into the model
|
||||
if extra_conditioning_info.wants_cross_attention_control:
|
||||
cross_attention_control_context = Context(
|
||||
arguments=extra_conditioning_info.cross_attention_control_args,
|
||||
step_count=step_count,
|
||||
)
|
||||
setup_cross_attention_control_attention_processors(
|
||||
unet,
|
||||
cross_attention_control_context,
|
||||
)
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
if old_attn_processor is not None:
|
||||
self.restore_default_cross_attention(old_attn_processor)
|
||||
if old_attn_processors is not None:
|
||||
unet.set_attn_processor(old_attn_processors)
|
||||
# TODO resuscitate attention map saving
|
||||
# self.remove_attention_map_saving()
|
||||
|
||||
|
@ -15,15 +15,3 @@ The `postinstall` script patches a few packages and runs the Chakra CLI to gener
|
||||
### Patch `@chakra-ui/cli`
|
||||
|
||||
See: <https://github.com/chakra-ui/chakra-ui/issues/7394>
|
||||
|
||||
### Patch `redux-persist`
|
||||
|
||||
We want to persist the canvas state to `localStorage` but many canvas operations change data very quickly, so we need to debounce the writes to `localStorage`.
|
||||
|
||||
`redux-persist` is unfortunately unmaintained. The repo's current code is nonfunctional, but the last release's code depends on a package that was removed from `npm` for being malware, so we cannot just fork it.
|
||||
|
||||
So, we have to patch it directly. Perhaps a better way would be to write a debounced storage adapter, but I couldn't figure out how to do that.
|
||||
|
||||
### Patch `redux-deep-persist`
|
||||
|
||||
This package makes blacklisting and whitelisting persist configs very simple, but we have to patch it to match `redux-persist` for the types to work.
|
||||
|
@ -89,18 +89,13 @@
|
||||
"react-i18next": "^12.2.2",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-konva": "^18.2.7",
|
||||
"react-konva-utils": "^1.0.4",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-resizable-panels": "^0.0.42",
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "^4.3.5",
|
||||
"react-zoom-pan-pinch": "^3.0.7",
|
||||
"reactflow": "^11.7.0",
|
||||
"redux-deep-persist": "^1.0.7",
|
||||
"redux-dynamic-middlewares": "^2.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-remember": "^3.3.1",
|
||||
"roarr": "^7.15.0",
|
||||
"serialize-error": "^11.0.0",
|
||||
|
@ -1,24 +0,0 @@
|
||||
diff --git a/node_modules/redux-deep-persist/lib/types.d.ts b/node_modules/redux-deep-persist/lib/types.d.ts
|
||||
index b67b8c2..7fc0fa1 100644
|
||||
--- a/node_modules/redux-deep-persist/lib/types.d.ts
|
||||
+++ b/node_modules/redux-deep-persist/lib/types.d.ts
|
||||
@@ -35,6 +35,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
|
||||
whitelist?: Array<string>;
|
||||
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||
throttle?: number;
|
||||
+ debounce?: number;
|
||||
migrate?: PersistMigrate;
|
||||
stateReconciler?: false | StateReconciler<S>;
|
||||
getStoredState?: (config: PersistConfig<S, RS, HSS, ESS>) => Promise<PersistedState>;
|
||||
diff --git a/node_modules/redux-deep-persist/src/types.ts b/node_modules/redux-deep-persist/src/types.ts
|
||||
index 398ac19..cbc5663 100644
|
||||
--- a/node_modules/redux-deep-persist/src/types.ts
|
||||
+++ b/node_modules/redux-deep-persist/src/types.ts
|
||||
@@ -91,6 +91,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
|
||||
whitelist?: Array<string>;
|
||||
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||
throttle?: number;
|
||||
+ debounce?: number;
|
||||
migrate?: PersistMigrate;
|
||||
stateReconciler?: false | StateReconciler<S>;
|
||||
/**
|
@ -1,116 +0,0 @@
|
||||
diff --git a/node_modules/redux-persist/es/createPersistoid.js b/node_modules/redux-persist/es/createPersistoid.js
|
||||
index 8b43b9a..184faab 100644
|
||||
--- a/node_modules/redux-persist/es/createPersistoid.js
|
||||
+++ b/node_modules/redux-persist/es/createPersistoid.js
|
||||
@@ -6,6 +6,7 @@ export default function createPersistoid(config) {
|
||||
var whitelist = config.whitelist || null;
|
||||
var transforms = config.transforms || [];
|
||||
var throttle = config.throttle || 0;
|
||||
+ var debounce = config.debounce || 0;
|
||||
var storageKey = "".concat(config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX).concat(config.key);
|
||||
var storage = config.storage;
|
||||
var serialize;
|
||||
@@ -28,30 +29,37 @@ export default function createPersistoid(config) {
|
||||
var timeIterator = null;
|
||||
var writePromise = null;
|
||||
|
||||
- var update = function update(state) {
|
||||
- // add any changed keys to the queue
|
||||
- Object.keys(state).forEach(function (key) {
|
||||
- if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
|
||||
+ // Timer for debounced `update()`
|
||||
+ let timer = 0;
|
||||
|
||||
- if (lastState[key] === state[key]) return; // value unchanged? noop
|
||||
+ function update(state) {
|
||||
+ // Debounce the update
|
||||
+ clearTimeout(timer);
|
||||
+ timer = setTimeout(() => {
|
||||
+ // add any changed keys to the queue
|
||||
+ Object.keys(state).forEach(function (key) {
|
||||
+ if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
|
||||
|
||||
- if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
|
||||
+ if (lastState[key] === state[key]) return; // value unchanged? noop
|
||||
|
||||
- keysToProcess.push(key); // add key to queue
|
||||
- }); //if any key is missing in the new state which was present in the lastState,
|
||||
- //add it for processing too
|
||||
+ if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
|
||||
|
||||
- Object.keys(lastState).forEach(function (key) {
|
||||
- if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
|
||||
- keysToProcess.push(key);
|
||||
- }
|
||||
- }); // start the time iterator if not running (read: throttle)
|
||||
+ keysToProcess.push(key); // add key to queue
|
||||
+ }); //if any key is missing in the new state which was present in the lastState,
|
||||
+ //add it for processing too
|
||||
|
||||
- if (timeIterator === null) {
|
||||
- timeIterator = setInterval(processNextKey, throttle);
|
||||
- }
|
||||
+ Object.keys(lastState).forEach(function (key) {
|
||||
+ if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
|
||||
+ keysToProcess.push(key);
|
||||
+ }
|
||||
+ }); // start the time iterator if not running (read: throttle)
|
||||
+
|
||||
+ if (timeIterator === null) {
|
||||
+ timeIterator = setInterval(processNextKey, throttle);
|
||||
+ }
|
||||
|
||||
- lastState = state;
|
||||
+ lastState = state;
|
||||
+ }, debounce)
|
||||
};
|
||||
|
||||
function processNextKey() {
|
||||
diff --git a/node_modules/redux-persist/es/types.js.flow b/node_modules/redux-persist/es/types.js.flow
|
||||
index c50d3cd..39d8be2 100644
|
||||
--- a/node_modules/redux-persist/es/types.js.flow
|
||||
+++ b/node_modules/redux-persist/es/types.js.flow
|
||||
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||
whitelist?: Array<string>,
|
||||
transforms?: Array<Transform>,
|
||||
throttle?: number,
|
||||
+ debounce?: number,
|
||||
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||
stateReconciler?: false | Function,
|
||||
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||
diff --git a/node_modules/redux-persist/lib/types.js.flow b/node_modules/redux-persist/lib/types.js.flow
|
||||
index c50d3cd..39d8be2 100644
|
||||
--- a/node_modules/redux-persist/lib/types.js.flow
|
||||
+++ b/node_modules/redux-persist/lib/types.js.flow
|
||||
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||
whitelist?: Array<string>,
|
||||
transforms?: Array<Transform>,
|
||||
throttle?: number,
|
||||
+ debounce?: number,
|
||||
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||
stateReconciler?: false | Function,
|
||||
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||
diff --git a/node_modules/redux-persist/src/types.js b/node_modules/redux-persist/src/types.js
|
||||
index c50d3cd..39d8be2 100644
|
||||
--- a/node_modules/redux-persist/src/types.js
|
||||
+++ b/node_modules/redux-persist/src/types.js
|
||||
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||
whitelist?: Array<string>,
|
||||
transforms?: Array<Transform>,
|
||||
throttle?: number,
|
||||
+ debounce?: number,
|
||||
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||
stateReconciler?: false | Function,
|
||||
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||
diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts
|
||||
index b3733bc..2a1696c 100644
|
||||
--- a/node_modules/redux-persist/types/types.d.ts
|
||||
+++ b/node_modules/redux-persist/types/types.d.ts
|
||||
@@ -35,6 +35,7 @@ declare module "redux-persist/es/types" {
|
||||
whitelist?: Array<string>;
|
||||
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||
throttle?: number;
|
||||
+ debounce?: number;
|
||||
migrate?: PersistMigrate;
|
||||
stateReconciler?: false | StateReconciler<S>;
|
||||
/**
|
@ -450,7 +450,7 @@
|
||||
"cfgScale": "CFG Scale",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"sampler": "Sampler",
|
||||
"scheduler": "Scheduler",
|
||||
"seed": "Seed",
|
||||
"imageToImage": "Image to Image",
|
||||
"randomizeSeed": "Randomize Seed",
|
||||
@ -552,8 +552,8 @@
|
||||
"canceled": "Processing Canceled",
|
||||
"tempFoldersEmptied": "Temp Folder Emptied",
|
||||
"uploadFailed": "Upload failed",
|
||||
"uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
|
||||
"uploadFailedUnableToLoadDesc": "Unable to load file",
|
||||
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
||||
"downloadImageStarted": "Image Download Started",
|
||||
"imageCopied": "Image Copied",
|
||||
"imageLinkCopied": "Image Link Copied",
|
||||
|
@ -2,14 +2,11 @@ import ImageUploader from 'common/components/ImageUploader';
|
||||
import SiteHeader from 'features/system/components/SiteHeader';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||
|
||||
import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
||||
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||
import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
|
||||
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
||||
import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel';
|
||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
||||
import Lightbox from 'features/lightbox/components/Lightbox';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
@ -17,13 +14,14 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
|
||||
import { PartialAppConfig } from 'app/types/invokeai';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||
import i18n from 'i18n';
|
||||
import Toaster from './Toaster';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
|
||||
const DEFAULT_CONFIG = {};
|
||||
|
||||
@ -38,9 +36,6 @@ const App = ({
|
||||
headerComponent,
|
||||
setIsReady,
|
||||
}: Props) => {
|
||||
useToastWatcher();
|
||||
useGlobalHotkeys();
|
||||
|
||||
const language = useAppSelector(languageSelector);
|
||||
|
||||
const log = useLogger();
|
||||
@ -77,65 +72,69 @@ const App = ({
|
||||
}, [isApplicationReady, setIsReady]);
|
||||
|
||||
return (
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{isLightboxEnabled && <Lightbox />}
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
{headerComponent || <SiteHeader />}
|
||||
<Flex
|
||||
<>
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{isLightboxEnabled && <Lightbox />}
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
w={{ base: '100vw', xl: 'full' }}
|
||||
h="full"
|
||||
flexDir={{ base: 'column', xl: 'row' }}
|
||||
p={4}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
<InvokeTabs />
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ImageUploader>
|
||||
{headerComponent || <SiteHeader />}
|
||||
<Flex
|
||||
gap={4}
|
||||
w={{ base: '100vw', xl: 'full' }}
|
||||
h="full"
|
||||
flexDir={{ base: 'column', xl: 'row' }}
|
||||
>
|
||||
<InvokeTabs />
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ImageUploader>
|
||||
|
||||
<GalleryDrawer />
|
||||
<ParametersDrawer />
|
||||
<GalleryDrawer />
|
||||
<ParametersDrawer />
|
||||
|
||||
<AnimatePresence>
|
||||
{!isApplicationReady && !loadingOverridden && (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ zIndex: 3 }}
|
||||
>
|
||||
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
||||
<Loading />
|
||||
</Box>
|
||||
<Box
|
||||
onClick={handleOverrideClicked}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
cursor="pointer"
|
||||
w="2rem"
|
||||
h="2rem"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{!isApplicationReady && !loadingOverridden && (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ zIndex: 3 }}
|
||||
>
|
||||
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
|
||||
<Loading />
|
||||
</Box>
|
||||
<Box
|
||||
onClick={handleOverrideClicked}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
cursor="pointer"
|
||||
w="2rem"
|
||||
h="2rem"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
<Toaster />
|
||||
<GlobalHotkeys />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { Flex, Spinner, Tooltip } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(systemSelector, (system) => {
|
||||
const { isUploading } = system;
|
||||
|
||||
let tooltip = '';
|
||||
|
||||
if (isUploading) {
|
||||
tooltip = 'Uploading...';
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip,
|
||||
shouldShow: isUploading,
|
||||
};
|
||||
});
|
||||
|
||||
export const AuxiliaryProgressIndicator = () => {
|
||||
const { shouldShow, tooltip } = useAppSelector(selector);
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'base.600',
|
||||
}}
|
||||
>
|
||||
<Tooltip label={tooltip} placement="right" hasArrow>
|
||||
<Spinner />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AuxiliaryProgressIndicator);
|
@ -10,6 +10,7 @@ import {
|
||||
togglePinParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import React, { memo } from 'react';
|
||||
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const globalHotkeysSelector = createSelector(
|
||||
@ -27,7 +28,11 @@ const globalHotkeysSelector = createSelector(
|
||||
|
||||
// TODO: Does not catch keypresses while focused in an input. Maybe there is a way?
|
||||
|
||||
export const useGlobalHotkeys = () => {
|
||||
/**
|
||||
* Logical component. Handles app-level global hotkeys.
|
||||
* @returns null
|
||||
*/
|
||||
const GlobalHotkeys: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { shift } = useAppSelector(globalHotkeysSelector);
|
||||
|
||||
@ -75,4 +80,8 @@ export const useGlobalHotkeys = () => {
|
||||
useHotkeys('4', () => {
|
||||
dispatch(setActiveTab('nodes'));
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(GlobalHotkeys);
|
65
invokeai/frontend/web/src/app/components/Toaster.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { useToast, UseToastOptions } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { toastQueueSelector } from 'features/system/store/systemSelectors';
|
||||
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export type MakeToastArg = string | UseToastOptions;
|
||||
|
||||
/**
|
||||
* Makes a toast from a string or a UseToastOptions object.
|
||||
* If a string is passed, the toast will have the status 'info' and will be closable with a duration of 2500ms.
|
||||
*/
|
||||
export const makeToast = (arg: MakeToastArg): UseToastOptions => {
|
||||
if (typeof arg === 'string') {
|
||||
return {
|
||||
title: arg,
|
||||
status: 'info',
|
||||
isClosable: true,
|
||||
duration: 2500,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'info', isClosable: true, duration: 2500, ...arg };
|
||||
};
|
||||
|
||||
/**
|
||||
* Logical component. Watches the toast queue and makes toasts when the queue is not empty.
|
||||
* @returns null
|
||||
*/
|
||||
const Toaster = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toastQueue = useAppSelector(toastQueueSelector);
|
||||
const toast = useToast();
|
||||
useEffect(() => {
|
||||
toastQueue.forEach((t) => {
|
||||
toast(t);
|
||||
});
|
||||
toastQueue.length > 0 && dispatch(clearToastQueue());
|
||||
}, [dispatch, toast, toastQueue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that can be used to make a toast.
|
||||
* @example
|
||||
* const toaster = useAppToaster();
|
||||
* toaster('Hello world!');
|
||||
* toaster({ title: 'Hello world!', status: 'success' });
|
||||
* @returns A function that can be used to make a toast.
|
||||
* @see makeToast
|
||||
* @see MakeToastArg
|
||||
* @see UseToastOptions
|
||||
*/
|
||||
export const useAppToaster = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toaster = useCallback(
|
||||
(arg: MakeToastArg) => dispatch(addToast(makeToast(arg))),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return toaster;
|
||||
};
|
||||
|
||||
export default Toaster;
|
@ -1,6 +1,6 @@
|
||||
// TODO: use Enums?
|
||||
|
||||
export const SCHEDULERS: Array<string> = [
|
||||
export const SCHEDULERS = [
|
||||
'ddim',
|
||||
'lms',
|
||||
'euler',
|
||||
@ -17,7 +17,12 @@ export const SCHEDULERS: Array<string> = [
|
||||
'heun',
|
||||
'heun_k',
|
||||
'unipc',
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type Scheduler = (typeof SCHEDULERS)[number];
|
||||
|
||||
export const isScheduler = (x: string): x is Scheduler =>
|
||||
SCHEDULERS.includes(x as Scheduler);
|
||||
|
||||
// Valid image widths
|
||||
export const WIDTHS: Array<number> = Array.from(Array(64)).map(
|
||||
|
@ -15,6 +15,10 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
|
||||
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
|
||||
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
|
||||
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
|
||||
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
|
||||
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
|
||||
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
|
||||
import { addCanvasMergedListener } from './listeners/canvasMerged';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -43,3 +47,8 @@ addUserInvokedCanvasListener();
|
||||
addUserInvokedNodesListener();
|
||||
addUserInvokedTextToImageListener();
|
||||
addUserInvokedImageToImageListener();
|
||||
|
||||
addCanvasSavedToGalleryListener();
|
||||
addCanvasDownloadedAsImageListener();
|
||||
addCanvasCopiedToClipboardListener();
|
||||
addCanvasMergedListener();
|
||||
|
@ -0,0 +1,33 @@
|
||||
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||
|
||||
export const addCanvasCopiedToClipboardListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: canvasCopiedToClipboard,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state);
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Problem Copying Canvas',
|
||||
description: 'Unable to export base layer',
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
copyBlobToClipboard(blob);
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { downloadBlob } from 'features/canvas/util/downloadBlob';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
export const addCanvasDownloadedAsImageListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: canvasDownloadedAsImage,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state);
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Problem Downloading Canvas',
|
||||
description: 'Unable to export base layer',
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, 'mergedCanvas.png');
|
||||
},
|
||||
});
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import {
|
||||
canvasSessionIdChanged,
|
||||
stagingAreaInitialized,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { sessionInvoked } from 'services/thunks/session';
|
||||
|
||||
export const addCanvasGraphBuiltListener = () =>
|
||||
startAppListening({
|
||||
actionCreator: canvasGraphBuilt,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const [{ meta }] = await take(sessionInvoked.fulfilled.match);
|
||||
const { sessionId } = meta.arg;
|
||||
const state = getState();
|
||||
|
||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||
dispatch(
|
||||
stagingAreaInitialized({
|
||||
sessionId,
|
||||
boundingBox: {
|
||||
...state.canvas.boundingBoxCoordinates,
|
||||
...state.canvas.boundingBoxDimensions,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(canvasSessionIdChanged(sessionId));
|
||||
},
|
||||
});
|
@ -0,0 +1,88 @@
|
||||
import { canvasMerged } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||
|
||||
export const addCanvasMergedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: canvasMerged,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state, true);
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Problem Merging Canvas',
|
||||
description: 'Unable to export base layer',
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
moduleLog.error('Problem getting canvas base layer');
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Problem Merging Canvas',
|
||||
description: 'Unable to export base layer',
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseLayerRect = canvasBaseLayer.getClientRect({
|
||||
relativeTo: canvasBaseLayer.getParent(),
|
||||
});
|
||||
|
||||
const filename = `mergedCanvas_${uuidv4()}.png`;
|
||||
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
imageType: 'intermediates',
|
||||
formData: {
|
||||
file: new File([blob], filename, { type: 'image/png' }),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const [{ payload }] = await take(
|
||||
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
|
||||
imageUploaded.fulfilled.match(action) &&
|
||||
action.meta.arg.formData.file.name === filename
|
||||
);
|
||||
|
||||
const mergedCanvasImage = deserializeImageResponse(payload.response);
|
||||
|
||||
dispatch(
|
||||
setMergedCanvas({
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
image: mergedCanvasImage,
|
||||
...baseLayerRect,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Canvas Merged',
|
||||
status: 'success',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
export const addCanvasSavedToGalleryListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: canvasSavedToGallery,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
const blob = await getBaseLayerBlob(state);
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Problem Saving Canvas',
|
||||
description: 'Unable to export base layer',
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
imageType: 'results',
|
||||
formData: {
|
||||
file: new File([blob], 'mergedCanvas.png', { type: 'image/png' }),
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
@ -3,6 +3,10 @@ import { startAppListening } from '..';
|
||||
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { resultAdded } from 'features/gallery/store/resultsSlice';
|
||||
|
||||
export const addImageUploadedListener = () => {
|
||||
startAppListening({
|
||||
@ -11,14 +15,31 @@ export const addImageUploadedListener = () => {
|
||||
action.payload.response.image_type !== 'intermediates',
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { response } = action.payload;
|
||||
const { imageType } = action.meta.arg;
|
||||
|
||||
const state = getState();
|
||||
const image = deserializeImageResponse(response);
|
||||
|
||||
dispatch(uploadAdded(image));
|
||||
if (imageType === 'uploads') {
|
||||
dispatch(uploadAdded(image));
|
||||
|
||||
if (state.gallery.shouldAutoSwitchToNewImages) {
|
||||
dispatch(imageSelected(image));
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
|
||||
if (state.gallery.shouldAutoSwitchToNewImages) {
|
||||
dispatch(imageSelected(image));
|
||||
}
|
||||
|
||||
if (action.meta.arg.activeTabName === 'img2img') {
|
||||
dispatch(initialImageSelected(image));
|
||||
}
|
||||
|
||||
if (action.meta.arg.activeTabName === 'unifiedCanvas') {
|
||||
dispatch(setInitialCanvasImage(image));
|
||||
}
|
||||
}
|
||||
|
||||
if (imageType === 'results') {
|
||||
dispatch(resultAdded(image));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -2,11 +2,11 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { Image, isInvokeAIImage } from 'app/types/invokeai';
|
||||
import { selectResultsById } from 'features/gallery/store/resultsSlice';
|
||||
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
|
||||
import { makeToast } from 'features/system/hooks/useToastWatcher';
|
||||
import { t } from 'i18next';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { startAppListening } from '..';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { makeToast } from 'app/components/Toaster';
|
||||
|
||||
export const addInitialImageSelectedListener = () => {
|
||||
startAppListening({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { startAppListening } from '..';
|
||||
import { sessionCreated, sessionInvoked } from 'services/thunks/session';
|
||||
import { buildCanvasGraphAndBlobs } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
|
||||
import { buildCanvasGraphComponents } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
@ -11,9 +11,17 @@ import {
|
||||
stagingAreaInitialized,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import { userInvoked } from 'app/store/actions';
|
||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'invoke' });
|
||||
|
||||
/**
|
||||
* This listener is responsible for building the canvas graph and blobs when the user invokes the canvas.
|
||||
* It is also responsible for uploading the base and mask layers to the server.
|
||||
*/
|
||||
export const addUserInvokedCanvasListener = () => {
|
||||
startAppListening({
|
||||
predicate: (action): action is ReturnType<typeof userInvoked> =>
|
||||
@ -21,25 +29,49 @@ export const addUserInvokedCanvasListener = () => {
|
||||
effect: async (action, { getState, dispatch, take }) => {
|
||||
const state = getState();
|
||||
|
||||
const data = await buildCanvasGraphAndBlobs(state);
|
||||
// Build canvas blobs
|
||||
const canvasBlobsAndImageData = await getCanvasData(state);
|
||||
|
||||
if (!data) {
|
||||
if (!canvasBlobsAndImageData) {
|
||||
moduleLog.error('Unable to create canvas data');
|
||||
return;
|
||||
}
|
||||
|
||||
const { baseBlob, baseImageData, maskBlob, maskImageData } =
|
||||
canvasBlobsAndImageData;
|
||||
|
||||
// Determine the generation mode
|
||||
const generationMode = getCanvasGenerationMode(
|
||||
baseImageData,
|
||||
maskImageData
|
||||
);
|
||||
|
||||
if (state.system.enableImageDebugging) {
|
||||
const baseDataURL = await blobToDataURL(baseBlob);
|
||||
const maskDataURL = await blobToDataURL(maskBlob);
|
||||
openBase64ImageInTab([
|
||||
{ base64: maskDataURL, caption: 'mask b64' },
|
||||
{ base64: baseDataURL, caption: 'image b64' },
|
||||
]);
|
||||
}
|
||||
|
||||
moduleLog.debug(`Generation mode: ${generationMode}`);
|
||||
|
||||
// Build the canvas graph
|
||||
const graphComponents = await buildCanvasGraphComponents(
|
||||
state,
|
||||
generationMode
|
||||
);
|
||||
|
||||
if (!graphComponents) {
|
||||
moduleLog.error('Problem building graph');
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
rangeNode,
|
||||
iterateNode,
|
||||
baseNode,
|
||||
edges,
|
||||
baseBlob,
|
||||
maskBlob,
|
||||
generationMode,
|
||||
} = data;
|
||||
const { rangeNode, iterateNode, baseNode, edges } = graphComponents;
|
||||
|
||||
// Upload the base layer, to be used as init image
|
||||
const baseFilename = `${uuidv4()}.png`;
|
||||
const maskFilename = `${uuidv4()}.png`;
|
||||
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
@ -66,6 +98,9 @@ export const addUserInvokedCanvasListener = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// Upload the mask layer image
|
||||
const maskFilename = `${uuidv4()}.png`;
|
||||
|
||||
if (baseNode.type === 'inpaint') {
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
@ -103,9 +138,12 @@ export const addUserInvokedCanvasListener = () => {
|
||||
dispatch(canvasGraphBuilt(graph));
|
||||
moduleLog({ data: graph }, 'Canvas graph built');
|
||||
|
||||
// Actually create the session
|
||||
dispatch(sessionCreated({ graph }));
|
||||
|
||||
// Wait for the session to be invoked (this is just the HTTP request to start processing)
|
||||
const [{ meta }] = await take(sessionInvoked.fulfilled.match);
|
||||
|
||||
const { sessionId } = meta.arg;
|
||||
|
||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Input,
|
||||
InputProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { stopPastePropagation } from 'common/util/stopPastePropagation';
|
||||
import { ChangeEvent, memo } from 'react';
|
||||
|
||||
interface IAIInputProps extends InputProps {
|
||||
@ -31,7 +32,7 @@ const IAIInput = (props: IAIInputProps) => {
|
||||
{...formControlProps}
|
||||
>
|
||||
{label !== '' && <FormLabel>{label}</FormLabel>}
|
||||
<Input {...rest} />
|
||||
<Input {...rest} onPaste={stopPastePropagation} />
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { stopPastePropagation } from 'common/util/stopPastePropagation';
|
||||
import { clamp } from 'lodash-es';
|
||||
|
||||
import { FocusEvent, memo, useEffect, useState } from 'react';
|
||||
@ -125,6 +126,7 @@ const IAINumberInput = (props: Props) => {
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleBlur}
|
||||
{...rest}
|
||||
onPaste={stopPastePropagation}
|
||||
>
|
||||
<NumberInputField {...numberInputFieldProps} />
|
||||
{showStepper && (
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react';
|
||||
import { stopPastePropagation } from 'common/util/stopPastePropagation';
|
||||
import { memo } from 'react';
|
||||
|
||||
const IAITextarea = forwardRef((props: TextareaProps, ref) => {
|
||||
return <Textarea ref={ref} onPaste={stopPastePropagation} {...props} />;
|
||||
});
|
||||
|
||||
export default memo(IAITextarea);
|
@ -1,4 +1,4 @@
|
||||
import { Box, useToast } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
@ -10,12 +10,33 @@ import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { filter, map, some } from 'lodash-es';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { ErrorCode } from 'react-dropzone';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector, activeTabNameSelector],
|
||||
(system, activeTabName) => {
|
||||
const { isConnected, isUploading } = system;
|
||||
|
||||
const isUploaderDisabled = !isConnected || isUploading;
|
||||
|
||||
return {
|
||||
isUploaderDisabled,
|
||||
activeTabName,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
type ImageUploaderProps = {
|
||||
children: ReactNode;
|
||||
@ -24,38 +45,49 @@ type ImageUploaderProps = {
|
||||
const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const { children } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const toast = useToast({});
|
||||
const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
const { setOpenUploader } = useImageUploader();
|
||||
const { setOpenUploaderFunction } = useImageUploader();
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
setIsHandlingUpload(true);
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
|
||||
''
|
||||
);
|
||||
toast({
|
||||
|
||||
toaster({
|
||||
title: t('toast.uploadFailed'),
|
||||
description: msg,
|
||||
description: rejection.errors.map((error) => error.message).join('\n'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[t, toast]
|
||||
[t, toaster]
|
||||
);
|
||||
|
||||
const fileAcceptedCallback = useCallback(
|
||||
async (file: File) => {
|
||||
dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
imageType: 'uploads',
|
||||
formData: { file },
|
||||
activeTabName,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, activeTabName]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
if (fileRejections.length > 1) {
|
||||
toaster({
|
||||
title: t('toast.uploadFailed'),
|
||||
description: t('toast.uploadFailedInvalidUploadDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
fileRejectionCallback(rejection);
|
||||
});
|
||||
@ -64,7 +96,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
fileAcceptedCallback(file);
|
||||
});
|
||||
},
|
||||
[fileAcceptedCallback, fileRejectionCallback]
|
||||
[t, toaster, fileAcceptedCallback, fileRejectionCallback]
|
||||
);
|
||||
|
||||
const {
|
||||
@ -73,92 +105,73 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
isDragActive,
|
||||
inputRef,
|
||||
open,
|
||||
} = useDropzone({
|
||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||
noClick: true,
|
||||
onDrop,
|
||||
onDragOver: () => setIsHandlingUpload(true),
|
||||
maxFiles: 1,
|
||||
disabled: isUploaderDisabled,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
setOpenUploader(open);
|
||||
|
||||
useEffect(() => {
|
||||
const pasteImageListener = (e: ClipboardEvent) => {
|
||||
const dataTransferItemList = e.clipboardData?.items;
|
||||
if (!dataTransferItemList) return;
|
||||
|
||||
const imageItems: Array<DataTransferItem> = [];
|
||||
|
||||
for (const item of dataTransferItemList) {
|
||||
if (
|
||||
item.kind === 'file' &&
|
||||
['image/png', 'image/jpg'].includes(item.type)
|
||||
) {
|
||||
imageItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageItems.length) return;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (imageItems.length > 1) {
|
||||
toast({
|
||||
description: t('toast.uploadFailedMultipleImagesDesc'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
// This is a hack to allow pasting images into the uploader
|
||||
const handlePaste = async (e: ClipboardEvent) => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = imageItems[0].getAsFile();
|
||||
|
||||
if (!file) {
|
||||
toast({
|
||||
description: t('toast.uploadFailedUnableToLoadDesc'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
if (e.clipboardData?.files) {
|
||||
// Set the files on the inputRef
|
||||
inputRef.current.files = e.clipboardData.files;
|
||||
// Dispatch the change event, dropzone catches this and we get to use its own validation
|
||||
inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
|
||||
};
|
||||
document.addEventListener('paste', pasteImageListener);
|
||||
|
||||
// Set the open function so we can open the uploader from anywhere
|
||||
setOpenUploaderFunction(open);
|
||||
|
||||
// Add the paste event listener
|
||||
document.addEventListener('paste', handlePaste);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('paste', pasteImageListener);
|
||||
document.removeEventListener('paste', handlePaste);
|
||||
setOpenUploaderFunction(() => {
|
||||
return;
|
||||
});
|
||||
};
|
||||
}, [t, dispatch, toast, activeTabName]);
|
||||
}, [inputRef, open, setOpenUploaderFunction]);
|
||||
|
||||
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
|
||||
activeTabName
|
||||
)
|
||||
? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`
|
||||
: ``;
|
||||
const overlaySecondaryText = useMemo(() => {
|
||||
if (['img2img', 'unifiedCanvas'].includes(activeTabName)) {
|
||||
return ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, [t, activeTabName]);
|
||||
|
||||
return (
|
||||
<ImageUploaderTriggerContext.Provider value={open}>
|
||||
<Box
|
||||
{...getRootProps({ style: {} })}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
// Bail out if user hits spacebar - do not open the uploader
|
||||
if (e.key === ' ') return;
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{isDragActive && isHandlingUpload && (
|
||||
<ImageUploadOverlay
|
||||
isDragAccept={isDragAccept}
|
||||
isDragReject={isDragReject}
|
||||
overlaySecondaryText={overlaySecondaryText}
|
||||
setIsHandlingUpload={setIsHandlingUpload}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ImageUploaderTriggerContext.Provider>
|
||||
<Box
|
||||
{...getRootProps({ style: {} })}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
// Bail out if user hits spacebar - do not open the uploader
|
||||
if (e.key === ' ') return;
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{isDragActive && isHandlingUpload && (
|
||||
<ImageUploadOverlay
|
||||
isDragAccept={isDragAccept}
|
||||
isDragReject={isDragReject}
|
||||
overlaySecondaryText={overlaySecondaryText}
|
||||
setIsHandlingUpload={setIsHandlingUpload}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Flex, Heading, Icon } from '@chakra-ui/react';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import { useContext } from 'react';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
|
||||
type ImageUploaderButtonProps = {
|
||||
@ -9,11 +8,7 @@ type ImageUploaderButtonProps = {
|
||||
|
||||
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
|
||||
const { styleClass } = props;
|
||||
const open = useContext(ImageUploaderTriggerContext);
|
||||
|
||||
const handleClickUpload = () => {
|
||||
open && open();
|
||||
};
|
||||
const { openUploader } = useImageUploader();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -26,7 +21,7 @@ const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
|
||||
className={styleClass}
|
||||
>
|
||||
<Flex
|
||||
onClick={handleClickUpload}
|
||||
onClick={openUploader}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
@ -1,19 +1,18 @@
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import IAIIconButton from './IAIIconButton';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
|
||||
const ImageUploaderIconButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const openImageUploader = useContext(ImageUploaderTriggerContext);
|
||||
const { openUploader } = useImageUploader();
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label={t('accessibility.uploadImage')}
|
||||
tooltip="Upload Image"
|
||||
icon={<FaUpload />}
|
||||
onClick={openImageUploader || undefined}
|
||||
onClick={openUploader}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -6,10 +6,12 @@ import { FaUndo, FaUpload } from 'react-icons/fa';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCallback } from 'react';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
|
||||
const InitialImageButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { openUploader } = useImageUploader();
|
||||
|
||||
const handleResetInitialImage = useCallback(() => {
|
||||
dispatch(clearInitialImage());
|
||||
@ -27,7 +29,11 @@ const InitialImageButtons = () => {
|
||||
aria-label={t('accessibility.reset')}
|
||||
onClick={handleResetInitialImage}
|
||||
/>
|
||||
<IAIIconButton icon={<FaUpload />} aria-label={t('common.upload')} />
|
||||
<IAIIconButton
|
||||
icon={<FaUpload />}
|
||||
onClick={openUploader}
|
||||
aria-label={t('common.upload')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
@ -24,7 +24,6 @@ const Loading = () => {
|
||||
height="24px !important"
|
||||
right="1.5rem"
|
||||
bottom="1.5rem"
|
||||
speed="1.2s"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WorkInProgress from './WorkInProgress';
|
||||
|
||||
export const PostProcessingWIP = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<WorkInProgress>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: '100%',
|
||||
h: '100%',
|
||||
gap: 4,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Heading>{t('common.postProcessing')}</Heading>
|
||||
<VStack maxW="50rem" gap={4}>
|
||||
<Text>{t('common.postProcessDesc1')}</Text>
|
||||
<Text>{t('common.postProcessDesc2')}</Text>
|
||||
<Text>{t('common.postProcessDesc3')}</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
</WorkInProgress>
|
||||
);
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WorkInProgress from './WorkInProgress';
|
||||
|
||||
export default function TrainingWIP() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<WorkInProgress>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: '100%',
|
||||
h: '100%',
|
||||
gap: 4,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Heading>{t('common.training')}</Heading>
|
||||
<VStack maxW="50rem" gap={4}>
|
||||
<Text>{t('common.trainingDesc1')}</Text>
|
||||
<Text>{t('common.trainingDesc2')}</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
</WorkInProgress>
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type WorkInProgressProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const WorkInProgress = (props: WorkInProgressProps) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: 'base.850',
|
||||
borderRadius: 'base',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkInProgress;
|
@ -1,35 +0,0 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
const watchers: {
|
||||
ref: RefObject<HTMLElement>;
|
||||
enable: boolean;
|
||||
callback: () => void;
|
||||
}[] = [];
|
||||
|
||||
const useClickOutsideWatcher = () => {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
watchers.forEach(({ ref, enable, callback }) => {
|
||||
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addWatcher: (watcher: {
|
||||
ref: RefObject<HTMLElement>;
|
||||
callback: () => void;
|
||||
enable: boolean;
|
||||
}) => {
|
||||
watchers.push(watcher);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useClickOutsideWatcher;
|
@ -1,13 +1,22 @@
|
||||
let openFunction: () => void;
|
||||
import { useCallback } from 'react';
|
||||
|
||||
let openUploader = () => {
|
||||
return;
|
||||
};
|
||||
|
||||
const useImageUploader = () => {
|
||||
return {
|
||||
setOpenUploader: (open?: () => void) => {
|
||||
if (open) {
|
||||
openFunction = open;
|
||||
const setOpenUploaderFunction = useCallback(
|
||||
(openUploaderFunction?: () => void) => {
|
||||
if (openUploaderFunction) {
|
||||
openUploader = openUploaderFunction;
|
||||
}
|
||||
},
|
||||
openUploader: openFunction,
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
setOpenUploaderFunction,
|
||||
openUploader,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function useUpdateTranslations(fn: () => void) {
|
||||
const { i18n } = useTranslation();
|
||||
const currentLang = localStorage.getItem('i18nextLng');
|
||||
|
||||
React.useEffect(() => {
|
||||
fn();
|
||||
}, [fn]);
|
||||
|
||||
React.useEffect(() => {
|
||||
i18n.on('languageChanged', () => {
|
||||
fn();
|
||||
});
|
||||
}, [fn, i18n, currentLang]);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { createIcon } from '@chakra-ui/react';
|
||||
|
||||
const ImageToImageIcon = createIcon({
|
||||
displayName: 'ImageToImageIcon',
|
||||
viewBox: '0 0 3543 3543',
|
||||
path: (
|
||||
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"
|
||||
/>
|
||||
</g>
|
||||
),
|
||||
defaultProps: {
|
||||
boxSize: '24px',
|
||||
},
|
||||
});
|
||||
export default ImageToImageIcon;
|
@ -1,19 +0,0 @@
|
||||
import { createIcon } from '@chakra-ui/react';
|
||||
|
||||
const NodesIcon = createIcon({
|
||||
displayName: 'NodesIcon',
|
||||
viewBox: '0 0 3543 3543',
|
||||
path: (
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"
|
||||
/>
|
||||
),
|
||||
defaultProps: {
|
||||
boxSize: '24px',
|
||||
},
|
||||
});
|
||||
|
||||
export default NodesIcon;
|
@ -1,19 +0,0 @@
|
||||
import { createIcon } from '@chakra-ui/react';
|
||||
|
||||
const PostprocessingIcon = createIcon({
|
||||
displayName: 'PostprocessingIcon',
|
||||
viewBox: '0 0 3543 3543',
|
||||
path: (
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z"
|
||||
/>
|
||||
),
|
||||
defaultProps: {
|
||||
boxSize: '24px',
|
||||
},
|
||||
});
|
||||
|
||||
export default PostprocessingIcon;
|
@ -1,19 +0,0 @@
|
||||
import { createIcon } from '@chakra-ui/react';
|
||||
|
||||
const TrainingIcon = createIcon({
|
||||
displayName: 'TrainingIcon',
|
||||
viewBox: '0 0 3544 3544',
|
||||
path: (
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"
|
||||
/>
|
||||
),
|
||||
defaultProps: {
|
||||
boxSize: '24px',
|
||||
},
|
||||
});
|
||||
|
||||
export default TrainingIcon;
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
|
||||
<path d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 8.9 KiB |
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 6.3 KiB |
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||
<path d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z" style="stroke:white;stroke-opacity:0;stroke-width:1px;"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 8.1 KiB |
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 3544 3544" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 9.8 KiB |
@ -1,348 +0,0 @@
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
|
||||
import { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||
import { GenerationState } from 'features/parameters/store/generationSlice';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
|
||||
import {
|
||||
CanvasState,
|
||||
isCanvasMaskLine,
|
||||
} from 'features/canvas/store/canvasTypes';
|
||||
import generateMask from 'features/canvas/util/generateMask';
|
||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import type {
|
||||
FacetoolType,
|
||||
UpscalingLevel,
|
||||
} from 'features/parameters/store/postprocessingSlice';
|
||||
import { PostprocessingState } from 'features/parameters/store/postprocessingSlice';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import openBase64ImageInTab from './openBase64ImageInTab';
|
||||
import randomInt from './randomInt';
|
||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||
import { getIsImageDataTransparent, getIsImageDataWhite } from './arrayBuffer';
|
||||
|
||||
export type FrontendToBackendParametersConfig = {
|
||||
generationMode: InvokeTabName;
|
||||
generationState: GenerationState;
|
||||
postprocessingState: PostprocessingState;
|
||||
canvasState: CanvasState;
|
||||
systemState: SystemState;
|
||||
imageToProcessUrl?: string;
|
||||
};
|
||||
|
||||
export type BackendGenerationParameters = {
|
||||
prompt: string;
|
||||
iterations: number;
|
||||
steps: number;
|
||||
cfg_scale: number;
|
||||
threshold: number;
|
||||
perlin: number;
|
||||
height: number;
|
||||
width: number;
|
||||
sampler_name: string;
|
||||
seed: number;
|
||||
progress_images: boolean;
|
||||
progress_latents: boolean;
|
||||
save_intermediates: number;
|
||||
generation_mode: InvokeTabName;
|
||||
init_mask: string;
|
||||
init_img?: string;
|
||||
fit?: boolean;
|
||||
seam_size?: number;
|
||||
seam_blur?: number;
|
||||
seam_strength?: number;
|
||||
seam_steps?: number;
|
||||
tile_size?: number;
|
||||
infill_method?: string;
|
||||
force_outpaint?: boolean;
|
||||
seamless?: boolean;
|
||||
hires_fix?: boolean;
|
||||
strength?: number;
|
||||
invert_mask?: boolean;
|
||||
inpaint_replace?: number;
|
||||
bounding_box?: Vector2d & Dimensions;
|
||||
inpaint_width?: number;
|
||||
inpaint_height?: number;
|
||||
with_variations?: Array<Array<number>>;
|
||||
variation_amount?: number;
|
||||
enable_image_debugging?: boolean;
|
||||
h_symmetry_time_pct?: number;
|
||||
v_symmetry_time_pct?: number;
|
||||
};
|
||||
|
||||
export type BackendEsrGanParameters = {
|
||||
level: UpscalingLevel;
|
||||
denoise_str: number;
|
||||
strength: number;
|
||||
};
|
||||
|
||||
export type BackendFacetoolParameters = {
|
||||
type: FacetoolType;
|
||||
strength: number;
|
||||
codeformer_fidelity?: number;
|
||||
};
|
||||
|
||||
export type BackendParameters = {
|
||||
generationParameters: BackendGenerationParameters;
|
||||
esrganParameters: false | BackendEsrGanParameters;
|
||||
facetoolParameters: false | BackendFacetoolParameters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates/formats frontend state into parameters suitable
|
||||
* for consumption by the API.
|
||||
*/
|
||||
export const frontendToBackendParameters = (
|
||||
config: FrontendToBackendParametersConfig
|
||||
): BackendParameters => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
const {
|
||||
generationMode,
|
||||
generationState,
|
||||
postprocessingState,
|
||||
canvasState,
|
||||
systemState,
|
||||
} = config;
|
||||
|
||||
const {
|
||||
codeformerFidelity,
|
||||
facetoolStrength,
|
||||
facetoolType,
|
||||
hiresFix,
|
||||
hiresStrength,
|
||||
shouldRunESRGAN,
|
||||
shouldRunFacetool,
|
||||
upscalingLevel,
|
||||
upscalingStrength,
|
||||
upscalingDenoising,
|
||||
} = postprocessingState;
|
||||
|
||||
const {
|
||||
cfgScale,
|
||||
height,
|
||||
img2imgStrength,
|
||||
infillMethod,
|
||||
initialImage,
|
||||
iterations,
|
||||
perlin,
|
||||
prompt,
|
||||
negativePrompt,
|
||||
sampler,
|
||||
seamBlur,
|
||||
seamless,
|
||||
seamSize,
|
||||
seamSteps,
|
||||
seamStrength,
|
||||
seed,
|
||||
seedWeights,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
shouldRandomizeSeed,
|
||||
steps,
|
||||
threshold,
|
||||
tileSize,
|
||||
variationAmount,
|
||||
width,
|
||||
shouldUseSymmetry,
|
||||
horizontalSymmetrySteps,
|
||||
verticalSymmetrySteps,
|
||||
} = generationState;
|
||||
|
||||
const {
|
||||
shouldDisplayInProgressType,
|
||||
saveIntermediatesInterval,
|
||||
enableImageDebugging,
|
||||
} = systemState;
|
||||
|
||||
const generationParameters: BackendGenerationParameters = {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfg_scale: cfgScale,
|
||||
threshold,
|
||||
perlin,
|
||||
height,
|
||||
width,
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||
save_intermediates: saveIntermediatesInterval,
|
||||
generation_mode: generationMode,
|
||||
init_mask: '',
|
||||
};
|
||||
|
||||
let esrganParameters: false | BackendEsrGanParameters = false;
|
||||
let facetoolParameters: false | BackendFacetoolParameters = false;
|
||||
|
||||
if (negativePrompt !== '') {
|
||||
generationParameters.prompt = `${prompt} [${negativePrompt}]`;
|
||||
}
|
||||
|
||||
generationParameters.seed = shouldRandomizeSeed
|
||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||
: seed;
|
||||
|
||||
// Symmetry Settings
|
||||
if (shouldUseSymmetry) {
|
||||
if (horizontalSymmetrySteps > 0) {
|
||||
generationParameters.h_symmetry_time_pct = Math.max(
|
||||
0,
|
||||
Math.min(1, horizontalSymmetrySteps / steps)
|
||||
);
|
||||
}
|
||||
|
||||
if (verticalSymmetrySteps > 0) {
|
||||
generationParameters.v_symmetry_time_pct = Math.max(
|
||||
0,
|
||||
Math.min(1, verticalSymmetrySteps / steps)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// txt2img exclusive parameters
|
||||
if (generationMode === 'txt2img') {
|
||||
generationParameters.hires_fix = hiresFix;
|
||||
|
||||
if (hiresFix) generationParameters.strength = hiresStrength;
|
||||
}
|
||||
|
||||
// parameters common to txt2img and img2img
|
||||
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||
generationParameters.seamless = seamless;
|
||||
|
||||
if (shouldRunESRGAN) {
|
||||
esrganParameters = {
|
||||
level: upscalingLevel,
|
||||
denoise_str: upscalingDenoising,
|
||||
strength: upscalingStrength,
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldRunFacetool) {
|
||||
facetoolParameters = {
|
||||
type: facetoolType,
|
||||
strength: facetoolStrength,
|
||||
};
|
||||
if (facetoolType === 'codeformer') {
|
||||
facetoolParameters.codeformer_fidelity = codeformerFidelity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// img2img exclusive parameters
|
||||
if (generationMode === 'img2img' && initialImage) {
|
||||
generationParameters.init_img =
|
||||
typeof initialImage === 'string' ? initialImage : initialImage.url;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = shouldFitToWidthHeight;
|
||||
}
|
||||
|
||||
// inpainting exclusive parameters
|
||||
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
|
||||
const {
|
||||
layerState: { objects },
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
stageScale,
|
||||
isMaskEnabled,
|
||||
shouldPreserveMaskedArea,
|
||||
boundingBoxScaleMethod: boundingBoxScale,
|
||||
scaledBoundingBoxDimensions,
|
||||
} = canvasState;
|
||||
|
||||
const boundingBox = {
|
||||
...boundingBoxCoordinates,
|
||||
...boundingBoxDimensions,
|
||||
};
|
||||
|
||||
const { dataURL: maskDataURL, imageData: maskImageData } = generateMask(
|
||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||
boundingBox
|
||||
);
|
||||
|
||||
generationParameters.init_mask = maskDataURL;
|
||||
|
||||
generationParameters.fit = false;
|
||||
|
||||
generationParameters.strength = img2imgStrength;
|
||||
|
||||
generationParameters.invert_mask = shouldPreserveMaskedArea;
|
||||
|
||||
generationParameters.bounding_box = boundingBox;
|
||||
|
||||
const tempScale = canvasBaseLayer.scale();
|
||||
|
||||
canvasBaseLayer.scale({
|
||||
x: 1 / stageScale,
|
||||
y: 1 / stageScale,
|
||||
});
|
||||
|
||||
const absPos = canvasBaseLayer.getAbsolutePosition();
|
||||
|
||||
const imageDataURL = canvasBaseLayer.toDataURL({
|
||||
x: boundingBox.x + absPos.x,
|
||||
y: boundingBox.y + absPos.y,
|
||||
width: boundingBox.width,
|
||||
height: boundingBox.height,
|
||||
});
|
||||
|
||||
const ctx = canvasBaseLayer.getContext();
|
||||
const imageData = ctx.getImageData(
|
||||
boundingBox.x + absPos.x,
|
||||
boundingBox.y + absPos.y,
|
||||
boundingBox.width,
|
||||
boundingBox.height
|
||||
);
|
||||
|
||||
const doesBaseHaveTransparency = getIsImageDataTransparent(imageData);
|
||||
const doesMaskHaveTransparency = getIsImageDataWhite(maskImageData);
|
||||
|
||||
if (enableImageDebugging) {
|
||||
openBase64ImageInTab([
|
||||
{ base64: maskDataURL, caption: 'mask sent as init_mask' },
|
||||
{ base64: imageDataURL, caption: 'image sent as init_img' },
|
||||
]);
|
||||
}
|
||||
|
||||
canvasBaseLayer.scale(tempScale);
|
||||
|
||||
generationParameters.init_img = imageDataURL;
|
||||
|
||||
generationParameters.progress_images = false;
|
||||
|
||||
if (boundingBoxScale !== 'none') {
|
||||
generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
|
||||
generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
|
||||
}
|
||||
|
||||
generationParameters.seam_size = seamSize;
|
||||
generationParameters.seam_blur = seamBlur;
|
||||
generationParameters.seam_strength = seamStrength;
|
||||
generationParameters.seam_steps = seamSteps;
|
||||
generationParameters.tile_size = tileSize;
|
||||
generationParameters.infill_method = infillMethod;
|
||||
generationParameters.force_outpaint = false;
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
generationParameters.variation_amount = variationAmount;
|
||||
if (seedWeights) {
|
||||
generationParameters.with_variations =
|
||||
stringToSeedWeightsArray(seedWeights);
|
||||
}
|
||||
} else {
|
||||
generationParameters.variation_amount = 0;
|
||||
}
|
||||
|
||||
if (enableImageDebugging) {
|
||||
generationParameters.enable_image_debugging = enableImageDebugging;
|
||||
}
|
||||
|
||||
return {
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
facetoolParameters,
|
||||
};
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
import { ClipboardEvent } from 'react';
|
||||
|
||||
export const stopPastePropagation = (e: ClipboardEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
@ -81,7 +81,7 @@ const IAICanvasResizer = () => {
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Spinner thickness="2px" speed="1s" size="xl" />
|
||||
<Spinner thickness="2px" size="xl" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
CanvasLayer,
|
||||
LAYER_NAMES_DICT,
|
||||
} from 'features/canvas/store/canvasTypes';
|
||||
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
@ -44,6 +43,12 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
|
||||
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||
import {
|
||||
canvasCopiedToClipboard,
|
||||
canvasDownloadedAsImage,
|
||||
canvasMerged,
|
||||
canvasSavedToGallery,
|
||||
} from 'features/canvas/store/actions';
|
||||
|
||||
export const selector = createSelector(
|
||||
[systemSelector, canvasSelector, isStagingSelector],
|
||||
@ -70,14 +75,8 @@ export const selector = createSelector(
|
||||
|
||||
const IAICanvasToolbar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
isProcessing,
|
||||
isStaging,
|
||||
isMaskEnabled,
|
||||
layer,
|
||||
tool,
|
||||
shouldCropToBoundingBoxOnSave,
|
||||
} = useAppSelector(selector);
|
||||
const { isProcessing, isStaging, isMaskEnabled, layer, tool } =
|
||||
useAppSelector(selector);
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -183,42 +182,19 @@ const IAICanvasToolbar = () => {
|
||||
};
|
||||
|
||||
const handleMergeVisible = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: false,
|
||||
shouldSetAsInitialImage: true,
|
||||
})
|
||||
);
|
||||
dispatch(canvasMerged());
|
||||
};
|
||||
|
||||
const handleSaveToGallery = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||
shouldSaveToGallery: true,
|
||||
})
|
||||
);
|
||||
dispatch(canvasSavedToGallery());
|
||||
};
|
||||
|
||||
const handleCopyImageToClipboard = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||
shouldCopy: true,
|
||||
})
|
||||
);
|
||||
dispatch(canvasCopiedToClipboard());
|
||||
};
|
||||
|
||||
const handleDownloadAsImage = () => {
|
||||
dispatch(
|
||||
mergeAndUploadCanvas({
|
||||
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||
shouldDownload: true,
|
||||
})
|
||||
);
|
||||
dispatch(canvasDownloadedAsImage());
|
||||
};
|
||||
|
||||
const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
|
13
invokeai/frontend/web/src/features/canvas/store/actions.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||
|
||||
export const canvasCopiedToClipboard = createAction(
|
||||
'canvas/canvasCopiedToClipboard'
|
||||
);
|
||||
|
||||
export const canvasDownloadedAsImage = createAction(
|
||||
'canvas/canvasDownloadedAsImage'
|
||||
);
|
||||
|
||||
export const canvasMerged = createAction('canvas/canvasMerged');
|
@ -3,18 +3,8 @@ import { CanvasState } from './canvasTypes';
|
||||
/**
|
||||
* Canvas slice persist denylist
|
||||
*/
|
||||
const itemsToDenylist: (keyof CanvasState)[] = [
|
||||
'cursorPosition',
|
||||
'isCanvasInitialized',
|
||||
'doesCanvasNeedScaling',
|
||||
];
|
||||
|
||||
export const canvasPersistDenylist: (keyof CanvasState)[] = [
|
||||
'cursorPosition',
|
||||
'isCanvasInitialized',
|
||||
'doesCanvasNeedScaling',
|
||||
];
|
||||
|
||||
export const canvasDenylist = itemsToDenylist.map(
|
||||
(denylistItem) => `canvas.${denylistItem}`
|
||||
);
|
||||
|
@ -1,172 +0,0 @@
|
||||
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
|
||||
import * as InvokeAI from 'app/types/invokeai';
|
||||
import { RootState } from 'app/store/store';
|
||||
// import { addImage } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
addToast,
|
||||
setCurrentStatus,
|
||||
setIsCancelable,
|
||||
setIsProcessing,
|
||||
setProcessingIndeterminateTask,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import i18n from 'i18n';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import copyImage from '../../util/copyImage';
|
||||
import downloadFile from '../../util/downloadFile';
|
||||
import { getCanvasBaseLayer } from '../../util/konvaInstanceProvider';
|
||||
import layerToDataURL from '../../util/layerToDataURL';
|
||||
import { setMergedCanvas } from '../canvasSlice';
|
||||
import { CanvasState } from '../canvasTypes';
|
||||
|
||||
type MergeAndUploadCanvasConfig = {
|
||||
cropVisible?: boolean;
|
||||
cropToBoundingBox?: boolean;
|
||||
shouldSaveToGallery?: boolean;
|
||||
shouldDownload?: boolean;
|
||||
shouldCopy?: boolean;
|
||||
shouldSetAsInitialImage?: boolean;
|
||||
};
|
||||
|
||||
const defaultConfig: MergeAndUploadCanvasConfig = {
|
||||
cropVisible: false,
|
||||
cropToBoundingBox: false,
|
||||
shouldSaveToGallery: false,
|
||||
shouldDownload: false,
|
||||
shouldCopy: false,
|
||||
shouldSetAsInitialImage: true,
|
||||
};
|
||||
|
||||
export const mergeAndUploadCanvas =
|
||||
(config = defaultConfig): ThunkAction<void, RootState, unknown, AnyAction> =>
|
||||
async (dispatch, getState) => {
|
||||
const {
|
||||
cropVisible,
|
||||
cropToBoundingBox,
|
||||
shouldSaveToGallery,
|
||||
shouldDownload,
|
||||
shouldCopy,
|
||||
shouldSetAsInitialImage,
|
||||
} = config;
|
||||
|
||||
dispatch(setProcessingIndeterminateTask('Exporting Image'));
|
||||
dispatch(setIsCancelable(false));
|
||||
|
||||
const state = getState() as RootState;
|
||||
|
||||
const {
|
||||
stageScale,
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
stageCoordinates,
|
||||
} = state.canvas as CanvasState;
|
||||
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(true));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
|
||||
canvasBaseLayer,
|
||||
stageScale,
|
||||
stageCoordinates,
|
||||
cropToBoundingBox
|
||||
? { ...boundingBoxCoordinates, ...boundingBoxDimensions }
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (!dataURL) {
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(true));
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
dataURL,
|
||||
filename: 'merged_canvas.png',
|
||||
kind: shouldSaveToGallery ? 'result' : 'temp',
|
||||
cropVisible,
|
||||
})
|
||||
);
|
||||
|
||||
const response = await fetch(`${window.location.origin}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const image = (await response.json()) as InvokeAI.ImageUploadResponse;
|
||||
|
||||
const { url, width, height } = image;
|
||||
|
||||
const newImage: InvokeAI._Image = {
|
||||
uuid: uuidv4(),
|
||||
category: shouldSaveToGallery ? 'result' : 'user',
|
||||
...image,
|
||||
};
|
||||
|
||||
if (shouldDownload) {
|
||||
downloadFile(url);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: i18n.t('toast.downloadImageStarted'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldCopy) {
|
||||
copyImage(url, width, height);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: i18n.t('toast.imageCopied'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldSaveToGallery) {
|
||||
dispatch(addImage({ image: newImage, category: 'result' }));
|
||||
dispatch(
|
||||
addToast({
|
||||
title: i18n.t('toast.imageSavedToGallery'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldSetAsInitialImage) {
|
||||
dispatch(
|
||||
setMergedCanvas({
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
...originalBoundingBox,
|
||||
image: newImage,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: i18n.t('toast.canvasMerged'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
|
||||
dispatch(setIsCancelable(true));
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
export const blobToDataURL = (blob: Blob): Promise<string> => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (_e) => resolve(reader.result as string);
|
||||
reader.onerror = (_e) => reject(reader.error);
|
||||
reader.onabort = (_e) => reject(new Error('Read aborted'));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
@ -9,8 +9,12 @@ const calculateCoordinates = (
|
||||
contentHeight: number,
|
||||
scale: number
|
||||
): Vector2d => {
|
||||
const x = containerWidth / 2 - (containerX + contentWidth / 2) * scale;
|
||||
const y = containerHeight / 2 - (containerY + contentHeight / 2) * scale;
|
||||
const x = Math.floor(
|
||||
containerWidth / 2 - (containerX + contentWidth / 2) * scale
|
||||
);
|
||||
const y = Math.floor(
|
||||
containerHeight / 2 - (containerY + contentHeight / 2) * scale
|
||||
);
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copies a blob to the clipboard by calling navigator.clipboard.write().
|
||||
*/
|
||||
export const copyBlobToClipboard = (blob: Blob) => {
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
]);
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copies an image to the clipboard by drawing it to a canvas and then
|
||||
* calling toBlob() on the canvas.
|
||||
*/
|
||||
const copyImage = (url: string, width: number, height: number) => {
|
||||
const imageElement = document.createElement('img');
|
||||
|
||||
imageElement.addEventListener('load', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) return;
|
||||
|
||||
context.drawImage(imageElement, 0, 0);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
blob &&
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
canvas.remove();
|
||||
imageElement.remove();
|
||||
});
|
||||
|
||||
imageElement.src = url;
|
||||
};
|
||||
|
||||
export default copyImage;
|
@ -0,0 +1,61 @@
|
||||
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
||||
import Konva from 'konva';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
|
||||
/**
|
||||
* Creates a stage from array of mask objects.
|
||||
* We cannot just convert the mask layer to a blob because it uses a texture with transparent areas.
|
||||
* So instead we create a new stage with the mask layer and composite it onto a white background.
|
||||
*/
|
||||
const createMaskStage = async (
|
||||
lines: CanvasMaskLine[],
|
||||
boundingBox: IRect
|
||||
): Promise<Konva.Stage> => {
|
||||
// create an offscreen canvas and add the mask to it
|
||||
const { width, height } = boundingBox;
|
||||
|
||||
const offscreenContainer = document.createElement('div');
|
||||
|
||||
const maskStage = new Konva.Stage({
|
||||
container: offscreenContainer,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
const baseLayer = new Konva.Layer();
|
||||
const maskLayer = new Konva.Layer();
|
||||
|
||||
// composite the image onto the mask layer
|
||||
baseLayer.add(
|
||||
new Konva.Rect({
|
||||
...boundingBox,
|
||||
fill: 'white',
|
||||
})
|
||||
);
|
||||
|
||||
lines.forEach((line) =>
|
||||
maskLayer.add(
|
||||
new Konva.Line({
|
||||
points: line.points,
|
||||
stroke: 'black',
|
||||
strokeWidth: line.strokeWidth * 2,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation:
|
||||
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
maskStage.add(baseLayer);
|
||||
maskStage.add(maskLayer);
|
||||
|
||||
// you'd think we can't do this until we finish with the maskStage, but we can
|
||||
offscreenContainer.remove();
|
||||
|
||||
return maskStage;
|
||||
};
|
||||
|
||||
export default createMaskStage;
|
@ -0,0 +1,11 @@
|
||||
/** Download a blob as a file */
|
||||
export const downloadBlob = (blob: Blob, fileName: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
a.remove();
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Downloads a file, given its URL.
|
||||
*/
|
||||
const downloadFile = (url: string) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
a.remove();
|
||||
};
|
||||
|
||||
export default downloadFile;
|
@ -1,170 +0,0 @@
|
||||
// import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
||||
// import Konva from 'konva';
|
||||
// import { Stage } from 'konva/lib/Stage';
|
||||
// import { IRect } from 'konva/lib/types';
|
||||
|
||||
// /**
|
||||
// * Generating a mask image from InpaintingCanvas.tsx is not as simple
|
||||
// * as calling toDataURL() on the canvas, because the mask may be represented
|
||||
// * by colored lines or transparency, or the user may have inverted the mask
|
||||
// * display.
|
||||
// *
|
||||
// * So we need to regenerate the mask image by creating an offscreen canvas,
|
||||
// * drawing the mask and compositing everything correctly to output a valid
|
||||
// * mask image.
|
||||
// */
|
||||
// export const getStageDataURL = (stage: Stage, boundingBox: IRect): string => {
|
||||
// // create an offscreen canvas and add the mask to it
|
||||
// // const { stage, offscreenContainer } = buildMaskStage(lines, boundingBox);
|
||||
|
||||
// const dataURL = stage.toDataURL({ ...boundingBox });
|
||||
|
||||
// // const imageData = stage
|
||||
// // .toCanvas()
|
||||
// // .getContext('2d')
|
||||
// // ?.getImageData(
|
||||
// // boundingBox.x,
|
||||
// // boundingBox.y,
|
||||
// // boundingBox.width,
|
||||
// // boundingBox.height
|
||||
// // );
|
||||
|
||||
// // offscreenContainer.remove();
|
||||
|
||||
// // return { dataURL, imageData };
|
||||
|
||||
// return dataURL;
|
||||
// };
|
||||
|
||||
// export const getStageImageData = (
|
||||
// stage: Stage,
|
||||
// boundingBox: IRect
|
||||
// ): ImageData | undefined => {
|
||||
// const imageData = stage
|
||||
// .toCanvas()
|
||||
// .getContext('2d')
|
||||
// ?.getImageData(
|
||||
// boundingBox.x,
|
||||
// boundingBox.y,
|
||||
// boundingBox.width,
|
||||
// boundingBox.height
|
||||
// );
|
||||
|
||||
// return imageData;
|
||||
// };
|
||||
|
||||
// export const buildMaskStage = (
|
||||
// lines: CanvasMaskLine[],
|
||||
// boundingBox: IRect
|
||||
// ): { stage: Stage; offscreenContainer: HTMLDivElement } => {
|
||||
// // create an offscreen canvas and add the mask to it
|
||||
// const { width, height } = boundingBox;
|
||||
|
||||
// const offscreenContainer = document.createElement('div');
|
||||
|
||||
// const stage = new Konva.Stage({
|
||||
// container: offscreenContainer,
|
||||
// width: width,
|
||||
// height: height,
|
||||
// });
|
||||
|
||||
// const baseLayer = new Konva.Layer();
|
||||
// const maskLayer = new Konva.Layer();
|
||||
|
||||
// // composite the image onto the mask layer
|
||||
// baseLayer.add(
|
||||
// new Konva.Rect({
|
||||
// ...boundingBox,
|
||||
// fill: 'white',
|
||||
// })
|
||||
// );
|
||||
|
||||
// lines.forEach((line) =>
|
||||
// maskLayer.add(
|
||||
// new Konva.Line({
|
||||
// points: line.points,
|
||||
// stroke: 'black',
|
||||
// strokeWidth: line.strokeWidth * 2,
|
||||
// tension: 0,
|
||||
// lineCap: 'round',
|
||||
// lineJoin: 'round',
|
||||
// shadowForStrokeEnabled: false,
|
||||
// globalCompositeOperation:
|
||||
// line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
// })
|
||||
// )
|
||||
// );
|
||||
|
||||
// stage.add(baseLayer);
|
||||
// stage.add(maskLayer);
|
||||
|
||||
// return { stage, offscreenContainer };
|
||||
// };
|
||||
|
||||
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
|
||||
import Konva from 'konva';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
|
||||
/**
|
||||
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
||||
* as calling toDataURL() on the canvas, because the mask may be represented
|
||||
* by colored lines or transparency, or the user may have inverted the mask
|
||||
* display.
|
||||
*
|
||||
* So we need to regenerate the mask image by creating an offscreen canvas,
|
||||
* drawing the mask and compositing everything correctly to output a valid
|
||||
* mask image.
|
||||
*/
|
||||
const generateMask = async (lines: CanvasMaskLine[], boundingBox: IRect) => {
|
||||
// create an offscreen canvas and add the mask to it
|
||||
const { width, height } = boundingBox;
|
||||
|
||||
const offscreenContainer = document.createElement('div');
|
||||
|
||||
const stage = new Konva.Stage({
|
||||
container: offscreenContainer,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
const baseLayer = new Konva.Layer();
|
||||
const maskLayer = new Konva.Layer();
|
||||
|
||||
// composite the image onto the mask layer
|
||||
baseLayer.add(
|
||||
new Konva.Rect({
|
||||
...boundingBox,
|
||||
fill: 'white',
|
||||
})
|
||||
);
|
||||
|
||||
lines.forEach((line) =>
|
||||
maskLayer.add(
|
||||
new Konva.Line({
|
||||
points: line.points,
|
||||
stroke: 'black',
|
||||
strokeWidth: line.strokeWidth * 2,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation:
|
||||
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
stage.add(baseLayer);
|
||||
stage.add(maskLayer);
|
||||
|
||||
const maskDataURL = stage.toDataURL(boundingBox);
|
||||
|
||||
const maskBlob = await canvasToBlob(stage.toCanvas(boundingBox));
|
||||
|
||||
offscreenContainer.remove();
|
||||
|
||||
return { maskDataURL, maskBlob };
|
||||
};
|
||||
|
||||
export default generateMask;
|
@ -0,0 +1,38 @@
|
||||
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
export const getBaseLayerBlob = async (
|
||||
state: RootState,
|
||||
withoutBoundingBox?: boolean
|
||||
) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
shouldCropToBoundingBoxOnSave,
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
} = state.canvas;
|
||||
|
||||
const clonedBaseLayer = canvasBaseLayer.clone();
|
||||
|
||||
clonedBaseLayer.scale({ x: 1, y: 1 });
|
||||
|
||||
const absPos = clonedBaseLayer.getAbsolutePosition();
|
||||
|
||||
const boundingBox =
|
||||
shouldCropToBoundingBoxOnSave && !withoutBoundingBox
|
||||
? {
|
||||
x: boundingBoxCoordinates.x + absPos.x,
|
||||
y: boundingBoxCoordinates.y + absPos.y,
|
||||
width: boundingBoxDimensions.width,
|
||||
height: boundingBoxDimensions.height,
|
||||
}
|
||||
: clonedBaseLayer.getClientRect();
|
||||
|
||||
return konvaNodeToBlob(clonedBaseLayer, boundingBox);
|
||||
};
|
@ -2,17 +2,15 @@ import { RootState } from 'app/store/store';
|
||||
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
|
||||
import { isCanvasMaskLine } from '../store/canvasTypes';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import {
|
||||
areAnyPixelsBlack,
|
||||
getImageDataTransparency,
|
||||
} from 'common/util/arrayBuffer';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import generateMask from './generateMask';
|
||||
import { dataURLToImageData } from './dataURLToImageData';
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
import createMaskStage from './createMaskStage';
|
||||
import { konvaNodeToImageData } from './konvaNodeToImageData';
|
||||
import { konvaNodeToBlob } from './konvaNodeToBlob';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'getCanvasDataURLs' });
|
||||
|
||||
/**
|
||||
* Gets Blob and ImageData objects for the base and mask layers
|
||||
*/
|
||||
export const getCanvasData = async (state: RootState) => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
const canvasStage = getCanvasStage();
|
||||
@ -26,11 +24,7 @@ export const getCanvasData = async (state: RootState) => {
|
||||
layerState: { objects },
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
stageScale,
|
||||
isMaskEnabled,
|
||||
shouldPreserveMaskedArea,
|
||||
boundingBoxScaleMethod: boundingBoxScale,
|
||||
scaledBoundingBoxDimensions,
|
||||
} = state.canvas;
|
||||
|
||||
const boundingBox = {
|
||||
@ -38,22 +32,14 @@ export const getCanvasData = async (state: RootState) => {
|
||||
...boundingBoxDimensions,
|
||||
};
|
||||
|
||||
// generationParameters.fit = false;
|
||||
// Clone the base layer so we don't affect the visible base layer
|
||||
const clonedBaseLayer = canvasBaseLayer.clone();
|
||||
|
||||
// generationParameters.strength = img2imgStrength;
|
||||
// Scale it to 100% so we get full resolution
|
||||
clonedBaseLayer.scale({ x: 1, y: 1 });
|
||||
|
||||
// generationParameters.invert_mask = shouldPreserveMaskedArea;
|
||||
|
||||
// generationParameters.bounding_box = boundingBox;
|
||||
|
||||
const tempScale = canvasBaseLayer.scale();
|
||||
|
||||
canvasBaseLayer.scale({
|
||||
x: 1 / stageScale,
|
||||
y: 1 / stageScale,
|
||||
});
|
||||
|
||||
const absPos = canvasBaseLayer.getAbsolutePosition();
|
||||
// absolute position is needed to get the bounding box coords relative to the base layer
|
||||
const absPos = clonedBaseLayer.getAbsolutePosition();
|
||||
|
||||
const offsetBoundingBox = {
|
||||
x: boundingBox.x + absPos.x,
|
||||
@ -62,67 +48,25 @@ export const getCanvasData = async (state: RootState) => {
|
||||
height: boundingBox.height,
|
||||
};
|
||||
|
||||
const baseDataURL = canvasBaseLayer.toDataURL(offsetBoundingBox);
|
||||
const baseBlob = await canvasToBlob(
|
||||
canvasBaseLayer.toCanvas(offsetBoundingBox)
|
||||
// For the base layer, use the offset boundingBox
|
||||
const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox);
|
||||
const baseImageData = await konvaNodeToImageData(
|
||||
clonedBaseLayer,
|
||||
offsetBoundingBox
|
||||
);
|
||||
|
||||
canvasBaseLayer.scale(tempScale);
|
||||
|
||||
const { maskDataURL, maskBlob } = await generateMask(
|
||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||
// For the mask layer, use the normal boundingBox
|
||||
const maskStage = await createMaskStage(
|
||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled
|
||||
boundingBox
|
||||
);
|
||||
|
||||
const baseImageData = await dataURLToImageData(
|
||||
baseDataURL,
|
||||
boundingBox.width,
|
||||
boundingBox.height
|
||||
);
|
||||
|
||||
const maskImageData = await dataURLToImageData(
|
||||
maskDataURL,
|
||||
boundingBox.width,
|
||||
boundingBox.height
|
||||
);
|
||||
|
||||
const {
|
||||
isPartiallyTransparent: baseIsPartiallyTransparent,
|
||||
isFullyTransparent: baseIsFullyTransparent,
|
||||
} = getImageDataTransparency(baseImageData.data);
|
||||
|
||||
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
|
||||
|
||||
if (state.system.enableImageDebugging) {
|
||||
openBase64ImageInTab([
|
||||
{ base64: maskDataURL, caption: 'mask b64' },
|
||||
{ base64: baseDataURL, caption: 'image b64' },
|
||||
]);
|
||||
}
|
||||
|
||||
// generationParameters.init_img = imageDataURL;
|
||||
// generationParameters.progress_images = false;
|
||||
|
||||
// if (boundingBoxScale !== 'none') {
|
||||
// generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
|
||||
// generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
|
||||
// }
|
||||
|
||||
// generationParameters.seam_size = seamSize;
|
||||
// generationParameters.seam_blur = seamBlur;
|
||||
// generationParameters.seam_strength = seamStrength;
|
||||
// generationParameters.seam_steps = seamSteps;
|
||||
// generationParameters.tile_size = tileSize;
|
||||
// generationParameters.infill_method = infillMethod;
|
||||
// generationParameters.force_outpaint = false;
|
||||
const maskBlob = await konvaNodeToBlob(maskStage, boundingBox);
|
||||
const maskImageData = await konvaNodeToImageData(maskStage, boundingBox);
|
||||
|
||||
return {
|
||||
baseDataURL,
|
||||
baseBlob,
|
||||
maskDataURL,
|
||||
baseImageData,
|
||||
maskBlob,
|
||||
baseIsPartiallyTransparent,
|
||||
baseIsFullyTransparent,
|
||||
doesMaskHaveBlackPixels,
|
||||
maskImageData,
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,31 @@
|
||||
import {
|
||||
areAnyPixelsBlack,
|
||||
getImageDataTransparency,
|
||||
} from 'common/util/arrayBuffer';
|
||||
|
||||
export const getCanvasGenerationMode = (
|
||||
baseImageData: ImageData,
|
||||
maskImageData: ImageData
|
||||
) => {
|
||||
const {
|
||||
isPartiallyTransparent: baseIsPartiallyTransparent,
|
||||
isFullyTransparent: baseIsFullyTransparent,
|
||||
} = getImageDataTransparency(baseImageData.data);
|
||||
|
||||
// check mask for black
|
||||
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
|
||||
|
||||
if (baseIsPartiallyTransparent) {
|
||||
if (baseIsFullyTransparent) {
|
||||
return 'txt2img';
|
||||
}
|
||||
|
||||
return 'outpaint';
|
||||
} else {
|
||||
if (doesMaskHaveBlackPixels) {
|
||||
return 'inpaint';
|
||||
}
|
||||
|
||||
return 'img2img';
|
||||
}
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import Konva from 'konva';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
|
||||
/**
|
||||
* Converts a Konva node to a Blob
|
||||
* @param node - The Konva node to convert to a Blob
|
||||
* @param boundingBox - The bounding box to crop to
|
||||
* @returns A Promise that resolves with Blob of the node cropped to the bounding box
|
||||
*/
|
||||
export const konvaNodeToBlob = async (
|
||||
node: Konva.Node,
|
||||
boundingBox: IRect
|
||||
): Promise<Blob> => {
|
||||
return await canvasToBlob(node.toCanvas(boundingBox));
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import Konva from 'konva';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
|
||||
/**
|
||||
* Converts a Konva node to a dataURL
|
||||
* @param node - The Konva node to convert to a dataURL
|
||||
* @param boundingBox - The bounding box to crop to
|
||||
* @returns A dataURL of the node cropped to the bounding box
|
||||
*/
|
||||
export const konvaNodeToDataURL = (
|
||||
node: Konva.Node,
|
||||
boundingBox: IRect
|
||||
): string => {
|
||||
// get a dataURL of the bbox'd region
|
||||
return node.toDataURL(boundingBox);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import Konva from 'konva';
|
||||
import { IRect } from 'konva/lib/types';
|
||||
import { dataURLToImageData } from './dataURLToImageData';
|
||||
|
||||
/**
|
||||
* Converts a Konva node to an ImageData object
|
||||
* @param node - The Konva node to convert to an ImageData object
|
||||
* @param boundingBox - The bounding box to crop to
|
||||
* @returns A Promise that resolves with ImageData object of the node cropped to the bounding box
|
||||
*/
|
||||
export const konvaNodeToImageData = async (
|
||||
node: Konva.Node,
|
||||
boundingBox: IRect
|
||||
): Promise<ImageData> => {
|
||||
// get a dataURL of the bbox'd region
|
||||
const dataURL = node.toDataURL(boundingBox);
|
||||
|
||||
return await dataURLToImageData(
|
||||
dataURL,
|
||||
boundingBox.width,
|
||||
boundingBox.height
|
||||
);
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
import Konva from 'konva';
|
||||
import { IRect, Vector2d } from 'konva/lib/types';
|
||||
|
||||
const layerToDataURL = (
|
||||
layer: Konva.Layer,
|
||||
stageScale: number,
|
||||
stageCoordinates: Vector2d,
|
||||
boundingBox?: IRect
|
||||
) => {
|
||||
const tempScale = layer.scale();
|
||||
|
||||
const relativeClientRect = layer.getClientRect({
|
||||
relativeTo: layer.getParent(),
|
||||
});
|
||||
|
||||
// Scale the canvas before getting it as a Blob
|
||||
layer.scale({
|
||||
x: 1 / stageScale,
|
||||
y: 1 / stageScale,
|
||||
});
|
||||
|
||||
const { x, y, width, height } = layer.getClientRect();
|
||||
const dataURLBoundingBox = boundingBox
|
||||
? {
|
||||
x: boundingBox.x + stageCoordinates.x,
|
||||
y: boundingBox.y + stageCoordinates.y,
|
||||
width: boundingBox.width,
|
||||
height: boundingBox.height,
|
||||
}
|
||||
: {
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
const dataURL = layer.toDataURL(dataURLBoundingBox);
|
||||
|
||||
// Unscale the canvas
|
||||
layer.scale(tempScale);
|
||||
|
||||
return {
|
||||
dataURL,
|
||||
boundingBox: {
|
||||
x: relativeClientRect.x,
|
||||
y: relativeClientRect.y,
|
||||
width: width,
|
||||
height: height,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default layerToDataURL;
|
@ -5,15 +5,8 @@ import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
FlexProps,
|
||||
IconButton,
|
||||
Link,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItemOption,
|
||||
MenuList,
|
||||
MenuOptionGroup,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
@ -54,10 +47,7 @@ import {
|
||||
FaTrash,
|
||||
FaWrench,
|
||||
} from 'react-icons/fa';
|
||||
import {
|
||||
gallerySelector,
|
||||
selectedImageSelector,
|
||||
} from '../store/gallerySelectors';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { useCallback } from 'react';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
@ -70,6 +60,7 @@ import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceR
|
||||
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
|
||||
import { allParametersSet } from 'features/parameters/store/generationSlice';
|
||||
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[
|
||||
@ -79,15 +70,15 @@ const currentImageButtonsSelector = createSelector(
|
||||
uiSelector,
|
||||
lightboxSelector,
|
||||
activeTabNameSelector,
|
||||
selectedImageSelector,
|
||||
],
|
||||
(system, gallery, postprocessing, ui, lightbox, activeTabName, image) => {
|
||||
(system, gallery, postprocessing, ui, lightbox, activeTabName) => {
|
||||
const {
|
||||
isProcessing,
|
||||
isConnected,
|
||||
isGFPGANAvailable,
|
||||
isESRGANAvailable,
|
||||
shouldConfirmOnDelete,
|
||||
progressImage,
|
||||
} = system;
|
||||
|
||||
const { upscalingLevel, facetoolStrength } = postprocessing;
|
||||
@ -96,7 +87,7 @@ const currentImageButtonsSelector = createSelector(
|
||||
|
||||
const { shouldShowImageDetails, shouldHidePreview } = ui;
|
||||
|
||||
const { intermediateImage, currentImage } = gallery;
|
||||
const { selectedImage } = gallery;
|
||||
|
||||
return {
|
||||
canDeleteImage: isConnected && !isProcessing,
|
||||
@ -107,15 +98,14 @@ const currentImageButtonsSelector = createSelector(
|
||||
isESRGANAvailable,
|
||||
upscalingLevel,
|
||||
facetoolStrength,
|
||||
shouldDisableToolbarButtons: Boolean(intermediateImage) || !currentImage,
|
||||
currentImage,
|
||||
shouldDisableToolbarButtons: Boolean(progressImage) || !selectedImage,
|
||||
shouldShowImageDetails,
|
||||
activeTabName,
|
||||
isLightboxOpen,
|
||||
shouldHidePreview,
|
||||
image,
|
||||
seed: image?.metadata?.invokeai?.node?.seed,
|
||||
prompt: image?.metadata?.invokeai?.node?.prompt,
|
||||
image: selectedImage,
|
||||
seed: selectedImage?.metadata?.invokeai?.node?.seed,
|
||||
prompt: selectedImage?.metadata?.invokeai?.node?.prompt,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -164,7 +154,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
onClose: onDeleteDialogClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const toast = useToast();
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { recallPrompt, recallSeed, recallAllParameters } = useParameters();
|
||||
@ -213,7 +203,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
const url = getImageUrl();
|
||||
|
||||
if (!url) {
|
||||
toast({
|
||||
toaster({
|
||||
title: t('toast.problemCopyingImageLink'),
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
@ -224,14 +214,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toast({
|
||||
toaster({
|
||||
title: t('toast.imageLinkCopied'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
}, [toast, shouldTransformUrls, getUrl, t, image]);
|
||||
}, [toaster, shouldTransformUrls, getUrl, t, image]);
|
||||
|
||||
const handlePreviewVisibility = useCallback(() => {
|
||||
dispatch(setShouldHidePreview(!shouldHidePreview));
|
||||
@ -346,13 +336,13 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
dispatch(setActiveTab('unifiedCanvas'));
|
||||
}
|
||||
|
||||
toast({
|
||||
toaster({
|
||||
title: t('toast.sentToUnifiedCanvas'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}, [image, isLightboxOpen, dispatch, activeTabName, toast, t]);
|
||||
}, [image, isLightboxOpen, dispatch, activeTabName, toaster, t]);
|
||||
|
||||
useHotkeys(
|
||||
'i',
|
||||
@ -360,7 +350,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
if (image) {
|
||||
handleClickShowImageDetails();
|
||||
} else {
|
||||
toast({
|
||||
toaster({
|
||||
title: t('toast.metadataLoadFailed'),
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
@ -368,7 +358,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[image, shouldShowImageDetails]
|
||||
[image, shouldShowImageDetails, toaster]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
|
@ -4,18 +4,18 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { selectedImageSelector } from '../store/gallerySelectors';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
|
||||
export const currentImageDisplaySelector = createSelector(
|
||||
[systemSelector, selectedImageSelector],
|
||||
(system, selectedImage) => {
|
||||
[systemSelector, gallerySelector],
|
||||
(system, gallery) => {
|
||||
const { progressImage } = system;
|
||||
|
||||
return {
|
||||
hasAnImageToDisplay: selectedImage || progressImage,
|
||||
hasAnImageToDisplay: gallery.selectedImage || progressImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
@ -8,11 +8,13 @@ import { isEqual } from 'lodash-es';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
||||
import CurrentImageHidden from './CurrentImageHidden';
|
||||
import { DragEvent, memo, useCallback } from 'react';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import ImageFallbackSpinner from './ImageFallbackSpinner';
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { configSelector } from '../../system/store/configSelectors';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { imageSelected } from '../store/gallerySlice';
|
||||
|
||||
export const imagesSelector = createSelector(
|
||||
[uiSelector, gallerySelector, systemSelector],
|
||||
@ -49,7 +51,10 @@ const CurrentImagePreview = () => {
|
||||
shouldShowProgressInViewer,
|
||||
shouldAntialiasProgressImage,
|
||||
} = useAppSelector(imagesSelector);
|
||||
const { shouldFetchImages } = useAppSelector(configSelector);
|
||||
const { getUrl } = useGetUrl();
|
||||
const toaster = useAppToaster();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
@ -63,6 +68,17 @@ const CurrentImagePreview = () => {
|
||||
[image]
|
||||
);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
dispatch(imageSelected());
|
||||
if (shouldFetchImages) {
|
||||
toaster({
|
||||
title: 'Something went wrong, please refresh',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [dispatch, toaster, shouldFetchImages]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@ -104,6 +120,7 @@ const CurrentImagePreview = () => {
|
||||
position: 'absolute',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
<ImageMetadataOverlay image={image} />
|
||||
</>
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
// selectNextImage,
|
||||
// selectPrevImage,
|
||||
setGalleryImageMinimumWidth,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -23,20 +19,7 @@ import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvas
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
// const GALLERY_TAB_WIDTHS: Record<
|
||||
// InvokeTabName,
|
||||
// { galleryMinWidth: number; galleryMaxWidth: number }
|
||||
// > = {
|
||||
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// generate: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
|
||||
// nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// };
|
||||
|
||||
const galleryPanelSelector = createSelector(
|
||||
const selector = createSelector(
|
||||
[
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
@ -76,41 +59,13 @@ const GalleryDrawer = () => {
|
||||
// isStaging,
|
||||
// isResizable,
|
||||
// isLightboxOpen,
|
||||
} = useAppSelector(galleryPanelSelector);
|
||||
|
||||
// const handleSetShouldPinGallery = () => {
|
||||
// dispatch(togglePinGalleryPanel());
|
||||
// dispatch(requestCanvasRescale());
|
||||
// };
|
||||
|
||||
// const handleToggleGallery = () => {
|
||||
// dispatch(toggleGalleryPanel());
|
||||
// shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
// };
|
||||
} = useAppSelector(selector);
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
// const resolution = useResolution();
|
||||
|
||||
// useHotkeys(
|
||||
// 'g',
|
||||
// () => {
|
||||
// handleToggleGallery();
|
||||
// },
|
||||
// [shouldPinGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'shift+g',
|
||||
// () => {
|
||||
// handleSetShouldPinGallery();
|
||||
// },
|
||||
// [shouldPinGallery]
|
||||
// );
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
@ -155,54 +110,6 @@ const GalleryDrawer = () => {
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
// const calcGalleryMinHeight = () => {
|
||||
// if (resolution === 'desktop') return;
|
||||
// return 300;
|
||||
// };
|
||||
|
||||
// const imageGalleryContent = () => {
|
||||
// return (
|
||||
// <Flex
|
||||
// w="100vw"
|
||||
// h={{ base: 300, xl: '100vh' }}
|
||||
// paddingRight={{ base: 8, xl: 0 }}
|
||||
// paddingBottom={{ base: 4, xl: 0 }}
|
||||
// >
|
||||
// <ImageGalleryContent />
|
||||
// </Flex>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const resizableImageGalleryContent = () => {
|
||||
// return (
|
||||
// <ResizableDrawer
|
||||
// direction="right"
|
||||
// isResizable={isResizable || !shouldPinGallery}
|
||||
// isOpen={shouldShowGallery}
|
||||
// onClose={handleCloseGallery}
|
||||
// isPinned={shouldPinGallery && !isLightboxOpen}
|
||||
// minWidth={
|
||||
// shouldPinGallery
|
||||
// ? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth
|
||||
// : 200
|
||||
// }
|
||||
// maxWidth={
|
||||
// shouldPinGallery
|
||||
// ? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth
|
||||
// : undefined
|
||||
// }
|
||||
// minHeight={calcGalleryMinHeight()}
|
||||
// >
|
||||
// <ImageGalleryContent />
|
||||
// </ResizableDrawer>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const renderImageGallery = () => {
|
||||
// if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent();
|
||||
// return resizableImageGalleryContent();
|
||||
// };
|
||||
|
||||
if (shouldPinGallery) {
|
||||
return null;
|
||||
}
|
||||
@ -218,8 +125,6 @@ const GalleryDrawer = () => {
|
||||
<ImageGalleryContent />
|
||||
</ResizableDrawer>
|
||||
);
|
||||
|
||||
// return renderImageGallery();
|
||||
};
|
||||
|
||||
export default memo(GalleryDrawer);
|
@ -62,7 +62,10 @@ const GalleryProgressImage = () => {
|
||||
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
|
||||
}}
|
||||
/>
|
||||
<Spinner sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }} />
|
||||
<Spinner
|
||||
sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }}
|
||||
speed="1.2s"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
MenuItem,
|
||||
MenuList,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
@ -35,6 +34,7 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useParameters } from 'features/parameters/hooks/useParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { requestedImageDeletion } from '../store/actions';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
|
||||
export const selector = createSelector(
|
||||
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
|
||||
@ -101,7 +101,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
|
||||
const toast = useToast();
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -176,7 +176,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
dispatch(setActiveTab('unifiedCanvas'));
|
||||
}
|
||||
|
||||
toast({
|
||||
toaster({
|
||||
title: t('toast.sentToUnifiedCanvas'),
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
|
@ -1,442 +0,0 @@
|
||||
// import { NumberSize, Resizable } from 're-resizable';
|
||||
|
||||
// import {
|
||||
// Box,
|
||||
// ButtonGroup,
|
||||
// Flex,
|
||||
// Grid,
|
||||
// Icon,
|
||||
// chakra,
|
||||
// useTheme,
|
||||
// } from '@chakra-ui/react';
|
||||
// import { requestImages } from 'app/socketio/actions';
|
||||
// import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
// import IAIButton from 'common/components/IAIButton';
|
||||
// import IAICheckbox from 'common/components/IAICheckbox';
|
||||
// import IAIIconButton from 'common/components/IAIIconButton';
|
||||
// import IAIPopover from 'common/components/IAIPopover';
|
||||
// import IAISlider from 'common/components/IAISlider';
|
||||
// import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
// import { imageGallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
// import {
|
||||
// selectNextImage,
|
||||
// selectPrevImage,
|
||||
// setCurrentCategory,
|
||||
// setGalleryImageMinimumWidth,
|
||||
// setGalleryImageObjectFit,
|
||||
// setGalleryWidth,
|
||||
// setShouldAutoSwitchToNewImages,
|
||||
// setShouldHoldGalleryOpen,
|
||||
// setShouldUseSingleGalleryColumn,
|
||||
// } from 'features/gallery/store/gallerySlice';
|
||||
// import {
|
||||
// setShouldPinGallery,
|
||||
// setShouldShowGallery,
|
||||
// } from 'features/ui/store/uiSlice';
|
||||
// import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
|
||||
// import { clamp } from 'lodash-es';
|
||||
// import { Direction } from 're-resizable/lib/resizer';
|
||||
// import React, {
|
||||
// ChangeEvent,
|
||||
// useCallback,
|
||||
// useEffect,
|
||||
// useRef,
|
||||
// useState,
|
||||
// } from 'react';
|
||||
// import { useHotkeys } from 'react-hotkeys-hook';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
// import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
// import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
||||
// import { MdPhotoLibrary } from 'react-icons/md';
|
||||
// import { CSSTransition } from 'react-transition-group';
|
||||
// import HoverableImage from './HoverableImage';
|
||||
// import { APP_GALLERY_HEIGHT_PINNED } from 'theme/util/constants';
|
||||
|
||||
// import './ImageGallery.css';
|
||||
// import { no_scrollbar } from 'theme/components/scrollbar';
|
||||
// import ImageGalleryContent from './ImageGalleryContent';
|
||||
|
||||
// const ChakraResizeable = chakra(Resizable, {
|
||||
// shouldForwardProp: (prop) => !['sx'].includes(prop),
|
||||
// });
|
||||
|
||||
// const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
|
||||
// const GALLERY_IMAGE_WIDTH_OFFSET = 40;
|
||||
|
||||
// const GALLERY_TAB_WIDTHS: Record<
|
||||
// InvokeTabName,
|
||||
// { galleryMinWidth: number; galleryMaxWidth: number }
|
||||
// > = {
|
||||
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
|
||||
// nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// postprocess: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
|
||||
// };
|
||||
|
||||
// const LIGHTBOX_GALLERY_WIDTH = 400;
|
||||
|
||||
// export default function ImageGallery() {
|
||||
// const dispatch = useAppDispatch();
|
||||
// const { direction } = useTheme();
|
||||
|
||||
// const { t } = useTranslation();
|
||||
|
||||
// const {
|
||||
// images,
|
||||
// currentCategory,
|
||||
// currentImageUuid,
|
||||
// shouldPinGallery,
|
||||
// shouldShowGallery,
|
||||
// galleryImageMinimumWidth,
|
||||
// galleryGridTemplateColumns,
|
||||
// activeTabName,
|
||||
// galleryImageObjectFit,
|
||||
// shouldHoldGalleryOpen,
|
||||
// shouldAutoSwitchToNewImages,
|
||||
// areMoreImagesAvailable,
|
||||
// galleryWidth,
|
||||
// isLightboxOpen,
|
||||
// isStaging,
|
||||
// shouldEnableResize,
|
||||
// shouldUseSingleGalleryColumn,
|
||||
// } = useAppSelector(imageGallerySelector);
|
||||
|
||||
// const { galleryMinWidth, galleryMaxWidth } = isLightboxOpen
|
||||
// ? {
|
||||
// galleryMinWidth: LIGHTBOX_GALLERY_WIDTH,
|
||||
// galleryMaxWidth: LIGHTBOX_GALLERY_WIDTH,
|
||||
// }
|
||||
// : GALLERY_TAB_WIDTHS[activeTabName];
|
||||
|
||||
// const [shouldShowButtons, setShouldShowButtons] = useState<boolean>(
|
||||
// galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH
|
||||
// );
|
||||
|
||||
// const [isResizing, setIsResizing] = useState(false);
|
||||
// const [galleryResizeHeight, setGalleryResizeHeight] = useState(0);
|
||||
|
||||
// const galleryRef = useRef<HTMLDivElement>(null);
|
||||
// const galleryContainerRef = useRef<HTMLDivElement>(null);
|
||||
// const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// setShouldShowButtons(galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH);
|
||||
// }, [galleryWidth]);
|
||||
|
||||
// const handleSetShouldPinGallery = () => {
|
||||
// !shouldPinGallery && dispatch(setShouldShowGallery(true));
|
||||
// dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||
// dispatch(setDoesCanvasNeedScaling(true));
|
||||
// };
|
||||
|
||||
// const handleToggleGallery = () => {
|
||||
// shouldShowGallery ? handleCloseGallery() : handleOpenGallery();
|
||||
// };
|
||||
|
||||
// const handleOpenGallery = () => {
|
||||
// dispatch(setShouldShowGallery(true));
|
||||
// shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true));
|
||||
// };
|
||||
|
||||
// const handleCloseGallery = useCallback(() => {
|
||||
// dispatch(setShouldShowGallery(false));
|
||||
// dispatch(setShouldHoldGalleryOpen(false));
|
||||
// setTimeout(
|
||||
// () => shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true)),
|
||||
// 400
|
||||
// );
|
||||
// }, [dispatch, shouldPinGallery]);
|
||||
|
||||
// const handleClickLoadMore = () => {
|
||||
// dispatch(requestImages(currentCategory));
|
||||
// };
|
||||
|
||||
// const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
// dispatch(setGalleryImageMinimumWidth(v));
|
||||
// };
|
||||
|
||||
// const setCloseGalleryTimer = () => {
|
||||
// if (shouldHoldGalleryOpen) return;
|
||||
// timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
|
||||
// };
|
||||
|
||||
// const cancelCloseGalleryTimer = () => {
|
||||
// timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
// };
|
||||
|
||||
// useHotkeys(
|
||||
// 'g',
|
||||
// () => {
|
||||
// handleToggleGallery();
|
||||
// },
|
||||
// [shouldShowGallery, shouldPinGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'left',
|
||||
// () => {
|
||||
// dispatch(selectPrevImage());
|
||||
// },
|
||||
// {
|
||||
// enabled: !isStaging || activeTabName !== 'unifiedCanvas',
|
||||
// },
|
||||
// [isStaging]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'right',
|
||||
// () => {
|
||||
// dispatch(selectNextImage());
|
||||
// },
|
||||
// {
|
||||
// enabled: !isStaging || activeTabName !== 'unifiedCanvas',
|
||||
// },
|
||||
// [isStaging]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'shift+g',
|
||||
// () => {
|
||||
// handleSetShouldPinGallery();
|
||||
// },
|
||||
// [shouldPinGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'esc',
|
||||
// () => {
|
||||
// dispatch(setShouldShowGallery(false));
|
||||
// },
|
||||
// {
|
||||
// enabled: () => !shouldPinGallery,
|
||||
// preventDefault: true,
|
||||
// },
|
||||
// [shouldPinGallery]
|
||||
// );
|
||||
|
||||
// const IMAGE_SIZE_STEP = 32;
|
||||
|
||||
// useHotkeys(
|
||||
// 'shift+up',
|
||||
// () => {
|
||||
// if (galleryImageMinimumWidth < 256) {
|
||||
// const newMinWidth = clamp(
|
||||
// galleryImageMinimumWidth + IMAGE_SIZE_STEP,
|
||||
// 32,
|
||||
// 256
|
||||
// );
|
||||
// dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
// }
|
||||
// },
|
||||
// [galleryImageMinimumWidth]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'shift+down',
|
||||
// () => {
|
||||
// if (galleryImageMinimumWidth > 32) {
|
||||
// const newMinWidth = clamp(
|
||||
// galleryImageMinimumWidth - IMAGE_SIZE_STEP,
|
||||
// 32,
|
||||
// 256
|
||||
// );
|
||||
// dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
// }
|
||||
// },
|
||||
// [galleryImageMinimumWidth]
|
||||
// );
|
||||
|
||||
// useEffect(() => {
|
||||
// function handleClickOutside(e: MouseEvent) {
|
||||
// if (
|
||||
// !shouldPinGallery &&
|
||||
// galleryRef.current &&
|
||||
// !galleryRef.current.contains(e.target as Node)
|
||||
// ) {
|
||||
// handleCloseGallery();
|
||||
// }
|
||||
// }
|
||||
// document.addEventListener('mousedown', handleClickOutside);
|
||||
// return () => {
|
||||
// document.removeEventListener('mousedown', handleClickOutside);
|
||||
// };
|
||||
// }, [handleCloseGallery, shouldPinGallery]);
|
||||
|
||||
// return (
|
||||
// <CSSTransition
|
||||
// nodeRef={galleryRef}
|
||||
// in={shouldShowGallery || shouldHoldGalleryOpen}
|
||||
// unmountOnExit
|
||||
// timeout={200}
|
||||
// classNames={`${direction}-image-gallery-css-transition`}
|
||||
// >
|
||||
// <Box
|
||||
// className={`${direction}-image-gallery-css-transition`}
|
||||
// sx={
|
||||
// shouldPinGallery
|
||||
// ? { zIndex: 1, insetInlineEnd: 0 }
|
||||
// : {
|
||||
// zIndex: 100,
|
||||
// position: 'fixed',
|
||||
// height: '100vh',
|
||||
// top: 0,
|
||||
// insetInlineEnd: 0,
|
||||
// }
|
||||
// }
|
||||
// ref={galleryRef}
|
||||
// onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||
// onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
||||
// onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
||||
// >
|
||||
// <ChakraResizeable
|
||||
// sx={{
|
||||
// padding: 4,
|
||||
// display: 'flex',
|
||||
// flexDirection: 'column',
|
||||
// rowGap: 4,
|
||||
// borderRadius: shouldPinGallery ? 'base' : 0,
|
||||
// borderInlineStartWidth: 5,
|
||||
// // boxShadow: '0 0 1rem blackAlpha.700',
|
||||
// bg: 'base.850',
|
||||
// borderColor: 'base.700',
|
||||
// }}
|
||||
// minWidth={galleryMinWidth}
|
||||
// maxWidth={shouldPinGallery ? galleryMaxWidth : window.innerWidth}
|
||||
// data-pinned={shouldPinGallery}
|
||||
// handleStyles={
|
||||
// direction === 'rtl'
|
||||
// ? {
|
||||
// right: {
|
||||
// width: '15px',
|
||||
// },
|
||||
// }
|
||||
// : {
|
||||
// left: {
|
||||
// width: '15px',
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// enable={
|
||||
// direction === 'rtl'
|
||||
// ? {
|
||||
// right: shouldEnableResize,
|
||||
// }
|
||||
// : {
|
||||
// left: shouldEnableResize,
|
||||
// }
|
||||
// }
|
||||
// size={{
|
||||
// width: galleryWidth,
|
||||
// height: shouldPinGallery ? '100%' : '100vh',
|
||||
// }}
|
||||
// onResizeStart={(
|
||||
// _event:
|
||||
// | React.MouseEvent<HTMLElement>
|
||||
// | React.TouchEvent<HTMLElement>,
|
||||
// _direction: Direction,
|
||||
// elementRef: HTMLElement
|
||||
// ) => {
|
||||
// setGalleryResizeHeight(elementRef.clientHeight);
|
||||
// elementRef.style.height = `${elementRef.clientHeight}px`;
|
||||
// if (shouldPinGallery) {
|
||||
// elementRef.style.position = 'fixed';
|
||||
// elementRef.style.insetInlineEnd = '1rem';
|
||||
// setIsResizing(true);
|
||||
// }
|
||||
// }}
|
||||
// onResizeStop={(
|
||||
// _event: MouseEvent | TouchEvent,
|
||||
// _direction: Direction,
|
||||
// elementRef: HTMLElement,
|
||||
// delta: NumberSize
|
||||
// ) => {
|
||||
// const newWidth = shouldPinGallery
|
||||
// ? clamp(
|
||||
// Number(galleryWidth) + delta.width,
|
||||
// galleryMinWidth,
|
||||
// Number(galleryMaxWidth)
|
||||
// )
|
||||
// : Number(galleryWidth) + delta.width;
|
||||
// dispatch(setGalleryWidth(newWidth));
|
||||
|
||||
// elementRef.removeAttribute('data-resize-alert');
|
||||
|
||||
// if (shouldPinGallery) {
|
||||
// console.log('unpin');
|
||||
// elementRef.style.position = 'relative';
|
||||
// elementRef.style.removeProperty('inset-inline-end');
|
||||
// elementRef.style.setProperty(
|
||||
// 'height',
|
||||
// shouldPinGallery ? '100%' : '100vh'
|
||||
// );
|
||||
// setIsResizing(false);
|
||||
// dispatch(setDoesCanvasNeedScaling(true));
|
||||
// }
|
||||
// }}
|
||||
// onResize={(
|
||||
// _event: MouseEvent | TouchEvent,
|
||||
// _direction: Direction,
|
||||
// elementRef: HTMLElement,
|
||||
// delta: NumberSize
|
||||
// ) => {
|
||||
// const newWidth = clamp(
|
||||
// Number(galleryWidth) + delta.width,
|
||||
// galleryMinWidth,
|
||||
// Number(
|
||||
// shouldPinGallery ? galleryMaxWidth : 0.95 * window.innerWidth
|
||||
// )
|
||||
// );
|
||||
|
||||
// if (
|
||||
// newWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH &&
|
||||
// !shouldShowButtons
|
||||
// ) {
|
||||
// setShouldShowButtons(true);
|
||||
// } else if (
|
||||
// newWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH &&
|
||||
// shouldShowButtons
|
||||
// ) {
|
||||
// setShouldShowButtons(false);
|
||||
// }
|
||||
|
||||
// if (
|
||||
// galleryImageMinimumWidth >
|
||||
// newWidth - GALLERY_IMAGE_WIDTH_OFFSET
|
||||
// ) {
|
||||
// dispatch(
|
||||
// setGalleryImageMinimumWidth(
|
||||
// newWidth - GALLERY_IMAGE_WIDTH_OFFSET
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (shouldPinGallery) {
|
||||
// if (newWidth >= galleryMaxWidth) {
|
||||
// elementRef.setAttribute('data-resize-alert', 'true');
|
||||
// } else {
|
||||
// elementRef.removeAttribute('data-resize-alert');
|
||||
// }
|
||||
// }
|
||||
|
||||
// elementRef.style.height = `${galleryResizeHeight}px`;
|
||||
// }}
|
||||
// >
|
||||
// <ImageGalleryContent />
|
||||
// </ChakraResizeable>
|
||||
// {isResizing && (
|
||||
// <Box
|
||||
// style={{
|
||||
// width: `${galleryWidth}px`,
|
||||
// height: '100%',
|
||||
// }}
|
||||
// />
|
||||
// )}
|
||||
// </Box>
|
||||
// </CSSTransition>
|
||||
// );
|
||||
// }
|
||||
|
||||
export default {};
|
@ -15,10 +15,7 @@ import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import {
|
||||
gallerySelector,
|
||||
imageGallerySelector,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
setCurrentCategory,
|
||||
setGalleryImageMinimumWidth,
|
||||
@ -57,11 +54,12 @@ import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
import { Image as ImageType } from 'app/types/invokeai';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import GalleryProgressImage from './GalleryProgressImage';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
|
||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
|
||||
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
|
||||
|
||||
const selector = createSelector(
|
||||
const categorySelector = createSelector(
|
||||
[(state: RootState) => state],
|
||||
(state) => {
|
||||
const { results, uploads, system, gallery } = state;
|
||||
@ -92,6 +90,33 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const mainSelector = createSelector(
|
||||
[gallerySelector, uiSelector],
|
||||
(gallery, ui) => {
|
||||
const {
|
||||
currentCategory,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
} = gallery;
|
||||
|
||||
const { shouldPinGallery } = ui;
|
||||
|
||||
return {
|
||||
currentCategory,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const ImageGalleryContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
@ -113,7 +138,6 @@ const ImageGalleryContent = () => {
|
||||
});
|
||||
|
||||
const {
|
||||
// images,
|
||||
currentCategory,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
@ -121,10 +145,10 @@ const ImageGalleryContent = () => {
|
||||
shouldAutoSwitchToNewImages,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
} = useAppSelector(imageGallerySelector);
|
||||
} = useAppSelector(mainSelector);
|
||||
|
||||
const { images, areMoreImagesAvailable, isLoading } =
|
||||
useAppSelector(selector);
|
||||
useAppSelector(categorySelector);
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
if (currentCategory === 'results') {
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
setHeight,
|
||||
setImg2imgStrength,
|
||||
setPerlin,
|
||||
setSampler,
|
||||
setScheduler,
|
||||
setSeamless,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
@ -202,9 +202,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
)}
|
||||
{node.scheduler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
label="Scheduler"
|
||||
value={node.scheduler}
|
||||
onClick={() => dispatch(setSampler(node.scheduler))}
|
||||
onClick={() => dispatch(setScheduler(node.scheduler))}
|
||||
/>
|
||||
)}
|
||||
{node.steps && (
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { gallerySelector } from '../store/gallerySelectors';
|
||||
|
||||
const selector = createSelector(gallerySelector, (gallery) => ({
|
||||
resultImages: gallery.categories.result.images,
|
||||
userImages: gallery.categories.user.images,
|
||||
}));
|
||||
|
||||
const useGetImageByUuid = () => {
|
||||
const { resultImages, userImages } = useAppSelector(selector);
|
||||
|
||||
return (uuid: string) => {
|
||||
const resultImagesResult = resultImages.find(
|
||||
(image) => image.uuid === uuid
|
||||
);
|
||||
if (resultImagesResult) {
|
||||
return resultImagesResult;
|
||||
}
|
||||
|
||||
const userImagesResult = userImages.find((image) => image.uuid === uuid);
|
||||
if (userImagesResult) {
|
||||
return userImagesResult;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default useGetImageByUuid;
|
@ -3,16 +3,7 @@ import { GalleryState } from './gallerySlice';
|
||||
/**
|
||||
* Gallery slice persist denylist
|
||||
*/
|
||||
const itemsToDenylist: (keyof GalleryState)[] = [
|
||||
'currentCategory',
|
||||
'shouldAutoSwitchToNewImages',
|
||||
];
|
||||
|
||||
export const galleryPersistDenylist: (keyof GalleryState)[] = [
|
||||
'currentCategory',
|
||||
'shouldAutoSwitchToNewImages',
|
||||
];
|
||||
|
||||
export const galleryDenylist = itemsToDenylist.map(
|
||||
(denylistItem) => `gallery.${denylistItem}`
|
||||
);
|
||||
|
@ -1,83 +1,3 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { selectResultsById, selectResultsEntities } from './resultsSlice';
|
||||
import { selectUploadsAll, selectUploadsById } from './uploadsSlice';
|
||||
|
||||
export const gallerySelector = (state: RootState) => state.gallery;
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state,
|
||||
gallerySelector,
|
||||
uiSelector,
|
||||
lightboxSelector,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(state, gallery, ui, lightbox, activeTabName) => {
|
||||
const {
|
||||
currentCategory,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
galleryWidth,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
} = gallery;
|
||||
|
||||
const { shouldPinGallery } = ui;
|
||||
|
||||
const { isLightboxOpen } = lightbox;
|
||||
|
||||
const images =
|
||||
currentCategory === 'results'
|
||||
? selectResultsEntities(state)
|
||||
: selectUploadsAll(state);
|
||||
|
||||
return {
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
galleryGridTemplateColumns: shouldUseSingleGalleryColumn
|
||||
? 'auto'
|
||||
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||
shouldAutoSwitchToNewImages,
|
||||
currentCategory,
|
||||
images,
|
||||
galleryWidth,
|
||||
shouldEnableResize:
|
||||
isLightboxOpen ||
|
||||
(activeTabName === 'unifiedCanvas' && shouldPinGallery)
|
||||
? false
|
||||
: true,
|
||||
shouldUseSingleGalleryColumn,
|
||||
selectedImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const selectedImageSelector = createSelector(
|
||||
[(state: RootState) => state, gallerySelector],
|
||||
(state, gallery) => {
|
||||
const selectedImage = gallery.selectedImage;
|
||||
|
||||
if (selectedImage?.type === 'results') {
|
||||
return selectResultsById(state, selectedImage.name);
|
||||
}
|
||||
|
||||
if (selectedImage?.type === 'uploads') {
|
||||
return selectUploadsById(state, selectedImage.name);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -10,14 +10,10 @@ import {
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
export interface GalleryState {
|
||||
/**
|
||||
* The selected image
|
||||
*/
|
||||
selectedImage?: Image;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
galleryWidth: number;
|
||||
shouldUseSingleGalleryColumn: boolean;
|
||||
currentCategory: 'results' | 'uploads';
|
||||
}
|
||||
@ -26,7 +22,6 @@ export const initialGalleryState: GalleryState = {
|
||||
galleryImageMinimumWidth: 64,
|
||||
galleryImageObjectFit: 'cover',
|
||||
shouldAutoSwitchToNewImages: true,
|
||||
galleryWidth: 300,
|
||||
shouldUseSingleGalleryColumn: false,
|
||||
currentCategory: 'results',
|
||||
};
|
||||
@ -58,9 +53,6 @@ export const gallerySlice = createSlice({
|
||||
) => {
|
||||
state.currentCategory = action.payload;
|
||||
},
|
||||
setGalleryWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryWidth = action.payload;
|
||||
},
|
||||
setShouldUseSingleGalleryColumn: (
|
||||
state,
|
||||
action: PayloadAction<boolean>
|
||||
@ -93,24 +85,28 @@ export const gallerySlice = createSlice({
|
||||
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
|
||||
// rehydrate selectedImage URL when results list comes in
|
||||
// solves case when outdated URL is in local storage
|
||||
if (state.selectedImage) {
|
||||
const selectedImage = state.selectedImage;
|
||||
if (selectedImage) {
|
||||
const selectedImageInResults = action.payload.items.find(
|
||||
(image) => image.image_name === state.selectedImage!.name
|
||||
(image) => image.image_name === selectedImage.name
|
||||
);
|
||||
if (selectedImageInResults) {
|
||||
state.selectedImage.url = selectedImageInResults.image_url;
|
||||
selectedImage.url = selectedImageInResults.image_url;
|
||||
state.selectedImage = selectedImage;
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
|
||||
// rehydrate selectedImage URL when results list comes in
|
||||
// solves case when outdated URL is in local storage
|
||||
if (state.selectedImage) {
|
||||
const selectedImage = state.selectedImage;
|
||||
if (selectedImage) {
|
||||
const selectedImageInResults = action.payload.items.find(
|
||||
(image) => image.image_name === state.selectedImage!.name
|
||||
(image) => image.image_name === selectedImage.name
|
||||
);
|
||||
if (selectedImageInResults) {
|
||||
state.selectedImage.url = selectedImageInResults.image_url;
|
||||
selectedImage.url = selectedImageInResults.image_url;
|
||||
state.selectedImage = selectedImage;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -122,7 +118,6 @@ export const {
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setGalleryWidth,
|
||||
setShouldUseSingleGalleryColumn,
|
||||
setCurrentCategory,
|
||||
} = gallerySlice.actions;
|
||||
|