mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'development' into main
This commit is contained in:
commit
74fad5f6ed
5
.github/workflows/create-caches.yml
vendored
5
.github/workflows/create-caches.yml
vendored
@ -54,9 +54,10 @@ jobs:
|
||||
[[ -d models/ldm/stable-diffusion-v1 ]] \
|
||||
|| mkdir -p models/ldm/stable-diffusion-v1
|
||||
[[ -r models/ldm/stable-diffusion-v1/model.ckpt ]] \
|
||||
|| curl --user "${{ secrets.HUGGINGFACE_TOKEN }}" \
|
||||
|| curl \
|
||||
-H "Authorization: Bearer ${{ secrets.HUGGINGFACE_TOKEN }}" \
|
||||
-o models/ldm/stable-diffusion-v1/model.ckpt \
|
||||
-O -L https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt
|
||||
-L https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt
|
||||
|
||||
- name: Activate Conda Env
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
|
8
.github/workflows/test-invoke-conda.yml
vendored
8
.github/workflows/test-invoke-conda.yml
vendored
@ -14,16 +14,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
stable-diffusion-model: ['https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt']
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
environment-file: environment.yml
|
||||
default-shell: bash -l {0}
|
||||
stable-diffusion-model: https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt
|
||||
- os: macos-latest
|
||||
environment-file: environment-mac.yml
|
||||
default-shell: bash -l {0}
|
||||
stable-diffusion-model: https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt
|
||||
name: Test invoke.py on ${{ matrix.os }} with conda
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
@ -71,9 +70,10 @@ jobs:
|
||||
[[ -d models/ldm/stable-diffusion-v1 ]] \
|
||||
|| mkdir -p models/ldm/stable-diffusion-v1
|
||||
[[ -r models/ldm/stable-diffusion-v1/model.ckpt ]] \
|
||||
|| curl --user "${{ secrets.HUGGINGFACE_TOKEN }}" \
|
||||
|| curl \
|
||||
-H "Authorization: Bearer ${{ secrets.HUGGINGFACE_TOKEN }}" \
|
||||
-o models/ldm/stable-diffusion-v1/model.ckpt \
|
||||
-O -L ${{ matrix.stable-diffusion-model }}
|
||||
-L ${{ matrix.stable-diffusion-model }}
|
||||
|
||||
- name: Activate Conda Env
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,822 +0,0 @@
|
||||
import mimetypes
|
||||
import transformers
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
import eventlet
|
||||
import glob
|
||||
import shlex
|
||||
import math
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
sys.path.append(".")
|
||||
|
||||
from argparse import ArgumentTypeError
|
||||
from modules.create_cmd_parser import create_cmd_parser
|
||||
|
||||
parser = create_cmd_parser()
|
||||
opt = parser.parse_args()
|
||||
|
||||
|
||||
from flask_socketio import SocketIO
|
||||
from flask import Flask, send_from_directory, url_for, jsonify
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from pytorch_lightning import logging
|
||||
from threading import Event
|
||||
from uuid import uuid4
|
||||
from send2trash import send2trash
|
||||
|
||||
|
||||
from ldm.generate import Generate
|
||||
from ldm.invoke.restoration import Restoration
|
||||
from ldm.invoke.pngwriter import PngWriter, retrieve_metadata
|
||||
from ldm.invoke.args import APP_ID, APP_VERSION, calculate_init_img_hash
|
||||
from ldm.invoke.prompt_parser import split_weighted_subprompts
|
||||
|
||||
from modules.parameters import parameters_to_command
|
||||
|
||||
|
||||
"""
|
||||
USER CONFIG
|
||||
"""
|
||||
if opt.cors and "*" in opt.cors:
|
||||
raise ArgumentTypeError('"*" is not an allowed CORS origin')
|
||||
|
||||
|
||||
output_dir = "outputs/" # Base output directory for images
|
||||
host = opt.host # Web & socket.io host
|
||||
port = opt.port # Web & socket.io port
|
||||
verbose = opt.verbose # enables copious socket.io logging
|
||||
precision = opt.precision
|
||||
free_gpu_mem = opt.free_gpu_mem
|
||||
embedding_path = opt.embedding_path
|
||||
additional_allowed_origins = (
|
||||
opt.cors if opt.cors else []
|
||||
) # additional CORS allowed origins
|
||||
model = "stable-diffusion-1.4"
|
||||
|
||||
"""
|
||||
END USER CONFIG
|
||||
"""
|
||||
|
||||
|
||||
print("* Initializing, be patient...\n")
|
||||
|
||||
|
||||
"""
|
||||
SERVER SETUP
|
||||
"""
|
||||
|
||||
|
||||
# fix missing mimetypes on windows due to registry wonkiness
|
||||
mimetypes.add_type("application/javascript", ".js")
|
||||
mimetypes.add_type("text/css", ".css")
|
||||
|
||||
app = Flask(__name__, static_url_path="", static_folder="../frontend/dist/")
|
||||
|
||||
|
||||
app.config["OUTPUTS_FOLDER"] = "../outputs"
|
||||
|
||||
|
||||
@app.route("/outputs/<path:filename>")
|
||||
def outputs(filename):
|
||||
return send_from_directory(app.config["OUTPUTS_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
def serve(path):
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
|
||||
|
||||
logger = True if verbose else False
|
||||
engineio_logger = True if verbose else False
|
||||
|
||||
# default 1,000,000, needs to be higher for socketio to accept larger images
|
||||
max_http_buffer_size = 10000000
|
||||
|
||||
cors_allowed_origins = [f"http://{host}:{port}"] + additional_allowed_origins
|
||||
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
logger=logger,
|
||||
engineio_logger=engineio_logger,
|
||||
max_http_buffer_size=max_http_buffer_size,
|
||||
cors_allowed_origins=cors_allowed_origins,
|
||||
ping_interval=(50, 50),
|
||||
ping_timeout=60,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
END SERVER SETUP
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
APP SETUP
|
||||
"""
|
||||
|
||||
|
||||
class CanceledException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
gfpgan, codeformer, esrgan = None, None, None
|
||||
from ldm.invoke.restoration.base import Restoration
|
||||
|
||||
restoration = Restoration()
|
||||
gfpgan, codeformer = restoration.load_face_restore_models()
|
||||
esrgan = restoration.load_esrgan()
|
||||
|
||||
# coreformer.process(self, image, strength, device, seed=None, fidelity=0.75)
|
||||
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
print(">> You may need to install the ESRGAN and/or GFPGAN modules")
|
||||
|
||||
canceled = Event()
|
||||
|
||||
# reduce logging outputs to error
|
||||
transformers.logging.set_verbosity_error()
|
||||
logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
|
||||
|
||||
# Initialize and load model
|
||||
generate = Generate(
|
||||
model,
|
||||
precision=precision,
|
||||
embedding_path=embedding_path,
|
||||
)
|
||||
generate.free_gpu_mem = free_gpu_mem
|
||||
generate.load_model()
|
||||
|
||||
|
||||
# location for "finished" images
|
||||
result_path = os.path.join(output_dir, "img-samples/")
|
||||
|
||||
# temporary path for intermediates
|
||||
intermediate_path = os.path.join(result_path, "intermediates/")
|
||||
|
||||
# path for user-uploaded init images and masks
|
||||
init_image_path = os.path.join(result_path, "init-images/")
|
||||
mask_image_path = os.path.join(result_path, "mask-images/")
|
||||
|
||||
# txt log
|
||||
log_path = os.path.join(result_path, "invoke_log.txt")
|
||||
|
||||
# make all output paths
|
||||
[
|
||||
os.makedirs(path, exist_ok=True)
|
||||
for path in [result_path, intermediate_path, init_image_path, mask_image_path]
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
END APP SETUP
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
SOCKET.IO LISTENERS
|
||||
"""
|
||||
|
||||
|
||||
@socketio.on("requestSystemConfig")
|
||||
def handle_request_capabilities():
|
||||
print(f">> System config requested")
|
||||
config = get_system_config()
|
||||
socketio.emit("systemConfig", config)
|
||||
|
||||
|
||||
@socketio.on("requestImages")
|
||||
def handle_request_images(page=1, offset=0, last_mtime=None):
|
||||
chunk_size = 50
|
||||
|
||||
if last_mtime:
|
||||
print(f">> Latest images requested")
|
||||
else:
|
||||
print(
|
||||
f">> Page {page} of images requested (page size {chunk_size} offset {offset})"
|
||||
)
|
||||
|
||||
paths = glob.glob(os.path.join(result_path, "*.png"))
|
||||
sorted_paths = sorted(paths, key=lambda x: os.path.getmtime(x), reverse=True)
|
||||
|
||||
if last_mtime:
|
||||
image_paths = filter(lambda x: os.path.getmtime(x) > last_mtime, sorted_paths)
|
||||
else:
|
||||
|
||||
image_paths = sorted_paths[
|
||||
slice(chunk_size * (page - 1) + offset, chunk_size * page + offset)
|
||||
]
|
||||
page = page + 1
|
||||
|
||||
image_array = []
|
||||
|
||||
for path in image_paths:
|
||||
metadata = retrieve_metadata(path)
|
||||
image_array.append(
|
||||
{
|
||||
"url": path,
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata["sd-metadata"],
|
||||
}
|
||||
)
|
||||
|
||||
socketio.emit(
|
||||
"galleryImages",
|
||||
{
|
||||
"images": image_array,
|
||||
"nextPage": page,
|
||||
"offset": offset,
|
||||
"onlyNewImages": True if last_mtime else False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@socketio.on("generateImage")
|
||||
def handle_generate_image_event(
|
||||
generation_parameters, esrgan_parameters, gfpgan_parameters
|
||||
):
|
||||
print(
|
||||
f">> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nGFPGAN parameters: {gfpgan_parameters}"
|
||||
)
|
||||
generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
||||
|
||||
|
||||
@socketio.on("runESRGAN")
|
||||
def handle_run_esrgan_event(original_image, esrgan_parameters):
|
||||
print(
|
||||
f'>> ESRGAN upscale requested for "{original_image["url"]}": {esrgan_parameters}'
|
||||
)
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": 1,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": 1,
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = Image.open(original_image["url"])
|
||||
|
||||
seed = (
|
||||
original_image["metadata"]["seed"]
|
||||
if "seed" in original_image["metadata"]
|
||||
else "unknown_seed"
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Upscaling"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = esrgan.process(
|
||||
image=image,
|
||||
upsampler_scale=esrgan_parameters["upscale"][0],
|
||||
strength=esrgan_parameters["upscale"][1],
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
esrgan_parameters["seed"] = seed
|
||||
metadata = parameters_to_post_processed_image_metadata(
|
||||
parameters=esrgan_parameters,
|
||||
original_image_path=original_image["url"],
|
||||
type="esrgan",
|
||||
)
|
||||
command = parameters_to_command(esrgan_parameters)
|
||||
|
||||
path = save_image(image, command, metadata, result_path, postprocessing="esrgan")
|
||||
|
||||
write_log_message(f'[Upscaled] "{original_image["url"]}" > "{path}": {command}')
|
||||
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["isProcessing"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
"esrganResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@socketio.on("runGFPGAN")
|
||||
def handle_run_gfpgan_event(original_image, gfpgan_parameters):
|
||||
print(
|
||||
f'>> GFPGAN face fix requested for "{original_image["url"]}": {gfpgan_parameters}'
|
||||
)
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": 1,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": 1,
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = Image.open(original_image["url"])
|
||||
|
||||
seed = (
|
||||
original_image["metadata"]["seed"]
|
||||
if "seed" in original_image["metadata"]
|
||||
else "unknown_seed"
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Fixing faces"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = gfpgan.process(
|
||||
image=image, strength=gfpgan_parameters["facetool_strength"], seed=seed
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
gfpgan_parameters["seed"] = seed
|
||||
metadata = parameters_to_post_processed_image_metadata(
|
||||
parameters=gfpgan_parameters,
|
||||
original_image_path=original_image["url"],
|
||||
type="gfpgan",
|
||||
)
|
||||
command = parameters_to_command(gfpgan_parameters)
|
||||
|
||||
path = save_image(image, command, metadata, result_path, postprocessing="gfpgan")
|
||||
|
||||
write_log_message(f'[Fixed faces] "{original_image["url"]}" > "{path}": {command}')
|
||||
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["isProcessing"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
"gfpganResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"mtime": os.path.mtime(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@socketio.on("cancel")
|
||||
def handle_cancel():
|
||||
print(f">> Cancel processing requested")
|
||||
canceled.set()
|
||||
socketio.emit("processingCanceled")
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on("deleteImage")
|
||||
def handle_delete_image(path, uuid):
|
||||
print(f'>> Delete requested "{path}"')
|
||||
send2trash(path)
|
||||
socketio.emit("imageDeleted", {"url": path, "uuid": uuid})
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on("uploadInitialImage")
|
||||
def handle_upload_initial_image(bytes, name):
|
||||
print(f'>> Init image upload requested "{name}"')
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
file_path = os.path.join(init_image_path, name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
newFile = open(file_path, "wb")
|
||||
newFile.write(bytes)
|
||||
socketio.emit("initialImageUploaded", {"url": file_path, "uuid": ""})
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on("uploadMaskImage")
|
||||
def handle_upload_mask_image(bytes, name):
|
||||
print(f'>> Mask image upload requested "{name}"')
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
file_path = os.path.join(mask_image_path, name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
newFile = open(file_path, "wb")
|
||||
newFile.write(bytes)
|
||||
socketio.emit("maskImageUploaded", {"url": file_path, "uuid": ""})
|
||||
|
||||
|
||||
"""
|
||||
END SOCKET.IO LISTENERS
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
ADDITIONAL FUNCTIONS
|
||||
"""
|
||||
|
||||
|
||||
def get_system_config():
|
||||
return {
|
||||
"model": "stable diffusion",
|
||||
"model_id": model,
|
||||
"model_hash": generate.model_hash,
|
||||
"app_id": APP_ID,
|
||||
"app_version": APP_VERSION,
|
||||
}
|
||||
|
||||
|
||||
def parameters_to_post_processed_image_metadata(parameters, original_image_path, type):
|
||||
# top-level metadata minus `image` or `images`
|
||||
metadata = get_system_config()
|
||||
|
||||
orig_hash = calculate_init_img_hash(original_image_path)
|
||||
|
||||
image = {"orig_path": original_image_path, "orig_hash": orig_hash}
|
||||
|
||||
if type == "esrgan":
|
||||
image["type"] = "esrgan"
|
||||
image["scale"] = parameters["upscale"][0]
|
||||
image["strength"] = parameters["upscale"][1]
|
||||
elif type == "gfpgan":
|
||||
image["type"] = "gfpgan"
|
||||
image["strength"] = parameters["facetool_strength"]
|
||||
else:
|
||||
raise TypeError(f"Invalid type: {type}")
|
||||
|
||||
metadata["image"] = image
|
||||
return metadata
|
||||
|
||||
|
||||
def parameters_to_generated_image_metadata(parameters):
|
||||
# top-level metadata minus `image` or `images`
|
||||
|
||||
metadata = get_system_config()
|
||||
# remove any image keys not mentioned in RFC #266
|
||||
rfc266_img_fields = [
|
||||
"type",
|
||||
"postprocessing",
|
||||
"sampler",
|
||||
"prompt",
|
||||
"seed",
|
||||
"variations",
|
||||
"steps",
|
||||
"cfg_scale",
|
||||
"threshold",
|
||||
"perlin",
|
||||
"step_number",
|
||||
"width",
|
||||
"height",
|
||||
"extra",
|
||||
"seamless",
|
||||
"hires_fix",
|
||||
]
|
||||
|
||||
rfc_dict = {}
|
||||
|
||||
for item in parameters.items():
|
||||
key, value = item
|
||||
if key in rfc266_img_fields:
|
||||
rfc_dict[key] = value
|
||||
|
||||
postprocessing = []
|
||||
|
||||
# 'postprocessing' is either null or an
|
||||
if "facetool_strength" in parameters:
|
||||
|
||||
postprocessing.append(
|
||||
{"type": "gfpgan", "strength": float(parameters["facetool_strength"])}
|
||||
)
|
||||
|
||||
if "upscale" in parameters:
|
||||
postprocessing.append(
|
||||
{
|
||||
"type": "esrgan",
|
||||
"scale": int(parameters["upscale"][0]),
|
||||
"strength": float(parameters["upscale"][1]),
|
||||
}
|
||||
)
|
||||
|
||||
rfc_dict["postprocessing"] = postprocessing if len(postprocessing) > 0 else None
|
||||
|
||||
# semantic drift
|
||||
rfc_dict["sampler"] = parameters["sampler_name"]
|
||||
|
||||
# display weighted subprompts (liable to change)
|
||||
subprompts = split_weighted_subprompts(parameters["prompt"])
|
||||
subprompts = [{"prompt": x[0], "weight": x[1]} for x in subprompts]
|
||||
rfc_dict["prompt"] = subprompts
|
||||
|
||||
# 'variations' should always exist and be an array, empty or consisting of {'seed': seed, 'weight': weight} pairs
|
||||
variations = []
|
||||
|
||||
if "with_variations" in parameters:
|
||||
variations = [
|
||||
{"seed": x[0], "weight": x[1]} for x in parameters["with_variations"]
|
||||
]
|
||||
|
||||
rfc_dict["variations"] = variations
|
||||
|
||||
if "init_img" in parameters:
|
||||
rfc_dict["type"] = "img2img"
|
||||
rfc_dict["strength"] = parameters["strength"]
|
||||
rfc_dict["fit"] = parameters["fit"] # TODO: Noncompliant
|
||||
rfc_dict["orig_hash"] = calculate_init_img_hash(parameters["init_img"])
|
||||
rfc_dict["init_image_path"] = parameters["init_img"] # TODO: Noncompliant
|
||||
rfc_dict["sampler"] = "ddim" # TODO: FIX ME WHEN IMG2IMG SUPPORTS ALL SAMPLERS
|
||||
if "init_mask" in parameters:
|
||||
rfc_dict["mask_hash"] = calculate_init_img_hash(
|
||||
parameters["init_mask"]
|
||||
) # TODO: Noncompliant
|
||||
rfc_dict["mask_image_path"] = parameters["init_mask"] # TODO: Noncompliant
|
||||
else:
|
||||
rfc_dict["type"] = "txt2img"
|
||||
|
||||
metadata["image"] = rfc_dict
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def make_unique_init_image_filename(name):
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
return name
|
||||
|
||||
|
||||
def write_log_message(message, log_path=log_path):
|
||||
"""Logs the filename and parameters used to generate or process that image to log file"""
|
||||
message = f"{message}\n"
|
||||
with open(log_path, "a", encoding="utf-8") as file:
|
||||
file.writelines(message)
|
||||
|
||||
|
||||
def save_image(
|
||||
image, command, metadata, output_dir, step_index=None, postprocessing=False
|
||||
):
|
||||
pngwriter = PngWriter(output_dir)
|
||||
prefix = pngwriter.unique_prefix()
|
||||
|
||||
seed = "unknown_seed"
|
||||
|
||||
if "image" in metadata:
|
||||
if "seed" in metadata["image"]:
|
||||
seed = metadata["image"]["seed"]
|
||||
|
||||
filename = f"{prefix}.{seed}"
|
||||
|
||||
if step_index:
|
||||
filename += f".{step_index}"
|
||||
if postprocessing:
|
||||
filename += f".postprocessed"
|
||||
|
||||
filename += ".png"
|
||||
|
||||
path = pngwriter.save_image_and_prompt_to_png(
|
||||
image=image, dream_prompt=command, metadata=metadata, name=filename
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def calculate_real_steps(steps, strength, has_init_image):
|
||||
return math.floor(strength * steps) if has_init_image else steps
|
||||
|
||||
|
||||
def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters):
|
||||
canceled.clear()
|
||||
|
||||
step_index = 1
|
||||
prior_variations = (
|
||||
generation_parameters["with_variations"]
|
||||
if "with_variations" in generation_parameters
|
||||
else []
|
||||
)
|
||||
"""
|
||||
If a result image is used as an init image, and then deleted, we will want to be
|
||||
able to use it as an init image in the future. Need to copy it.
|
||||
|
||||
If the init/mask image doesn't exist in the init_image_path/mask_image_path,
|
||||
make a unique filename for it and copy it there.
|
||||
"""
|
||||
if "init_img" in generation_parameters:
|
||||
filename = os.path.basename(generation_parameters["init_img"])
|
||||
if not os.path.exists(os.path.join(init_image_path, filename)):
|
||||
unique_filename = make_unique_init_image_filename(filename)
|
||||
new_path = os.path.join(init_image_path, unique_filename)
|
||||
shutil.copy(generation_parameters["init_img"], new_path)
|
||||
generation_parameters["init_img"] = new_path
|
||||
if "init_mask" in generation_parameters:
|
||||
filename = os.path.basename(generation_parameters["init_mask"])
|
||||
if not os.path.exists(os.path.join(mask_image_path, filename)):
|
||||
unique_filename = make_unique_init_image_filename(filename)
|
||||
new_path = os.path.join(init_image_path, unique_filename)
|
||||
shutil.copy(generation_parameters["init_img"], new_path)
|
||||
generation_parameters["init_mask"] = new_path
|
||||
|
||||
totalSteps = calculate_real_steps(
|
||||
steps=generation_parameters["steps"],
|
||||
strength=generation_parameters["strength"]
|
||||
if "strength" in generation_parameters
|
||||
else None,
|
||||
has_init_image="init_img" in generation_parameters,
|
||||
)
|
||||
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": totalSteps,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": generation_parameters["iterations"],
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
def image_progress(sample, step):
|
||||
if canceled.is_set():
|
||||
raise CanceledException
|
||||
|
||||
nonlocal step_index
|
||||
nonlocal generation_parameters
|
||||
nonlocal progress
|
||||
|
||||
progress["currentStep"] = step + 1
|
||||
progress["currentStatus"] = "Generating"
|
||||
progress["currentStatusHasSteps"] = True
|
||||
|
||||
if (
|
||||
generation_parameters["progress_images"]
|
||||
and step % 5 == 0
|
||||
and step < generation_parameters["steps"] - 1
|
||||
):
|
||||
image = generate.sample_to_image(sample)
|
||||
|
||||
metadata = parameters_to_generated_image_metadata(generation_parameters)
|
||||
command = parameters_to_command(generation_parameters)
|
||||
path = save_image(image, command, metadata, intermediate_path, step_index=step_index, postprocessing=False)
|
||||
|
||||
step_index += 1
|
||||
socketio.emit(
|
||||
"intermediateResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
def image_done(image, seed, first_seed):
|
||||
nonlocal generation_parameters
|
||||
nonlocal esrgan_parameters
|
||||
nonlocal gfpgan_parameters
|
||||
nonlocal progress
|
||||
|
||||
step_index = 1
|
||||
nonlocal prior_variations
|
||||
|
||||
progress["currentStatus"] = "Generation complete"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
all_parameters = generation_parameters
|
||||
postprocessing = False
|
||||
|
||||
if (
|
||||
"variation_amount" in all_parameters
|
||||
and all_parameters["variation_amount"] > 0
|
||||
):
|
||||
first_seed = first_seed or seed
|
||||
this_variation = [[seed, all_parameters["variation_amount"]]]
|
||||
all_parameters["with_variations"] = prior_variations + this_variation
|
||||
all_parameters["seed"] = first_seed
|
||||
elif ("with_variations" in all_parameters):
|
||||
all_parameters["seed"] = first_seed
|
||||
else:
|
||||
all_parameters["seed"] = seed
|
||||
|
||||
if esrgan_parameters:
|
||||
progress["currentStatus"] = "Upscaling"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = esrgan.process(
|
||||
image=image,
|
||||
upsampler_scale=esrgan_parameters["level"],
|
||||
strength=esrgan_parameters["strength"],
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
postprocessing = True
|
||||
all_parameters["upscale"] = [
|
||||
esrgan_parameters["level"],
|
||||
esrgan_parameters["strength"],
|
||||
]
|
||||
|
||||
if gfpgan_parameters:
|
||||
progress["currentStatus"] = "Fixing faces"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = gfpgan.process(
|
||||
image=image, strength=gfpgan_parameters["strength"], seed=seed
|
||||
)
|
||||
postprocessing = True
|
||||
all_parameters["facetool_strength"] = gfpgan_parameters["strength"]
|
||||
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
metadata = parameters_to_generated_image_metadata(all_parameters)
|
||||
command = parameters_to_command(all_parameters)
|
||||
|
||||
path = save_image(
|
||||
image, command, metadata, result_path, postprocessing=postprocessing
|
||||
)
|
||||
|
||||
print(f'>> Image generated: "{path}"')
|
||||
write_log_message(f'[Generated] "{path}": {command}')
|
||||
|
||||
if progress["totalIterations"] > progress["currentIteration"]:
|
||||
progress["currentStep"] = 1
|
||||
progress["currentIteration"] += 1
|
||||
progress["currentStatus"] = "Iteration finished"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
else:
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["isProcessing"] = False
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
"generationResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
eventlet.sleep(0)
|
||||
|
||||
try:
|
||||
generate.prompt2image(
|
||||
**generation_parameters,
|
||||
step_callback=image_progress,
|
||||
image_callback=image_done,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except CanceledException:
|
||||
pass
|
||||
except Exception as e:
|
||||
socketio.emit("error", {"message": (str(e))})
|
||||
print("\n")
|
||||
traceback.print_exc()
|
||||
print("\n")
|
||||
|
||||
|
||||
"""
|
||||
END ADDITIONAL FUNCTIONS
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f">> Starting server at http://{host}:{port}")
|
||||
socketio.run(app, host=host, port=port)
|
@ -34,16 +34,6 @@ original unedited image and the masked (partially transparent) image:
|
||||
invoke> "man with cat on shoulder" -I./images/man.png -M./images/man-transparent.png
|
||||
```
|
||||
|
||||
If you are using Photoshop to make your transparent masks, here is a
|
||||
protocol contributed by III_Communication36 (Discord name):
|
||||
|
||||
Create your alpha channel for mask in photoshop, then run
|
||||
image/adjust/threshold on that channel. Export as Save a copy using
|
||||
superpng (3rd party free download plugin) making sure alpha channel
|
||||
is selected. Then masking works as it should for the img2img
|
||||
process 100%. Can feed just one image this way without needing to
|
||||
feed the -M mask behind it
|
||||
|
||||
## **Masking using Text**
|
||||
|
||||
You can also create a mask using a text prompt to select the part of
|
||||
|
58
docs/features/WEBUIHOTKEYS.md
Normal file
58
docs/features/WEBUIHOTKEYS.md
Normal file
@ -0,0 +1,58 @@
|
||||
# **WebUI Hotkey List**
|
||||
|
||||
## General
|
||||
|
||||
| Setting | Hotkey |
|
||||
| ------------ | ---------------------- |
|
||||
| a | Set All Parameters |
|
||||
| s | Set Seed |
|
||||
| u | Upscale |
|
||||
| r | Restoration |
|
||||
| i | Show Metadata |
|
||||
| Ddl | Delete Image |
|
||||
| alt + a | Focus prompt input |
|
||||
| shift + i | Send To Image to Image |
|
||||
| ctrl + enter | Start processing |
|
||||
| shift + x | cancel Processing |
|
||||
| shift + d | Toggle Dark Mode |
|
||||
| ` | Toggle console |
|
||||
|
||||
## Tabs
|
||||
|
||||
| Setting | Hotkey |
|
||||
| ------- | ------------------------- |
|
||||
| 1 | Go to Text To Image Tab |
|
||||
| 2 | Go to Image to Image Tab |
|
||||
| 3 | Go to Inpainting Tab |
|
||||
| 4 | Go to Outpainting Tab |
|
||||
| 5 | Go to Nodes Tab |
|
||||
| 6 | Go to Post Processing Tab |
|
||||
|
||||
## Gallery
|
||||
|
||||
| Setting | Hotkey |
|
||||
| ------------ | ------------------------------- |
|
||||
| g | Toggle Gallery |
|
||||
| left arrow | Go to previous image in gallery |
|
||||
| right arrow | Go to next image in gallery |
|
||||
| shift + p | Pin gallery |
|
||||
| shift + up | Increase gallery image size |
|
||||
| shift + down | Decrease gallery image size |
|
||||
| shift + r | Reset image gallery size |
|
||||
|
||||
## Inpainting
|
||||
|
||||
| Setting | Hotkey |
|
||||
| -------------------------- | --------------------- |
|
||||
| [ | Decrease brush size |
|
||||
| ] | Increase brush size |
|
||||
| alt + [ | Decrease mask opacity |
|
||||
| alt + ] | Increase mask opacity |
|
||||
| b | Select brush |
|
||||
| e | Select eraser |
|
||||
| ctrl + z | Undo brush stroke |
|
||||
| ctrl + shift + z, ctrl + y | Redo brush stroke |
|
||||
| h | Hide mask |
|
||||
| shift + m | Invert mask |
|
||||
| shift + c | Clear mask |
|
||||
| shift + j | Expand canvas |
|
@ -3,57 +3,57 @@ channels:
|
||||
- pytorch
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- python==3.9.13
|
||||
- pip==22.2.2
|
||||
- python=3.9.13
|
||||
- pip=22.2.2
|
||||
|
||||
# pytorch left unpinned
|
||||
- pytorch==1.12.1
|
||||
- torchvision==0.13.1
|
||||
- pytorch=1.12.1
|
||||
- torchvision=0.13.1
|
||||
|
||||
# I suggest to keep the other deps sorted for convenience.
|
||||
# To determine what the latest versions should be, run:
|
||||
#
|
||||
# ```shell
|
||||
# sed -E 's/ldm/ldm-updated/;20,99s/- ([^=]+)==.+/- \1/' environment-mac.yml > environment-mac-updated.yml
|
||||
# CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac-updated.yml && conda list -n ldm-updated | awk ' {print " - " $1 "==" $2;} '
|
||||
# sed -E 's/invokeai/invokeai-updated/;20,99s/- ([^=]+)==.+/- \1/' environment-mac.yml > environment-mac-updated.yml
|
||||
# CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac-updated.yml && conda list -n invokeai-updated | awk ' {print " - " $1 "==" $2;} '
|
||||
# ```
|
||||
- albumentations==1.2.1
|
||||
- coloredlogs==15.0.1
|
||||
- diffusers==0.6.0
|
||||
- einops==0.4.1
|
||||
- grpcio==1.46.4
|
||||
- humanfriendly==10.0
|
||||
- imageio==2.21.2
|
||||
- imageio-ffmpeg==0.4.7
|
||||
- imgaug==0.4.0
|
||||
- kornia==0.6.7
|
||||
- mpmath==1.2.1
|
||||
- nomkl=1.0
|
||||
- numpy==1.23.2
|
||||
- omegaconf==2.1.1
|
||||
- openh264==2.3.0
|
||||
- onnx==1.12.0
|
||||
- onnxruntime==1.12.1
|
||||
- pudb==2022.1
|
||||
- pytorch-lightning==1.7.5
|
||||
- scipy==1.9.1
|
||||
- streamlit==1.12.2
|
||||
- sympy==1.10.1
|
||||
- tensorboard==2.10.0
|
||||
- torchmetrics==0.9.3
|
||||
|
||||
- albumentations=1.2.1
|
||||
- coloredlogs=15.0.1
|
||||
- diffusers=0.6.0
|
||||
- einops=0.4.1
|
||||
- grpcio=1.46.4
|
||||
- humanfriendly=10.0
|
||||
- imageio=2.21.2
|
||||
- imageio-ffmpeg=0.4.7
|
||||
- imgaug=0.4.0
|
||||
- kornia=0.6.7
|
||||
- mpmath=1.2.1
|
||||
- nomkl # arm64 has only 1.0 while x64 needs 3.0
|
||||
- numpy=1.23.4
|
||||
- omegaconf=2.1.1
|
||||
- openh264=2.3.0
|
||||
- onnx=1.12.0
|
||||
- onnxruntime=1.12.1
|
||||
- pudb=2022.1
|
||||
- pytorch-lightning=1.7.7
|
||||
- scipy=1.9.3
|
||||
- streamlit=1.12.2
|
||||
- sympy=1.10.1
|
||||
- tensorboard=2.10.0
|
||||
- torchmetrics=0.10.1
|
||||
- py-opencv=4.6.0
|
||||
- flask=2.1.3
|
||||
- flask-socketio=5.3.0
|
||||
- flask-cors=3.0.10
|
||||
- eventlet=0.33.1
|
||||
- protobuf=3.20.1
|
||||
- send2trash=1.8.0
|
||||
- transformers=4.23.1
|
||||
- torch-fidelity=0.3.0
|
||||
- pip:
|
||||
- flask==2.1.3
|
||||
- flask_socketio==5.3.0
|
||||
- flask_cors==3.0.10
|
||||
- dependency_injector==4.40.0
|
||||
- eventlet==0.33.1
|
||||
- opencv-python==4.6.0
|
||||
- protobuf==3.19.6
|
||||
- realesrgan==0.2.5.0
|
||||
- send2trash==1.8.0
|
||||
- test-tube==0.7.5
|
||||
- transformers==4.21.3
|
||||
- torch-fidelity==0.3.0
|
||||
- -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers
|
||||
- -e git+https://github.com/openai/CLIP.git@main#egg=clip
|
||||
- -e git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
||||
|
483
frontend/dist/assets/index.0a6593a2.js
vendored
483
frontend/dist/assets/index.0a6593a2.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.193aec6f.css
vendored
1
frontend/dist/assets/index.193aec6f.css
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.352e4760.css
vendored
Normal file
1
frontend/dist/assets/index.352e4760.css
vendored
Normal file
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.64b87783.js
vendored
Normal file
517
frontend/dist/assets/index.64b87783.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.0a6593a2.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.193aec6f.css">
|
||||
<script type="module" crossorigin src="./assets/index.64b87783.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.352e4760.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -15,27 +15,36 @@
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@radix-ui/react-context-menu": "^2.0.1",
|
||||
"@radix-ui/react-slider": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.2",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"add": "^2.0.6",
|
||||
"dateformat": "^5.0.3",
|
||||
"framer-motion": "^7.2.1",
|
||||
"konva": "^8.3.13",
|
||||
"lodash": "^4.17.21",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-konva": "^18.2.3",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"socket.io": "^4.5.2",
|
||||
"socket.io-client": "^4.5.2",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.0",
|
||||
"yarn": "^1.22.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
|
@ -2,22 +2,20 @@
|
||||
|
||||
.App {
|
||||
display: grid;
|
||||
width: max-content;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
margin: 0.6rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-color);
|
||||
row-gap: 0.5rem;
|
||||
padding: $app-padding;
|
||||
grid-auto-rows: max-content;
|
||||
width: $app-width;
|
||||
height: $app-height;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.app-console {
|
||||
z-index: 9999;
|
||||
z-index: 20;
|
||||
}
|
||||
|
@ -7,11 +7,13 @@ import { useAppDispatch } from './store';
|
||||
import { requestSystemConfig } from './socketio/actions';
|
||||
import { keepGUIAlive } from './utils';
|
||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||
import ImageUploader from '../common/components/ImageUploader';
|
||||
|
||||
keepGUIAlive();
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isReady, setIsReady] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -21,14 +23,16 @@ const App = () => {
|
||||
|
||||
return isReady ? (
|
||||
<div className="App">
|
||||
<ProgressBar />
|
||||
<div className="app-content">
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
</div>
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<div className="app-content">
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
</div>
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
</ImageUploader>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
|
8
frontend/src/app/contexts/ImageUploaderTriggerContext.ts
Normal file
8
frontend/src/app/contexts/ImageUploaderTriggerContext.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type VoidFunc = () => void;
|
||||
|
||||
type ImageUploaderTriggerContextType = VoidFunc | null;
|
||||
|
||||
export const ImageUploaderTriggerContext =
|
||||
createContext<ImageUploaderTriggerContextType>(null);
|
43
frontend/src/app/invokeai.d.ts
vendored
43
frontend/src/app/invokeai.d.ts
vendored
@ -12,6 +12,8 @@
|
||||
* 'gfpgan'.
|
||||
*/
|
||||
|
||||
import { Category as GalleryCategory } from '../features/gallery/gallerySlice';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Once an image has been generated, if it is postprocessed again,
|
||||
@ -105,12 +107,15 @@ export declare type Metadata = SystemConfig & {
|
||||
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
||||
};
|
||||
|
||||
// An Image has a UUID, url (path?) and Metadata.
|
||||
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
|
||||
export declare type Image = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
mtime: number;
|
||||
metadata: Metadata;
|
||||
metadata?: Metadata;
|
||||
width: number;
|
||||
height: number;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
|
||||
// GalleryImages is an array of Image.
|
||||
@ -140,20 +145,35 @@ export declare type SystemConfig = {
|
||||
model_hash: string;
|
||||
app_id: string;
|
||||
app_version: string;
|
||||
model_list: ModelList;
|
||||
};
|
||||
|
||||
export declare type ModelStatus = 'active' | 'cached' | 'not loaded';
|
||||
|
||||
export declare type Model = {
|
||||
status: ModelStatus;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export declare type ModelList = Record<string, Model>;
|
||||
|
||||
/**
|
||||
* These types type data received from the server via socketio.
|
||||
*/
|
||||
|
||||
export declare type ModelChangeResponse = {
|
||||
model_name: string;
|
||||
model_list: ModelList;
|
||||
};
|
||||
|
||||
export declare type SystemStatusResponse = SystemStatus;
|
||||
|
||||
export declare type SystemConfigResponse = SystemConfig;
|
||||
|
||||
export declare type ImageResultResponse = {
|
||||
url: string;
|
||||
mtime: number;
|
||||
metadata: Metadata;
|
||||
export declare type ImageResultResponse = Omit<Image, 'uuid'>;
|
||||
|
||||
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
||||
destination: 'img2img' | 'inpainting';
|
||||
};
|
||||
|
||||
export declare type ErrorResponse = {
|
||||
@ -164,13 +184,22 @@ export declare type ErrorResponse = {
|
||||
export declare type GalleryImagesResponse = {
|
||||
images: Array<Omit<Image, 'uuid'>>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
|
||||
export declare type ImageUrlAndUuidResponse = {
|
||||
export declare type ImageDeletedResponse = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
|
||||
export declare type ImageUrlResponse = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export declare type ImageUploadDestination = 'img2img' | 'inpainting';
|
||||
|
||||
export declare type UploadImagePayload = {
|
||||
file: File;
|
||||
destination?: ImageUploadDestination;
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { GalleryCategory } from '../../features/gallery/gallerySlice';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
|
||||
/**
|
||||
* We can't use redux-toolkit's createSlice() to make these actions,
|
||||
* because they have no associated reducer. They only exist to dispatch
|
||||
@ -8,24 +11,28 @@ import * as InvokeAI from '../invokeai';
|
||||
* by the middleware.
|
||||
*/
|
||||
|
||||
export const generateImage = createAction<undefined>('socketio/generateImage');
|
||||
export const generateImage = createAction<InvokeTabName>(
|
||||
'socketio/generateImage'
|
||||
);
|
||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
||||
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
||||
export const requestImages = createAction<undefined>(
|
||||
export const requestImages = createAction<GalleryCategory>(
|
||||
'socketio/requestImages'
|
||||
);
|
||||
export const requestNewImages = createAction<undefined>(
|
||||
export const requestNewImages = createAction<GalleryCategory>(
|
||||
'socketio/requestNewImages'
|
||||
);
|
||||
export const cancelProcessing = createAction<undefined>(
|
||||
'socketio/cancelProcessing'
|
||||
);
|
||||
export const uploadInitialImage = createAction<File>(
|
||||
'socketio/uploadInitialImage'
|
||||
);
|
||||
export const uploadImage = createAction<InvokeAI.UploadImagePayload>('socketio/uploadImage');
|
||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
||||
|
||||
export const requestSystemConfig = createAction<undefined>(
|
||||
'socketio/requestSystemConfig'
|
||||
);
|
||||
|
||||
export const requestModelChange = createAction<string>(
|
||||
'socketio/requestModelChange'
|
||||
);
|
||||
|
@ -1,13 +1,26 @@
|
||||
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||
import dateFormat from 'dateformat';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { frontendToBackendParameters } from '../../common/util/parameterTranslation';
|
||||
import {
|
||||
frontendToBackendParameters,
|
||||
FrontendToBackendParametersConfig,
|
||||
} from '../../common/util/parameterTranslation';
|
||||
import {
|
||||
GalleryCategory,
|
||||
GalleryState,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import {
|
||||
addLogEntry,
|
||||
errorOccurred,
|
||||
setCurrentStatus,
|
||||
setIsCancelable,
|
||||
setIsProcessing,
|
||||
} from '../../features/system/systemSlice';
|
||||
import { tabMap, tab_dict } from '../../features/tabs/InvokeTabs';
|
||||
import { inpaintingImageElementRef } from '../../features/tabs/Inpainting/InpaintingCanvas';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
import { RootState } from '../store';
|
||||
|
||||
/**
|
||||
* Returns an object containing all functions which use `socketio.emit()`.
|
||||
@ -21,17 +34,56 @@ const makeSocketIOEmitters = (
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
return {
|
||||
emitGenerateImage: () => {
|
||||
emitGenerateImage: (generationMode: InvokeTabName) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
|
||||
const options = { ...getState().options };
|
||||
const state: RootState = getState();
|
||||
|
||||
if (tabMap[options.activeTab] !== 'img2img') {
|
||||
options.shouldUseInitImage = false;
|
||||
const {
|
||||
options: optionsState,
|
||||
system: systemState,
|
||||
inpainting: inpaintingState,
|
||||
gallery: galleryState,
|
||||
} = state;
|
||||
|
||||
const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
|
||||
{
|
||||
generationMode,
|
||||
optionsState,
|
||||
inpaintingState,
|
||||
systemState,
|
||||
};
|
||||
|
||||
if (generationMode === 'inpainting') {
|
||||
if (
|
||||
!inpaintingImageElementRef.current ||
|
||||
!inpaintingState.imageToInpaint?.url
|
||||
) {
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: 'Inpainting image not loaded, cannot generate image.',
|
||||
level: 'error',
|
||||
})
|
||||
);
|
||||
dispatch(errorOccurred());
|
||||
return;
|
||||
}
|
||||
|
||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||
inpaintingState.imageToInpaint.url;
|
||||
|
||||
frontendToBackendParametersConfig.maskImageElement =
|
||||
inpaintingImageElementRef.current;
|
||||
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||
if (!galleryState.currentImage?.url) return;
|
||||
|
||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||
galleryState.currentImage.url;
|
||||
}
|
||||
|
||||
const { generationParameters, esrganParameters, facetoolParameters } =
|
||||
frontendToBackendParameters(options, getState().system);
|
||||
frontendToBackendParameters(frontendToBackendParametersConfig);
|
||||
|
||||
socketio.emit(
|
||||
'generateImage',
|
||||
@ -40,6 +92,14 @@ const makeSocketIOEmitters = (
|
||||
facetoolParameters
|
||||
);
|
||||
|
||||
// we need to truncate the init_mask base64 else it takes up the whole log
|
||||
// TODO: handle maintaining masks for reproducibility in future
|
||||
if (generationParameters.init_mask) {
|
||||
generationParameters.init_mask = generationParameters.init_mask
|
||||
.substr(0, 20)
|
||||
.concat('...');
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
@ -53,7 +113,8 @@ const makeSocketIOEmitters = (
|
||||
},
|
||||
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
const { upscalingLevel, upscalingStrength } = getState().options;
|
||||
const options: OptionsState = getState().options;
|
||||
const { upscalingLevel, upscalingStrength } = options;
|
||||
const esrganParameters = {
|
||||
upscale: [upscalingLevel, upscalingStrength],
|
||||
};
|
||||
@ -73,8 +134,8 @@ const makeSocketIOEmitters = (
|
||||
},
|
||||
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
const { facetoolType, facetoolStrength, codeformerFidelity } =
|
||||
getState().options;
|
||||
const options: OptionsState = getState().options;
|
||||
const { facetoolType, facetoolStrength, codeformerFidelity } = options;
|
||||
|
||||
const facetoolParameters: Record<string, any> = {
|
||||
facetool_strength: facetoolStrength,
|
||||
@ -101,22 +162,25 @@ const makeSocketIOEmitters = (
|
||||
);
|
||||
},
|
||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||
const { url, uuid } = imageToDelete;
|
||||
socketio.emit('deleteImage', url, uuid);
|
||||
const { url, uuid, category } = imageToDelete;
|
||||
socketio.emit('deleteImage', url, uuid, category);
|
||||
},
|
||||
emitRequestImages: () => {
|
||||
const { earliest_mtime } = getState().gallery;
|
||||
socketio.emit('requestImages', earliest_mtime);
|
||||
emitRequestImages: (category: GalleryCategory) => {
|
||||
const gallery: GalleryState = getState().gallery;
|
||||
const { earliest_mtime } = gallery.categories[category];
|
||||
socketio.emit('requestImages', category, earliest_mtime);
|
||||
},
|
||||
emitRequestNewImages: () => {
|
||||
const { latest_mtime } = getState().gallery;
|
||||
socketio.emit('requestLatestImages', latest_mtime);
|
||||
emitRequestNewImages: (category: GalleryCategory) => {
|
||||
const gallery: GalleryState = getState().gallery;
|
||||
const { latest_mtime } = gallery.categories[category];
|
||||
socketio.emit('requestLatestImages', category, latest_mtime);
|
||||
},
|
||||
emitCancelProcessing: () => {
|
||||
socketio.emit('cancel');
|
||||
},
|
||||
emitUploadInitialImage: (file: File) => {
|
||||
socketio.emit('uploadInitialImage', file, file.name);
|
||||
emitUploadImage: (payload: InvokeAI.UploadImagePayload) => {
|
||||
const { file, destination } = payload;
|
||||
socketio.emit('uploadImage', file, file.name, destination);
|
||||
},
|
||||
emitUploadMaskImage: (file: File) => {
|
||||
socketio.emit('uploadMaskImage', file, file.name);
|
||||
@ -124,6 +188,12 @@ const makeSocketIOEmitters = (
|
||||
emitRequestSystemConfig: () => {
|
||||
socketio.emit('requestSystemConfig');
|
||||
},
|
||||
emitRequestModelChange: (modelName: string) => {
|
||||
dispatch(setCurrentStatus('Changing Model'));
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setIsCancelable(false));
|
||||
socketio.emit('requestModelChange', modelName);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -13,21 +13,27 @@ import {
|
||||
setSystemConfig,
|
||||
processingCanceled,
|
||||
errorOccurred,
|
||||
setModelList,
|
||||
setIsCancelable,
|
||||
} from '../../features/system/systemSlice';
|
||||
|
||||
import {
|
||||
addGalleryImages,
|
||||
addImage,
|
||||
clearIntermediateImage,
|
||||
GalleryState,
|
||||
removeImage,
|
||||
setCurrentImage,
|
||||
setIntermediateImage,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
|
||||
import {
|
||||
setInitialImagePath,
|
||||
clearInitialImage,
|
||||
setInitialImage,
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
import { requestImages, requestNewImages } from './actions';
|
||||
import { clearImageToInpaint, setImageToInpaint } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
/**
|
||||
* Returns an object containing listener callbacks for socketio events.
|
||||
@ -46,10 +52,18 @@ const makeSocketIOListeners = (
|
||||
try {
|
||||
dispatch(setIsConnected(true));
|
||||
dispatch(setCurrentStatus('Connected'));
|
||||
if (getState().gallery.latest_mtime) {
|
||||
dispatch(requestNewImages());
|
||||
const gallery: GalleryState = getState().gallery;
|
||||
|
||||
if (gallery.categories.user.latest_mtime) {
|
||||
dispatch(requestNewImages('user'));
|
||||
} else {
|
||||
dispatch(requestImages());
|
||||
dispatch(requestImages('user'));
|
||||
}
|
||||
|
||||
if (gallery.categories.result.latest_mtime) {
|
||||
dispatch(requestNewImages('result'));
|
||||
} else {
|
||||
dispatch(requestImages('result'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -79,21 +93,19 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { url, mtime, metadata } = data;
|
||||
const newUuid = uuidv4();
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: newUuid,
|
||||
url,
|
||||
mtime,
|
||||
metadata: metadata,
|
||||
category: 'result',
|
||||
image: {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
},
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Image generated: ${url}`,
|
||||
message: `Image generated: ${data.url}`,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
@ -105,20 +117,16 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
const { url, metadata, mtime } = data;
|
||||
dispatch(
|
||||
setIntermediateImage({
|
||||
uuid,
|
||||
url,
|
||||
mtime,
|
||||
metadata,
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Intermediate image generated: ${url}`,
|
||||
message: `Intermediate image generated: ${data.url}`,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
@ -130,21 +138,20 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { url, metadata, mtime } = data;
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
mtime,
|
||||
metadata,
|
||||
category: 'result',
|
||||
image: {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Postprocessed: ${url}`,
|
||||
message: `Postprocessed: ${data.url}`,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
@ -191,7 +198,7 @@ const makeSocketIOListeners = (
|
||||
* Callback to run when we receive a 'galleryImages' event.
|
||||
*/
|
||||
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
|
||||
const { images, areMoreImagesAvailable } = data;
|
||||
const { images, areMoreImagesAvailable, category } = data;
|
||||
|
||||
/**
|
||||
* the logic here ideally would be in the reducer but we have a side effect:
|
||||
@ -200,17 +207,18 @@ const makeSocketIOListeners = (
|
||||
|
||||
// Generate a UUID for each image
|
||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
||||
const { url, metadata, mtime } = image;
|
||||
return {
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
mtime,
|
||||
metadata,
|
||||
...image,
|
||||
};
|
||||
});
|
||||
|
||||
dispatch(
|
||||
addGalleryImages({ images: preparedImages, areMoreImagesAvailable })
|
||||
addGalleryImages({
|
||||
images: preparedImages,
|
||||
areMoreImagesAvailable,
|
||||
category,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
@ -229,7 +237,12 @@ const makeSocketIOListeners = (
|
||||
const { intermediateImage } = getState().gallery;
|
||||
|
||||
if (intermediateImage) {
|
||||
dispatch(addImage(intermediateImage));
|
||||
dispatch(
|
||||
addImage({
|
||||
category: 'result',
|
||||
image: intermediateImage,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
@ -250,14 +263,22 @@ const makeSocketIOListeners = (
|
||||
/**
|
||||
* Callback to run when we receive a 'imageDeleted' event.
|
||||
*/
|
||||
onImageDeleted: (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
||||
const { url, uuid } = data;
|
||||
dispatch(removeImage(uuid));
|
||||
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
||||
const { url, uuid, category } = data;
|
||||
|
||||
const { initialImagePath, maskPath } = getState().options;
|
||||
// remove image from gallery
|
||||
dispatch(removeImage(data));
|
||||
|
||||
if (initialImagePath === url) {
|
||||
dispatch(setInitialImagePath(''));
|
||||
// remove references to image in options
|
||||
const { initialImage, maskPath } = getState().options;
|
||||
const { imageToInpaint } = getState().inpainting;
|
||||
|
||||
if (initialImage?.url === url || initialImage === url) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
||||
if (imageToInpaint?.url === url) {
|
||||
dispatch(clearImageToInpaint());
|
||||
}
|
||||
|
||||
if (maskPath === url) {
|
||||
@ -271,18 +292,40 @@ const makeSocketIOListeners = (
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'initialImageUploaded' event.
|
||||
*/
|
||||
onInitialImageUploaded: (data: InvokeAI.ImageUrlResponse) => {
|
||||
const { url } = data;
|
||||
dispatch(setInitialImagePath(url));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Initial image uploaded: ${url}`,
|
||||
})
|
||||
);
|
||||
onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
|
||||
const { destination, ...rest } = data;
|
||||
const image = {
|
||||
uuid: uuidv4(),
|
||||
...rest,
|
||||
};
|
||||
|
||||
try {
|
||||
dispatch(addImage({ image, category: 'user' }));
|
||||
|
||||
switch (destination) {
|
||||
case 'img2img': {
|
||||
dispatch(setInitialImage(image));
|
||||
break;
|
||||
}
|
||||
case 'inpainting': {
|
||||
dispatch(setImageToInpaint(image));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
dispatch(setCurrentImage(image));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Image uploaded: ${data.url}`,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'maskImageUploaded' event.
|
||||
@ -300,6 +343,34 @@ const makeSocketIOListeners = (
|
||||
onSystemConfig: (data: InvokeAI.SystemConfig) => {
|
||||
dispatch(setSystemConfig(data));
|
||||
},
|
||||
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
||||
const { model_name, model_list } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setCurrentStatus('Model Changed'));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(false));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Model changed: ${model_name}`,
|
||||
level: 'info',
|
||||
})
|
||||
);
|
||||
},
|
||||
onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => {
|
||||
const { model_name, model_list } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(false));
|
||||
dispatch(errorOccurred());
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Model change failed: ${model_name}`,
|
||||
level: 'error',
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -43,9 +43,11 @@ export const socketioMiddleware = () => {
|
||||
onGalleryImages,
|
||||
onProcessingCanceled,
|
||||
onImageDeleted,
|
||||
onInitialImageUploaded,
|
||||
onImageUploaded,
|
||||
onMaskImageUploaded,
|
||||
onSystemConfig,
|
||||
onModelChanged,
|
||||
onModelChangeFailed,
|
||||
} = makeSocketIOListeners(store);
|
||||
|
||||
const {
|
||||
@ -56,9 +58,10 @@ export const socketioMiddleware = () => {
|
||||
emitRequestImages,
|
||||
emitRequestNewImages,
|
||||
emitCancelProcessing,
|
||||
emitUploadInitialImage,
|
||||
emitUploadImage,
|
||||
emitUploadMaskImage,
|
||||
emitRequestSystemConfig,
|
||||
emitRequestModelChange,
|
||||
} = makeSocketIOEmitters(store, socketio);
|
||||
|
||||
/**
|
||||
@ -97,13 +100,16 @@ export const socketioMiddleware = () => {
|
||||
onProcessingCanceled();
|
||||
});
|
||||
|
||||
socketio.on('imageDeleted', (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
||||
socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
|
||||
onImageDeleted(data);
|
||||
});
|
||||
|
||||
socketio.on('initialImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||
onInitialImageUploaded(data);
|
||||
});
|
||||
socketio.on(
|
||||
'imageUploaded',
|
||||
(data: InvokeAI.ImageUploadResponse) => {
|
||||
onImageUploaded(data);
|
||||
}
|
||||
);
|
||||
|
||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||
onMaskImageUploaded(data);
|
||||
@ -113,6 +119,14 @@ export const socketioMiddleware = () => {
|
||||
onSystemConfig(data);
|
||||
});
|
||||
|
||||
socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
|
||||
onModelChanged(data);
|
||||
});
|
||||
|
||||
socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => {
|
||||
onModelChangeFailed(data);
|
||||
});
|
||||
|
||||
areListenersSet = true;
|
||||
}
|
||||
|
||||
@ -121,7 +135,7 @@ export const socketioMiddleware = () => {
|
||||
*/
|
||||
switch (action.type) {
|
||||
case 'socketio/generateImage': {
|
||||
emitGenerateImage();
|
||||
emitGenerateImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -141,12 +155,12 @@ export const socketioMiddleware = () => {
|
||||
}
|
||||
|
||||
case 'socketio/requestImages': {
|
||||
emitRequestImages();
|
||||
emitRequestImages(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestNewImages': {
|
||||
emitRequestNewImages();
|
||||
emitRequestNewImages(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -155,8 +169,8 @@ export const socketioMiddleware = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/uploadInitialImage': {
|
||||
emitUploadInitialImage(action.payload);
|
||||
case 'socketio/uploadImage': {
|
||||
emitUploadImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -169,6 +183,11 @@ export const socketioMiddleware = () => {
|
||||
emitRequestSystemConfig();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestModelChange': {
|
||||
emitRequestModelChange(action.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
next(action);
|
||||
|
@ -7,6 +7,7 @@ import storage from 'redux-persist/lib/storage'; // defaults to localStorage for
|
||||
|
||||
import optionsReducer from '../features/options/optionsSlice';
|
||||
import galleryReducer from '../features/gallery/gallerySlice';
|
||||
import inpaintingReducer from '../features/tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
import systemReducer from '../features/system/systemSlice';
|
||||
import { socketioMiddleware } from './socketio/middleware';
|
||||
@ -32,13 +33,14 @@ import { socketioMiddleware } from './socketio/middleware';
|
||||
const rootPersistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
blacklist: ['gallery', 'system'],
|
||||
blacklist: ['gallery', 'system', 'inpainting'],
|
||||
};
|
||||
|
||||
const systemPersistConfig = {
|
||||
key: 'system',
|
||||
storage,
|
||||
blacklist: [
|
||||
'isCancelable',
|
||||
'isConnected',
|
||||
'isProcessing',
|
||||
'currentStep',
|
||||
@ -53,10 +55,30 @@ const systemPersistConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
const galleryPersistConfig = {
|
||||
key: 'gallery',
|
||||
storage,
|
||||
whitelist: [
|
||||
'galleryWidth',
|
||||
'shouldPinGallery',
|
||||
'shouldShowGallery',
|
||||
'galleryScrollPosition',
|
||||
'galleryImageMinimumWidth',
|
||||
'galleryImageObjectFit',
|
||||
],
|
||||
};
|
||||
|
||||
const inpaintingPersistConfig = {
|
||||
key: 'inpainting',
|
||||
storage,
|
||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
options: optionsReducer,
|
||||
gallery: galleryReducer,
|
||||
gallery: persistReducer(galleryPersistConfig, galleryReducer),
|
||||
system: persistReducer(systemPersistConfig, systemReducer),
|
||||
inpainting: persistReducer(inpaintingPersistConfig, inpaintingReducer),
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
||||
|
@ -25,7 +25,10 @@ const systemSelector = createSelector(
|
||||
const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
const shouldDisplayGuides = useAppSelector(systemSelector);
|
||||
const { text } = FEATURES[feature];
|
||||
return shouldDisplayGuides ? (
|
||||
|
||||
if (!shouldDisplayGuides) return null;
|
||||
|
||||
return (
|
||||
<Popover trigger={'hover'}>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
@ -40,8 +43,6 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
<div className="guide-popover-guide-content">{text}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
|
26
frontend/src/common/components/IAICheckbox.scss
Normal file
26
frontend/src/common/components/IAICheckbox.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.invokeai__checkbox {
|
||||
.chakra-checkbox__label {
|
||||
margin-top: 1px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.chakra-checkbox__control {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: none;
|
||||
border-radius: 0.2rem;
|
||||
background-color: var(--input-checkbox-bg);
|
||||
|
||||
svg {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
stroke-width: 3px !important;
|
||||
}
|
||||
|
||||
&[data-checked] {
|
||||
color: var(--text-color);
|
||||
background-color: var(--input-checkbox-checked-bg);
|
||||
}
|
||||
}
|
||||
}
|
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||
|
||||
type IAICheckboxProps = CheckboxProps & {
|
||||
label: string;
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
const IAICheckbox = (props: IAICheckboxProps) => {
|
||||
const { label, styleClass, ...rest } = props;
|
||||
return (
|
||||
<Checkbox className={`invokeai__checkbox ${styleClass}`} {...rest}>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICheckbox;
|
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.invokeai__color-picker {
|
||||
.react-colorful__hue-pointer,
|
||||
.react-colorful__saturation-pointer {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-color: var(--white);
|
||||
}
|
||||
}
|
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { RgbaColorPicker } from 'react-colorful';
|
||||
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
|
||||
|
||||
type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
const { styleClass, ...rest } = props;
|
||||
|
||||
return (
|
||||
<RgbaColorPicker
|
||||
className={`invokeai__color-picker ${styleClass}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIColorPicker;
|
20
frontend/src/common/components/IAIIconButton.scss
Normal file
20
frontend/src/common/components/IAIIconButton.scss
Normal file
@ -0,0 +1,20 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.icon-button {
|
||||
background-color: var(--btn-grey);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-grey-hover);
|
||||
}
|
||||
|
||||
&[data-selected=true] {
|
||||
background-color: var(--accent-color);
|
||||
&:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
@ -8,20 +8,28 @@ import {
|
||||
interface Props extends IconButtonProps {
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: PlacementWithLogical | undefined;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*
|
||||
* TODO: Get rid of this.
|
||||
*/
|
||||
const IAIIconButton = (props: Props) => {
|
||||
const { tooltip = '', tooltipPlacement = 'bottom', onClick, ...rest } = props;
|
||||
const {
|
||||
tooltip = '',
|
||||
tooltipPlacement = 'bottom',
|
||||
styleClass,
|
||||
onClick,
|
||||
cursor,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
||||
<IconButton
|
||||
className={`icon-button ${styleClass}`}
|
||||
{...rest}
|
||||
cursor={onClick ? 'pointer' : 'unset'}
|
||||
cursor={cursor ? cursor : onClick ? 'pointer' : 'unset'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
@ -17,8 +17,8 @@
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
@ -1,15 +1,32 @@
|
||||
.number-input {
|
||||
.invokeai__number-input-form-control {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.number-input-label {
|
||||
.invokeai__number-input-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
flex-grow: 2;
|
||||
white-space: nowrap;
|
||||
|
||||
&[data-focus] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
|
||||
&[aria-invalid='true'] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
border: 2px solid var(--border-color-invalid);
|
||||
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
|
||||
}
|
||||
}
|
||||
|
||||
.number-input-field {
|
||||
.invokeai__number-input-root {
|
||||
height: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
column-gap: 0.5rem;
|
||||
@ -19,34 +36,39 @@
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.number-input-entry {
|
||||
.invokeai__number-input-field {
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
padding-inline-end: 0;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.number-input-stepper {
|
||||
.invokeai__number-input-stepper {
|
||||
display: grid;
|
||||
padding-right: 0.7rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.number-input-stepper-button {
|
||||
.invokeai__number-input-stepper-button {
|
||||
border: none;
|
||||
// expand arrow hitbox
|
||||
padding: 0 0.5rem;
|
||||
margin: 0 -0.5rem;
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,12 @@ import {
|
||||
NumberDecrementStepper,
|
||||
NumberInputProps,
|
||||
FormLabel,
|
||||
NumberInputFieldProps,
|
||||
NumberInputStepperProps,
|
||||
FormControlProps,
|
||||
FormLabelProps,
|
||||
TooltipProps,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import _ from 'lodash';
|
||||
import { FocusEvent, useEffect, useState } from 'react';
|
||||
@ -23,6 +29,12 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||
max: number;
|
||||
clamp?: boolean;
|
||||
isInteger?: boolean;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
numberInputProps?: NumberInputProps;
|
||||
numberInputFieldProps?: NumberInputFieldProps;
|
||||
numberInputStepperProps?: NumberInputStepperProps;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,8 +46,6 @@ const IAINumberInput = (props: Props) => {
|
||||
styleClass,
|
||||
isDisabled = false,
|
||||
showStepper = true,
|
||||
fontSize = '1rem',
|
||||
size = 'sm',
|
||||
width,
|
||||
textAlign,
|
||||
isInvalid,
|
||||
@ -44,6 +54,11 @@ const IAINumberInput = (props: Props) => {
|
||||
min,
|
||||
max,
|
||||
isInteger = true,
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
numberInputFieldProps,
|
||||
numberInputStepperProps,
|
||||
tooltipProps,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@ -65,7 +80,10 @@ const IAINumberInput = (props: Props) => {
|
||||
* from the current value.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!valueAsString.match(numberStringRegex) && value !== Number(valueAsString)) {
|
||||
if (
|
||||
!valueAsString.match(numberStringRegex) &&
|
||||
value !== Number(valueAsString)
|
||||
) {
|
||||
setValueAsString(String(value));
|
||||
}
|
||||
}, [value, valueAsString]);
|
||||
@ -94,47 +112,55 @@ const IAINumberInput = (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
className={`number-input ${styleClass}`}
|
||||
>
|
||||
{label && (
|
||||
<Tooltip {...tooltipProps}>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
className={
|
||||
styleClass
|
||||
? `invokeai__number-input-form-control ${styleClass}`
|
||||
: `invokeai__number-input-form-control`
|
||||
}
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
className="number-input-label"
|
||||
className="invokeai__number-input-form-label"
|
||||
style={{ display: label ? 'block' : 'none' }}
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<NumberInput
|
||||
size={size}
|
||||
{...rest}
|
||||
className="number-input-field"
|
||||
value={valueAsString}
|
||||
keepWithinRange={true}
|
||||
clampValueOnBlur={false}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<NumberInputField
|
||||
fontSize={fontSize}
|
||||
className="number-input-entry"
|
||||
<NumberInput
|
||||
className="invokeai__number-input-root"
|
||||
value={valueAsString}
|
||||
keepWithinRange={true}
|
||||
clampValueOnBlur={false}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleBlur}
|
||||
width={width}
|
||||
textAlign={textAlign}
|
||||
/>
|
||||
<div
|
||||
className="number-input-stepper"
|
||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||
{...rest}
|
||||
>
|
||||
<NumberIncrementStepper className="number-input-stepper-button" />
|
||||
<NumberDecrementStepper className="number-input-stepper-button" />
|
||||
</div>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
<NumberInputField
|
||||
className="invokeai__number-input-field"
|
||||
textAlign={textAlign}
|
||||
{...numberInputFieldProps}
|
||||
/>
|
||||
<div
|
||||
className="invokeai__number-input-stepper"
|
||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||
>
|
||||
<NumberIncrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
<NumberDecrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
</div>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
12
frontend/src/common/components/IAIPopover.scss
Normal file
12
frontend/src/common/components/IAIPopover.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.invokeai__popover-content {
|
||||
min-width: unset;
|
||||
width: unset !important;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem !important;
|
||||
background-color: var(--background-color) !important;
|
||||
border: 2px solid var(--border-color) !important;
|
||||
|
||||
.invokeai__popover-arrow {
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
}
|
39
frontend/src/common/components/IAIPopover.tsx
Normal file
39
frontend/src/common/components/IAIPopover.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { PopoverProps } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIPopoverProps = PopoverProps & {
|
||||
triggerComponent: ReactNode;
|
||||
children: ReactNode;
|
||||
styleClass?: string;
|
||||
hasArrow?: boolean;
|
||||
};
|
||||
|
||||
const IAIPopover = (props: IAIPopoverProps) => {
|
||||
const {
|
||||
triggerComponent,
|
||||
children,
|
||||
styleClass,
|
||||
hasArrow = true,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<Popover {...rest}>
|
||||
<PopoverTrigger>
|
||||
<Box>{triggerComponent}</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIPopover;
|
@ -1,28 +1,32 @@
|
||||
.iai-select {
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__select {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
|
||||
.iai-select-label {
|
||||
.invokeai__select-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.iai-select-picker {
|
||||
.invokeai__select-picker {
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: var(--background-color-secondary);
|
||||
font-weight: bold;
|
||||
height: 2rem;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.iai-select-option {
|
||||
.invokeai__select-option {
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,13 @@ const IAISelect = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} className={`iai-select ${styleClass}`}>
|
||||
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
className="iai-select-label"
|
||||
className="invokeai__select-label"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
@ -35,11 +35,11 @@ const IAISelect = (props: Props) => {
|
||||
fontSize={fontSize}
|
||||
size={size}
|
||||
{...rest}
|
||||
className="iai-select-picker"
|
||||
className="invokeai__select-picker"
|
||||
>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt} className="iai-select-option">
|
||||
<option key={opt} value={opt} className="invokeai__select-option">
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
|
40
frontend/src/common/components/IAISlider.scss
Normal file
40
frontend/src/common/components/IAISlider.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__slider-form-control {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
.invokeai__slider-inner-container {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.invokeai__slider-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.invokeai__slider-root {
|
||||
.invokeai__slider-filled-track {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
.invokeai__slider-track {
|
||||
background-color: var(--text-color-secondary);
|
||||
height: 5px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb-tooltip {
|
||||
}
|
87
frontend/src/common/components/IAISlider.tsx
Normal file
87
frontend/src/common/components/IAISlider.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import {
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Tooltip,
|
||||
SliderProps,
|
||||
FormControlProps,
|
||||
FormLabelProps,
|
||||
SliderTrackProps,
|
||||
SliderThumbProps,
|
||||
TooltipProps,
|
||||
SliderInnerTrackProps,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
type IAISliderProps = SliderProps & {
|
||||
label?: string;
|
||||
styleClass?: string;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
sliderTrackProps?: SliderTrackProps;
|
||||
sliderInnerTrackProps?: SliderInnerTrackProps;
|
||||
sliderThumbProps?: SliderThumbProps;
|
||||
sliderThumbTooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
};
|
||||
|
||||
const IAISlider = (props: IAISliderProps) => {
|
||||
const {
|
||||
label,
|
||||
styleClass,
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
sliderTrackProps,
|
||||
sliderInnerTrackProps,
|
||||
sliderThumbProps,
|
||||
sliderThumbTooltipProps,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl
|
||||
className={`invokeai__slider-form-control ${styleClass}`}
|
||||
{...formControlProps}
|
||||
>
|
||||
<div className="invokeai__slider-inner-container">
|
||||
<FormLabel
|
||||
className={`invokeai__slider-form-label`}
|
||||
whiteSpace="nowrap"
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
|
||||
<Slider
|
||||
className={`invokeai__slider-root`}
|
||||
aria-label={label}
|
||||
{...rest}
|
||||
>
|
||||
<SliderTrack
|
||||
className={`invokeai__slider-track`}
|
||||
{...sliderTrackProps}
|
||||
>
|
||||
<SliderFilledTrack
|
||||
className={`invokeai__slider-filled-track`}
|
||||
{...sliderInnerTrackProps}
|
||||
/>
|
||||
</SliderTrack>
|
||||
|
||||
<Tooltip
|
||||
className={`invokeai__slider-thumb-tooltip`}
|
||||
placement="top"
|
||||
hasArrow
|
||||
{...sliderThumbTooltipProps}
|
||||
>
|
||||
<SliderThumb
|
||||
className={`invokeai__slider-thumb`}
|
||||
{...sliderThumbProps}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Slider>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAISlider;
|
@ -1,18 +1,33 @@
|
||||
.chakra-switch,
|
||||
.switch-button {
|
||||
span {
|
||||
background-color: var(--switch-bg-color);
|
||||
.invokeai__switch-form-control {
|
||||
.invokeai__switch-form-label {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 1rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0.1rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
.invokeai__switch-root {
|
||||
span {
|
||||
background-color: var(--switch-bg-color);
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
span[data-checked] {
|
||||
background: var(--switch-bg-active-color);
|
||||
&[data-checked] {
|
||||
span {
|
||||
background: var(--switch-bg-active-color);
|
||||
|
||||
span {
|
||||
background-color: var(--white);
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormControlProps,
|
||||
FormLabel,
|
||||
FormLabelProps,
|
||||
Switch,
|
||||
SwitchProps,
|
||||
} from '@chakra-ui/react';
|
||||
@ -9,6 +10,9 @@ import {
|
||||
interface Props extends SwitchProps {
|
||||
label?: string;
|
||||
width?: string | number;
|
||||
styleClass?: string;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -18,26 +22,35 @@ const IAISwitch = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled = false,
|
||||
fontSize = 'md',
|
||||
size = 'md',
|
||||
// fontSize = 'md',
|
||||
// size = 'md',
|
||||
width = 'auto',
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
styleClass,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} width={width}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
{label && (
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Switch size={size} className="switch-button" {...rest} />
|
||||
</Flex>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
width={width}
|
||||
className={`invokeai__switch-form-control ${styleClass}`}
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel
|
||||
className="invokeai__switch-form-label"
|
||||
// fontSize={fontSize}
|
||||
whiteSpace="nowrap"
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
<Switch
|
||||
className="invokeai__switch-root"
|
||||
// size={size}
|
||||
// className="switch-button"
|
||||
{...rest}
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
72
frontend/src/common/components/ImageUploader.scss
Normal file
72
frontend/src/common/components/ImageUploader.scss
Normal file
@ -0,0 +1,72 @@
|
||||
.dropzone-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.dropzone-overlay {
|
||||
opacity: 0.5;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-drag-accept {
|
||||
box-shadow: inset 0 0 20rem 1rem var(--status-good-color);
|
||||
}
|
||||
|
||||
&.is-drag-reject {
|
||||
box-shadow: inset 0 0 20rem 1rem var(--status-bad-color);
|
||||
}
|
||||
|
||||
&.is-handling-upload {
|
||||
box-shadow: inset 0 0 20rem 1rem var(--status-working-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-uploader-button-outer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--subtext-color-bright);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--inpaint-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload-button-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 2rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
svg {
|
||||
width: 4rem !important;
|
||||
height: 4rem !important;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
}
|
173
frontend/src/common/components/ImageUploader.tsx
Normal file
173
frontend/src/common/components/ImageUploader.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { Heading, Spinner, useToast } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { uploadImage } from '../../app/socketio/actions';
|
||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
|
||||
const appSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
const { activeTab } = options;
|
||||
return {
|
||||
activeTabName: tabMap[activeTab],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
type ImageUploaderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const { children } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName } = useAppSelector(appSelector);
|
||||
const toast = useToast({});
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
setIsHandlingUpload(true);
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
const fileAcceptedCallback = useCallback(
|
||||
(file: File) => {
|
||||
setIsHandlingUpload(true);
|
||||
const payload: UploadImagePayload = { file };
|
||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||
payload.destination = activeTabName as ImageUploadDestination;
|
||||
}
|
||||
dispatch(uploadImage(payload));
|
||||
},
|
||||
[dispatch, activeTabName]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
fileRejectionCallback(rejection);
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
fileAcceptedCallback(file);
|
||||
});
|
||||
},
|
||||
[fileAcceptedCallback, fileRejectionCallback]
|
||||
);
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
isDragActive,
|
||||
open,
|
||||
} = useDropzone({
|
||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||
noClick: true,
|
||||
onDrop,
|
||||
maxFiles: 1,
|
||||
});
|
||||
|
||||
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:
|
||||
'Multiple images pasted, may only upload one image at a time',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const file = imageItems[0].getAsFile();
|
||||
|
||||
if (!file) {
|
||||
toast({
|
||||
description: 'Unable to load file',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: UploadImagePayload = { file };
|
||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||
payload.destination = activeTabName as ImageUploadDestination;
|
||||
}
|
||||
|
||||
dispatch(uploadImage(payload));
|
||||
};
|
||||
document.addEventListener('paste', pasteImageListener);
|
||||
return () => {
|
||||
document.removeEventListener('paste', pasteImageListener);
|
||||
};
|
||||
}, [dispatch, toast, activeTabName]);
|
||||
|
||||
return (
|
||||
<ImageUploaderTriggerContext.Provider value={open}>
|
||||
<div {...getRootProps({ style: {} })}>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{isDragActive && (
|
||||
<div className="dropzone-container">
|
||||
{isDragAccept && (
|
||||
<div className="dropzone-overlay is-drag-accept">
|
||||
<Heading size={'lg'}>Drop Images</Heading>
|
||||
</div>
|
||||
)}
|
||||
{isDragReject && (
|
||||
<div className="dropzone-overlay is-drag-reject">
|
||||
<Heading size={'lg'}>Invalid Upload</Heading>
|
||||
<Heading size={'md'}>Must be single JPEG or PNG image</Heading>
|
||||
</div>
|
||||
)}
|
||||
{isHandlingUpload && (
|
||||
<div className="dropzone-overlay is-handling-upload">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ImageUploaderTriggerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
31
frontend/src/common/components/ImageUploaderButton.tsx
Normal file
31
frontend/src/common/components/ImageUploaderButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import { useContext } from 'react';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
|
||||
type ImageUploaderButtonProps = {
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
|
||||
const { styleClass } = props;
|
||||
const open = useContext(ImageUploaderTriggerContext);
|
||||
|
||||
const handleClickUpload = () => {
|
||||
open && open();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`image-uploader-button-outer ${styleClass}`}
|
||||
onClick={handleClickUpload}
|
||||
>
|
||||
<div className="image-upload-button">
|
||||
<FaUpload />
|
||||
<Heading size={'lg'}>Click or Drag and Drop</Heading>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploaderButton;
|
@ -1,65 +0,0 @@
|
||||
import { Button, useToast } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import ImageUploader from '../../features/options/ImageUploader';
|
||||
|
||||
interface InvokeImageUploaderProps {
|
||||
label?: string;
|
||||
icon?: any;
|
||||
onMouseOver?: any;
|
||||
OnMouseout?: any;
|
||||
dispatcher: any;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
||||
const { label, icon, dispatcher, styleClass, onMouseOver, OnMouseout } =
|
||||
props;
|
||||
|
||||
const toast = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Callbacks to for handling file upload attempts
|
||||
const fileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(dispatcher(file)),
|
||||
[dispatch, dispatcher]
|
||||
);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={fileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
styleClass={styleClass}
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={OnMouseout}
|
||||
leftIcon={icon}
|
||||
width={'100%'}
|
||||
>
|
||||
{label ? label : null}
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
);
|
||||
}
|
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
@ -0,0 +1,62 @@
|
||||
.invokeai__slider-root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
width: 200px;
|
||||
|
||||
&[data-orientation='horizontal'] {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&[data-orientation='vertical'] {
|
||||
width: 20px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.invokeai__slider-track {
|
||||
background-color: black;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
border-radius: 9999px;
|
||||
|
||||
&[data-orientation='horizontal'] {
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
&[data-orientation='vertical'] {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.invokeai__slider-range {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border-radius: 9999px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.invokeai__slider-thumb-div {
|
||||
all: unset;
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 10px rgba(0, 2, 10, 0.3);
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: violet;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 5px rgba(0, 2, 10, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import * as Slider from '@radix-ui/react-slider';
|
||||
import React from 'react';
|
||||
import IAITooltip from './IAITooltip';
|
||||
|
||||
type IAISliderProps = Slider.SliderProps & {
|
||||
value: number[];
|
||||
tooltipLabel?: string;
|
||||
orientation?: 'horizontal' | 'vertial';
|
||||
trackProps?: Slider.SliderTrackProps;
|
||||
rangeProps?: Slider.SliderRangeProps;
|
||||
thumbProps?: Slider.SliderThumbProps;
|
||||
};
|
||||
|
||||
const _IAISlider = (props: IAISliderProps) => {
|
||||
const {
|
||||
value,
|
||||
tooltipLabel,
|
||||
orientation,
|
||||
trackProps,
|
||||
rangeProps,
|
||||
thumbProps,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<Slider.Root
|
||||
className="invokeai__slider-root"
|
||||
{...rest}
|
||||
data-orientation={orientation || 'horizontal'}
|
||||
>
|
||||
<Slider.Track {...trackProps} className="invokeai__slider-track">
|
||||
<Slider.Range {...rangeProps} className="invokeai__slider-range" />
|
||||
</Slider.Track>
|
||||
<Tooltip label={tooltipLabel ?? value[0]} placement="top">
|
||||
<Slider.Thumb {...thumbProps} className="invokeai__slider-thumb">
|
||||
<div className="invokeai__slider-thumb-div" />
|
||||
{/*<IAITooltip trigger={<div className="invokeai__slider-thumb-div" />}>
|
||||
{value && value[0]}
|
||||
</IAITooltip>*/}
|
||||
</Slider.Thumb>
|
||||
</Tooltip>
|
||||
</Slider.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default _IAISlider;
|
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.invokeai__tooltip-content {
|
||||
padding: 0.5rem;
|
||||
background-color: grey;
|
||||
border-radius: 0.25rem;
|
||||
.invokeai__tooltip-arrow {
|
||||
background-color: grey;
|
||||
}
|
||||
}
|
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAITooltipProps = Tooltip.TooltipProps & {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
triggerProps?: Tooltip.TooltipTriggerProps;
|
||||
contentProps?: Tooltip.TooltipContentProps;
|
||||
arrowProps?: Tooltip.TooltipArrowProps;
|
||||
};
|
||||
|
||||
const IAITooltip = (props: IAITooltipProps) => {
|
||||
const { trigger, children, triggerProps, contentProps, arrowProps, ...rest } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root {...rest} delayDuration={0}>
|
||||
<Tooltip.Trigger {...triggerProps}>{trigger}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
{...contentProps}
|
||||
onPointerDownOutside={(e: any) => {e.preventDefault()}}
|
||||
className="invokeai__tooltip-content"
|
||||
>
|
||||
<Tooltip.Arrow {...arrowProps} className="invokeai__tooltip-arrow" />
|
||||
{children}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAITooltip;
|
@ -1,65 +1,61 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||
|
||||
export const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
export const useCheckParametersSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.system,
|
||||
(state: RootState) => state.inpainting,
|
||||
],
|
||||
(options: OptionsState, system: SystemState, inpainting: InpaintingState) => {
|
||||
return {
|
||||
// options
|
||||
prompt: options.prompt,
|
||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||
seedWeights: options.seedWeights,
|
||||
maskPath: options.maskPath,
|
||||
initialImagePath: options.initialImagePath,
|
||||
initialImage: options.initialImage,
|
||||
seed: options.seed,
|
||||
activeTab: options.activeTab,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
// system
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
// inpainting
|
||||
hasInpaintingImage: Boolean(inpainting.imageToInpaint),
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
* This is used to prevent the 'Generate' button from being clicked.
|
||||
*/
|
||||
const useCheckParameters = (): boolean => {
|
||||
const { prompt } = useAppSelector(optionsSelector);
|
||||
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
initialImage,
|
||||
seed,
|
||||
activeTab,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
activeTabName,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
hasInpaintingImage,
|
||||
} = useAppSelector(useCheckParametersSelector);
|
||||
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
@ -67,12 +63,16 @@ const useCheckParameters = (): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prompt && !initialImagePath && activeTab === 1) {
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'inpainting' && !hasInpaintingImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImagePath) {
|
||||
if (maskPath && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -100,13 +100,14 @@ const useCheckParameters = (): boolean => {
|
||||
}, [
|
||||
prompt,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
isProcessing,
|
||||
initialImage,
|
||||
isConnected,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
activeTab,
|
||||
activeTabName,
|
||||
hasInpaintingImage,
|
||||
]);
|
||||
};
|
||||
|
||||
|
@ -1,22 +1,38 @@
|
||||
/*
|
||||
These functions translate frontend state into parameters
|
||||
suitable for consumption by the backend, and vice-versa.
|
||||
*/
|
||||
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
|
||||
import {
|
||||
seedWeightsToString,
|
||||
stringToSeedWeightsArray,
|
||||
} from './seedWeightPairs';
|
||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||
import randomInt from './randomInt';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
||||
|
||||
export type FrontendToBackendParametersConfig = {
|
||||
generationMode: InvokeTabName;
|
||||
optionsState: OptionsState;
|
||||
inpaintingState: InpaintingState;
|
||||
systemState: SystemState;
|
||||
imageToProcessUrl?: string;
|
||||
maskImageElement?: HTMLImageElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates/formats frontend state into parameters suitable
|
||||
* for consumption by the API.
|
||||
*/
|
||||
export const frontendToBackendParameters = (
|
||||
optionsState: OptionsState,
|
||||
systemState: SystemState
|
||||
config: FrontendToBackendParametersConfig
|
||||
): { [key: string]: any } => {
|
||||
const {
|
||||
generationMode,
|
||||
optionsState,
|
||||
inpaintingState,
|
||||
systemState,
|
||||
imageToProcessUrl,
|
||||
maskImageElement,
|
||||
} = config;
|
||||
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
@ -30,10 +46,8 @@ export const frontendToBackendParameters = (
|
||||
seed,
|
||||
seamless,
|
||||
hiresFix,
|
||||
shouldUseInitImage,
|
||||
img2imgStrength,
|
||||
initialImagePath,
|
||||
maskPath,
|
||||
initialImage,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
@ -61,8 +75,6 @@ export const frontendToBackendParameters = (
|
||||
width,
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
seamless,
|
||||
hires_fix: hiresFix,
|
||||
progress_images: shouldDisplayInProgress,
|
||||
};
|
||||
|
||||
@ -70,13 +82,65 @@ export const frontendToBackendParameters = (
|
||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||
: seed;
|
||||
|
||||
if (shouldUseInitImage) {
|
||||
generationParameters.init_img = initialImagePath;
|
||||
// parameters common to txt2img and img2img
|
||||
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||
generationParameters.seamless = seamless;
|
||||
generationParameters.hires_fix = hiresFix;
|
||||
}
|
||||
|
||||
// img2img exclusive parameters
|
||||
if (generationMode === 'img2img' && initialImage) {
|
||||
generationParameters.init_img =
|
||||
typeof initialImage === 'string' ? initialImage : initialImage.url;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = shouldFitToWidthHeight;
|
||||
if (maskPath) {
|
||||
generationParameters.init_mask = maskPath;
|
||||
}
|
||||
|
||||
// inpainting exclusive parameters
|
||||
if (generationMode === 'inpainting' && maskImageElement) {
|
||||
const {
|
||||
lines,
|
||||
boundingBoxCoordinate: { x, y },
|
||||
boundingBoxDimensions: { width, height },
|
||||
shouldShowBoundingBox,
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
} = inpaintingState;
|
||||
|
||||
let bx = x,
|
||||
by = y,
|
||||
bwidth = width,
|
||||
bheight = height;
|
||||
|
||||
if (!shouldShowBoundingBox) {
|
||||
bx = 0;
|
||||
by = 0;
|
||||
bwidth = maskImageElement.width;
|
||||
bheight = maskImageElement.height;
|
||||
}
|
||||
|
||||
const boundingBox = {
|
||||
x: bx,
|
||||
y: by,
|
||||
width: bwidth,
|
||||
height: bheight,
|
||||
};
|
||||
|
||||
if (shouldUseInpaintReplace) {
|
||||
generationParameters.inpaint_replace = inpaintReplace;
|
||||
}
|
||||
|
||||
generationParameters.init_img = imageToProcessUrl;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = false;
|
||||
|
||||
const maskDataURL = generateMask(maskImageElement, lines, boundingBox);
|
||||
|
||||
generationParameters.init_mask = maskDataURL.split(
|
||||
'data:image/png;base64,'
|
||||
)[1];
|
||||
|
||||
generationParameters.bounding_box = boundingBox;
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
@ -105,7 +169,7 @@ export const frontendToBackendParameters = (
|
||||
strength: facetoolStrength,
|
||||
};
|
||||
if (facetoolType === 'codeformer') {
|
||||
facetoolParameters.codeformer_fidelity = codeformerFidelity
|
||||
facetoolParameters.codeformer_fidelity = codeformerFidelity;
|
||||
}
|
||||
}
|
||||
|
||||
|
7
frontend/src/common/util/roundDownToMultiple.ts
Normal file
7
frontend/src/common/util/roundDownToMultiple.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const roundDownToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.floor(num / multiple) * multiple;
|
||||
};
|
||||
|
||||
export const roundToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.round(num / multiple) * multiple;
|
||||
};
|
@ -8,7 +8,7 @@ import { RootState } from '../../app/store';
|
||||
import {
|
||||
setActiveTab,
|
||||
setAllParameters,
|
||||
setInitialImagePath,
|
||||
setInitialImage,
|
||||
setSeed,
|
||||
setShouldShowImageDetails,
|
||||
} from '../options/optionsSlice';
|
||||
@ -17,12 +17,21 @@ import { SystemState } from '../system/systemSlice';
|
||||
import IAIButton from '../../common/components/IAIButton';
|
||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
||||
import {
|
||||
MdDelete,
|
||||
MdFace,
|
||||
MdHd,
|
||||
MdImage,
|
||||
MdInfo,
|
||||
} from 'react-icons/md';
|
||||
import InvokePopover from './InvokePopover';
|
||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@ -51,6 +60,7 @@ type CurrentImageButtonsProps = {
|
||||
*/
|
||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName } = useAppSelector(hoverableImageSelector);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
@ -74,7 +84,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const handleClickUseAsInitialImage = () => {
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
dispatch(setInitialImage(image));
|
||||
dispatch(setActiveTab(1));
|
||||
};
|
||||
|
||||
@ -103,7 +113,8 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
);
|
||||
|
||||
const handleClickUseAllParameters = () =>
|
||||
dispatch(setAllParameters(image.metadata));
|
||||
image.metadata && dispatch(setAllParameters(image.metadata));
|
||||
|
||||
useHotkeys(
|
||||
'a',
|
||||
() => {
|
||||
@ -128,9 +139,8 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
[image]
|
||||
);
|
||||
|
||||
// Non-null assertion: this button is disabled if there is no seed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const handleClickUseSeed = () => dispatch(setSeed(image.metadata.image.seed));
|
||||
const handleClickUseSeed = () =>
|
||||
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
||||
useHotkeys(
|
||||
's',
|
||||
() => {
|
||||
@ -221,6 +231,19 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
const handleClickShowImageDetails = () =>
|
||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||
|
||||
const handleSendToInpainting = () => {
|
||||
dispatch(setImageToInpaint(image));
|
||||
if (activeTabName !== 'inpainting') {
|
||||
dispatch(setActiveTab('inpainting'));
|
||||
}
|
||||
toast({
|
||||
title: 'Sent to Inpainting',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
@ -247,7 +270,32 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
|
||||
<IAIButton
|
||||
<IAIIconButton
|
||||
icon={<FaPaintBrush />}
|
||||
tooltip="Send To Inpainting"
|
||||
aria-label="Send To Inpainting"
|
||||
onClick={handleSendToInpainting}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaCopy />}
|
||||
tooltip="Use All"
|
||||
aria-label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||
}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaSeedling />}
|
||||
tooltip="Use Seed"
|
||||
aria-label="Use Seed"
|
||||
isDisabled={!image?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
{/* <IAIButton
|
||||
label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||
@ -259,7 +307,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
label="Use Seed"
|
||||
isDisabled={!image?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<InvokePopover
|
||||
title="Restore Faces"
|
||||
|
@ -1,57 +1,42 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.current-image-display {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'current-image-tools'
|
||||
'current-image-preview';
|
||||
grid-template-rows: auto 1fr;
|
||||
justify-items: center;
|
||||
.current-image-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
row-gap: 1rem;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.current-image-tools {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-image-options {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
padding: 1rem;
|
||||
height: fit-content;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
button {
|
||||
@include Button(
|
||||
$btn-width: 3rem,
|
||||
$icon-size: 22px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
.chakra-popover__popper {
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-preview {
|
||||
grid-area: current-image-preview;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
grid-template-areas: 'current-image-content';
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
grid-area: current-image-content;
|
||||
background-color: var(--img2img-img-bg-color);
|
||||
border-radius: 0.5rem;
|
||||
object-fit: contain;
|
||||
width: auto;
|
||||
height: $app-gallery-height;
|
||||
max-height: $app-gallery-height;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,39 +2,57 @@ import { RootState, useAppSelector } from '../../app/store';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { MdPhoto } from 'react-icons/md';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
export const currentImageDisplaySelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
const { currentImage, intermediateImage } = gallery;
|
||||
const { activeTab, shouldShowImageDetails } = options;
|
||||
|
||||
return {
|
||||
currentImage,
|
||||
intermediateImage,
|
||||
activeTabName: tabMap[activeTab],
|
||||
shouldShowImageDetails,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
const {
|
||||
currentImage,
|
||||
intermediateImage,
|
||||
activeTabName,
|
||||
} = useAppSelector(currentImageDisplaySelector);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return imageToDisplay ? (
|
||||
<div className="current-image-display">
|
||||
<div className="current-image-tools">
|
||||
<CurrentImageButtons image={imageToDisplay} />
|
||||
</div>
|
||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||
{shouldShowImageDetails && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
styleClass="current-image-metadata"
|
||||
/>
|
||||
return (
|
||||
<div className="current-image-area" data-tab-name={activeTabName}>
|
||||
{imageToDisplay ? (
|
||||
<>
|
||||
<CurrentImageButtons image={imageToDisplay} />
|
||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||
</>
|
||||
) : (
|
||||
<div className="current-image-display-placeholder">
|
||||
<MdPhoto />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="current-image-display-placeholder">
|
||||
<MdPhoto />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,23 +1,31 @@
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
|
||||
const imagesSelector = createSelector(
|
||||
(state: RootState) => state.gallery,
|
||||
(gallery: GalleryState) => {
|
||||
const currentImageIndex = gallery.images.findIndex(
|
||||
export const imagesSelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
const { currentCategory } = gallery;
|
||||
const { shouldShowImageDetails } = options;
|
||||
|
||||
const tempImages = gallery.categories[currentCategory].images;
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||
);
|
||||
const imagesLength = gallery.images.length;
|
||||
const imagesLength = tempImages.length;
|
||||
return {
|
||||
currentCategory,
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
shouldShowImageDetails,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -35,11 +43,12 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
const { imageToDisplay } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isOnFirstImage, isOnLastImage } = useAppSelector(imagesSelector);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
const {
|
||||
isOnFirstImage,
|
||||
isOnLastImage,
|
||||
currentCategory,
|
||||
shouldShowImageDetails,
|
||||
} = useAppSelector(imagesSelector);
|
||||
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
useState<boolean>(false);
|
||||
@ -53,20 +62,19 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
};
|
||||
|
||||
const handleClickPrevButton = () => {
|
||||
dispatch(selectPrevImage());
|
||||
dispatch(selectPrevImage(currentCategory));
|
||||
};
|
||||
|
||||
const handleClickNextButton = () => {
|
||||
dispatch(selectNextImage());
|
||||
dispatch(selectNextImage(currentCategory));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="current-image-preview">
|
||||
<div className={'current-image-preview'}>
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
width={imageToDisplay.width}
|
||||
height={imageToDisplay.height}
|
||||
/>
|
||||
{!shouldShowImageDetails && (
|
||||
<div className="current-image-next-prev-buttons">
|
||||
@ -100,6 +108,12 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowImageDetails && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
styleClass="current-image-metadata"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
.hoverable-image-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
@ -65,7 +64,7 @@
|
||||
}
|
||||
|
||||
.hoverable-image-context-menu {
|
||||
z-index: 999;
|
||||
z-index: 15;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--context-menu-bg-color);
|
||||
|
@ -6,8 +6,12 @@ import {
|
||||
Tooltip,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setCurrentImage } from './gallerySlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import {
|
||||
setCurrentImage,
|
||||
setShouldHoldGalleryOpen,
|
||||
setShouldShowGallery,
|
||||
} from './gallerySlice';
|
||||
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { memo, useState } from 'react';
|
||||
@ -15,13 +19,15 @@ import {
|
||||
setActiveTab,
|
||||
setAllImageToImageParameters,
|
||||
setAllTextToImageParameters,
|
||||
setInitialImagePath,
|
||||
setInitialImage,
|
||||
setPrompt,
|
||||
setSeed,
|
||||
} from '../options/optionsSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: InvokeAI.Image;
|
||||
@ -38,23 +44,21 @@ const memoEqualityCheck = (
|
||||
*/
|
||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
const { activeTabName, galleryImageObjectFit, galleryImageMinimumWidth } =
|
||||
useAppSelector(hoverableImageSelector);
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
|
||||
const handleUsePrompt = () => {
|
||||
dispatch(setPrompt(image.metadata.image.prompt));
|
||||
image.metadata && dispatch(setPrompt(image.metadata.image.prompt));
|
||||
toast({
|
||||
title: 'Prompt Set',
|
||||
status: 'success',
|
||||
@ -64,7 +68,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
};
|
||||
|
||||
const handleUseSeed = () => {
|
||||
dispatch(setSeed(image.metadata.image.seed));
|
||||
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
||||
toast({
|
||||
title: 'Seed Set',
|
||||
status: 'success',
|
||||
@ -74,9 +78,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
};
|
||||
|
||||
const handleSendToImageToImage = () => {
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
if (activeTab !== 1) {
|
||||
dispatch(setActiveTab(1));
|
||||
dispatch(setInitialImage(image));
|
||||
if (activeTabName !== 'img2img') {
|
||||
dispatch(setActiveTab('img2img'));
|
||||
}
|
||||
toast({
|
||||
title: 'Sent to Image To Image',
|
||||
@ -86,8 +90,21 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendToInpainting = () => {
|
||||
dispatch(setImageToInpaint(image));
|
||||
if (activeTabName !== 'inpainting') {
|
||||
dispatch(setActiveTab('inpainting'));
|
||||
}
|
||||
toast({
|
||||
title: 'Sent to Inpainting',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUseAllParameters = () => {
|
||||
dispatch(setAllTextToImageParameters(metadata));
|
||||
metadata && dispatch(setAllTextToImageParameters(metadata));
|
||||
toast({
|
||||
title: 'Parameters Set',
|
||||
status: 'success',
|
||||
@ -124,7 +141,12 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const handleSelectImage = () => dispatch(setCurrentImage(image));
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Root
|
||||
onOpenChange={(open: boolean) => {
|
||||
dispatch(setShouldHoldGalleryOpen(open));
|
||||
dispatch(setShouldShowGallery(true));
|
||||
}}
|
||||
>
|
||||
<ContextMenu.Trigger>
|
||||
<Box
|
||||
position={'relative'}
|
||||
@ -135,7 +157,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
>
|
||||
<Image
|
||||
className="hoverable-image-image"
|
||||
objectFit="cover"
|
||||
objectFit={galleryImageObjectFit}
|
||||
rounded={'md'}
|
||||
src={url}
|
||||
loading={'lazy'}
|
||||
@ -150,7 +172,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isHovered && (
|
||||
{isHovered && galleryImageMinimumWidth >= 64 && (
|
||||
<div className="hoverable-image-delete-button">
|
||||
<Tooltip label={'Delete image'} hasArrow>
|
||||
<DeleteImageModal image={image}>
|
||||
@ -167,7 +189,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
)}
|
||||
</Box>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content className="hoverable-image-context-menu">
|
||||
<ContextMenu.Content
|
||||
className="hoverable-image-context-menu"
|
||||
sticky={'always'}
|
||||
>
|
||||
<ContextMenu.Item
|
||||
onClickCapture={handleUsePrompt}
|
||||
disabled={image?.metadata?.image?.prompt === undefined}
|
||||
@ -200,6 +225,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
<ContextMenu.Item onClickCapture={handleSendToImageToImage}>
|
||||
Send to Image To Image
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item onClickCapture={handleSendToInpainting}>
|
||||
Send to Inpainting
|
||||
</ContextMenu.Item>
|
||||
<DeleteImageModal image={image}>
|
||||
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
|
||||
</DeleteImageModal>
|
||||
|
@ -1,64 +1,159 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.image-gallery-area-enter {
|
||||
transform: translateX(150%);
|
||||
}
|
||||
|
||||
.image-gallery-area-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.image-gallery-area-exit-active {
|
||||
transform: translateX(150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area {
|
||||
.image-gallery-popup-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 6rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
z-index: 10;
|
||||
|
||||
&[data-pinned='false'] {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
.image-gallery-popup {
|
||||
border-radius: 0;
|
||||
.image-gallery-container {
|
||||
max-height: calc($app-height + 5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-popup {
|
||||
background-color: var(--tab-color);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-left-width: 0.3rem;
|
||||
|
||||
border-color: var(--resizeable-handle-border-color);
|
||||
|
||||
&[data-resize-alert='true'] {
|
||||
border-color: var(--status-bad-color);
|
||||
}
|
||||
|
||||
.image-gallery-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.image-gallery-icon-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-settings-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: $app-gallery-popover-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
|
||||
.image-gallery-container-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
place-items: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
color: var(--subtext-color-bright);
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-load-more-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
font-size: 0.85rem !important;
|
||||
padding: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:disabled {
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-popup {
|
||||
background-color: var(--tab-color);
|
||||
padding: 1rem;
|
||||
animation: slideOut 0.3s ease-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-left-width: 0.2rem;
|
||||
min-width: 300px;
|
||||
border-color: var(--gallery-resizeable-color);
|
||||
}
|
||||
.image-gallery-category-btn-group {
|
||||
width: 100% !important;
|
||||
column-gap: 0 !important;
|
||||
justify-content: stretch !important;
|
||||
|
||||
.image-gallery-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
button {
|
||||
flex-grow: 1;
|
||||
&[data-selected='true'] {
|
||||
background-color: var(--accent-color);
|
||||
&:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-close-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: $app-gallery-popover-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
// from https://css-tricks.com/a-grid-of-logos-in-squares/
|
||||
.image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, auto));
|
||||
grid-gap: 0.5rem;
|
||||
.hoverable-image {
|
||||
padding: 0.5rem;
|
||||
@ -86,38 +181,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-load-more-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
font-size: 0.85rem !important;
|
||||
font-family: Inter;
|
||||
|
||||
&:disabled {
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-container-placeholder {
|
||||
display: flex;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
place-items: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
p {
|
||||
color: var(--subtext-color-bright);
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,146 @@
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { NumberSize, Resizable, Size } from 're-resizable';
|
||||
|
||||
import React from 'react';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
||||
import { BsPinAngleFill } from 'react-icons/bs';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import {
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setCurrentCategory,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setGalleryScrollPosition,
|
||||
setGalleryWidth,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setShouldHoldGalleryOpen,
|
||||
setShouldPinGallery,
|
||||
} from './gallerySlice';
|
||||
import HoverableImage from './HoverableImage';
|
||||
import { setShouldShowGallery } from '../options/optionsSlice';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
import { ButtonGroup, useToast } from '@chakra-ui/react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { Direction } from 're-resizable/lib/resizer';
|
||||
import { imageGallerySelector } from './gallerySliceSelectors';
|
||||
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
||||
import IAIPopover from '../../common/components/IAIPopover';
|
||||
import IAISlider from '../../common/components/IAISlider';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
import IAICheckbox from '../../common/components/IAICheckbox';
|
||||
import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import _ from 'lodash';
|
||||
|
||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
|
||||
|
||||
export default function ImageGallery() {
|
||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const shouldShowGallery = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowGallery
|
||||
);
|
||||
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const handleShowGalleryToggle = () => {
|
||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||
const {
|
||||
images,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryScrollPosition,
|
||||
galleryImageMinimumWidth,
|
||||
galleryGridTemplateColumns,
|
||||
activeTabName,
|
||||
galleryImageObjectFit,
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
areMoreImagesAvailable,
|
||||
galleryWidth,
|
||||
} = useAppSelector(imageGallerySelector);
|
||||
|
||||
const [galleryMinWidth, setGalleryMinWidth] = useState<number>(300);
|
||||
const [galleryMaxWidth, setGalleryMaxWidth] = useState<number>(590);
|
||||
|
||||
const [shouldShowButtons, setShouldShowButtons] = useState<boolean>(
|
||||
galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldPinGallery) return;
|
||||
|
||||
if (activeTabName === 'inpainting') {
|
||||
dispatch(setGalleryWidth(220));
|
||||
setGalleryMinWidth(220);
|
||||
setGalleryMaxWidth(220);
|
||||
} else if (activeTabName === 'img2img') {
|
||||
dispatch(
|
||||
setGalleryWidth(Math.min(Math.max(Number(galleryWidth), 0), 490))
|
||||
);
|
||||
setGalleryMaxWidth(490);
|
||||
} else {
|
||||
dispatch(
|
||||
setGalleryWidth(Math.min(Math.max(Number(galleryWidth), 0), 590))
|
||||
);
|
||||
setGalleryMaxWidth(590);
|
||||
}
|
||||
dispatch(setNeedsCache(true));
|
||||
}, [dispatch, activeTabName, shouldPinGallery, galleryWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldPinGallery) {
|
||||
setGalleryMaxWidth(window.innerWidth);
|
||||
}
|
||||
}, [shouldPinGallery]);
|
||||
|
||||
const galleryRef = useRef<HTMLDivElement>(null);
|
||||
const galleryContainerRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(setNeedsCache(true));
|
||||
dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||
};
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
const handleToggleGallery = () => {
|
||||
shouldShowGallery ? handleCloseGallery() : handleOpenGallery();
|
||||
};
|
||||
|
||||
const handleOpenGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
dispatch(
|
||||
setGalleryScrollPosition(
|
||||
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowGallery(false));
|
||||
dispatch(setShouldHoldGalleryOpen(false));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(requestImages());
|
||||
dispatch(requestImages(currentCategory));
|
||||
};
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
dispatch(setGalleryImageMinimumWidth(v));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const setCloseGalleryTimer = () => {
|
||||
timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
|
||||
};
|
||||
|
||||
const cancelCloseGalleryTimer = () => {
|
||||
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'g',
|
||||
() => {
|
||||
handleShowGalleryToggle();
|
||||
handleToggleGallery();
|
||||
},
|
||||
[shouldShowGallery]
|
||||
);
|
||||
@ -49,81 +148,353 @@ export default function ImageGallery() {
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage());
|
||||
dispatch(selectPrevImage(currentCategory));
|
||||
},
|
||||
[]
|
||||
[currentCategory]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage());
|
||||
dispatch(selectNextImage(currentCategory));
|
||||
},
|
||||
[]
|
||||
[currentCategory]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="image-gallery-area">
|
||||
{!shouldShowGallery && (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
onClick={handleShowGalleryToggle}
|
||||
className="image-gallery-popup-btn"
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
)}
|
||||
useHotkeys(
|
||||
'shift+p',
|
||||
() => {
|
||||
handleSetShouldPinGallery();
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
{shouldShowGallery && (
|
||||
const IMAGE_SIZE_STEP = 32;
|
||||
|
||||
useHotkeys(
|
||||
'shift+up',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth >= 256) {
|
||||
return;
|
||||
}
|
||||
if (galleryImageMinimumWidth < 256) {
|
||||
const newMinWidth = galleryImageMinimumWidth + IMAGE_SIZE_STEP;
|
||||
if (newMinWidth <= 256) {
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
toast({
|
||||
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
||||
status: 'success',
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
dispatch(setGalleryImageMinimumWidth(256));
|
||||
toast({
|
||||
title: `Gallery Thumbnail Size set to 256`,
|
||||
status: 'success',
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+down',
|
||||
() => {
|
||||
if (galleryImageMinimumWidth <= 32) {
|
||||
return;
|
||||
}
|
||||
if (galleryImageMinimumWidth > 32) {
|
||||
const newMinWidth = galleryImageMinimumWidth - IMAGE_SIZE_STEP;
|
||||
if (newMinWidth > 32) {
|
||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||
toast({
|
||||
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
||||
status: 'success',
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
dispatch(setGalleryImageMinimumWidth(32));
|
||||
toast({
|
||||
title: `Gallery Thumbnail Size set to 32`,
|
||||
status: 'success',
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+r',
|
||||
() => {
|
||||
dispatch(setGalleryImageMinimumWidth(64));
|
||||
toast({
|
||||
title: `Reset Gallery Image Size`,
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[galleryImageMinimumWidth]
|
||||
);
|
||||
|
||||
// set gallery scroll position
|
||||
useEffect(() => {
|
||||
if (!galleryContainerRef.current) return;
|
||||
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
||||
}, [galleryScrollPosition, shouldShowGallery]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={galleryRef}
|
||||
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames="image-gallery-area"
|
||||
>
|
||||
<div
|
||||
className="image-gallery-area"
|
||||
data-pinned={shouldPinGallery}
|
||||
ref={galleryRef}
|
||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||
onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
||||
onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
||||
>
|
||||
<Resizable
|
||||
defaultSize={{ width: '300', height: '100%' }}
|
||||
minWidth={'300'}
|
||||
maxWidth={activeTab == 1 ? '300' : '600'}
|
||||
className="image-gallery-popup"
|
||||
minWidth={galleryMinWidth}
|
||||
maxWidth={galleryMaxWidth}
|
||||
// maxHeight={'100%'}
|
||||
className={'image-gallery-popup'}
|
||||
handleStyles={{ left: { width: '15px' } }}
|
||||
enable={{
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: true,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
}}
|
||||
size={{
|
||||
width: galleryWidth,
|
||||
height: shouldPinGallery ? '100%' : '100vh',
|
||||
}}
|
||||
onResizeStop={(
|
||||
_event: MouseEvent | TouchEvent,
|
||||
_direction: Direction,
|
||||
elementRef: HTMLElement,
|
||||
delta: NumberSize
|
||||
) => {
|
||||
dispatch(
|
||||
setGalleryWidth(
|
||||
_.clamp(
|
||||
Number(galleryWidth) + delta.width,
|
||||
0,
|
||||
Number(galleryMaxWidth)
|
||||
)
|
||||
)
|
||||
);
|
||||
elementRef.removeAttribute('data-resize-alert');
|
||||
}}
|
||||
onResize={(
|
||||
_event: MouseEvent | TouchEvent,
|
||||
_direction: Direction,
|
||||
elementRef: HTMLElement,
|
||||
delta: NumberSize
|
||||
) => {
|
||||
const newWidth = _.clamp(
|
||||
Number(galleryWidth) + delta.width,
|
||||
0,
|
||||
Number(galleryMaxWidth)
|
||||
);
|
||||
|
||||
if (newWidth >= 320 && !shouldShowButtons) {
|
||||
setShouldShowButtons(true);
|
||||
} else if (newWidth < 320 && shouldShowButtons) {
|
||||
setShouldShowButtons(false);
|
||||
}
|
||||
|
||||
if (newWidth >= galleryMaxWidth) {
|
||||
elementRef.setAttribute('data-resize-alert', 'true');
|
||||
} else {
|
||||
elementRef.removeAttribute('data-resize-alert');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-gallery-header">
|
||||
<h1>Your Invocations</h1>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
aria-label={'Close Gallery'}
|
||||
onClick={handleGalleryClose}
|
||||
className="image-gallery-close-btn"
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
</div>
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
<div>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
isAttached
|
||||
variant="solid"
|
||||
className="image-gallery-category-btn-group"
|
||||
>
|
||||
{shouldShowButtons ? (
|
||||
<>
|
||||
<Button
|
||||
data-selected={currentCategory === 'result'}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
>
|
||||
Invocations
|
||||
</Button>
|
||||
<Button
|
||||
data-selected={currentCategory === 'user'}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
>
|
||||
User
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IAIIconButton
|
||||
aria-label="Show Invocations"
|
||||
tooltip="Show Invocations"
|
||||
data-selected={currentCategory === 'result'}
|
||||
icon={<FaImage />}
|
||||
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<IAIIconButton
|
||||
aria-label="Show Uploads"
|
||||
tooltip="Show Uploads"
|
||||
data-selected={currentCategory === 'user'}
|
||||
icon={<FaUser />}
|
||||
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
hasArrow={activeTabName === 'inpainting' ? false : true}
|
||||
placement={'left'}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Gallery Settings'}
|
||||
icon={<FaWrench />}
|
||||
className="image-gallery-icon-btn"
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="image-gallery-settings-popover">
|
||||
<div>
|
||||
<IAISlider
|
||||
value={galleryImageMinimumWidth}
|
||||
onChange={handleChangeGalleryImageMinimumWidth}
|
||||
min={32}
|
||||
max={256}
|
||||
width={100}
|
||||
label={'Image Size'}
|
||||
formLabelProps={{ style: { fontSize: '0.9rem' } }}
|
||||
sliderThumbTooltipProps={{
|
||||
label: `${galleryImageMinimumWidth}px`,
|
||||
}}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset'}
|
||||
tooltip={'Reset Size'}
|
||||
onClick={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||
icon={<BiReset />}
|
||||
data-selected={shouldPinGallery}
|
||||
styleClass="image-gallery-icon-btn"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IAICheckbox
|
||||
label="Maintain Aspect Ratio"
|
||||
isChecked={galleryImageObjectFit === 'contain'}
|
||||
onChange={() =>
|
||||
dispatch(
|
||||
setGalleryImageObjectFit(
|
||||
galleryImageObjectFit === 'contain'
|
||||
? 'cover'
|
||||
: 'contain'
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IAICheckbox
|
||||
label="Auto-Switch to New Images"
|
||||
isChecked={shouldAutoSwitchToNewImages}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(
|
||||
setShouldAutoSwitchToNewImages(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Pin Gallery'}
|
||||
tooltip={'Pin Gallery (Shift+P)'}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={<BsPinAngleFill />}
|
||||
data-selected={shouldPinGallery}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Close Gallery'}
|
||||
tooltip={'Close Gallery (G)'}
|
||||
onClick={handleCloseGallery}
|
||||
className="image-gallery-icon-btn"
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="image-gallery-container" ref={galleryContainerRef}>
|
||||
{images.length || areMoreImagesAvailable ? (
|
||||
<>
|
||||
<div
|
||||
className="image-gallery"
|
||||
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
||||
>
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
</Resizable>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
/**
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGalleryOld = () => {
|
||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
/**
|
||||
* I don't like that this needs to rerender whenever the current image is changed.
|
||||
* What if we have a large number of images? I suppose pagination (planned) will
|
||||
* mitigate this issue.
|
||||
*
|
||||
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
|
||||
*/
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(requestImages());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'g',
|
||||
() => {
|
||||
if (isOpen) {
|
||||
onClose();
|
||||
} else {
|
||||
onOpen();
|
||||
}
|
||||
},
|
||||
[isOpen]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="image-gallery-area">
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
onClick={onOpen}
|
||||
className="image-gallery-popup-btn"
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</Button>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
autoFocus={false}
|
||||
trapFocus={false}
|
||||
closeOnOverlayClick={false}
|
||||
>
|
||||
<DrawerContent className="image-gallery-popup">
|
||||
<div className="image-gallery-header">
|
||||
<DrawerHeader>Your Invocations</DrawerHeader>
|
||||
<DrawerCloseButton />
|
||||
</div>
|
||||
<DrawerBody className="image-gallery-body">
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGalleryOld;
|
@ -1,12 +1,15 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.image-metadata-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--metadata-bg-color);
|
||||
overflow: scroll;
|
||||
max-height: $app-metadata-height;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ import {
|
||||
setHeight,
|
||||
setHiresFix,
|
||||
setImg2imgStrength,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
setPrompt,
|
||||
setSampler,
|
||||
@ -32,6 +31,7 @@ import {
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
setWidth,
|
||||
setInitialImage,
|
||||
} from '../../options/optionsSlice';
|
||||
import promptToString from '../../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
||||
@ -248,7 +248,7 @@ const ImageMetadataViewer = memo(
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImagePath(init_image_path))}
|
||||
onClick={() => dispatch(setInitialImage(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
|
20
frontend/src/features/gallery/ShowHideGalleryButton.scss
Normal file
20
frontend/src/features/gallery/ShowHideGalleryButton.scss
Normal file
@ -0,0 +1,20 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.show-hide-gallery-button {
|
||||
position: absolute !important;
|
||||
top: 50%;
|
||||
right: -1rem;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 10;
|
||||
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 12rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
57
frontend/src/features/gallery/ShowHideGalleryButton.tsx
Normal file
57
frontend/src/features/gallery/ShowHideGalleryButton.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
|
||||
const ShowHideGalleryButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { shouldPinGallery, shouldShowGallery } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const handleShowGalleryToggle = () => {
|
||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||
};
|
||||
|
||||
// useHotkeys(
|
||||
// 'g',
|
||||
// () => {
|
||||
// handleShowGalleryToggle();
|
||||
// },
|
||||
// [shouldShowGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'left',
|
||||
// () => {
|
||||
// dispatch(selectPrevImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'right',
|
||||
// () => {
|
||||
// dispatch(selectNextImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
onClick={handleShowGalleryToggle}
|
||||
styleClass="show-hide-gallery-button"
|
||||
onMouseOver={!shouldPinGallery ? handleShowGalleryToggle : undefined}
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowHideGalleryButton;
|
@ -3,20 +3,67 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import _, { clamp } from 'lodash';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
export type GalleryCategory = 'user' | 'result';
|
||||
|
||||
export type AddImagesPayload = {
|
||||
images: Array<InvokeAI.Image>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
export type Gallery = {
|
||||
images: InvokeAI.Image[];
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
areMoreImagesAvailable: boolean;
|
||||
};
|
||||
|
||||
export interface GalleryState {
|
||||
currentImage?: InvokeAI.Image;
|
||||
currentImageUuid: string;
|
||||
images: Array<InvokeAI.Image>;
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
areMoreImagesAvailable: boolean;
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
shouldPinGallery: boolean;
|
||||
shouldShowGallery: boolean;
|
||||
galleryScrollPosition: number;
|
||||
galleryImageMinimumWidth: number;
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldHoldGalleryOpen: boolean;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
categories: {
|
||||
user: Gallery;
|
||||
result: Gallery;
|
||||
};
|
||||
currentCategory: GalleryCategory;
|
||||
galleryWidth: number;
|
||||
}
|
||||
|
||||
const initialState: GalleryState = {
|
||||
currentImageUuid: '',
|
||||
images: [],
|
||||
areMoreImagesAvailable: true,
|
||||
shouldPinGallery: true,
|
||||
shouldShowGallery: true,
|
||||
galleryScrollPosition: 0,
|
||||
galleryImageMinimumWidth: 64,
|
||||
galleryImageObjectFit: 'cover',
|
||||
shouldHoldGalleryOpen: false,
|
||||
shouldAutoSwitchToNewImages: true,
|
||||
currentCategory: 'result',
|
||||
categories: {
|
||||
user: {
|
||||
images: [],
|
||||
latest_mtime: undefined,
|
||||
earliest_mtime: undefined,
|
||||
areMoreImagesAvailable: true,
|
||||
},
|
||||
result: {
|
||||
images: [],
|
||||
latest_mtime: undefined,
|
||||
earliest_mtime: undefined,
|
||||
areMoreImagesAvailable: true,
|
||||
},
|
||||
},
|
||||
galleryWidth: 300,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@ -27,10 +74,15 @@ export const gallerySlice = createSlice({
|
||||
state.currentImage = action.payload;
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
},
|
||||
removeImage: (state, action: PayloadAction<string>) => {
|
||||
const uuid = action.payload;
|
||||
removeImage: (
|
||||
state,
|
||||
action: PayloadAction<InvokeAI.ImageDeletedResponse>
|
||||
) => {
|
||||
const { uuid, category } = action.payload;
|
||||
|
||||
const newImages = state.images.filter((image) => image.uuid !== uuid);
|
||||
const tempImages = state.categories[category as GalleryCategory].images;
|
||||
|
||||
const newImages = tempImages.filter((image) => image.uuid !== uuid);
|
||||
|
||||
if (uuid === state.currentImageUuid) {
|
||||
/**
|
||||
@ -42,7 +94,7 @@ export const gallerySlice = createSlice({
|
||||
*
|
||||
* Get the currently selected image's index.
|
||||
*/
|
||||
const imageToDeleteIndex = state.images.findIndex(
|
||||
const imageToDeleteIndex = tempImages.findIndex(
|
||||
(image) => image.uuid === uuid
|
||||
);
|
||||
|
||||
@ -68,22 +120,35 @@ export const gallerySlice = createSlice({
|
||||
: '';
|
||||
}
|
||||
|
||||
state.images = newImages;
|
||||
state.categories[category as GalleryCategory].images = newImages;
|
||||
},
|
||||
addImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
const newImage = action.payload;
|
||||
addImage: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
image: InvokeAI.Image;
|
||||
category: GalleryCategory;
|
||||
}>
|
||||
) => {
|
||||
const { image: newImage, category } = action.payload;
|
||||
const { uuid, url, mtime } = newImage;
|
||||
|
||||
const tempCategory = state.categories[category as GalleryCategory];
|
||||
|
||||
// Do not add duplicate images
|
||||
if (state.images.find((i) => i.url === url && i.mtime === mtime)) {
|
||||
if (tempCategory.images.find((i) => i.url === url && i.mtime === mtime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.images.unshift(newImage);
|
||||
state.currentImageUuid = uuid;
|
||||
tempCategory.images.unshift(newImage);
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.currentImageUuid = uuid;
|
||||
state.currentImage = newImage;
|
||||
if (category === 'result') {
|
||||
state.currentCategory = 'result';
|
||||
}
|
||||
}
|
||||
state.intermediateImage = undefined;
|
||||
state.currentImage = newImage;
|
||||
state.latest_mtime = mtime;
|
||||
tempCategory.latest_mtime = mtime;
|
||||
},
|
||||
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.intermediateImage = action.payload;
|
||||
@ -91,49 +156,53 @@ export const gallerySlice = createSlice({
|
||||
clearIntermediateImage: (state) => {
|
||||
state.intermediateImage = undefined;
|
||||
},
|
||||
selectNextImage: (state) => {
|
||||
const { images, currentImage } = state;
|
||||
selectNextImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
const category = action.payload;
|
||||
const { currentImage } = state;
|
||||
const tempImages = state.categories[category].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = images.findIndex(
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (_.inRange(currentImageIndex, 0, images.length)) {
|
||||
const newCurrentImage = images[currentImageIndex + 1];
|
||||
if (_.inRange(currentImageIndex, 0, tempImages.length)) {
|
||||
const newCurrentImage = tempImages[currentImageIndex + 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectPrevImage: (state) => {
|
||||
const { images, currentImage } = state;
|
||||
selectPrevImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
const category = action.payload;
|
||||
const { currentImage } = state;
|
||||
const tempImages = state.categories[category].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = images.findIndex(
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (_.inRange(currentImageIndex, 1, images.length + 1)) {
|
||||
const newCurrentImage = images[currentImageIndex - 1];
|
||||
if (_.inRange(currentImageIndex, 1, tempImages.length + 1)) {
|
||||
const newCurrentImage = tempImages[currentImageIndex - 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
addGalleryImages: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
images: Array<InvokeAI.Image>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
}>
|
||||
) => {
|
||||
const { images, areMoreImagesAvailable } = action.payload;
|
||||
addGalleryImages: (state, action: PayloadAction<AddImagesPayload>) => {
|
||||
const { images, areMoreImagesAvailable, category } = action.payload;
|
||||
const tempImages = state.categories[category].images;
|
||||
|
||||
// const prevImages = category === 'user' ? state.userImages : state.resultImages
|
||||
|
||||
if (images.length > 0) {
|
||||
// Filter images that already exist in the gallery
|
||||
const newImages = images.filter(
|
||||
(newImage) =>
|
||||
!state.images.find(
|
||||
!tempImages.find(
|
||||
(i) => i.url === newImage.url && i.mtime === newImage.mtime
|
||||
)
|
||||
);
|
||||
state.images = state.images
|
||||
state.categories[category].images = tempImages
|
||||
.concat(newImages)
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
@ -144,13 +213,46 @@ export const gallerySlice = createSlice({
|
||||
}
|
||||
|
||||
// keep track of the timestamps of latest and earliest images received
|
||||
state.latest_mtime = images[0].mtime;
|
||||
state.earliest_mtime = images[images.length - 1].mtime;
|
||||
state.categories[category].latest_mtime = images[0].mtime;
|
||||
state.categories[category].earliest_mtime =
|
||||
images[images.length - 1].mtime;
|
||||
}
|
||||
|
||||
if (areMoreImagesAvailable !== undefined) {
|
||||
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
||||
state.categories[category].areMoreImagesAvailable =
|
||||
areMoreImagesAvailable;
|
||||
}
|
||||
},
|
||||
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldPinGallery = action.payload;
|
||||
},
|
||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowGallery = action.payload;
|
||||
},
|
||||
setGalleryScrollPosition: (state, action: PayloadAction<number>) => {
|
||||
state.galleryScrollPosition = action.payload;
|
||||
},
|
||||
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryImageMinimumWidth = action.payload;
|
||||
},
|
||||
setGalleryImageObjectFit: (
|
||||
state,
|
||||
action: PayloadAction<GalleryImageObjectFitType>
|
||||
) => {
|
||||
state.galleryImageObjectFit = action.payload;
|
||||
},
|
||||
setShouldHoldGalleryOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldHoldGalleryOpen = action.payload;
|
||||
},
|
||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldAutoSwitchToNewImages = action.payload;
|
||||
},
|
||||
setCurrentCategory: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
state.currentCategory = action.payload;
|
||||
},
|
||||
setGalleryWidth: (state, action: PayloadAction<number>) => {
|
||||
state.galleryWidth = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -163,6 +265,15 @@ export const {
|
||||
setIntermediateImage,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setShouldPinGallery,
|
||||
setShouldShowGallery,
|
||||
setGalleryScrollPosition,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldHoldGalleryOpen,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setCurrentCategory,
|
||||
setGalleryWidth,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
55
frontend/src/features/gallery/gallerySliceSelectors.ts
Normal file
55
frontend/src/features/gallery/gallerySliceSelectors.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
const {
|
||||
categories,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryScrollPosition,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
galleryWidth,
|
||||
} = gallery;
|
||||
|
||||
const { activeTab } = options;
|
||||
|
||||
return {
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
shouldShowGallery,
|
||||
galleryScrollPosition,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||
activeTabName: tabMap[activeTab],
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
images: categories[currentCategory].images,
|
||||
areMoreImagesAvailable:
|
||||
categories[currentCategory].areMoreImagesAvailable,
|
||||
currentCategory,
|
||||
galleryWidth,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const hoverableImageSelector = createSelector(
|
||||
[(state: RootState) => state.options, (state: RootState) => state.gallery],
|
||||
(options: OptionsState, gallery: GalleryState) => {
|
||||
return {
|
||||
galleryImageObjectFit: gallery.galleryImageObjectFit,
|
||||
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
};
|
||||
}
|
||||
);
|
@ -8,7 +8,7 @@ import {
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldRunFacetool } from '../../optionsSlice';
|
||||
|
||||
export default function FaceRestore() {
|
||||
export default function FaceRestoreHeader() {
|
||||
const isGFPGANAvailable = useAppSelector(
|
||||
(state: RootState) => state.system.isGFPGANAvailable
|
||||
);
|
@ -1,39 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldUseInitImage } from '../../optionsSlice';
|
||||
|
||||
export default function ImageToImageAccordion() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const initialImagePath = useAppSelector(
|
||||
(state: RootState) => state.options.initialImagePath
|
||||
);
|
||||
|
||||
const shouldUseInitImage = useAppSelector(
|
||||
(state: RootState) => state.options.shouldUseInitImage
|
||||
);
|
||||
|
||||
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInitImage(e.target.checked));
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<p>Image to Image</p>
|
||||
<IAISwitch
|
||||
isDisabled={!initialImagePath}
|
||||
isChecked={shouldUseInitImage}
|
||||
onChange={handleChangeShouldUseInitImage}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import InitAndMaskImage from '../../InitAndMaskImage';
|
||||
import ImageFit from './ImageFit';
|
||||
import ImageToImageStrength from './ImageToImageStrength';
|
||||
|
||||
/**
|
||||
* Options for img2img generation (strength, fit, init/mask upload).
|
||||
*/
|
||||
const ImageToImageOptions = () => {
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<ImageToImageStrength />
|
||||
<ImageFit />
|
||||
<InitAndMaskImage />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageOptions;
|
@ -30,7 +30,7 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
|
||||
max={0.99}
|
||||
onChange={handleChangeStrength}
|
||||
value={img2imgStrength}
|
||||
width="90px"
|
||||
width="100%"
|
||||
isInteger={false}
|
||||
styleClass={styleClass}
|
||||
/>
|
||||
|
@ -0,0 +1,43 @@
|
||||
.inpainting-bounding-box-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0.4rem;
|
||||
border: 2px solid var(--tab-color);
|
||||
}
|
||||
|
||||
.inpainting-bounding-box-header {
|
||||
background-color: var(--tab-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.4rem 0.4rem 0 0;
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.inpainting-bounding-box-settings-items {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
|
||||
.inpainting-bounding-box-reset-icon-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inpainting-bounding-box-dimensions-slider-numberinput {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.inpainting-bounding-box-darken {
|
||||
width: max-content;
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAICheckbox from '../../../../common/components/IAICheckbox';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISlider from '../../../../common/components/IAISlider';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||
import {
|
||||
InpaintingState,
|
||||
setBoundingBoxDimensions,
|
||||
setShouldLockBoundingBox,
|
||||
setShouldShowBoundingBox,
|
||||
setShouldShowBoundingBoxFill,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
const boundingBoxDimensionsSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
pastLines,
|
||||
futureLines,
|
||||
shouldLockBoundingBox,
|
||||
} = inpainting;
|
||||
return {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
pastLines,
|
||||
futureLines,
|
||||
shouldLockBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const BoundingBoxSettings = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
shouldLockBoundingBox,
|
||||
} = useAppSelector(boundingBoxDimensionsSelector);
|
||||
|
||||
const handleChangeBoundingBoxWidth = (v: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, width: Math.floor(v) }));
|
||||
};
|
||||
|
||||
const handleChangeBoundingBoxHeight = (v: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, height: Math.floor(v) }));
|
||||
};
|
||||
|
||||
const handleShowBoundingBox = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldShowBoundingBox(e.target.checked));
|
||||
|
||||
const handleChangeShouldShowBoundingBoxFill = () => {
|
||||
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
||||
};
|
||||
|
||||
const handleChangeShouldLockBoundingBox = () => {
|
||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||
};
|
||||
|
||||
const handleResetWidth = () => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(canvasDimensions.width),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleResetHeight = () => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(canvasDimensions.height),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inpainting-bounding-box-settings">
|
||||
<div className="inpainting-bounding-box-header">
|
||||
<p>Inpaint Box</p>
|
||||
<IAISwitch
|
||||
isChecked={shouldShowBoundingBox}
|
||||
width={'auto'}
|
||||
onChange={handleShowBoundingBox}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-settings-items">
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Box W"
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeBoundingBoxWidth}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeBoundingBoxWidth}
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||
step={64}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset Width'}
|
||||
tooltip={'Reset Width'}
|
||||
onClick={handleResetWidth}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
!shouldShowBoundingBox ||
|
||||
canvasDimensions.width === boundingBoxDimensions.width
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Box H"
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeBoundingBoxHeight}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeBoundingBoxHeight}
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||
step={64}
|
||||
padding="0"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset Height'}
|
||||
tooltip={'Reset Height'}
|
||||
onClick={handleResetHeight}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
!shouldShowBoundingBox ||
|
||||
canvasDimensions.height === boundingBoxDimensions.height
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<IAICheckbox
|
||||
label="Darken Outside Box"
|
||||
isChecked={shouldShowBoundingBoxFill}
|
||||
onChange={handleChangeShouldShowBoundingBoxFill}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
/>
|
||||
<IAICheckbox
|
||||
label="Lock Bounding Box"
|
||||
isChecked={shouldLockBoundingBox}
|
||||
onChange={handleChangeShouldLockBoundingBox}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundingBoxSettings;
|
@ -0,0 +1,95 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIButton from '../../../../common/components/IAIButton';
|
||||
import {
|
||||
InpaintingState,
|
||||
setClearBrushHistory,
|
||||
setInpaintReplace,
|
||||
setShouldUseInpaintReplace,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
import BoundingBoxSettings from './BoundingBoxSettings';
|
||||
import _ from 'lodash';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
|
||||
const inpaintingSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const { pastLines, futureLines, inpaintReplace, shouldUseInpaintReplace } =
|
||||
inpainting;
|
||||
return {
|
||||
pastLines,
|
||||
futureLines,
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingSettings() {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const { pastLines, futureLines, inpaintReplace, shouldUseInpaintReplace } =
|
||||
useAppSelector(inpaintingSelector);
|
||||
|
||||
const handleClearBrushHistory = () => {
|
||||
dispatch(setClearBrushHistory());
|
||||
toast({
|
||||
title: 'Brush Stroke History Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 1rem 0 0.2rem',
|
||||
}}
|
||||
>
|
||||
<IAINumberInput
|
||||
label="Inpaint Replace"
|
||||
value={inpaintReplace}
|
||||
min={0}
|
||||
max={1.0}
|
||||
step={0.05}
|
||||
width={'auto'}
|
||||
formControlProps={{ style: { paddingRight: '1rem' } }}
|
||||
isInteger={false}
|
||||
isDisabled={!shouldUseInpaintReplace}
|
||||
onChange={(v: number) => {
|
||||
dispatch(setInpaintReplace(v));
|
||||
}}
|
||||
/>
|
||||
<IAISwitch
|
||||
isChecked={shouldUseInpaintReplace}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInpaintReplace(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<BoundingBoxSettings />
|
||||
<IAIButton
|
||||
label="Clear Brush History"
|
||||
onClick={handleClearBrushHistory}
|
||||
tooltip="Clears brush stroke history"
|
||||
disabled={futureLines.length > 0 || pastLines.length > 0 ? false : true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setHiresFix } from './optionsSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import IAISwitch from '../../common/components/IAISwitch';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setHiresFix } from '../../optionsSlice';
|
||||
|
||||
/**
|
||||
* Image output options. Includes width, height, seamless tiling.
|
||||
* Hires Fix Toggle
|
||||
*/
|
||||
const HiresOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -16,7 +19,6 @@ const HiresOptions = () => {
|
||||
const handleChangeHiresFix = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setHiresFix(e.target.checked));
|
||||
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<IAISwitch
|
@ -0,0 +1,11 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const OutputHeader = () => {
|
||||
return (
|
||||
<Box flex="1" textAlign="left">
|
||||
Other Options
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutputHeader;
|
@ -1,10 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import HiresOptions from './HiresOptions';
|
||||
import SeamlessOptions from './SeamlessOptions';
|
||||
|
||||
const OutputOptions = () => {
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SeamlessOptions />
|
@ -1,10 +1,16 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setSeamless } from './optionsSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import IAISwitch from '../../common/components/IAISwitch';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setSeamless } from '../../optionsSlice';
|
||||
|
||||
/**
|
||||
* Seamless tiling toggle
|
||||
*/
|
||||
const SeamlessOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -25,4 +31,4 @@ const SeamlessOptions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SeamlessOptions;
|
||||
export default SeamlessOptions;
|
@ -0,0 +1,11 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const SeedHeader = () => {
|
||||
return (
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeedHeader;
|
@ -8,7 +8,7 @@ import {
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldRunESRGAN } from '../../optionsSlice';
|
||||
|
||||
export default function Upscale() {
|
||||
export default function UpscaleHeader() {
|
||||
const isESRGANAvailable = useAppSelector(
|
||||
(state: RootState) => state.system.isESRGANAvailable
|
||||
);
|
@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import GenerateVariations from './GenerateVariations';
|
||||
|
||||
export default function Variations() {
|
||||
export default function VariationsHeader() {
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
@ -1,67 +0,0 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
/**
|
||||
* Component which, on click, should open the upload interface.
|
||||
*/
|
||||
children: ReactElement;
|
||||
/**
|
||||
* Callback to handle uploading the selected file.
|
||||
*/
|
||||
fileAcceptedCallback: (file: File) => void;
|
||||
/**
|
||||
* Callback to handle a file being rejected.
|
||||
*/
|
||||
fileRejectionCallback: (rejection: FileRejection) => void;
|
||||
// Styling
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* File upload using react-dropzone.
|
||||
* Needs a child to be the button to activate the upload interface.
|
||||
*/
|
||||
const ImageUploader = ({
|
||||
children,
|
||||
fileAcceptedCallback,
|
||||
fileRejectionCallback,
|
||||
styleClass,
|
||||
}: ImageUploaderProps) => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
fileRejectionCallback(rejection);
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
fileAcceptedCallback(file);
|
||||
});
|
||||
},
|
||||
[fileAcceptedCallback, fileRejectionCallback]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box {...getRootProps()} flexGrow={3} className={`${styleClass}`}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
@ -1,20 +0,0 @@
|
||||
.checkerboard {
|
||||
background-position: 0px 0px, 10px 10px;
|
||||
background-size: 20px 20px;
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
#eee 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
#eee 75%,
|
||||
#eee 100%
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
#eee 25%,
|
||||
white 25%,
|
||||
white 75%,
|
||||
#eee 75%,
|
||||
#eee 100%
|
||||
);
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { Flex, Image } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState, setInitialImagePath, setMaskPath } from './optionsSlice';
|
||||
import './InitAndMaskImage.css';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import InitAndMaskUploadButtons from './InitAndMaskUploadButtons';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
maskPath: options.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays init and mask images and buttons to upload/delete them.
|
||||
*/
|
||||
const InitAndMaskImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
||||
|
||||
const handleInitImageOnError = () => {
|
||||
dispatch(setInitialImagePath(''));
|
||||
};
|
||||
|
||||
const handleMaskImageOnError = () => {
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} alignItems={'center'} gap={2}>
|
||||
<InitAndMaskUploadButtons setShouldShowMask={setShouldShowMask} />
|
||||
{initialImagePath && (
|
||||
<Flex position={'relative'} width={'100%'}>
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
maxWidth={320}
|
||||
onError={handleInitImageOnError}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
maxWidth={320}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
zIndex={1}
|
||||
onError={handleMaskImageOnError}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskImage;
|
@ -1,147 +0,0 @@
|
||||
import { Button, Flex, IconButton, useToast } from '@chakra-ui/react';
|
||||
import { SyntheticEvent, useCallback } from 'react';
|
||||
import { FaTrash, FaUpload } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState, setInitialImagePath, setMaskPath } from './optionsSlice';
|
||||
import {
|
||||
uploadInitialImage,
|
||||
uploadMaskImage,
|
||||
} from '../../app/socketio/actions';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import ImageUploader from './ImageUploader';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
maskPath: options.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
type InitAndMaskUploadButtonsProps = {
|
||||
setShouldShowMask: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Init and mask image upload buttons.
|
||||
*/
|
||||
const InitAndMaskUploadButtons = ({
|
||||
setShouldShowMask,
|
||||
}: InitAndMaskUploadButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
||||
|
||||
// Use a toast to alert user when a file upload is rejected
|
||||
const toast = useToast();
|
||||
|
||||
// Clear the init and mask images
|
||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(''));
|
||||
};
|
||||
|
||||
// Clear the init and mask images
|
||||
const handleClickResetMask = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
// Handle hover to view initial image and mask image
|
||||
const handleMouseOverInitialImageUploadButton = () =>
|
||||
setShouldShowMask(false);
|
||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
// Callbacks to for handling file upload attempts
|
||||
const initImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadInitialImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const maskImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadMaskImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={initImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
||||
leftIcon={<FaUpload />}
|
||||
width={'100%'}
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<IconButton
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset mask'}
|
||||
onClick={handleClickResetInitialImage}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={maskImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverMaskUploadButton}
|
||||
onMouseOut={handleMouseOutMaskUploadButton}
|
||||
leftIcon={<FaUpload />}
|
||||
width={'100%'}
|
||||
>
|
||||
Mask
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<IconButton
|
||||
isDisabled={!maskPath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset mask'}
|
||||
onClick={handleClickResetMask}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskUploadButtons;
|
@ -1,8 +1,10 @@
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAICheckbox from '../../../common/components/IAICheckbox';
|
||||
import { setShowAdvancedOptions } from '../optionsSlice';
|
||||
|
||||
export default function MainAdvancedOptions() {
|
||||
export default function MainAdvancedOptionsCheckbox() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
(state: RootState) => state.options.showAdvancedOptions
|
||||
);
|
||||
@ -12,15 +14,11 @@ export default function MainAdvancedOptions() {
|
||||
dispatch(setShowAdvancedOptions(e.target.checked));
|
||||
|
||||
return (
|
||||
<div className="advanced_options_checker">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="advanced_options"
|
||||
id=""
|
||||
onChange={handleShowAdvancedOptions}
|
||||
checked={showAdvancedOptions}
|
||||
/>
|
||||
<label htmlFor="advanced_options">Advanced Options</label>
|
||||
</div>
|
||||
<IAICheckbox
|
||||
label="Advanced Options"
|
||||
styleClass="advanced-options-checkbox"
|
||||
onChange={handleShowAdvancedOptions}
|
||||
isChecked={showAdvancedOptions}
|
||||
/>
|
||||
);
|
||||
}
|
@ -14,7 +14,7 @@ export default function MainCFGScale() {
|
||||
<IAINumberInput
|
||||
label="CFG Scale"
|
||||
step={0.5}
|
||||
min={1}
|
||||
min={1.01}
|
||||
max={30}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
|
@ -2,11 +2,14 @@ import React, { ChangeEvent } from 'react';
|
||||
import { HEIGHTS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { setHeight } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainHeight() {
|
||||
const height = useAppSelector((state: RootState) => state.options.height);
|
||||
const { activeTab, height } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
@ -14,6 +17,7 @@ export default function MainHeight() {
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||
label="Height"
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
|
@ -22,10 +22,10 @@
|
||||
grid-template-columns: auto !important;
|
||||
row-gap: 0.4rem;
|
||||
|
||||
.number-input-label,
|
||||
.iai-select-label {
|
||||
.invokeai__number-input-form-label,
|
||||
.invokeai__select-label {
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.9rem !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@ -40,43 +40,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.advanced_options_checker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
background-color: var(--background-color-secondary);
|
||||
.advanced-options-checkbox {
|
||||
padding: 1rem;
|
||||
font-weight: bold;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--input-checkbox-bg);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: inset 1rem 1rem var(--input-checkbox-checked-tick);
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: var(--input-checkbox-checked-bg);
|
||||
&::before {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@ import React, { ChangeEvent } from 'react';
|
||||
import { WIDTHS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { setWidth } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainWidth() {
|
||||
const width = useAppSelector((state: RootState) => state.options.width);
|
||||
const { width, activeTab } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
@ -14,6 +17,7 @@ export default function MainWidth() {
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||
label="Width"
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
|
@ -1,24 +1,42 @@
|
||||
import React from 'react';
|
||||
import { MdCancel } from 'react-icons/md';
|
||||
import { cancelProcessing } from '../../../app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import { systemSelector } from '../../../common/hooks/useCheckParameters';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../../system/systemSlice';
|
||||
import _ from 'lodash';
|
||||
|
||||
const cancelButtonSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isCancelable: system.isCancelable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function CancelButton() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const { isProcessing, isConnected, isCancelable } =
|
||||
useAppSelector(cancelButtonSelector);
|
||||
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||
|
||||
useHotkeys(
|
||||
'shift+x',
|
||||
() => {
|
||||
if (isConnected || isProcessing) {
|
||||
if ((isConnected || isProcessing) && isCancelable) {
|
||||
handleClickCancel();
|
||||
}
|
||||
},
|
||||
[isConnected, isProcessing]
|
||||
[isConnected, isProcessing, isCancelable]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -26,9 +44,9 @@ export default function CancelButton() {
|
||||
icon={<MdCancel />}
|
||||
tooltip="Cancel"
|
||||
aria-label="Cancel"
|
||||
isDisabled={!isConnected || !isProcessing}
|
||||
isDisabled={!isConnected || !isProcessing || !isCancelable}
|
||||
onClick={handleClickCancel}
|
||||
className="cancel-btn"
|
||||
styleClass="cancel-btn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import { generateImage } from '../../../app/socketio/actions';
|
||||
import { useAppDispatch } from '../../../app/store';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIButton from '../../../common/components/IAIButton';
|
||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
|
||||
export default function InvokeButton() {
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
|
||||
const handleClickGenerate = () => {
|
||||
dispatch(generateImage());
|
||||
dispatch(generateImage(tabMap[activeTab]));
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -7,16 +7,16 @@
|
||||
|
||||
.invoke-btn {
|
||||
@include Button(
|
||||
$btn-color: var(--btn-purple),
|
||||
$btn-color-hover: var(--btn-purple-hover),
|
||||
$btn-color: var(--accent-color),
|
||||
$btn-color-hover: var(--accent-color-hover),
|
||||
$btn-width: 5rem
|
||||
);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
@include Button(
|
||||
$btn-color: var(--btn-red),
|
||||
$btn-color-hover: var(--btn-red-hover),
|
||||
$btn-color: var(--destructive-color),
|
||||
$btn-color-hover: var(--destructive-color-hover),
|
||||
$btn-width: 3rem
|
||||
);
|
||||
}
|
||||
|
@ -13,8 +13,8 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
|
||||
&[aria-invalid='true'] {
|
||||
|
@ -5,22 +5,22 @@ import { generateImage } from '../../../app/socketio/actions';
|
||||
|
||||
import { OptionsState, setPrompt } from '../optionsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import useCheckParameters, {
|
||||
systemSelector,
|
||||
} from '../../../common/hooks/useCheckParameters';
|
||||
import _ from 'lodash';
|
||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
|
||||
export const optionsSelector = createSelector(
|
||||
const promptInputSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
prompt: options.prompt,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -30,8 +30,7 @@ export const optionsSelector = createSelector(
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { prompt } = useAppSelector(optionsSelector);
|
||||
const { isProcessing } = useAppSelector(systemSelector);
|
||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
@ -40,13 +39,13 @@ const PromptInput = () => {
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+enter',
|
||||
'ctrl+enter, cmd+enter',
|
||||
() => {
|
||||
if (isReady) {
|
||||
dispatch(generateImage());
|
||||
dispatch(generateImage(activeTabName));
|
||||
}
|
||||
},
|
||||
[isReady]
|
||||
[isReady, activeTabName]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@ -60,7 +59,7 @@ const PromptInput = () => {
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
|
||||
e.preventDefault();
|
||||
dispatch(generateImage());
|
||||
dispatch(generateImage(activeTabName));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,104 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import {
|
||||
setCfgScale,
|
||||
setSampler,
|
||||
setThreshold,
|
||||
setPerlin,
|
||||
setSteps,
|
||||
OptionsState,
|
||||
} from './optionsSlice';
|
||||
|
||||
import { SAMPLERS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import IAINumberInput from '../../common/components/IAINumberInput';
|
||||
import IAISelect from '../../common/components/IAISelect';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
steps: options.steps,
|
||||
cfgScale: options.cfgScale,
|
||||
sampler: options.sampler,
|
||||
threshold: options.threshold,
|
||||
perlin: options.perlin,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Sampler options. Includes steps, CFG scale, sampler.
|
||||
*/
|
||||
const SamplerOptions = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { steps, cfgScale, sampler, threshold, perlin } = useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeSteps = (v: string | number) =>
|
||||
dispatch(setSteps(Number(v)));
|
||||
|
||||
const handleChangeCfgScale = (v: string | number) =>
|
||||
dispatch(setCfgScale(Number(v)));
|
||||
|
||||
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setSampler(e.target.value));
|
||||
|
||||
const handleChangeThreshold = (v: string | number) =>
|
||||
dispatch(setThreshold(Number(v)));
|
||||
|
||||
const handleChangePerlin = (v: string | number) =>
|
||||
dispatch(setPerlin(Number(v)));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
{/* <IAINumberInput
|
||||
label="Steps"
|
||||
min={1}
|
||||
step={1}
|
||||
precision={0}
|
||||
onChange={handleChangeSteps}
|
||||
value={steps}
|
||||
/> */}
|
||||
{/* <IAINumberInput
|
||||
label="CFG scale"
|
||||
step={0.5}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
/> */}
|
||||
<IAISelect
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={SAMPLERS}
|
||||
/>
|
||||
{/* <IAINumberInput
|
||||
label='Threshold'
|
||||
min={0}
|
||||
step={0.1}
|
||||
onChange={handleChangeThreshold}
|
||||
value={threshold}
|
||||
/> */}
|
||||
{/* <IAINumberInput
|
||||
label='Perlin'
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={handleChangePerlin}
|
||||
value={perlin}
|
||||
/> */}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SamplerOptions;
|
@ -4,6 +4,7 @@ import * as InvokeAI from '../../app/invokeai';
|
||||
import promptToString from '../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
||||
import { FACETOOL_TYPES } from '../../app/constants';
|
||||
import { InvokeTabName, tabMap } from '../tabs/InvokeTabs';
|
||||
|
||||
export type UpscalingLevel = 2 | 4;
|
||||
|
||||
@ -26,8 +27,7 @@ export interface OptionsState {
|
||||
codeformerFidelity: number;
|
||||
upscalingLevel: UpscalingLevel;
|
||||
upscalingStrength: number;
|
||||
shouldUseInitImage: boolean;
|
||||
initialImagePath: string | null;
|
||||
initialImage?: InvokeAI.Image | string; // can be an Image or url
|
||||
maskPath: string;
|
||||
seamless: boolean;
|
||||
hiresFix: boolean;
|
||||
@ -41,7 +41,7 @@ export interface OptionsState {
|
||||
showAdvancedOptions: boolean;
|
||||
activeTab: number;
|
||||
shouldShowImageDetails: boolean;
|
||||
shouldShowGallery: boolean;
|
||||
showDualDisplay: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@ -57,9 +57,7 @@ const initialOptionsState: OptionsState = {
|
||||
seed: 0,
|
||||
seamless: false,
|
||||
hiresFix: false,
|
||||
shouldUseInitImage: false,
|
||||
img2imgStrength: 0.75,
|
||||
initialImagePath: null,
|
||||
maskPath: '',
|
||||
shouldFitToWidthHeight: true,
|
||||
shouldGenerateVariations: false,
|
||||
@ -76,7 +74,7 @@ const initialOptionsState: OptionsState = {
|
||||
showAdvancedOptions: true,
|
||||
activeTab: 0,
|
||||
shouldShowImageDetails: false,
|
||||
shouldShowGallery: false,
|
||||
showDualDisplay: true,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@ -136,14 +134,6 @@ export const optionsSlice = createSlice({
|
||||
setUpscalingStrength: (state, action: PayloadAction<number>) => {
|
||||
state.upscalingStrength = action.payload;
|
||||
},
|
||||
setShouldUseInitImage: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldUseInitImage = action.payload;
|
||||
},
|
||||
setInitialImagePath: (state, action: PayloadAction<string | null>) => {
|
||||
const newInitialImagePath = action.payload;
|
||||
state.shouldUseInitImage = newInitialImagePath ? true : false;
|
||||
state.initialImagePath = newInitialImagePath;
|
||||
},
|
||||
setMaskPath: (state, action: PayloadAction<string>) => {
|
||||
state.maskPath = action.payload;
|
||||
},
|
||||
@ -169,9 +159,6 @@ export const optionsSlice = createSlice({
|
||||
if (key === 'seed') {
|
||||
temp.shouldRandomizeSeed = false;
|
||||
}
|
||||
if (key === 'initialImagePath' && value === '') {
|
||||
temp.shouldUseInitImage = false;
|
||||
}
|
||||
return temp;
|
||||
},
|
||||
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
||||
@ -235,13 +222,10 @@ export const optionsSlice = createSlice({
|
||||
action.payload.image;
|
||||
|
||||
if (type === 'img2img') {
|
||||
if (init_image_path) state.initialImagePath = init_image_path;
|
||||
if (init_image_path) state.initialImage = init_image_path;
|
||||
if (mask_image_path) state.maskPath = mask_image_path;
|
||||
if (strength) state.img2imgStrength = strength;
|
||||
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
||||
state.shouldUseInitImage = true;
|
||||
} else {
|
||||
state.shouldUseInitImage = false;
|
||||
}
|
||||
},
|
||||
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
|
||||
@ -266,13 +250,10 @@ export const optionsSlice = createSlice({
|
||||
} = action.payload.image;
|
||||
|
||||
if (type === 'img2img') {
|
||||
if (init_image_path) state.initialImagePath = init_image_path;
|
||||
if (init_image_path) state.initialImage = init_image_path;
|
||||
if (mask_image_path) state.maskPath = mask_image_path;
|
||||
if (strength) state.img2imgStrength = strength;
|
||||
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
||||
state.shouldUseInitImage = true;
|
||||
} else {
|
||||
state.shouldUseInitImage = false;
|
||||
}
|
||||
|
||||
if (variations && variations.length > 0) {
|
||||
@ -321,14 +302,27 @@ export const optionsSlice = createSlice({
|
||||
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvancedOptions = action.payload;
|
||||
},
|
||||
setActiveTab: (state, action: PayloadAction<number>) => {
|
||||
state.activeTab = action.payload;
|
||||
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
|
||||
if (typeof action.payload === 'number') {
|
||||
state.activeTab = action.payload;
|
||||
} else {
|
||||
state.activeTab = tabMap.indexOf(action.payload);
|
||||
}
|
||||
},
|
||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowImageDetails = action.payload;
|
||||
},
|
||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowGallery = action.payload;
|
||||
setShowDualDisplay: (state, action: PayloadAction<boolean>) => {
|
||||
state.showDualDisplay = action.payload;
|
||||
},
|
||||
setInitialImage: (
|
||||
state,
|
||||
action: PayloadAction<InvokeAI.Image | string>
|
||||
) => {
|
||||
state.initialImage = action.payload;
|
||||
},
|
||||
clearInitialImage: (state) => {
|
||||
state.initialImage = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -352,8 +346,6 @@ export const {
|
||||
setCodeformerFidelity,
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
setShouldUseInitImage,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
resetSeed,
|
||||
resetOptionsState,
|
||||
@ -369,9 +361,11 @@ export const {
|
||||
setShowAdvancedOptions,
|
||||
setActiveTab,
|
||||
setShouldShowImageDetails,
|
||||
setShouldShowGallery,
|
||||
setAllTextToImageParameters,
|
||||
setAllImageToImageParameters,
|
||||
setShowDualDisplay,
|
||||
setInitialImage,
|
||||
clearInitialImage,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
@ -1,4 +1,5 @@
|
||||
.console {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--console-bg-color);
|
||||
@ -7,7 +8,7 @@
|
||||
font-family: monospace;
|
||||
padding: 0 1rem 1rem 3rem;
|
||||
border-top-width: 0.3rem;
|
||||
border-color: var(--console-border-color);
|
||||
border-color: var(--resizeable-handle-border-color);
|
||||
|
||||
.console-info-color {
|
||||
color: var(--error-level-info);
|
||||
@ -64,9 +65,9 @@
|
||||
}
|
||||
|
||||
&.autoscroll-enabled {
|
||||
background: var(--btn-purple) !important;
|
||||
background: var(--accent-color) !important;
|
||||
&:hover {
|
||||
background: var(--btn-purple-hover) !important;
|
||||
background: var(--accent-color-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.hotkeys-modal {
|
||||
width: 36rem !important;
|
||||
max-width: 36rem !important;
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
background-color: var(--settings-modal-bg) !important;
|
||||
@ -11,14 +13,47 @@
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.hotkeys-modal-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hotkeys-modal-items {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
max-height: 32rem;
|
||||
max-height: 36rem;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
|
||||
.chakra-accordion {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chakra-accordion__item {
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
background-color: var(--tab-hover-color);
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0.3rem !important;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hotkey-modal-category {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hotkey-modal-item {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
@ -13,6 +18,12 @@ type HotkeysModalProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
type HotkeyList = {
|
||||
title: string;
|
||||
desc: string;
|
||||
hotkey: string;
|
||||
};
|
||||
|
||||
export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
const {
|
||||
isOpen: isHotkeyModalOpen,
|
||||
@ -20,51 +31,18 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
onClose: onHotkeysModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const hotkeys = [
|
||||
const appHotkeys = [
|
||||
{ title: 'Invoke', desc: 'Generate an image', hotkey: 'Ctrl+Enter' },
|
||||
{ title: 'Cancel', desc: 'Cancel image generation', hotkey: 'Shift+X' },
|
||||
{
|
||||
title: 'Toggle Gallery',
|
||||
desc: 'Open and close the gallery drawer',
|
||||
hotkey: 'G',
|
||||
},
|
||||
{
|
||||
title: 'Set Seed',
|
||||
desc: 'Use the seed of the current image',
|
||||
hotkey: 'S',
|
||||
},
|
||||
{
|
||||
title: 'Set Parameters',
|
||||
desc: 'Use all parameters of the current image',
|
||||
hotkey: 'A',
|
||||
},
|
||||
{ title: 'Restore Faces', desc: 'Restore the current image', hotkey: 'R' },
|
||||
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'U' },
|
||||
{
|
||||
title: 'Show Info',
|
||||
desc: 'Show metadata info of the current image',
|
||||
hotkey: 'I',
|
||||
},
|
||||
{
|
||||
title: 'Send To Image To Image',
|
||||
desc: 'Send the current image to Image to Image module',
|
||||
hotkey: 'Shift+I',
|
||||
},
|
||||
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
||||
{
|
||||
title: 'Focus Prompt',
|
||||
desc: 'Focus the prompt input area',
|
||||
hotkey: 'Alt+A',
|
||||
},
|
||||
{
|
||||
title: 'Previous Image',
|
||||
desc: 'Display the previous image in the gallery',
|
||||
hotkey: 'Arrow left',
|
||||
},
|
||||
{
|
||||
title: 'Next Image',
|
||||
desc: 'Display the next image in the gallery',
|
||||
hotkey: 'Arrow right',
|
||||
title: 'Toggle Gallery',
|
||||
desc: 'Open and close the gallery drawer',
|
||||
hotkey: 'G',
|
||||
},
|
||||
{
|
||||
title: 'Change Tabs',
|
||||
@ -83,7 +61,144 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const renderHotkeyModalItems = () => {
|
||||
const generalHotkeys = [
|
||||
{
|
||||
title: 'Set Parameters',
|
||||
desc: 'Use all parameters of the current image',
|
||||
hotkey: 'A',
|
||||
},
|
||||
{
|
||||
title: 'Set Seed',
|
||||
desc: 'Use the seed of the current image',
|
||||
hotkey: 'S',
|
||||
},
|
||||
{ title: 'Restore Faces', desc: 'Restore the current image', hotkey: 'R' },
|
||||
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'U' },
|
||||
{
|
||||
title: 'Show Info',
|
||||
desc: 'Show metadata info of the current image',
|
||||
hotkey: 'I',
|
||||
},
|
||||
{
|
||||
title: 'Send To Image To Image',
|
||||
desc: 'Send current image to Image to Image',
|
||||
hotkey: 'Shift+I',
|
||||
},
|
||||
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
||||
];
|
||||
|
||||
const galleryHotkeys = [
|
||||
{
|
||||
title: 'Previous Image',
|
||||
desc: 'Display the previous image in gallery',
|
||||
hotkey: 'Arrow left',
|
||||
},
|
||||
{
|
||||
title: 'Next Image',
|
||||
desc: 'Display the next image in gallery',
|
||||
hotkey: 'Arrow right',
|
||||
},
|
||||
{
|
||||
title: 'Toggle Gallery Pin',
|
||||
desc: 'Pins and unpins the gallery to the UI',
|
||||
hotkey: 'Shift+P',
|
||||
},
|
||||
{
|
||||
title: 'Increase Gallery Image Size',
|
||||
desc: 'Increases gallery thumbnails size',
|
||||
hotkey: 'Shift+Up',
|
||||
},
|
||||
{
|
||||
title: 'Decrease Gallery Image Size',
|
||||
desc: 'Decreases gallery thumbnails size',
|
||||
hotkey: 'Shift+Down',
|
||||
},
|
||||
{
|
||||
title: 'Reset Gallery Image Size',
|
||||
desc: 'Resets image gallery size',
|
||||
hotkey: 'Shift+R',
|
||||
},
|
||||
];
|
||||
|
||||
const inpaintingHotkeys = [
|
||||
{
|
||||
title: 'Select Brush',
|
||||
desc: 'Selects the inpainting brush',
|
||||
hotkey: 'B',
|
||||
},
|
||||
{
|
||||
title: 'Select Eraser',
|
||||
desc: 'Selects the inpainting eraser',
|
||||
hotkey: 'E',
|
||||
},
|
||||
{
|
||||
title: 'Quick Toggle Brush/Eraser',
|
||||
desc: 'Quick toggle between brush and eraser',
|
||||
hotkey: 'Z',
|
||||
},
|
||||
{
|
||||
title: 'Decrease Brush Size',
|
||||
desc: 'Decreases the size of the inpainting brush/eraser',
|
||||
hotkey: '[',
|
||||
},
|
||||
{
|
||||
title: 'Increase Brush Size',
|
||||
desc: 'Increases the size of the inpainting brush/eraser',
|
||||
hotkey: ']',
|
||||
},
|
||||
{
|
||||
title: 'Hide Mask',
|
||||
desc: 'Hide and unhide mask',
|
||||
hotkey: 'H',
|
||||
},
|
||||
{
|
||||
title: 'Decrease Mask Opacity',
|
||||
desc: 'Decreases the opacity of the mask',
|
||||
hotkey: 'Shift+[',
|
||||
},
|
||||
{
|
||||
title: 'Increase Mask Opacity',
|
||||
desc: 'Increases the opacity of the mask',
|
||||
hotkey: 'Shift+]',
|
||||
},
|
||||
{
|
||||
title: 'Invert Mask',
|
||||
desc: 'Invert the mask preview',
|
||||
hotkey: 'Shift+M',
|
||||
},
|
||||
{
|
||||
title: 'Clear Mask',
|
||||
desc: 'Clear the entire mask',
|
||||
hotkey: 'Shift+C',
|
||||
},
|
||||
{
|
||||
title: 'Undo Stroke',
|
||||
desc: 'Undo a brush stroke',
|
||||
hotkey: 'Ctrl+Z',
|
||||
},
|
||||
{
|
||||
title: 'Redo Stroke',
|
||||
desc: 'Redo a brush stroke',
|
||||
hotkey: 'Ctrl+Shift+Z, Ctrl+Y',
|
||||
},
|
||||
{
|
||||
title: 'Lock Bounding Box',
|
||||
desc: 'Locks the bounding box',
|
||||
hotkey: 'M',
|
||||
},
|
||||
{
|
||||
title: 'Quick Toggle Lock Bounding Box',
|
||||
desc: 'Hold to toggle locking the bounding box',
|
||||
hotkey: 'Space',
|
||||
},
|
||||
{
|
||||
title: 'Expand Inpainting Area',
|
||||
desc: 'Expand your inpainting work area',
|
||||
hotkey: 'Shift+J',
|
||||
},
|
||||
];
|
||||
|
||||
const renderHotkeyModalItems = (hotkeys: HotkeyList[]) => {
|
||||
const hotkeyModalItemsToRender: ReactElement[] = [];
|
||||
|
||||
hotkeys.forEach((hotkey, i) => {
|
||||
@ -97,7 +212,9 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
);
|
||||
});
|
||||
|
||||
return hotkeyModalItemsToRender;
|
||||
return (
|
||||
<div className="hotkey-modal-category">{hotkeyModalItemsToRender}</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -109,8 +226,51 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
<ModalOverlay />
|
||||
<ModalContent className="hotkeys-modal">
|
||||
<ModalCloseButton />
|
||||
|
||||
<h1>Keyboard Shorcuts</h1>
|
||||
<div className="hotkeys-modal-items">{renderHotkeyModalItems()}</div>
|
||||
<div className="hotkeys-modal-items">
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton className="hotkeys-modal-button">
|
||||
<h2>App Hotkeys</h2>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(appHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton className="hotkeys-modal-button">
|
||||
<h2>General Hotkeys</h2>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(generalHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton className="hotkeys-modal-button">
|
||||
<h2>Gallery Hotkeys</h2>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(galleryHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton className="hotkeys-modal-button">
|
||||
<h2>Inpainting Hotkeys</h2>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{renderHotkeyModalItems(inpaintingHotkeys)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
|
@ -1,7 +1,19 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.progress-bar {
|
||||
background-color: var(--root-bg-color);
|
||||
height: $progress-bar-thickness !important;
|
||||
|
||||
div {
|
||||
background-color: var(--progress-bar-color);
|
||||
&[data-indeterminate] {
|
||||
background-color: unset;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
var(--progress-bar-color) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user