mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'user-image-uploads' into inpainting-rebase
Adds - Separate user uploads gallery - Drag and drop uploads for img2img and inpainting - Many bugfixes, scss refactored
This commit is contained in:
commit
8dee3387fd
@ -160,23 +160,34 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
@socketio.on("requestModelChange")
|
@socketio.on("requestModelChange")
|
||||||
def handle_set_model(model_name: str):
|
def handle_set_model(model_name: str):
|
||||||
print(f">> Model change requested: {model_name}")
|
try:
|
||||||
model = self.generate.set_model(model_name)
|
print(f">> Model change requested: {model_name}")
|
||||||
model_list = self.generate.model_cache.list_models()
|
model = self.generate.set_model(model_name)
|
||||||
if model is None:
|
model_list = self.generate.model_cache.list_models()
|
||||||
socketio.emit(
|
if model is None:
|
||||||
"modelChangeFailed",
|
socketio.emit(
|
||||||
{"model_name": model_name, "model_list": model_list},
|
"modelChangeFailed",
|
||||||
)
|
{"model_name": model_name, "model_list": model_list},
|
||||||
else:
|
)
|
||||||
socketio.emit(
|
else:
|
||||||
"modelChanged", {"model_name": model_name, "model_list": model_list}
|
socketio.emit(
|
||||||
)
|
"modelChanged",
|
||||||
|
{"model_name": model_name, "model_list": model_list},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.socketio.emit("error", {"message": (str(e))})
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
print("\n")
|
||||||
|
|
||||||
@socketio.on("requestLatestImages")
|
@socketio.on("requestLatestImages")
|
||||||
def handle_request_latest_images(latest_mtime):
|
def handle_request_latest_images(category, latest_mtime):
|
||||||
try:
|
try:
|
||||||
paths = glob.glob(os.path.join(self.result_path, "*.png"))
|
base_path = (
|
||||||
|
self.result_path if category == "result" else self.init_image_path
|
||||||
|
)
|
||||||
|
paths = glob.glob(os.path.join(base_path, "*.png"))
|
||||||
|
|
||||||
image_paths = sorted(
|
image_paths = sorted(
|
||||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
||||||
@ -201,14 +212,13 @@ class InvokeAIWebServer:
|
|||||||
"metadata": metadata["sd-metadata"],
|
"metadata": metadata["sd-metadata"],
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"category": category,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"galleryImages",
|
"galleryImages",
|
||||||
{
|
{"images": image_array, "category": category},
|
||||||
"images": image_array,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socketio.emit("error", {"message": (str(e))})
|
self.socketio.emit("error", {"message": (str(e))})
|
||||||
@ -218,11 +228,15 @@ class InvokeAIWebServer:
|
|||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
@socketio.on("requestImages")
|
@socketio.on("requestImages")
|
||||||
def handle_request_images(earliest_mtime=None):
|
def handle_request_images(category, earliest_mtime=None):
|
||||||
try:
|
try:
|
||||||
page_size = 50
|
page_size = 50
|
||||||
|
|
||||||
paths = glob.glob(os.path.join(self.result_path, "*.png"))
|
base_path = (
|
||||||
|
self.result_path if category == "result" else self.init_image_path
|
||||||
|
)
|
||||||
|
|
||||||
|
paths = glob.glob(os.path.join(base_path, "*.png"))
|
||||||
|
|
||||||
image_paths = sorted(
|
image_paths = sorted(
|
||||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
||||||
@ -253,6 +267,7 @@ class InvokeAIWebServer:
|
|||||||
"metadata": metadata["sd-metadata"],
|
"metadata": metadata["sd-metadata"],
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"category": category,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -261,6 +276,7 @@ class InvokeAIWebServer:
|
|||||||
{
|
{
|
||||||
"images": image_array,
|
"images": image_array,
|
||||||
"areMoreImagesAvailable": areMoreImagesAvailable,
|
"areMoreImagesAvailable": areMoreImagesAvailable,
|
||||||
|
"category": category,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -416,14 +432,17 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@socketio.on("deleteImage")
|
@socketio.on("deleteImage")
|
||||||
def handle_delete_image(url, uuid):
|
def handle_delete_image(url, uuid, category):
|
||||||
try:
|
try:
|
||||||
print(f'>> Delete requested "{url}"')
|
print(f'>> Delete requested "{url}"')
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
|
|
||||||
path = self.get_image_path_from_url(url)
|
path = self.get_image_path_from_url(url)
|
||||||
|
print(path)
|
||||||
send2trash(path)
|
send2trash(path)
|
||||||
socketio.emit("imageDeleted", {"url": url, "uuid": uuid})
|
socketio.emit(
|
||||||
|
"imageDeleted", {"url": url, "uuid": uuid, "category": category}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socketio.emit("error", {"message": (str(e))})
|
self.socketio.emit("error", {"message": (str(e))})
|
||||||
print("\n")
|
print("\n")
|
||||||
@ -432,18 +451,25 @@ class InvokeAIWebServer:
|
|||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@socketio.on("uploadInitialImage")
|
@socketio.on("uploadImage")
|
||||||
def handle_upload_initial_image(bytes, name):
|
def handle_upload_image(bytes, name, destination):
|
||||||
try:
|
try:
|
||||||
print(f'>> Init image upload requested "{name}"')
|
print(f'>> Image upload requested "{name}"')
|
||||||
file_path = self.save_file_unique_uuid_name(
|
file_path = self.save_file_unique_uuid_name(
|
||||||
bytes=bytes, name=name, path=self.init_image_path
|
bytes=bytes, name=name, path=self.init_image_path
|
||||||
)
|
)
|
||||||
|
mtime = os.path.getmtime(file_path)
|
||||||
|
(width, height) = Image.open(file_path).size
|
||||||
|
print(file_path)
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"initialImageUploaded",
|
"imageUploaded",
|
||||||
{
|
{
|
||||||
"url": self.get_url_from_image_path(file_path),
|
"url": self.get_url_from_image_path(file_path),
|
||||||
|
"mtime": mtime,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"category": "user",
|
||||||
|
"destination": destination,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -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)
|
|
1
frontend/dist/assets/index.165b9042.css
vendored
Normal file
1
frontend/dist/assets/index.165b9042.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.25bb1c01.css
vendored
1
frontend/dist/assets/index.25bb1c01.css
vendored
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.3e1dd33d.js
vendored
Normal file
517
frontend/dist/assets/index.3e1dd33d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.7f3a2425.js
vendored
517
frontend/dist/assets/index.7f3a2425.js
vendored
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.7f3a2425.js"></script>
|
<script type="module" crossorigin src="./assets/index.3e1dd33d.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.25bb1c01.css">
|
<link rel="stylesheet" href="./assets/index.165b9042.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
26
frontend/src/app/invokeai.d.ts
vendored
26
frontend/src/app/invokeai.d.ts
vendored
@ -12,6 +12,8 @@
|
|||||||
* 'gfpgan'.
|
* 'gfpgan'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Category as GalleryCategory } from '../features/gallery/gallerySlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* Once an image has been generated, if it is postprocessed again,
|
* Once an image has been generated, if it is postprocessed again,
|
||||||
@ -105,14 +107,15 @@ export declare type Metadata = SystemConfig & {
|
|||||||
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
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 = {
|
export declare type Image = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
url: string;
|
url: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
metadata: Metadata;
|
metadata?: Metadata;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
category: GalleryCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GalleryImages is an array of Image.
|
// GalleryImages is an array of Image.
|
||||||
@ -167,12 +170,10 @@ export declare type SystemStatusResponse = SystemStatus;
|
|||||||
|
|
||||||
export declare type SystemConfigResponse = SystemConfig;
|
export declare type SystemConfigResponse = SystemConfig;
|
||||||
|
|
||||||
export declare type ImageResultResponse = {
|
export declare type ImageResultResponse = Omit<Image, 'uuid'>;
|
||||||
url: string;
|
|
||||||
mtime: number;
|
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
||||||
metadata: Metadata;
|
destination: 'img2img' | 'inpainting';
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ErrorResponse = {
|
export declare type ErrorResponse = {
|
||||||
@ -183,13 +184,20 @@ export declare type ErrorResponse = {
|
|||||||
export declare type GalleryImagesResponse = {
|
export declare type GalleryImagesResponse = {
|
||||||
images: Array<Omit<Image, 'uuid'>>;
|
images: Array<Omit<Image, 'uuid'>>;
|
||||||
areMoreImagesAvailable: boolean;
|
areMoreImagesAvailable: boolean;
|
||||||
|
category: GalleryCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUrlAndUuidResponse = {
|
export declare type ImageDeletedResponse = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
category: GalleryCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUrlResponse = {
|
export declare type ImageUrlResponse = {
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export declare type UploadImagePayload = {
|
||||||
|
file: File;
|
||||||
|
destination: 'img2img' | 'inpainting';
|
||||||
|
};
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { GalleryCategory } from '../../features/gallery/gallerySlice';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from '../invokeai';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We can't use redux-toolkit's createSlice() to make these actions,
|
* We can't use redux-toolkit's createSlice() to make these actions,
|
||||||
* because they have no associated reducer. They only exist to dispatch
|
* because they have no associated reducer. They only exist to dispatch
|
||||||
@ -15,20 +17,22 @@ export const generateImage = createAction<InvokeTabName>(
|
|||||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
||||||
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
||||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
||||||
export const requestImages = createAction<undefined>('socketio/requestImages');
|
export const requestImages = createAction<GalleryCategory>(
|
||||||
export const requestNewImages = createAction<undefined>(
|
'socketio/requestImages'
|
||||||
|
);
|
||||||
|
export const requestNewImages = createAction<GalleryCategory>(
|
||||||
'socketio/requestNewImages'
|
'socketio/requestNewImages'
|
||||||
);
|
);
|
||||||
export const cancelProcessing = createAction<undefined>(
|
export const cancelProcessing = createAction<undefined>(
|
||||||
'socketio/cancelProcessing'
|
'socketio/cancelProcessing'
|
||||||
);
|
);
|
||||||
export const uploadInitialImage = createAction<File>(
|
export const uploadImage = createAction<InvokeAI.UploadImagePayload>('socketio/uploadImage');
|
||||||
'socketio/uploadInitialImage'
|
|
||||||
);
|
|
||||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
||||||
|
|
||||||
export const requestSystemConfig = createAction<undefined>(
|
export const requestSystemConfig = createAction<undefined>(
|
||||||
'socketio/requestSystemConfig'
|
'socketio/requestSystemConfig'
|
||||||
);
|
);
|
||||||
|
|
||||||
export const requestModelChange = createAction<string>('socketio/requestModelChange');
|
export const requestModelChange = createAction<string>(
|
||||||
|
'socketio/requestModelChange'
|
||||||
|
);
|
||||||
|
@ -5,6 +5,11 @@ import {
|
|||||||
frontendToBackendParameters,
|
frontendToBackendParameters,
|
||||||
FrontendToBackendParametersConfig,
|
FrontendToBackendParametersConfig,
|
||||||
} from '../../common/util/parameterTranslation';
|
} from '../../common/util/parameterTranslation';
|
||||||
|
import {
|
||||||
|
GalleryCategory,
|
||||||
|
GalleryState,
|
||||||
|
} from '../../features/gallery/gallerySlice';
|
||||||
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
errorOccurred,
|
errorOccurred,
|
||||||
@ -108,7 +113,8 @@ const makeSocketIOEmitters = (
|
|||||||
},
|
},
|
||||||
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
||||||
dispatch(setIsProcessing(true));
|
dispatch(setIsProcessing(true));
|
||||||
const { upscalingLevel, upscalingStrength } = getState().options;
|
const options: OptionsState = getState().options;
|
||||||
|
const { upscalingLevel, upscalingStrength } = options;
|
||||||
const esrganParameters = {
|
const esrganParameters = {
|
||||||
upscale: [upscalingLevel, upscalingStrength],
|
upscale: [upscalingLevel, upscalingStrength],
|
||||||
};
|
};
|
||||||
@ -128,8 +134,8 @@ const makeSocketIOEmitters = (
|
|||||||
},
|
},
|
||||||
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
|
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
|
||||||
dispatch(setIsProcessing(true));
|
dispatch(setIsProcessing(true));
|
||||||
const { facetoolType, facetoolStrength, codeformerFidelity } =
|
const options: OptionsState = getState().options;
|
||||||
getState().options;
|
const { facetoolType, facetoolStrength, codeformerFidelity } = options;
|
||||||
|
|
||||||
const facetoolParameters: Record<string, any> = {
|
const facetoolParameters: Record<string, any> = {
|
||||||
facetool_strength: facetoolStrength,
|
facetool_strength: facetoolStrength,
|
||||||
@ -156,22 +162,25 @@ const makeSocketIOEmitters = (
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||||
const { url, uuid } = imageToDelete;
|
const { url, uuid, category } = imageToDelete;
|
||||||
socketio.emit('deleteImage', url, uuid);
|
socketio.emit('deleteImage', url, uuid, category);
|
||||||
},
|
},
|
||||||
emitRequestImages: () => {
|
emitRequestImages: (category: GalleryCategory) => {
|
||||||
const { earliest_mtime } = getState().gallery;
|
const gallery: GalleryState = getState().gallery;
|
||||||
socketio.emit('requestImages', earliest_mtime);
|
const { earliest_mtime } = gallery.categories[category];
|
||||||
|
socketio.emit('requestImages', category, earliest_mtime);
|
||||||
},
|
},
|
||||||
emitRequestNewImages: () => {
|
emitRequestNewImages: (category: GalleryCategory) => {
|
||||||
const { latest_mtime } = getState().gallery;
|
const gallery: GalleryState = getState().gallery;
|
||||||
socketio.emit('requestLatestImages', latest_mtime);
|
const { latest_mtime } = gallery.categories[category];
|
||||||
|
socketio.emit('requestLatestImages', category, latest_mtime);
|
||||||
},
|
},
|
||||||
emitCancelProcessing: () => {
|
emitCancelProcessing: () => {
|
||||||
socketio.emit('cancel');
|
socketio.emit('cancel');
|
||||||
},
|
},
|
||||||
emitUploadInitialImage: (file: File) => {
|
emitUploadImage: (payload: InvokeAI.UploadImagePayload) => {
|
||||||
socketio.emit('uploadInitialImage', file, file.name);
|
const { file, destination } = payload;
|
||||||
|
socketio.emit('uploadImage', file, file.name, destination);
|
||||||
},
|
},
|
||||||
emitUploadMaskImage: (file: File) => {
|
emitUploadMaskImage: (file: File) => {
|
||||||
socketio.emit('uploadMaskImage', file, file.name);
|
socketio.emit('uploadMaskImage', file, file.name);
|
||||||
|
@ -21,15 +21,18 @@ import {
|
|||||||
addGalleryImages,
|
addGalleryImages,
|
||||||
addImage,
|
addImage,
|
||||||
clearIntermediateImage,
|
clearIntermediateImage,
|
||||||
|
GalleryState,
|
||||||
removeImage,
|
removeImage,
|
||||||
setIntermediateImage,
|
setIntermediateImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from '../../features/gallery/gallerySlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setInitialImagePath,
|
clearInitialImage,
|
||||||
|
setInitialImage,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
} from '../../features/options/optionsSlice';
|
} from '../../features/options/optionsSlice';
|
||||||
import { requestImages, requestNewImages } from './actions';
|
import { requestImages, requestNewImages } from './actions';
|
||||||
|
import { setImageToInpaint } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing listener callbacks for socketio events.
|
* Returns an object containing listener callbacks for socketio events.
|
||||||
@ -48,10 +51,18 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
dispatch(setIsConnected(true));
|
dispatch(setIsConnected(true));
|
||||||
dispatch(setCurrentStatus('Connected'));
|
dispatch(setCurrentStatus('Connected'));
|
||||||
if (getState().gallery.latest_mtime) {
|
const gallery: GalleryState = getState().gallery;
|
||||||
dispatch(requestNewImages());
|
|
||||||
|
if (gallery.categories.user.latest_mtime) {
|
||||||
|
dispatch(requestNewImages('user'));
|
||||||
} else {
|
} else {
|
||||||
dispatch(requestImages());
|
dispatch(requestImages('user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gallery.categories.result.latest_mtime) {
|
||||||
|
dispatch(requestNewImages('result'));
|
||||||
|
} else {
|
||||||
|
dispatch(requestImages('result'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -83,8 +94,11 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
dispatch(
|
dispatch(
|
||||||
addImage({
|
addImage({
|
||||||
uuid: uuidv4(),
|
category: 'result',
|
||||||
...data,
|
image: {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -125,8 +139,11 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
dispatch(
|
dispatch(
|
||||||
addImage({
|
addImage({
|
||||||
uuid: uuidv4(),
|
category: 'result',
|
||||||
...data,
|
image: {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -180,7 +197,7 @@ const makeSocketIOListeners = (
|
|||||||
* Callback to run when we receive a 'galleryImages' event.
|
* Callback to run when we receive a 'galleryImages' event.
|
||||||
*/
|
*/
|
||||||
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
|
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:
|
* the logic here ideally would be in the reducer but we have a side effect:
|
||||||
@ -189,19 +206,18 @@ const makeSocketIOListeners = (
|
|||||||
|
|
||||||
// Generate a UUID for each image
|
// Generate a UUID for each image
|
||||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
const preparedImages = images.map((image): InvokeAI.Image => {
|
||||||
const { url, metadata, mtime, width, height } = image;
|
|
||||||
return {
|
return {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
url,
|
...image,
|
||||||
mtime,
|
|
||||||
metadata,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addGalleryImages({ images: preparedImages, areMoreImagesAvailable })
|
addGalleryImages({
|
||||||
|
images: preparedImages,
|
||||||
|
areMoreImagesAvailable,
|
||||||
|
category,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -220,7 +236,12 @@ const makeSocketIOListeners = (
|
|||||||
const { intermediateImage } = getState().gallery;
|
const { intermediateImage } = getState().gallery;
|
||||||
|
|
||||||
if (intermediateImage) {
|
if (intermediateImage) {
|
||||||
dispatch(addImage(intermediateImage));
|
dispatch(
|
||||||
|
addImage({
|
||||||
|
category: 'result',
|
||||||
|
image: intermediateImage,
|
||||||
|
})
|
||||||
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
@ -241,14 +262,17 @@ const makeSocketIOListeners = (
|
|||||||
/**
|
/**
|
||||||
* Callback to run when we receive a 'imageDeleted' event.
|
* Callback to run when we receive a 'imageDeleted' event.
|
||||||
*/
|
*/
|
||||||
onImageDeleted: (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
||||||
const { url, uuid } = data;
|
const { url, uuid, category } = data;
|
||||||
dispatch(removeImage(uuid));
|
|
||||||
|
|
||||||
const { initialImagePath, maskPath } = getState().options;
|
// remove image from gallery
|
||||||
|
dispatch(removeImage(data));
|
||||||
|
|
||||||
if (initialImagePath === url) {
|
// remove references to image in options
|
||||||
dispatch(setInitialImagePath(''));
|
const { initialImage, maskPath } = getState().options;
|
||||||
|
|
||||||
|
if (initialImage?.url === url || initialImage === url) {
|
||||||
|
dispatch(clearInitialImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maskPath === url) {
|
if (maskPath === url) {
|
||||||
@ -262,18 +286,36 @@ const makeSocketIOListeners = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
/**
|
onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
|
||||||
* Callback to run when we receive a 'initialImageUploaded' event.
|
const { destination, ...rest } = data;
|
||||||
*/
|
const image = {
|
||||||
onInitialImageUploaded: (data: InvokeAI.ImageUrlResponse) => {
|
uuid: uuidv4(),
|
||||||
const { url } = data;
|
...rest,
|
||||||
dispatch(setInitialImagePath(url));
|
};
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
try {
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
dispatch(addImage({ image, category: 'user' }));
|
||||||
message: `Initial image uploaded: ${url}`,
|
|
||||||
})
|
switch (destination) {
|
||||||
);
|
case 'img2img': {
|
||||||
|
dispatch(setInitialImage(image));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'inpainting': {
|
||||||
|
dispatch(setImageToInpaint(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.
|
* Callback to run when we receive a 'maskImageUploaded' event.
|
||||||
|
@ -43,7 +43,7 @@ export const socketioMiddleware = () => {
|
|||||||
onGalleryImages,
|
onGalleryImages,
|
||||||
onProcessingCanceled,
|
onProcessingCanceled,
|
||||||
onImageDeleted,
|
onImageDeleted,
|
||||||
onInitialImageUploaded,
|
onImageUploaded,
|
||||||
onMaskImageUploaded,
|
onMaskImageUploaded,
|
||||||
onSystemConfig,
|
onSystemConfig,
|
||||||
onModelChanged,
|
onModelChanged,
|
||||||
@ -58,7 +58,7 @@ export const socketioMiddleware = () => {
|
|||||||
emitRequestImages,
|
emitRequestImages,
|
||||||
emitRequestNewImages,
|
emitRequestNewImages,
|
||||||
emitCancelProcessing,
|
emitCancelProcessing,
|
||||||
emitUploadInitialImage,
|
emitUploadImage,
|
||||||
emitUploadMaskImage,
|
emitUploadMaskImage,
|
||||||
emitRequestSystemConfig,
|
emitRequestSystemConfig,
|
||||||
emitRequestModelChange,
|
emitRequestModelChange,
|
||||||
@ -100,13 +100,16 @@ export const socketioMiddleware = () => {
|
|||||||
onProcessingCanceled();
|
onProcessingCanceled();
|
||||||
});
|
});
|
||||||
|
|
||||||
socketio.on('imageDeleted', (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
|
||||||
onImageDeleted(data);
|
onImageDeleted(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketio.on('initialImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
socketio.on(
|
||||||
onInitialImageUploaded(data);
|
'imageUploaded',
|
||||||
});
|
(data: InvokeAI.ImageUploadResponse) => {
|
||||||
|
onImageUploaded(data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||||
onMaskImageUploaded(data);
|
onMaskImageUploaded(data);
|
||||||
@ -152,12 +155,12 @@ export const socketioMiddleware = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'socketio/requestImages': {
|
case 'socketio/requestImages': {
|
||||||
emitRequestImages();
|
emitRequestImages(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'socketio/requestNewImages': {
|
case 'socketio/requestNewImages': {
|
||||||
emitRequestNewImages();
|
emitRequestNewImages(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +169,8 @@ export const socketioMiddleware = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'socketio/uploadInitialImage': {
|
case 'socketio/uploadImage': {
|
||||||
emitUploadInitialImage(action.payload);
|
emitUploadImage(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
frontend/src/common/components/InvokeImageUploader.scss
Normal file
30
frontend/src/common/components/InvokeImageUploader.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.image-upload-zone {
|
||||||
|
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-child-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-child {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
@ -1,29 +1,23 @@
|
|||||||
import { Button, useToast } from '@chakra-ui/react';
|
import { Button, Heading, useToast } from '@chakra-ui/react';
|
||||||
import React, { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FileRejection } from 'react-dropzone';
|
import { FileRejection } from 'react-dropzone';
|
||||||
import { useAppDispatch } from '../../app/store';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
import ImageUploader from '../../features/options/ImageUploader';
|
import ImageUploader from '../../features/options/ImageUploader';
|
||||||
|
|
||||||
interface InvokeImageUploaderProps {
|
interface InvokeImageUploaderProps {
|
||||||
label?: string;
|
handleFile: (file: File) => void;
|
||||||
icon?: any;
|
|
||||||
onMouseOver?: any;
|
|
||||||
OnMouseout?: any;
|
|
||||||
dispatcher: any;
|
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
||||||
const { label, icon, dispatcher, styleClass, onMouseOver, OnMouseout } =
|
const { handleFile, styleClass } = props;
|
||||||
props;
|
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
// Callbacks to for handling file upload attempts
|
// Callbacks to for handling file upload attempts
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
(file: File) => dispatch(dispatcher(file)),
|
(file: File) => handleFile(file),
|
||||||
[dispatch, dispatcher]
|
[handleFile]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileRejectionCallback = useCallback(
|
const fileRejectionCallback = useCallback(
|
||||||
@ -44,22 +38,17 @@ export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageUploader
|
<div className="image-upload-zone">
|
||||||
fileAcceptedCallback={fileAcceptedCallback}
|
<ImageUploader
|
||||||
fileRejectionCallback={fileRejectionCallback}
|
fileAcceptedCallback={fileAcceptedCallback}
|
||||||
styleClass={styleClass}
|
fileRejectionCallback={fileRejectionCallback}
|
||||||
>
|
styleClass={`${styleClass} image-upload-child-wrapper`}
|
||||||
<Button
|
|
||||||
size={'sm'}
|
|
||||||
fontSize={'md'}
|
|
||||||
fontWeight={'normal'}
|
|
||||||
onMouseOver={onMouseOver}
|
|
||||||
onMouseOut={OnMouseout}
|
|
||||||
leftIcon={icon}
|
|
||||||
width={'100%'}
|
|
||||||
>
|
>
|
||||||
{label ? label : null}
|
<div className="image-upload-child">
|
||||||
</Button>
|
<FaUpload size={'7rem'} />
|
||||||
</ImageUploader>
|
<Heading size={'lg'}>Upload or Drop Image Here</Heading>
|
||||||
|
</div>
|
||||||
|
</ImageUploader>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export const useCheckParametersSelector = createSelector(
|
|||||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||||
seedWeights: options.seedWeights,
|
seedWeights: options.seedWeights,
|
||||||
maskPath: options.maskPath,
|
maskPath: options.maskPath,
|
||||||
initialImagePath: options.initialImagePath,
|
initialImage: options.initialImage,
|
||||||
seed: options.seed,
|
seed: options.seed,
|
||||||
activeTabName: tabMap[options.activeTab],
|
activeTabName: tabMap[options.activeTab],
|
||||||
// system
|
// system
|
||||||
@ -49,7 +49,7 @@ const useCheckParameters = (): boolean => {
|
|||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
maskPath,
|
maskPath,
|
||||||
initialImagePath,
|
initialImage,
|
||||||
seed,
|
seed,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
@ -63,7 +63,7 @@ const useCheckParameters = (): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'img2img' && !initialImagePath) {
|
if (activeTabName === 'img2img' && !initialImage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ const useCheckParameters = (): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cannot generate with a mask without img2img
|
// Cannot generate with a mask without img2img
|
||||||
if (maskPath && !initialImagePath) {
|
if (maskPath && !initialImage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +100,8 @@ const useCheckParameters = (): boolean => {
|
|||||||
}, [
|
}, [
|
||||||
prompt,
|
prompt,
|
||||||
maskPath,
|
maskPath,
|
||||||
initialImagePath,
|
|
||||||
isProcessing,
|
isProcessing,
|
||||||
|
initialImage,
|
||||||
isConnected,
|
isConnected,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
|
@ -47,7 +47,7 @@ export const frontendToBackendParameters = (
|
|||||||
seamless,
|
seamless,
|
||||||
hiresFix,
|
hiresFix,
|
||||||
img2imgStrength,
|
img2imgStrength,
|
||||||
initialImagePath,
|
initialImage,
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
variationAmount,
|
variationAmount,
|
||||||
@ -89,8 +89,9 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// img2img exclusive parameters
|
// img2img exclusive parameters
|
||||||
if (generationMode === 'img2img') {
|
if (generationMode === 'img2img' && initialImage) {
|
||||||
generationParameters.init_img = initialImagePath;
|
generationParameters.init_img =
|
||||||
|
typeof initialImage === 'string' ? initialImage : initialImage.url;
|
||||||
generationParameters.strength = img2imgStrength;
|
generationParameters.strength = img2imgStrength;
|
||||||
generationParameters.fit = shouldFitToWidthHeight;
|
generationParameters.fit = shouldFitToWidthHeight;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { RootState } from '../../app/store';
|
|||||||
import {
|
import {
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImagePath,
|
setInitialImage,
|
||||||
setSeed,
|
setSeed,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
} from '../options/optionsSlice';
|
} from '../options/optionsSlice';
|
||||||
@ -85,7 +85,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
useAppSelector(systemSelector);
|
useAppSelector(systemSelector);
|
||||||
|
|
||||||
const handleClickUseAsInitialImage = () => {
|
const handleClickUseAsInitialImage = () => {
|
||||||
dispatch(setInitialImagePath(image.url));
|
dispatch(setInitialImage(image));
|
||||||
dispatch(setActiveTab(1));
|
dispatch(setActiveTab(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,7 +114,8 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClickUseAllParameters = () =>
|
const handleClickUseAllParameters = () =>
|
||||||
dispatch(setAllParameters(image.metadata));
|
image.metadata && dispatch(setAllParameters(image.metadata));
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'a',
|
'a',
|
||||||
() => {
|
() => {
|
||||||
@ -139,9 +140,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
[image]
|
[image]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Non-null assertion: this button is disabled if there is no seed.
|
const handleClickUseSeed = () => image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const handleClickUseSeed = () => dispatch(setSeed(image.metadata.image.seed));
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
's',
|
's',
|
||||||
() => {
|
() => {
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.current-image-display {
|
.current-image-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
row-gap: 1rem;
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
&[data-tab-name='txt2img'] {
|
||||||
|
height: $app-text-to-image-height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-image-options {
|
.current-image-options {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
button {
|
|
||||||
@include Button(
|
|
||||||
$btn-width: 3rem,
|
|
||||||
$icon-size: 22px,
|
|
||||||
$btn-color: var(--btn-grey),
|
|
||||||
$btn-color-hover: var(--btn-grey-hover)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-image-viewer {
|
.current-image-viewer {
|
||||||
@ -32,25 +27,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current-image-preview {
|
.current-image-preview {
|
||||||
position: absolute;
|
|
||||||
top:0;
|
|
||||||
grid-area: current-image-preview;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: grid;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
grid-template-areas: 'current-image-content';
|
height: $app-text-to-image-height;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
grid-area: current-image-content;
|
|
||||||
background-color: var(--img2img-img-bg-color);
|
background-color: var(--img2img-img-bg-color);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
// width: auto;
|
|
||||||
// height: $app-gallery-height;
|
|
||||||
max-height: $app-gallery-height;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,39 +3,65 @@ import CurrentImageButtons from './CurrentImageButtons';
|
|||||||
import { MdPhoto } from 'react-icons/md';
|
import { MdPhoto } from 'react-icons/md';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
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.
|
* Displays the current image if there is one, plus associated actions.
|
||||||
*/
|
*/
|
||||||
const CurrentImageDisplay = () => {
|
const CurrentImageDisplay = () => {
|
||||||
const { currentImage, intermediateImage } = useAppSelector(
|
const {
|
||||||
(state: RootState) => state.gallery
|
currentImage,
|
||||||
);
|
intermediateImage,
|
||||||
|
activeTabName,
|
||||||
const shouldShowImageDetails = useAppSelector(
|
shouldShowImageDetails,
|
||||||
(state: RootState) => state.options.shouldShowImageDetails
|
} = useAppSelector(currentImageDisplaySelector);
|
||||||
);
|
|
||||||
|
|
||||||
const imageToDisplay = intermediateImage || currentImage;
|
const imageToDisplay = intermediateImage || currentImage;
|
||||||
|
|
||||||
return imageToDisplay ? (
|
return (
|
||||||
<div className="current-image-display">
|
<div className="current-image-area" data-tab-name={activeTabName}>
|
||||||
<div className="current-image-tools">
|
{imageToDisplay ? (
|
||||||
<CurrentImageButtons image={imageToDisplay} />
|
<>
|
||||||
</div>
|
<CurrentImageButtons image={imageToDisplay} />
|
||||||
<div className="current-image-viewer">
|
<div className="current-image-viewer">
|
||||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||||
{shouldShowImageDetails && (
|
{shouldShowImageDetails && (
|
||||||
<ImageMetadataViewer
|
<ImageMetadataViewer
|
||||||
image={imageToDisplay}
|
image={imageToDisplay}
|
||||||
styleClass="current-image-metadata"
|
styleClass="current-image-metadata"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="current-image-display-placeholder">
|
<div className="current-image-display-placeholder">
|
||||||
<MdPhoto />
|
<MdPhoto />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,18 +6,25 @@ import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from '../../app/invokeai';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { OptionsState } from '../options/optionsSlice';
|
||||||
|
|
||||||
const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
(state: RootState) => state.gallery,
|
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||||
(gallery: GalleryState) => {
|
(gallery: GalleryState, options: OptionsState) => {
|
||||||
const currentImageIndex = gallery.images.findIndex(
|
const { currentCategory } = gallery;
|
||||||
|
const { shouldShowImageDetails } = options;
|
||||||
|
|
||||||
|
const tempImages = gallery.categories[currentCategory].images;
|
||||||
|
const currentImageIndex = tempImages.findIndex(
|
||||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||||
);
|
);
|
||||||
const imagesLength = gallery.images.length;
|
const imagesLength = tempImages.length;
|
||||||
return {
|
return {
|
||||||
|
currentCategory,
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
isOnLastImage:
|
isOnLastImage:
|
||||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||||
|
shouldShowImageDetails,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,11 +42,12 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|||||||
const { imageToDisplay } = props;
|
const { imageToDisplay } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { isOnFirstImage, isOnLastImage } = useAppSelector(imagesSelector);
|
const {
|
||||||
|
isOnFirstImage,
|
||||||
const shouldShowImageDetails = useAppSelector(
|
isOnLastImage,
|
||||||
(state: RootState) => state.options.shouldShowImageDetails
|
currentCategory,
|
||||||
);
|
shouldShowImageDetails,
|
||||||
|
} = useAppSelector(imagesSelector);
|
||||||
|
|
||||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
@ -53,15 +61,15 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickPrevButton = () => {
|
const handleClickPrevButton = () => {
|
||||||
dispatch(selectPrevImage());
|
dispatch(selectPrevImage(currentCategory));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickNextButton = () => {
|
const handleClickNextButton = () => {
|
||||||
dispatch(selectNextImage());
|
dispatch(selectNextImage(currentCategory));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="current-image-preview">
|
<div className={'current-image-preview'}>
|
||||||
<Image
|
<Image
|
||||||
src={imageToDisplay.url}
|
src={imageToDisplay.url}
|
||||||
fit="contain"
|
fit="contain"
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
setAllImageToImageParameters,
|
setAllImageToImageParameters,
|
||||||
setAllTextToImageParameters,
|
setAllTextToImageParameters,
|
||||||
setInitialImagePath,
|
setInitialImage,
|
||||||
setPrompt,
|
setPrompt,
|
||||||
setSeed,
|
setSeed,
|
||||||
} from '../options/optionsSlice';
|
} from '../options/optionsSlice';
|
||||||
@ -58,7 +58,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
const handleMouseOut = () => setIsHovered(false);
|
const handleMouseOut = () => setIsHovered(false);
|
||||||
|
|
||||||
const handleUsePrompt = () => {
|
const handleUsePrompt = () => {
|
||||||
dispatch(setPrompt(image.metadata.image.prompt));
|
image.metadata && dispatch(setPrompt(image.metadata.image.prompt));
|
||||||
toast({
|
toast({
|
||||||
title: 'Prompt Set',
|
title: 'Prompt Set',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -68,7 +68,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUseSeed = () => {
|
const handleUseSeed = () => {
|
||||||
dispatch(setSeed(image.metadata.image.seed));
|
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
||||||
toast({
|
toast({
|
||||||
title: 'Seed Set',
|
title: 'Seed Set',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -78,7 +78,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToImageToImage = () => {
|
const handleSendToImageToImage = () => {
|
||||||
dispatch(setInitialImagePath(image.url));
|
dispatch(setInitialImage(image));
|
||||||
if (activeTabName !== 'img2img') {
|
if (activeTabName !== 'img2img') {
|
||||||
dispatch(setActiveTab('img2img'));
|
dispatch(setActiveTab('img2img'));
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUseAllParameters = () => {
|
const handleUseAllParameters = () => {
|
||||||
dispatch(setAllTextToImageParameters(metadata));
|
metadata && dispatch(setAllTextToImageParameters(metadata));
|
||||||
toast({
|
toast({
|
||||||
title: 'Parameters Set',
|
title: 'Parameters Set',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
@ -126,6 +126,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-gallery-category-btn-group {
|
||||||
|
width: 100% !important;
|
||||||
|
column-gap: 0 !important;
|
||||||
|
justify-content: stretch !important;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
&[data-selected='true'] {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// from https://css-tricks.com/a-grid-of-logos-in-squares/
|
// from https://css-tricks.com/a-grid-of-logos-in-squares/
|
||||||
.image-gallery {
|
.image-gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -11,6 +11,7 @@ import IAIIconButton from '../../common/components/IAIIconButton';
|
|||||||
import {
|
import {
|
||||||
selectNextImage,
|
selectNextImage,
|
||||||
selectPrevImage,
|
selectPrevImage,
|
||||||
|
setCurrentCategory,
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
setGalleryImageObjectFit,
|
setGalleryImageObjectFit,
|
||||||
setGalleryScrollPosition,
|
setGalleryScrollPosition,
|
||||||
@ -20,11 +21,11 @@ import {
|
|||||||
} from './gallerySlice';
|
} from './gallerySlice';
|
||||||
import HoverableImage from './HoverableImage';
|
import HoverableImage from './HoverableImage';
|
||||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||||
import { Spacer, useToast } from '@chakra-ui/react';
|
import { ButtonGroup, Spacer, useToast } from '@chakra-ui/react';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import { Direction } from 're-resizable/lib/resizer';
|
import { Direction } from 're-resizable/lib/resizer';
|
||||||
import { imageGallerySelector } from './gallerySliceSelectors';
|
import { imageGallerySelector } from './gallerySliceSelectors';
|
||||||
import { FaWrench } from 'react-icons/fa';
|
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
||||||
import IAIPopover from '../../common/components/IAIPopover';
|
import IAIPopover from '../../common/components/IAIPopover';
|
||||||
import IAISlider from '../../common/components/IAISlider';
|
import IAISlider from '../../common/components/IAISlider';
|
||||||
import { BiReset } from 'react-icons/bi';
|
import { BiReset } from 'react-icons/bi';
|
||||||
@ -36,8 +37,8 @@ export default function ImageGallery() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
images,
|
images,
|
||||||
|
currentCategory,
|
||||||
currentImageUuid,
|
currentImageUuid,
|
||||||
areMoreImagesAvailable,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
shouldShowGallery,
|
shouldShowGallery,
|
||||||
galleryScrollPosition,
|
galleryScrollPosition,
|
||||||
@ -47,6 +48,7 @@ export default function ImageGallery() {
|
|||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
shouldHoldGalleryOpen,
|
shouldHoldGalleryOpen,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
|
areMoreImagesAvailable,
|
||||||
} = useAppSelector(imageGallerySelector);
|
} = useAppSelector(imageGallerySelector);
|
||||||
|
|
||||||
const [gallerySize, setGallerySize] = useState<Size>({
|
const [gallerySize, setGallerySize] = useState<Size>({
|
||||||
@ -128,7 +130,7 @@ export default function ImageGallery() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickLoadMore = () => {
|
const handleClickLoadMore = () => {
|
||||||
dispatch(requestImages());
|
dispatch(requestImages(currentCategory));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
@ -151,13 +153,21 @@ export default function ImageGallery() {
|
|||||||
[shouldShowGallery]
|
[shouldShowGallery]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys('left', () => {
|
useHotkeys(
|
||||||
dispatch(selectPrevImage());
|
'left',
|
||||||
});
|
() => {
|
||||||
|
dispatch(selectPrevImage(currentCategory));
|
||||||
|
},
|
||||||
|
[currentCategory]
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeys('right', () => {
|
useHotkeys(
|
||||||
dispatch(selectNextImage());
|
'right',
|
||||||
});
|
() => {
|
||||||
|
dispatch(selectNextImage(currentCategory));
|
||||||
|
},
|
||||||
|
[currentCategory]
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+p',
|
'shift+p',
|
||||||
@ -309,17 +319,38 @@ export default function ImageGallery() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="image-gallery-header">
|
<div className="image-gallery-header">
|
||||||
{activeTabName !== 'inpainting' ? (
|
{/*{activeTabName !== 'inpainting' ? (
|
||||||
<>
|
<>
|
||||||
<h1>Your Invocations</h1>
|
<h1>Your Invocations</h1>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}*/}
|
||||||
|
<div>
|
||||||
|
<ButtonGroup
|
||||||
|
size="sm"
|
||||||
|
isAttached
|
||||||
|
variant="solid"
|
||||||
|
className="image-gallery-category-btn-group"
|
||||||
|
>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Show Invocations"
|
||||||
|
tooltip="Show Invocations"
|
||||||
|
data-selected={currentCategory === 'result'}
|
||||||
|
icon={<FaImage />}
|
||||||
|
onClick={() => dispatch(setCurrentCategory('result'))}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Show Uploads"
|
||||||
|
tooltip="Show Uploads"
|
||||||
|
data-selected={currentCategory === 'user'}
|
||||||
|
icon={<FaUser />}
|
||||||
|
onClick={() => dispatch(setCurrentCategory('user'))}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
trigger="click"
|
trigger="hover"
|
||||||
hasArrow={activeTabName === 'inpainting' ? false : true}
|
hasArrow={activeTabName === 'inpainting' ? false : true}
|
||||||
// styleClass="image-gallery-settings-popover"
|
|
||||||
triggerComponent={
|
triggerComponent={
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
@ -438,3 +469,18 @@ export default function ImageGallery() {
|
|||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <IAIIconButton
|
||||||
|
// aria-label="Show Invocations"
|
||||||
|
// tooltip="Show Invocations"
|
||||||
|
// data-selected={currentCategory === 'result'}
|
||||||
|
// icon={<FaImage />}
|
||||||
|
// onClick={() => dispatch(setCurrentCategory('result'))}
|
||||||
|
// />
|
||||||
|
// <IAIIconButton
|
||||||
|
// aria-label="Show Uploads"
|
||||||
|
// tooltip="Show Uploads"
|
||||||
|
// data-selected={currentCategory === 'user'}
|
||||||
|
// icon={<FaUser />}
|
||||||
|
// onClick={() => dispatch(setCurrentCategory('user'))}
|
||||||
|
// />
|
||||||
|
@ -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;
|
|
@ -20,7 +20,6 @@ import {
|
|||||||
setHeight,
|
setHeight,
|
||||||
setHiresFix,
|
setHiresFix,
|
||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setInitialImagePath,
|
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
setPrompt,
|
setPrompt,
|
||||||
setSampler,
|
setSampler,
|
||||||
@ -32,6 +31,7 @@ import {
|
|||||||
setUpscalingLevel,
|
setUpscalingLevel,
|
||||||
setUpscalingStrength,
|
setUpscalingStrength,
|
||||||
setWidth,
|
setWidth,
|
||||||
|
setInitialImage,
|
||||||
} from '../../options/optionsSlice';
|
} from '../../options/optionsSlice';
|
||||||
import promptToString from '../../../common/util/promptToString';
|
import promptToString from '../../../common/util/promptToString';
|
||||||
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
||||||
@ -248,7 +248,7 @@ const ImageMetadataViewer = memo(
|
|||||||
label="Initial image"
|
label="Initial image"
|
||||||
value={init_image_path}
|
value={init_image_path}
|
||||||
isLink
|
isLink
|
||||||
onClick={() => dispatch(setInitialImagePath(init_image_path))}
|
onClick={() => dispatch(setInitialImage(init_image_path))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mask_image_path && (
|
{mask_image_path && (
|
||||||
|
@ -3,16 +3,27 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import _, { clamp } from 'lodash';
|
import _, { clamp } from 'lodash';
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
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';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
|
|
||||||
|
export type Gallery = {
|
||||||
|
images: InvokeAI.Image[];
|
||||||
|
latest_mtime?: number;
|
||||||
|
earliest_mtime?: number;
|
||||||
|
areMoreImagesAvailable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface GalleryState {
|
export interface GalleryState {
|
||||||
currentImage?: InvokeAI.Image;
|
currentImage?: InvokeAI.Image;
|
||||||
currentImageUuid: string;
|
currentImageUuid: string;
|
||||||
images: Array<InvokeAI.Image>;
|
|
||||||
intermediateImage?: InvokeAI.Image;
|
intermediateImage?: InvokeAI.Image;
|
||||||
areMoreImagesAvailable: boolean;
|
|
||||||
latest_mtime?: number;
|
|
||||||
earliest_mtime?: number;
|
|
||||||
shouldPinGallery: boolean;
|
shouldPinGallery: boolean;
|
||||||
shouldShowGallery: boolean;
|
shouldShowGallery: boolean;
|
||||||
galleryScrollPosition: number;
|
galleryScrollPosition: number;
|
||||||
@ -20,12 +31,15 @@ export interface GalleryState {
|
|||||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||||
shouldHoldGalleryOpen: boolean;
|
shouldHoldGalleryOpen: boolean;
|
||||||
shouldAutoSwitchToNewImages: boolean;
|
shouldAutoSwitchToNewImages: boolean;
|
||||||
|
categories: {
|
||||||
|
user: Gallery;
|
||||||
|
result: Gallery;
|
||||||
|
};
|
||||||
|
currentCategory: GalleryCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GalleryState = {
|
const initialState: GalleryState = {
|
||||||
currentImageUuid: '',
|
currentImageUuid: '',
|
||||||
images: [],
|
|
||||||
areMoreImagesAvailable: true,
|
|
||||||
shouldPinGallery: true,
|
shouldPinGallery: true,
|
||||||
shouldShowGallery: true,
|
shouldShowGallery: true,
|
||||||
galleryScrollPosition: 0,
|
galleryScrollPosition: 0,
|
||||||
@ -33,6 +47,21 @@ const initialState: GalleryState = {
|
|||||||
galleryImageObjectFit: 'cover',
|
galleryImageObjectFit: 'cover',
|
||||||
shouldHoldGalleryOpen: false,
|
shouldHoldGalleryOpen: false,
|
||||||
shouldAutoSwitchToNewImages: true,
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -43,10 +72,15 @@ export const gallerySlice = createSlice({
|
|||||||
state.currentImage = action.payload;
|
state.currentImage = action.payload;
|
||||||
state.currentImageUuid = action.payload.uuid;
|
state.currentImageUuid = action.payload.uuid;
|
||||||
},
|
},
|
||||||
removeImage: (state, action: PayloadAction<string>) => {
|
removeImage: (
|
||||||
const uuid = action.payload;
|
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) {
|
if (uuid === state.currentImageUuid) {
|
||||||
/**
|
/**
|
||||||
@ -58,7 +92,7 @@ export const gallerySlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Get the currently selected image's index.
|
* Get the currently selected image's index.
|
||||||
*/
|
*/
|
||||||
const imageToDeleteIndex = state.images.findIndex(
|
const imageToDeleteIndex = tempImages.findIndex(
|
||||||
(image) => image.uuid === uuid
|
(image) => image.uuid === uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -84,24 +118,35 @@ export const gallerySlice = createSlice({
|
|||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
state.images = newImages;
|
state.categories[category as GalleryCategory].images = newImages;
|
||||||
},
|
},
|
||||||
addImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
addImage: (
|
||||||
const newImage = action.payload;
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
image: InvokeAI.Image;
|
||||||
|
category: GalleryCategory;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { image: newImage, category } = action.payload;
|
||||||
const { uuid, url, mtime } = newImage;
|
const { uuid, url, mtime } = newImage;
|
||||||
|
|
||||||
|
const tempCategory = state.categories[category as GalleryCategory];
|
||||||
|
|
||||||
// Do not add duplicate images
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.images.unshift(newImage);
|
tempCategory.images.unshift(newImage);
|
||||||
if (state.shouldAutoSwitchToNewImages) {
|
if (state.shouldAutoSwitchToNewImages) {
|
||||||
state.currentImageUuid = uuid;
|
state.currentImageUuid = uuid;
|
||||||
state.currentImage = newImage;
|
state.currentImage = newImage;
|
||||||
|
if (category === 'result') {
|
||||||
|
state.currentCategory = 'result';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.intermediateImage = undefined;
|
state.intermediateImage = undefined;
|
||||||
state.latest_mtime = mtime;
|
tempCategory.latest_mtime = mtime;
|
||||||
},
|
},
|
||||||
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
state.intermediateImage = action.payload;
|
state.intermediateImage = action.payload;
|
||||||
@ -109,49 +154,53 @@ export const gallerySlice = createSlice({
|
|||||||
clearIntermediateImage: (state) => {
|
clearIntermediateImage: (state) => {
|
||||||
state.intermediateImage = undefined;
|
state.intermediateImage = undefined;
|
||||||
},
|
},
|
||||||
selectNextImage: (state) => {
|
selectNextImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||||
const { images, currentImage } = state;
|
const category = action.payload;
|
||||||
|
const { currentImage } = state;
|
||||||
|
const tempImages = state.categories[category].images;
|
||||||
|
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
const currentImageIndex = images.findIndex(
|
const currentImageIndex = tempImages.findIndex(
|
||||||
(i) => i.uuid === currentImage.uuid
|
(i) => i.uuid === currentImage.uuid
|
||||||
);
|
);
|
||||||
if (_.inRange(currentImageIndex, 0, images.length)) {
|
if (_.inRange(currentImageIndex, 0, tempImages.length)) {
|
||||||
const newCurrentImage = images[currentImageIndex + 1];
|
const newCurrentImage = tempImages[currentImageIndex + 1];
|
||||||
state.currentImage = newCurrentImage;
|
state.currentImage = newCurrentImage;
|
||||||
state.currentImageUuid = newCurrentImage.uuid;
|
state.currentImageUuid = newCurrentImage.uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectPrevImage: (state) => {
|
selectPrevImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||||
const { images, currentImage } = state;
|
const category = action.payload;
|
||||||
|
const { currentImage } = state;
|
||||||
|
const tempImages = state.categories[category].images;
|
||||||
|
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
const currentImageIndex = images.findIndex(
|
const currentImageIndex = tempImages.findIndex(
|
||||||
(i) => i.uuid === currentImage.uuid
|
(i) => i.uuid === currentImage.uuid
|
||||||
);
|
);
|
||||||
if (_.inRange(currentImageIndex, 1, images.length + 1)) {
|
if (_.inRange(currentImageIndex, 1, tempImages.length + 1)) {
|
||||||
const newCurrentImage = images[currentImageIndex - 1];
|
const newCurrentImage = tempImages[currentImageIndex - 1];
|
||||||
state.currentImage = newCurrentImage;
|
state.currentImage = newCurrentImage;
|
||||||
state.currentImageUuid = newCurrentImage.uuid;
|
state.currentImageUuid = newCurrentImage.uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addGalleryImages: (
|
addGalleryImages: (state, action: PayloadAction<AddImagesPayload>) => {
|
||||||
state,
|
const { images, areMoreImagesAvailable, category } = action.payload;
|
||||||
action: PayloadAction<{
|
const tempImages = state.categories[category].images;
|
||||||
images: Array<InvokeAI.Image>;
|
|
||||||
areMoreImagesAvailable: boolean;
|
// const prevImages = category === 'user' ? state.userImages : state.resultImages
|
||||||
}>
|
|
||||||
) => {
|
|
||||||
const { images, areMoreImagesAvailable } = action.payload;
|
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
// Filter images that already exist in the gallery
|
// Filter images that already exist in the gallery
|
||||||
const newImages = images.filter(
|
const newImages = images.filter(
|
||||||
(newImage) =>
|
(newImage) =>
|
||||||
!state.images.find(
|
!tempImages.find(
|
||||||
(i) => i.url === newImage.url && i.mtime === newImage.mtime
|
(i) => i.url === newImage.url && i.mtime === newImage.mtime
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
state.images = state.images
|
state.categories[category].images = tempImages
|
||||||
.concat(newImages)
|
.concat(newImages)
|
||||||
.sort((a, b) => b.mtime - a.mtime);
|
.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
|
||||||
@ -162,11 +211,14 @@ export const gallerySlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keep track of the timestamps of latest and earliest images received
|
// keep track of the timestamps of latest and earliest images received
|
||||||
state.latest_mtime = images[0].mtime;
|
state.categories[category].latest_mtime = images[0].mtime;
|
||||||
state.earliest_mtime = images[images.length - 1].mtime;
|
state.categories[category].earliest_mtime =
|
||||||
|
images[images.length - 1].mtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areMoreImagesAvailable !== undefined) {
|
if (areMoreImagesAvailable !== undefined) {
|
||||||
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
state.categories[category].areMoreImagesAvailable =
|
||||||
|
areMoreImagesAvailable;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
|
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
|
||||||
@ -193,6 +245,9 @@ export const gallerySlice = createSlice({
|
|||||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitchToNewImages = action.payload;
|
state.shouldAutoSwitchToNewImages = action.payload;
|
||||||
},
|
},
|
||||||
|
setCurrentCategory: (state, action: PayloadAction<GalleryCategory>) => {
|
||||||
|
state.currentCategory = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,6 +267,7 @@ export const {
|
|||||||
setGalleryImageObjectFit,
|
setGalleryImageObjectFit,
|
||||||
setShouldHoldGalleryOpen,
|
setShouldHoldGalleryOpen,
|
||||||
setShouldAutoSwitchToNewImages,
|
setShouldAutoSwitchToNewImages,
|
||||||
|
setCurrentCategory,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
@ -8,24 +8,22 @@ export const imageGallerySelector = createSelector(
|
|||||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||||
(gallery: GalleryState, options: OptionsState) => {
|
(gallery: GalleryState, options: OptionsState) => {
|
||||||
const {
|
const {
|
||||||
images,
|
categories,
|
||||||
|
currentCategory,
|
||||||
currentImageUuid,
|
currentImageUuid,
|
||||||
areMoreImagesAvailable,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
shouldShowGallery,
|
shouldShowGallery,
|
||||||
galleryScrollPosition,
|
galleryScrollPosition,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
shouldHoldGalleryOpen,
|
shouldHoldGalleryOpen,
|
||||||
shouldAutoSwitchToNewImages
|
shouldAutoSwitchToNewImages,
|
||||||
} = gallery;
|
} = gallery;
|
||||||
|
|
||||||
const { activeTab } = options;
|
const { activeTab } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
images,
|
|
||||||
currentImageUuid,
|
currentImageUuid,
|
||||||
areMoreImagesAvailable,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
shouldShowGallery,
|
shouldShowGallery,
|
||||||
galleryScrollPosition,
|
galleryScrollPosition,
|
||||||
@ -34,7 +32,11 @@ export const imageGallerySelector = createSelector(
|
|||||||
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||||
activeTabName: tabMap[activeTab],
|
activeTabName: tabMap[activeTab],
|
||||||
shouldHoldGalleryOpen,
|
shouldHoldGalleryOpen,
|
||||||
shouldAutoSwitchToNewImages
|
shouldAutoSwitchToNewImages,
|
||||||
|
images: categories[currentCategory].images,
|
||||||
|
areMoreImagesAvailable:
|
||||||
|
categories[currentCategory].areMoreImagesAvailable,
|
||||||
|
currentCategory,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
import {
|
||||||
|
cloneElement,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
@ -55,7 +61,7 @@ const ImageUploader = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box {...getRootProps()} flexGrow={3} className={`${styleClass}`}>
|
<Box {...getRootProps()} className={styleClass}>
|
||||||
<input {...getInputProps({ multiple: false })} />
|
<input {...getInputProps({ multiple: false })} />
|
||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
onClick: handleClickUploadIcon,
|
onClick: handleClickUploadIcon,
|
||||||
|
@ -27,8 +27,7 @@ export interface OptionsState {
|
|||||||
codeformerFidelity: number;
|
codeformerFidelity: number;
|
||||||
upscalingLevel: UpscalingLevel;
|
upscalingLevel: UpscalingLevel;
|
||||||
upscalingStrength: number;
|
upscalingStrength: number;
|
||||||
shouldUseInitImage: boolean;
|
initialImage?: InvokeAI.Image | string; // can be an Image or url
|
||||||
initialImagePath: string | null;
|
|
||||||
maskPath: string;
|
maskPath: string;
|
||||||
seamless: boolean;
|
seamless: boolean;
|
||||||
hiresFix: boolean;
|
hiresFix: boolean;
|
||||||
@ -58,9 +57,7 @@ const initialOptionsState: OptionsState = {
|
|||||||
seed: 0,
|
seed: 0,
|
||||||
seamless: false,
|
seamless: false,
|
||||||
hiresFix: false,
|
hiresFix: false,
|
||||||
shouldUseInitImage: false,
|
|
||||||
img2imgStrength: 0.75,
|
img2imgStrength: 0.75,
|
||||||
initialImagePath: null,
|
|
||||||
maskPath: '',
|
maskPath: '',
|
||||||
shouldFitToWidthHeight: true,
|
shouldFitToWidthHeight: true,
|
||||||
shouldGenerateVariations: false,
|
shouldGenerateVariations: false,
|
||||||
@ -137,14 +134,6 @@ export const optionsSlice = createSlice({
|
|||||||
setUpscalingStrength: (state, action: PayloadAction<number>) => {
|
setUpscalingStrength: (state, action: PayloadAction<number>) => {
|
||||||
state.upscalingStrength = action.payload;
|
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>) => {
|
setMaskPath: (state, action: PayloadAction<string>) => {
|
||||||
state.maskPath = action.payload;
|
state.maskPath = action.payload;
|
||||||
},
|
},
|
||||||
@ -170,9 +159,6 @@ export const optionsSlice = createSlice({
|
|||||||
if (key === 'seed') {
|
if (key === 'seed') {
|
||||||
temp.shouldRandomizeSeed = false;
|
temp.shouldRandomizeSeed = false;
|
||||||
}
|
}
|
||||||
if (key === 'initialImagePath' && value === '') {
|
|
||||||
temp.shouldUseInitImage = false;
|
|
||||||
}
|
|
||||||
return temp;
|
return temp;
|
||||||
},
|
},
|
||||||
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
||||||
@ -236,13 +222,10 @@ export const optionsSlice = createSlice({
|
|||||||
action.payload.image;
|
action.payload.image;
|
||||||
|
|
||||||
if (type === 'img2img') {
|
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 (mask_image_path) state.maskPath = mask_image_path;
|
||||||
if (strength) state.img2imgStrength = strength;
|
if (strength) state.img2imgStrength = strength;
|
||||||
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
||||||
state.shouldUseInitImage = true;
|
|
||||||
} else {
|
|
||||||
state.shouldUseInitImage = false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
|
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
|
||||||
@ -267,13 +250,10 @@ export const optionsSlice = createSlice({
|
|||||||
} = action.payload.image;
|
} = action.payload.image;
|
||||||
|
|
||||||
if (type === 'img2img') {
|
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 (mask_image_path) state.maskPath = mask_image_path;
|
||||||
if (strength) state.img2imgStrength = strength;
|
if (strength) state.img2imgStrength = strength;
|
||||||
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
||||||
state.shouldUseInitImage = true;
|
|
||||||
} else {
|
|
||||||
state.shouldUseInitImage = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variations && variations.length > 0) {
|
if (variations && variations.length > 0) {
|
||||||
@ -335,6 +315,15 @@ export const optionsSlice = createSlice({
|
|||||||
setShowDualDisplay: (state, action: PayloadAction<boolean>) => {
|
setShowDualDisplay: (state, action: PayloadAction<boolean>) => {
|
||||||
state.showDualDisplay = action.payload;
|
state.showDualDisplay = action.payload;
|
||||||
},
|
},
|
||||||
|
setInitialImage: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<InvokeAI.Image | string>
|
||||||
|
) => {
|
||||||
|
state.initialImage = action.payload;
|
||||||
|
},
|
||||||
|
clearInitialImage: (state) => {
|
||||||
|
state.initialImage = undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -357,8 +346,6 @@ export const {
|
|||||||
setCodeformerFidelity,
|
setCodeformerFidelity,
|
||||||
setUpscalingLevel,
|
setUpscalingLevel,
|
||||||
setUpscalingStrength,
|
setUpscalingStrength,
|
||||||
setShouldUseInitImage,
|
|
||||||
setInitialImagePath,
|
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
resetSeed,
|
resetSeed,
|
||||||
resetOptionsState,
|
resetOptionsState,
|
||||||
@ -377,6 +364,8 @@ export const {
|
|||||||
setAllTextToImageParameters,
|
setAllTextToImageParameters,
|
||||||
setAllImageToImageParameters,
|
setAllImageToImageParameters,
|
||||||
setShowDualDisplay,
|
setShowDualDisplay,
|
||||||
|
setInitialImage,
|
||||||
|
clearInitialImage,
|
||||||
} = optionsSlice.actions;
|
} = optionsSlice.actions;
|
||||||
|
|
||||||
export default optionsSlice.reducer;
|
export default optionsSlice.reducer;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
.console {
|
.console {
|
||||||
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--console-bg-color);
|
background: var(--console-bg-color);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
@use '../../../styles/Mixins/' as *;
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.image-to-image-workarea {
|
.image-to-image-area {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: max-content auto;
|
flex-direction: column;
|
||||||
column-gap: 1rem;
|
row-gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-to-image-panel {
|
.image-to-image-panel {
|
||||||
@ -15,16 +17,6 @@
|
|||||||
@include HideScrollbar;
|
@include HideScrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-to-image-display-area {
|
|
||||||
display: grid;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
|
|
||||||
.image-gallery-area {
|
|
||||||
z-index: 2;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-to-image-strength-main-option {
|
.image-to-image-strength-main-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: none !important;
|
grid-template-columns: none !important;
|
||||||
@ -34,110 +26,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-to-image-display {
|
.init-image-preview-header {
|
||||||
border-radius: 0.5rem;
|
display: flex;
|
||||||
background-color: var(--background-color-secondary);
|
align-items: center;
|
||||||
display: grid;
|
justify-content: space-between;
|
||||||
height: $app-content-height;
|
width: 100%;
|
||||||
|
|
||||||
.current-image-options {
|
h2 {
|
||||||
grid-auto-columns: max-content;
|
font-weight: bold;
|
||||||
justify-self: center;
|
font-size: 0.9rem;
|
||||||
align-self: start;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-to-image-single-preview {
|
|
||||||
display: grid;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
place-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-to-image-dual-preview-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'img2img-preview';
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-to-image-dual-preview {
|
|
||||||
grid-area: img2img-preview;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
place-content: center;
|
|
||||||
|
|
||||||
.current-image-preview {
|
|
||||||
img {
|
|
||||||
height: calc($app-gallery-height - 2rem);
|
|
||||||
max-height: calc($app-gallery-height - 2rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.img2img-metadata {
|
|
||||||
grid-area: img2img-preview;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.init-image-preview {
|
.init-image-preview {
|
||||||
display: grid;
|
height: 100%;
|
||||||
grid-template-areas: 'init-image-content';
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
.init-image-preview-header {
|
|
||||||
grid-area: init-image-content;
|
|
||||||
z-index: 2;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto max-content;
|
|
||||||
height: max-content;
|
|
||||||
align-items: center;
|
|
||||||
align-self: start;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
padding: 0.2rem 0.6rem;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
background-color: var(--tab-hover-color);
|
|
||||||
width: max-content;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.init-image-image {
|
|
||||||
grid-area: init-image-content;
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: var(--img2img-img-bg-color);
|
|
||||||
width: auto;
|
|
||||||
height: calc($app-gallery-height - 2rem);
|
|
||||||
max-height: calc($app-gallery-height - 2rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-to-image-upload-btn {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $app-content-height;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: $app-text-to-image-height;
|
||||||
|
|
||||||
button {
|
img {
|
||||||
overflow: hidden;
|
border-radius: 0.5rem;
|
||||||
width: 100%;
|
object-fit: contain;
|
||||||
height: 100%;
|
width: auto;
|
||||||
font-size: 1.5rem;
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--img2img-img-bg-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,74 +1,40 @@
|
|||||||
import React from 'react';
|
import { uploadImage } from '../../../app/socketio/actions';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import { uploadInitialImage } from '../../../app/socketio/actions';
|
|
||||||
import { RootState, useAppSelector } from '../../../app/store';
|
|
||||||
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
||||||
import CurrentImageButtons from '../../gallery/CurrentImageButtons';
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
import CurrentImagePreview from '../../gallery/CurrentImagePreview';
|
|
||||||
import ImageMetadataViewer from '../../gallery/ImageMetaDataViewer/ImageMetadataViewer';
|
|
||||||
|
|
||||||
import InitImagePreview from './InitImagePreview';
|
import InitImagePreview from './InitImagePreview';
|
||||||
|
|
||||||
export default function ImageToImageDisplay() {
|
const ImageToImageDisplay = () => {
|
||||||
const initialImagePath = useAppSelector(
|
const dispatch = useAppDispatch();
|
||||||
(state: RootState) => state.options.initialImagePath
|
|
||||||
|
const initialImage = useAppSelector(
|
||||||
|
(state: RootState) => state.options.initialImage
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentImage, intermediateImage } = useAppSelector(
|
const { currentImage } = useAppSelector((state: RootState) => state.gallery);
|
||||||
(state: RootState) => state.gallery
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowImageDetails = useAppSelector(
|
const imageToImageComponent = initialImage ? (
|
||||||
(state: RootState) => state.options.shouldShowImageDetails
|
<div className="image-to-image-area">
|
||||||
|
<InitImagePreview />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<InvokeImageUploader
|
||||||
|
handleFile={(file: File) =>
|
||||||
|
dispatch(uploadImage({ file, destination: 'img2img' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageToDisplay = intermediateImage || currentImage;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="workarea-split-view">
|
||||||
className="image-to-image-display"
|
<div className="workarea-split-view-left">{imageToImageComponent} </div>
|
||||||
style={
|
{currentImage && (
|
||||||
imageToDisplay
|
<div className="workarea-split-view-right">
|
||||||
? { gridAutoRows: 'max-content auto' }
|
<CurrentImageDisplay />
|
||||||
: { gridAutoRows: 'auto' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{initialImagePath ? (
|
|
||||||
<>
|
|
||||||
{imageToDisplay ? (
|
|
||||||
<>
|
|
||||||
<CurrentImageButtons image={imageToDisplay} />
|
|
||||||
<div className="image-to-image-dual-preview-container">
|
|
||||||
<div className="image-to-image-dual-preview">
|
|
||||||
<InitImagePreview />
|
|
||||||
<div className="image-to-image-current-image-display">
|
|
||||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
|
||||||
{shouldShowImageDetails && (
|
|
||||||
<ImageMetadataViewer
|
|
||||||
image={imageToDisplay}
|
|
||||||
styleClass="img2img-metadata"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="image-to-image-single-preview">
|
|
||||||
<InitImagePreview />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="upload-image">
|
|
||||||
<InvokeImageUploader
|
|
||||||
label="Upload or Drop Image Here"
|
|
||||||
icon={<FaUpload />}
|
|
||||||
styleClass="image-to-image-upload-btn"
|
|
||||||
dispatcher={uploadInitialImage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ImageToImageDisplay;
|
||||||
|
@ -2,12 +2,10 @@ import { IconButton, Image, useToast } from '@chakra-ui/react';
|
|||||||
import React, { SyntheticEvent } from 'react';
|
import React, { SyntheticEvent } from 'react';
|
||||||
import { MdClear } from 'react-icons/md';
|
import { MdClear } from 'react-icons/md';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import { setInitialImagePath } from '../../options/optionsSlice';
|
import { clearInitialImage } from '../../options/optionsSlice';
|
||||||
|
|
||||||
export default function InitImagePreview() {
|
export default function InitImagePreview() {
|
||||||
const initialImagePath = useAppSelector(
|
const { initialImage } = useAppSelector((state: RootState) => state.options);
|
||||||
(state: RootState) => state.options.initialImagePath
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -15,7 +13,7 @@ export default function InitImagePreview() {
|
|||||||
|
|
||||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dispatch(setInitialImagePath(null));
|
dispatch(clearInitialImage());
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertMissingInitImage = () => {
|
const alertMissingInitImage = () => {
|
||||||
@ -25,31 +23,33 @@ export default function InitImagePreview() {
|
|||||||
status: 'error',
|
status: 'error',
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
dispatch(setInitialImagePath(null));
|
dispatch(clearInitialImage());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="init-image-preview">
|
<>
|
||||||
<div className="init-image-preview-header">
|
<div className="init-image-preview-header">
|
||||||
<h1>Initial Image</h1>
|
<h2>Initial Image</h2>
|
||||||
<IconButton
|
<IconButton
|
||||||
isDisabled={!initialImagePath}
|
isDisabled={!initialImage}
|
||||||
size={'sm'}
|
|
||||||
aria-label={'Reset Initial Image'}
|
aria-label={'Reset Initial Image'}
|
||||||
onClick={handleClickResetInitialImage}
|
onClick={handleClickResetInitialImage}
|
||||||
icon={<MdClear />}
|
icon={<MdClear />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{initialImagePath && (
|
{initialImage && (
|
||||||
<div className="init-image-image">
|
<div className="init-image-preview">
|
||||||
<Image
|
<Image
|
||||||
fit={'contain'}
|
fit={'contain'}
|
||||||
src={initialImagePath}
|
maxWidth={'100%'}
|
||||||
rounded={'md'}
|
maxHeight={'100%'}
|
||||||
|
src={
|
||||||
|
typeof initialImage === 'string' ? initialImage : initialImage.url
|
||||||
|
}
|
||||||
onError={alertMissingInitImage}
|
onError={alertMissingInitImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,14 @@ import React from 'react';
|
|||||||
import { RootState, useAppSelector } from '../../../app/store';
|
import { RootState, useAppSelector } from '../../../app/store';
|
||||||
|
|
||||||
export default function InitialImageOverlay() {
|
export default function InitialImageOverlay() {
|
||||||
const initialImagePath = useAppSelector(
|
const initialImage = useAppSelector(
|
||||||
(state: RootState) => state.options.initialImagePath
|
(state: RootState) => state.options.initialImage
|
||||||
);
|
);
|
||||||
|
|
||||||
return initialImagePath ? (
|
return initialImage ? (
|
||||||
<Image
|
<Image
|
||||||
fit={'contain'}
|
fit={'contain'}
|
||||||
src={initialImagePath}
|
src={typeof initialImage === 'string' ? initialImage : initialImage.url}
|
||||||
rounded={'md'}
|
rounded={'md'}
|
||||||
className={'checkerboard'}
|
className={'checkerboard'}
|
||||||
/>
|
/>
|
||||||
|
@ -1,62 +1,47 @@
|
|||||||
@use '../../../styles/Mixins/' as *;
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.brush-preview-wrapper {
|
.inpainting-main-area {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brush-preview {
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px black solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-workarea {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: max-content auto;
|
|
||||||
column-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-display-area {
|
|
||||||
display: grid;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-display {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
// column-gap: 1rem;
|
|
||||||
height: $app-content-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-toolkit {
|
|
||||||
// display: grid;
|
|
||||||
// grid-template-rows: auto auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--inpaint-bg-color);
|
align-items: center;
|
||||||
border-radius: 0.5rem;
|
row-gap: 1rem;
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-canvas-container {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
.inpainting-settings {
|
||||||
padding-bottom: 1rem;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
column-gap: 1rem;
|
||||||
justify-content: center;
|
|
||||||
|
.inpainting-buttons-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-slider-numberinput {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-canvas-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.inpainting-canvas-wrapper {
|
.inpainting-canvas-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min-content;
|
height: 100%;
|
||||||
height: min-content;
|
width: 100%;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
.inpainting-alerts {
|
.inpainting-alerts {
|
||||||
@ -80,6 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inpainting-canvas-stage {
|
.inpainting-canvas-stage {
|
||||||
|
border-radius: 0.5rem;
|
||||||
canvas {
|
canvas {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -87,61 +73,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .canvas-scale-calculator {
|
|
||||||
// width: calc(100% - 1rem);
|
|
||||||
// height: calc(100% - 1rem);
|
|
||||||
// display: flex;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.inpainting-canvas-scale-wrapper {
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
height: calc(100% - 1rem);
|
|
||||||
// width: max-content;
|
|
||||||
// height: max-content;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
row-gap: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.inpainting-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 0.8rem;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 2.4rem;
|
|
||||||
svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-buttons-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inpainting-slider-numberinput {
|
|
||||||
display: flex;
|
|
||||||
column-gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides
|
// Overrides
|
||||||
.inpainting-workarea-container {
|
.inpainting-workarea-overrides {
|
||||||
.image-gallery-area {
|
.image-gallery-area {
|
||||||
.chakra-popover__popper {
|
.chakra-popover__popper {
|
||||||
inset: 0 auto auto -75px !important;
|
inset: 0 auto auto -75px !important;
|
||||||
@ -149,21 +82,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current-image-options {
|
.current-image-options {
|
||||||
button {
|
|
||||||
@include Button(
|
|
||||||
$btn-width: 2.5rem,
|
|
||||||
$icon-size: 18px,
|
|
||||||
$btn-color: var(--btn-grey),
|
|
||||||
$btn-color-hover: var(--btn-grey-hover)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chakra-popover__popper {
|
.chakra-popover__popper {
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-image-preview {
|
|
||||||
padding: 0 1rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ const InpaintingCanvas = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inpainting-canvas-wrapper checkerboard" tabIndex={1}>
|
<div className="inpainting-canvas-wrapper" tabIndex={1}>
|
||||||
<div className="inpainting-alerts">
|
<div className="inpainting-alerts">
|
||||||
{!shouldShowMask && (
|
{!shouldShowMask && (
|
||||||
<div style={{ pointerEvents: 'none' }}>Mask Hidden (H)</div>
|
<div style={{ pointerEvents: 'none' }}>Mask Hidden (H)</div>
|
||||||
@ -245,7 +245,7 @@ const InpaintingCanvas = () => {
|
|||||||
onMouseOut={handleMouseOutCanvas}
|
onMouseOut={handleMouseOutCanvas}
|
||||||
onMouseLeave={handleMouseOutCanvas}
|
onMouseLeave={handleMouseOutCanvas}
|
||||||
style={{ cursor: shouldShowMask ? 'none' : 'default' }}
|
style={{ cursor: shouldShowMask ? 'none' : 'default' }}
|
||||||
className="inpainting-canvas-stage"
|
className="inpainting-canvas-stage checkerboard"
|
||||||
ref={stageRef}
|
ref={stageRef}
|
||||||
>
|
>
|
||||||
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
||||||
|
@ -25,7 +25,7 @@ const InpaintingCanvasPlaceholder = () => {
|
|||||||
}, [dispatch, imageToInpaint, needsRepaint]);
|
}, [dispatch, imageToInpaint, needsRepaint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="inpainting-canvas-container">
|
<div ref={ref} className="inpainting-canvas-area">
|
||||||
<Spinner thickness="2px" speed="1s" size="xl" />
|
<Spinner thickness="2px" speed="1s" size="xl" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
FaPalette,
|
FaPalette,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaRedo,
|
FaRedo,
|
||||||
|
FaTrash,
|
||||||
FaUndo,
|
FaUndo,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { BiHide, BiShow } from 'react-icons/bi';
|
import { BiHide, BiShow } from 'react-icons/bi';
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
setShouldInvertMask,
|
setShouldInvertMask,
|
||||||
setNeedsRepaint,
|
setNeedsRepaint,
|
||||||
toggleShouldLockBoundingBox,
|
toggleShouldLockBoundingBox,
|
||||||
|
clearImageToInpaint,
|
||||||
} from './inpaintingSlice';
|
} from './inpaintingSlice';
|
||||||
|
|
||||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||||
@ -278,125 +280,131 @@ const InpaintingControls = () => {
|
|||||||
dispatch(setNeedsRepaint(true));
|
dispatch(setNeedsRepaint(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearImage = () => {
|
||||||
|
dispatch(clearImageToInpaint());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inpainting-settings">
|
<div className="inpainting-settings">
|
||||||
<div className="inpainting-buttons">
|
<div className="inpainting-buttons-group">
|
||||||
<div className="inpainting-buttons-group">
|
<IAIPopover
|
||||||
<IAIPopover
|
trigger="hover"
|
||||||
trigger="hover"
|
onOpen={handleShowBrushPreview}
|
||||||
onOpen={handleShowBrushPreview}
|
onClose={handleHideBrushPreview}
|
||||||
onClose={handleHideBrushPreview}
|
triggerComponent={
|
||||||
triggerComponent={
|
<IAIIconButton
|
||||||
<IAIIconButton
|
aria-label="Brush (B)"
|
||||||
aria-label="Brush (B)"
|
tooltip="Brush (B)"
|
||||||
tooltip="Brush (B)"
|
icon={<FaPaintBrush />}
|
||||||
icon={<FaPaintBrush />}
|
onClick={handleSelectBrushTool}
|
||||||
onClick={handleSelectBrushTool}
|
data-selected={tool === 'brush'}
|
||||||
data-selected={tool === 'brush'}
|
isDisabled={!shouldShowMask}
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="inpainting-slider-numberinput">
|
|
||||||
<IAISlider
|
|
||||||
label="Brush Size"
|
|
||||||
value={brushSize}
|
|
||||||
onChange={handleChangeBrushSize}
|
|
||||||
min={1}
|
|
||||||
max={200}
|
|
||||||
width="100px"
|
|
||||||
focusThumbOnChange={false}
|
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
|
||||||
<IAINumberInput
|
|
||||||
value={brushSize}
|
|
||||||
onChange={handleChangeBrushSize}
|
|
||||||
width={'80px'}
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IAIPopover>
|
|
||||||
<IAIIconButton
|
|
||||||
aria-label="Eraser (E)"
|
|
||||||
tooltip="Eraser (E)"
|
|
||||||
icon={<FaEraser />}
|
|
||||||
onClick={handleSelectEraserTool}
|
|
||||||
data-selected={tool === 'eraser'}
|
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="inpainting-buttons-group">
|
|
||||||
<IAIPopover
|
|
||||||
trigger="hover"
|
|
||||||
triggerComponent={
|
|
||||||
<IAIIconButton
|
|
||||||
aria-label="Mask Color"
|
|
||||||
tooltip="Mask Color"
|
|
||||||
icon={<FaPalette />}
|
|
||||||
isDisabled={!shouldShowMask}
|
|
||||||
cursor={'pointer'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IAIColorPicker
|
|
||||||
color={maskColor}
|
|
||||||
onChange={handleChangeMaskColor}
|
|
||||||
/>
|
/>
|
||||||
</IAIPopover>
|
}
|
||||||
<IAIIconButton
|
>
|
||||||
aria-label="Hide/Show Mask (H)"
|
<div className="inpainting-slider-numberinput">
|
||||||
tooltip="Hide/Show Mask (H)"
|
<IAISlider
|
||||||
data-selected={!shouldShowMask}
|
label="Brush Size"
|
||||||
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
|
value={brushSize}
|
||||||
onClick={handleToggleShouldShowMask}
|
onChange={handleChangeBrushSize}
|
||||||
/>
|
min={1}
|
||||||
<IAIIconButton
|
max={200}
|
||||||
tooltip="Invert Mask Display (Shift+M)"
|
width="100px"
|
||||||
aria-label="Invert Mask Display (Shift+M)"
|
focusThumbOnChange={false}
|
||||||
data-selected={shouldInvertMask}
|
isDisabled={!shouldShowMask}
|
||||||
icon={
|
/>
|
||||||
shouldInvertMask ? (
|
<IAINumberInput
|
||||||
<MdInvertColors size={22} />
|
value={brushSize}
|
||||||
) : (
|
onChange={handleChangeBrushSize}
|
||||||
<MdInvertColorsOff size={22} />
|
width={'80px'}
|
||||||
)
|
min={1}
|
||||||
}
|
max={999}
|
||||||
onClick={handleToggleShouldInvertMask}
|
isDisabled={!shouldShowMask}
|
||||||
isDisabled={!shouldShowMask}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</IAIPopover>
|
||||||
<div className="inpainting-buttons-group">
|
<IAIIconButton
|
||||||
<IAIIconButton
|
aria-label="Eraser (E)"
|
||||||
aria-label="Undo"
|
tooltip="Eraser (E)"
|
||||||
tooltip="Undo"
|
icon={<FaEraser />}
|
||||||
icon={<FaUndo />}
|
onClick={handleSelectEraserTool}
|
||||||
onClick={handleUndo}
|
data-selected={tool === 'eraser'}
|
||||||
isDisabled={!canUndo || !shouldShowMask}
|
isDisabled={!shouldShowMask}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
</div>
|
||||||
aria-label="Redo"
|
<div className="inpainting-buttons-group">
|
||||||
tooltip="Redo"
|
<IAIPopover
|
||||||
icon={<FaRedo />}
|
trigger="hover"
|
||||||
onClick={handleRedo}
|
triggerComponent={
|
||||||
isDisabled={!canRedo || !shouldShowMask}
|
<IAIIconButton
|
||||||
/>
|
aria-label="Mask Color"
|
||||||
<IAIIconButton
|
tooltip="Mask Color"
|
||||||
aria-label="Clear Mask (Shift + C)"
|
icon={<FaPalette />}
|
||||||
tooltip="Clear Mask (Shift + C)"
|
isDisabled={!shouldShowMask}
|
||||||
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
cursor={'pointer'}
|
||||||
onClick={handleClearMask}
|
/>
|
||||||
isDisabled={isMaskEmpty || !shouldShowMask}
|
}
|
||||||
/>
|
>
|
||||||
<IAIIconButton
|
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
|
||||||
aria-label="Split Layout (Shift+J)"
|
</IAIPopover>
|
||||||
tooltip="Split Layout (Shift+J)"
|
<IAIIconButton
|
||||||
icon={<VscSplitHorizontal />}
|
aria-label="Hide/Show Mask (H)"
|
||||||
data-selected={showDualDisplay}
|
tooltip="Hide/Show Mask (H)"
|
||||||
onClick={handleDualDisplay}
|
data-selected={!shouldShowMask}
|
||||||
/>
|
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||||
</div>
|
onClick={handleToggleShouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Invert Mask Display (Shift+M)"
|
||||||
|
aria-label="Invert Mask Display (Shift+M)"
|
||||||
|
data-selected={shouldInvertMask}
|
||||||
|
icon={
|
||||||
|
shouldInvertMask ? (
|
||||||
|
<MdInvertColors size={22} />
|
||||||
|
) : (
|
||||||
|
<MdInvertColorsOff size={22} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={handleToggleShouldInvertMask}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inpainting-buttons-group">
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Undo"
|
||||||
|
tooltip="Undo"
|
||||||
|
icon={<FaUndo />}
|
||||||
|
onClick={handleUndo}
|
||||||
|
isDisabled={!canUndo || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Redo"
|
||||||
|
tooltip="Redo"
|
||||||
|
icon={<FaRedo />}
|
||||||
|
onClick={handleRedo}
|
||||||
|
isDisabled={!canRedo || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Clear Mask Canvas (Shift + C)"
|
||||||
|
tooltip="Clear Mask Canvas (Shift + C)"
|
||||||
|
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
||||||
|
onClick={handleClearMask}
|
||||||
|
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Clear Image"
|
||||||
|
tooltip="Clear Image"
|
||||||
|
icon={<FaTrash size={18} />}
|
||||||
|
onClick={handleClearImage}
|
||||||
|
// isDisabled={}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Split Layout (Shift+J)"
|
||||||
|
tooltip="Split Layout (Shift+J)"
|
||||||
|
icon={<VscSplitHorizontal />}
|
||||||
|
data-selected={showDualDisplay}
|
||||||
|
onClick={handleDualDisplay}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
|
import { uploadImage } from '../../../app/socketio/actions';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
||||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
import { OptionsState } from '../../options/optionsSlice';
|
import { OptionsState } from '../../options/optionsSlice';
|
||||||
import InpaintingCanvas from './InpaintingCanvas';
|
import InpaintingCanvas from './InpaintingCanvas';
|
||||||
@ -12,11 +14,12 @@ import { InpaintingState, setNeedsRepaint } from './inpaintingSlice';
|
|||||||
const inpaintingDisplaySelector = createSelector(
|
const inpaintingDisplaySelector = createSelector(
|
||||||
[(state: RootState) => state.inpainting, (state: RootState) => state.options],
|
[(state: RootState) => state.inpainting, (state: RootState) => state.options],
|
||||||
(inpainting: InpaintingState, options: OptionsState) => {
|
(inpainting: InpaintingState, options: OptionsState) => {
|
||||||
const { needsRepaint } = inpainting;
|
const { needsRepaint, imageToInpaint } = inpainting;
|
||||||
const { showDualDisplay } = options;
|
const { showDualDisplay } = options;
|
||||||
return {
|
return {
|
||||||
needsRepaint,
|
needsRepaint,
|
||||||
showDualDisplay,
|
showDualDisplay,
|
||||||
|
imageToInpaint,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,7 +31,7 @@ const inpaintingDisplaySelector = createSelector(
|
|||||||
|
|
||||||
const InpaintingDisplay = () => {
|
const InpaintingDisplay = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { showDualDisplay, needsRepaint } = useAppSelector(
|
const { showDualDisplay, needsRepaint, imageToInpaint } = useAppSelector(
|
||||||
inpaintingDisplaySelector
|
inpaintingDisplaySelector
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -41,27 +44,33 @@ const InpaintingDisplay = () => {
|
|||||||
return () => window.removeEventListener('resize', resizeCallback);
|
return () => window.removeEventListener('resize', resizeCallback);
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const inpaintingComponent = imageToInpaint ? (
|
||||||
|
<div className="inpainting-main-area">
|
||||||
|
<InpaintingControls />
|
||||||
|
<div className="inpainting-canvas-area">
|
||||||
|
{needsRepaint ? <InpaintingCanvasPlaceholder /> : <InpaintingCanvas />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<InvokeImageUploader
|
||||||
|
handleFile={(file: File) =>
|
||||||
|
dispatch(uploadImage({ file, destination: 'inpainting' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="inpainting-display"
|
className={
|
||||||
style={
|
showDualDisplay ? 'workarea-split-view' : 'workarea-single-view'
|
||||||
showDualDisplay
|
|
||||||
? { gridTemplateColumns: '1fr 1fr' }
|
|
||||||
: { gridTemplateColumns: 'auto' }
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="inpainting-toolkit">
|
<div className="workarea-split-view-left">{inpaintingComponent} </div>
|
||||||
<InpaintingControls />
|
{showDualDisplay && (
|
||||||
|
<div className="workarea-split-view-right">
|
||||||
<div className="inpainting-canvas-container">
|
<CurrentImageDisplay />
|
||||||
{needsRepaint ? (
|
|
||||||
<InpaintingCanvasPlaceholder />
|
|
||||||
) : (
|
|
||||||
<InpaintingCanvas />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{showDualDisplay && <CurrentImageDisplay />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,10 +4,7 @@ import InvokeWorkarea from '../InvokeWorkarea';
|
|||||||
|
|
||||||
export default function InpaintingWorkarea() {
|
export default function InpaintingWorkarea() {
|
||||||
return (
|
return (
|
||||||
<InvokeWorkarea
|
<InvokeWorkarea optionsPanel={<InpaintingPanel />}>
|
||||||
optionsPanel={<InpaintingPanel />}
|
|
||||||
className="inpainting-workarea-container"
|
|
||||||
>
|
|
||||||
<InpaintingDisplay />
|
<InpaintingDisplay />
|
||||||
</InvokeWorkarea>
|
</InvokeWorkarea>
|
||||||
);
|
);
|
||||||
|
@ -156,10 +156,12 @@ export const inpaintingSlice = createSlice({
|
|||||||
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
|
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
state.maskColor = action.payload;
|
state.maskColor = action.payload;
|
||||||
},
|
},
|
||||||
// },
|
|
||||||
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
||||||
state.cursorPosition = action.payload;
|
state.cursorPosition = action.payload;
|
||||||
},
|
},
|
||||||
|
clearImageToInpaint: (state) => {
|
||||||
|
state.imageToInpaint = undefined;
|
||||||
|
},
|
||||||
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
const { width: imageWidth, height: imageHeight } = action.payload;
|
const { width: imageWidth, height: imageHeight } = action.payload;
|
||||||
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
||||||
@ -335,6 +337,7 @@ export const {
|
|||||||
setShouldShowBrushPreview,
|
setShouldShowBrushPreview,
|
||||||
setMaskColor,
|
setMaskColor,
|
||||||
clearMask,
|
clearMask,
|
||||||
|
clearImageToInpaint,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
setCursorPosition,
|
setCursorPosition,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.workarea-container {
|
.workarea-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.workarea {
|
.workarea-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
|
|
||||||
@ -15,8 +15,40 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workarea-content {
|
.workarea-split-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
height: $app-content-height;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workarea-single-view {
|
||||||
|
width: 100%;
|
||||||
|
height: $app-content-height;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workarea-split-view-left,
|
||||||
|
.workarea-split-view-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
row-gap: 1rem;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workarea-split-view-left {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workarea-split-view-right {
|
||||||
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,29 +5,23 @@ import ShowHideGalleryButton from '../gallery/ShowHideGalleryButton';
|
|||||||
|
|
||||||
type InvokeWorkareaProps = {
|
type InvokeWorkareaProps = {
|
||||||
optionsPanel: ReactNode;
|
optionsPanel: ReactNode;
|
||||||
className?: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||||
const { optionsPanel, className, children } = props;
|
const { optionsPanel, children } = props;
|
||||||
|
|
||||||
const { shouldShowGallery, shouldHoldGalleryOpen } = useAppSelector(
|
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
|
||||||
(state: RootState) => state.gallery
|
useAppSelector((state: RootState) => state.gallery);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="workarea-wrapper">
|
||||||
className={
|
<div className="workarea-main">
|
||||||
className ? `workarea-container ${className}` : `workarea-container`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="workarea">
|
|
||||||
<div className="workarea-options-panel">{optionsPanel}</div>
|
<div className="workarea-options-panel">{optionsPanel}</div>
|
||||||
<div className="workarea-content">{children}</div>
|
{children}
|
||||||
<ImageGallery />
|
<ImageGallery />
|
||||||
</div>
|
</div>
|
||||||
{!(shouldShowGallery || shouldHoldGalleryOpen) && (
|
{!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) && (
|
||||||
<ShowHideGalleryButton />
|
<ShowHideGalleryButton />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
@use '../../../styles/Mixins/' as *;
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.text-to-image-workarea {
|
.text-to-image-area {
|
||||||
display: grid;
|
padding: 1rem;
|
||||||
grid-template-columns: max-content auto;
|
|
||||||
column-gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-to-image-panel {
|
.text-to-image-panel {
|
||||||
@ -14,27 +12,3 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@include HideScrollbar;
|
@include HideScrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-to-image-display {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'text-to-image-display';
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
|
|
||||||
.current-image-display,
|
|
||||||
.current-image-display-placeholder {
|
|
||||||
grid-area: text-to-image-display;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery-area {
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
place-self: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides
|
|
||||||
.txt-to-image-workarea-container {
|
|
||||||
.current-image-preview {
|
|
||||||
padding: 0 1rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
|
|
||||||
|
const TextToImageDisplay = () => {
|
||||||
|
return (
|
||||||
|
<div className="workarea-single-view">
|
||||||
|
<div className="text-to-image-area">
|
||||||
|
<CurrentImageDisplay />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextToImageDisplay;
|
@ -1,14 +1,11 @@
|
|||||||
import TextToImagePanel from './TextToImagePanel';
|
import TextToImagePanel from './TextToImagePanel';
|
||||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
|
||||||
import InvokeWorkarea from '../InvokeWorkarea';
|
import InvokeWorkarea from '../InvokeWorkarea';
|
||||||
|
import TextToImageDisplay from './TextToImageDisplay';
|
||||||
|
|
||||||
export default function TextToImageWorkarea() {
|
export default function TextToImageWorkarea() {
|
||||||
return (
|
return (
|
||||||
<InvokeWorkarea
|
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
|
||||||
optionsPanel={<TextToImagePanel />}
|
<TextToImageDisplay />
|
||||||
className="txt-to-image-workarea-container"
|
|
||||||
>
|
|
||||||
<CurrentImageDisplay />
|
|
||||||
</InvokeWorkarea>
|
</InvokeWorkarea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,10 @@ $app-content-height-cutoff: 7rem; // default: 7rem
|
|||||||
$app-width: calc(100vw - $app-cutoff);
|
$app-width: calc(100vw - $app-cutoff);
|
||||||
$app-height: calc(100vh - $app-cutoff);
|
$app-height: calc(100vh - $app-cutoff);
|
||||||
$app-content-height: calc(100vh - $app-content-height-cutoff);
|
$app-content-height: calc(100vh - $app-content-height-cutoff);
|
||||||
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 6rem));
|
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 5.5rem));
|
||||||
$app-gallery-popover-height: calc(100vh - ($app-content-height-cutoff + 6rem));
|
$app-gallery-popover-height: calc(100vh - ($app-content-height-cutoff + 6rem));
|
||||||
$app-metadata-height: calc(100vh - ($app-content-height-cutoff + 4.4rem));
|
$app-metadata-height: calc(100vh - ($app-content-height-cutoff + 4.4rem));
|
||||||
|
$app-text-to-image-height: calc(100vh - 9.4375rem - 1.925rem - 1.15rem); // do not touch ffs
|
||||||
|
|
||||||
// option bar
|
// option bar
|
||||||
$options-bar-max-width: 22.5rem;
|
$options-bar-max-width: 22.5rem;
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
@use '../common/components/IAICheckbox.scss';
|
@use '../common/components/IAICheckbox.scss';
|
||||||
@use '../common/components/IAIPopover.scss';
|
@use '../common/components/IAIPopover.scss';
|
||||||
@use '../common/components/IAIColorPicker.scss';
|
@use '../common/components/IAIColorPicker.scss';
|
||||||
|
@use '../common/components/InvokeImageUploader.scss';
|
||||||
@use '../common/components/WorkInProgress/WorkInProgress.scss';
|
@use '../common/components/WorkInProgress/WorkInProgress.scss';
|
||||||
@use '../common/components/GuidePopover.scss';
|
@use '../common/components/GuidePopover.scss';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user