mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2025-06-19 17:39:16 +02:00
Merge ab547d10bfc25e7ce9c6ff37fde5a89af4bd326e into 5c7625c425f12330eec34870511c31667d9fe349
This commit is contained in:
commit
e85eb98d1f
@ -1120,3 +1120,29 @@ ExifReader is licensed under the Mozilla Public License:
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
Masonry
|
||||
=======
|
||||
https://masonry.desandro.com/
|
||||
|
||||
Masonry is licensed under the MIT license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2023 David DeSandro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
@ -15,9 +15,9 @@ Does not require technical knowledge, does not require pre-installed software. 1
|
||||
Click the download button for your operating system:
|
||||
|
||||
<p float="left">
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Linux.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-linux.png" width="200" /></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Mac.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-mac.png" width="200" /></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Windows.exe"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-win.png" width="200" /></a>
|
||||
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Windows.exe"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-win.png" width="200" /></a>
|
||||
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Linux.zip"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-linux.png" width="200" /></a>
|
||||
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Mac.zip"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-mac.png" width="200" /></a>
|
||||
</p>
|
||||
|
||||
**Hardware requirements:**
|
||||
|
@ -1,12 +1,14 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Response, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from easydiffusion.easydb import crud, models, schemas
|
||||
from easydiffusion.easydb.database import SessionLocal, engine
|
||||
|
||||
from requests.compat import urlparse
|
||||
from os.path import abspath
|
||||
|
||||
import base64, json
|
||||
|
||||
@ -92,7 +94,28 @@ def init():
|
||||
result.data = base64.encodestring(result.data)
|
||||
return result
|
||||
|
||||
@server_api.get("/image/{image_path:path}")
|
||||
def get_image(image_path: str, db: Session = Depends(get_db)):
|
||||
from easydiffusion.easydb.mappings import GalleryImage
|
||||
image_path = str(abspath(image_path))
|
||||
try:
|
||||
image = db.query(GalleryImage).filter(GalleryImage.path == image_path).first()
|
||||
return FileResponse(image.path)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
@server_api.get("/all_images")
|
||||
def get_all_images(prompt: str = "", model: str = "", page: int = 0, images_per_page: int = 50, workspace : str = "default", db: Session = Depends(get_db)):
|
||||
from easydiffusion.easydb.mappings import GalleryImage
|
||||
images = db.query(GalleryImage).filter(GalleryImage.workspace == workspace).order_by(GalleryImage.time_created.desc())
|
||||
if prompt != "":
|
||||
images = images.filter(GalleryImage.prompt.like("%"+prompt+"%"))
|
||||
if model != "":
|
||||
images = images.filter(GalleryImage.use_stable_diffusion_model.like("%"+model+"%"))
|
||||
images = images.offset(page*images_per_page).limit(images_per_page)
|
||||
return images.all()
|
||||
|
||||
|
||||
def get_filename_from_url(url):
|
||||
path = urlparse(url).path
|
||||
name = path[path.rfind('/')+1:]
|
||||
|
82
ui/easydiffusion/easydb/mappings.py
Normal file
82
ui/easydiffusion/easydb/mappings.py
Normal file
@ -0,0 +1,82 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class GalleryImage(Base):
|
||||
__tablename__ = 'images'
|
||||
|
||||
path = Column(String, primary_key=True)
|
||||
seed = Column(Integer)
|
||||
use_stable_diffusion_model = Column(String)
|
||||
clip_skip = Column(Boolean)
|
||||
use_vae_model = Column(String)
|
||||
sampler_name = Column(String)
|
||||
width = Column(Integer)
|
||||
height = Column(Integer)
|
||||
num_inference_steps = Column(Integer)
|
||||
guidance_scale = Column(Float)
|
||||
lora = Column(String)
|
||||
use_hypernetwork_model = Column(String)
|
||||
tiling = Column(String)
|
||||
use_face_correction = Column(String)
|
||||
use_upscale = Column(String)
|
||||
prompt = Column(String)
|
||||
negative_prompt = Column(String)
|
||||
workspace = Column(String, server_default="default")
|
||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||
nsfw = Column(Boolean, server_default=None)
|
||||
|
||||
def __repr__(self):
|
||||
return "<GalleryImage(path='%s', seed='%s', use_stable_diffusion_model='%s', clip_skip='%s', use_vae_model='%s', sampler_name='%s', width='%s', height='%s', num_inference_steps='%s', guidance_scale='%s', lora='%s', use_hypernetwork_model='%s', tiling='%s', use_face_correction='%s', use_upscale='%s', prompt='%s', negative_prompt='%s')>" % (
|
||||
self.path, self.seed, self.use_stable_diffusion_model, self.clip_skip, self.use_vae_model, self.sampler_name, self.width, self.height, self.num_inference_steps, self.guidance_scale, self.lora, self.use_hypernetwork_model, self.tiling, self.use_face_correction, self.use_upscale, self.prompt, self.negative_prompt)
|
||||
|
||||
def htmlForm(self) -> str:
|
||||
return "<div class='panel-box'><p>Path: " + str(self.path) + "</p>" + \
|
||||
"<p>Seed: " + str(self.seed) + "</p>" + \
|
||||
"<p>Stable Diffusion Model: " + str(self.use_stable_diffusion_model) + "</p>" + \
|
||||
"<p>Prompt: " + str(self.prompt) + "</p>" + \
|
||||
"<p>Negative Prompt: " + str(self.negative_prompt) + "</p>" + \
|
||||
"<p>Clip Skip: " + str(self.clip_skip) + "</p>" + \
|
||||
"<p>VAE Model: " + str(self.use_vae_model) + "</p>" + \
|
||||
"<p>Sampler: " + str(self.sampler_name) + "</p>" + \
|
||||
"<p>Size: " + str(self.height) + "x" + str(self.width) + "</p>" + \
|
||||
"<p>Inference Steps: " + str(self.num_inference_steps) + "</p>" + \
|
||||
"<p>Guidance Scale: " + str(self.guidance_scale) + "</p>" + \
|
||||
"<p>LoRA: " + str(self.lora) + "</p>" + \
|
||||
"<p>Hypernetwork: " + str(self.use_hypernetwork_model) + "</p>" + \
|
||||
"<p>Tiling: " + str(self.tiling) + "</p>" + \
|
||||
"<p>Face Correction: " + str(self.use_face_correction) + "</p>" + \
|
||||
"<p>Upscale: " + str(self.use_upscale) + "</p>" + \
|
||||
"<p>Time Created: " + str(self.time_created) + "</p>" + \
|
||||
"<p>NSFW: " + str(self.nsfw) + "</p></div>"
|
||||
|
||||
def settingsJSON(self) -> str:
|
||||
# some are still missing: prompt strength, lora
|
||||
json = {
|
||||
"numOutputsTotal": 1,
|
||||
"seed": self.seed,
|
||||
"reqBody": {
|
||||
"prompt": self.prompt,
|
||||
"negative_prompt": self.negative_prompt,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"seed": self.seed,
|
||||
"num_inference_steps": self.num_inference_steps,
|
||||
"guidance_scale": self.guidance_scale,
|
||||
"use_face_correction": self.use_face_correction,
|
||||
"use_upscale": self.use_upscale,
|
||||
"sampler_name": self.sampler_name,
|
||||
"use_stable_diffusion_model": self.use_stable_diffusion_model,
|
||||
"clip_skip": self.clip_skip,
|
||||
"tiling": self.tiling,
|
||||
"use_vae_model": self.use_vae_model,
|
||||
"use_hypernetwork_model": self.use_hypernetwork_model
|
||||
}}
|
||||
from json import dumps
|
||||
return dumps(json)
|
||||
|
||||
|
||||
from easydiffusion.easydb.database import engine
|
||||
GalleryImage.metadata.create_all(engine)
|
@ -155,6 +155,10 @@ def init():
|
||||
def read_root():
|
||||
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
|
||||
|
||||
@server_api.get("/gallery-image.html")
|
||||
def read_gallery_image():
|
||||
return FileResponse(os.path.join(app.SD_UI_DIR, "gallery-image.html"), headers=NOCACHE_HEADERS)
|
||||
|
||||
@server_api.on_event("shutdown")
|
||||
def shutdown_event(): # Signal render thread to close on shutdown
|
||||
task_manager.current_state_error = SystemExit("Application shutting down.")
|
||||
|
@ -96,6 +96,7 @@ class RenderTaskData(TaskData):
|
||||
clip_skip: bool = False
|
||||
codeformer_upscale_faces: bool = False
|
||||
codeformer_fidelity: float = 0.5
|
||||
use_gallery: str = None
|
||||
|
||||
|
||||
class MergeRequest(BaseModel):
|
||||
|
@ -159,6 +159,49 @@ def save_images_to_disk(
|
||||
output_quality=output_format.output_quality,
|
||||
output_lossless=output_format.output_lossless,
|
||||
)
|
||||
|
||||
for i in range(len(filtered_images)):
|
||||
path_i = f"{os.path.join(save_dir_path, make_filename(i))}.{output_format.output_format.lower()}"
|
||||
|
||||
def createLoraString(metadata_entries, i):
|
||||
if metadata_entries[i]["use_lora_model"] is None:
|
||||
return "None"
|
||||
elif isinstance(metadata_entries[i]["use_lora_model"], list):
|
||||
loraString = ""
|
||||
for j in range(len(metadata_entries[i]["use_lora_model"])):
|
||||
loraString += metadata_entries[i]["use_lora_model"][j] + ":" + str(metadata_entries[i]["lora_alpha"][j]) + " "
|
||||
return loraString.strip()
|
||||
else:
|
||||
return metadata_entries[i]["use_lora_model"] + ":" + str(metadata_entries[i]["lora_alpha"])
|
||||
|
||||
from easydiffusion.easydb.mappings import GalleryImage
|
||||
from easydiffusion.easydb.database import SessionLocal
|
||||
|
||||
if task_data.use_gallery != None:
|
||||
session = SessionLocal()
|
||||
session.add(GalleryImage(
|
||||
path = path_i,
|
||||
seed = metadata_entries[i]["seed"],
|
||||
use_stable_diffusion_model = metadata_entries[i]["use_stable_diffusion_model"],
|
||||
clip_skip = metadata_entries[i]["clip_skip"],
|
||||
use_vae_model = metadata_entries[i]["use_vae_model"],
|
||||
sampler_name = metadata_entries[i]["sampler_name"],
|
||||
width = metadata_entries[i]["width"],
|
||||
height = metadata_entries[i]["height"],
|
||||
num_inference_steps = metadata_entries[i]["num_inference_steps"],
|
||||
guidance_scale = metadata_entries[i]["guidance_scale"],
|
||||
lora = createLoraString(metadata_entries, i),
|
||||
use_hypernetwork_model = metadata_entries[i]["use_hypernetwork_model"] if "use_hypernetwork_model" in metadata_entries[i] else None,
|
||||
tiling = metadata_entries[i]["tiling"],
|
||||
use_face_correction = metadata_entries[i]["use_face_correction"],
|
||||
use_upscale = metadata_entries[i]["use_upscale"],
|
||||
prompt = metadata_entries[i]["prompt"],
|
||||
negative_prompt = metadata_entries[i]["negative_prompt"],
|
||||
workspace = task_data.use_gallery
|
||||
))
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
if save_data.metadata_output_format:
|
||||
for metadata_output_format in save_data.metadata_output_format.split(","):
|
||||
if metadata_output_format.lower() in ["json", "txt", "embed"]:
|
||||
|
14
ui/gallery-image.html
Normal file
14
ui/gallery-image.html
Normal file
@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/media/css/single-gallery.css">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<button id="use_these_settings" class="primaryButton">Use these settings</button>
|
||||
<button id="use_as_input" class="primaryButton">Use as Input</button>
|
||||
</div>
|
||||
|
||||
<img id="focusimg">
|
||||
<div id="focusbox" class="panel-box">
|
||||
</div>
|
||||
</body></html>
|
@ -27,6 +27,7 @@
|
||||
<script src="/media/js/marked.min.js"></script>
|
||||
<script src="/media/js/croppr.js"></script>
|
||||
<script src="/media/js/exif-reader.js"></script>
|
||||
<script src="/media/js/masonry.pkgd.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -58,6 +59,9 @@
|
||||
<span id="tab-about" class="tab">
|
||||
<span><i class="fa fa-comments icon"></i> Help & Community</span>
|
||||
</span>
|
||||
<span id="tab-gallery" class="tab">
|
||||
<span><i class="fa fa-images icon"></i> Gallery</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -670,6 +674,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-content-gallery" class="tab-content">
|
||||
<div id="gallery-search">
|
||||
<button id="gallery-prev" class="primaryButton"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
<input id="gallery-prompt-search" type="search" placeholder="Search for a prompt..."></input>
|
||||
<input id="gallery-model-search" type="search" placeholder="Search for a model..."></input>
|
||||
<label for="gallery-page">Page:</label>
|
||||
<input id="gallery-page" name="Page" value="0" size="1" onkeypress="gallery_keyDown_handler(event)">
|
||||
<input id="gallery-thumbnail-size" name="gallery-thumbnail-size" class="editor-slider" type="range" value="5.5" min="3" max="20" step="0.05">
|
||||
<button class="primaryButton" id="gallery-refresh" onclick="refreshGallery(true)">Load</button>
|
||||
<button id="gallery-next" class="primaryButton"><i class="fa-solid fa-arrow-right"></i></button>
|
||||
</div>
|
||||
<div class="gallery">
|
||||
<div class="gallery-container" id="imagecontainer"></div>
|
||||
</div>
|
||||
<dialog id="gallery-imginfo">
|
||||
<div id="gallery-imginfo-header" class="dialog-header">
|
||||
<div id="gallery-imginfo-header-left" class="dialog-header-left">
|
||||
<h4>Image Settings</h4>
|
||||
<span></span>
|
||||
</div>
|
||||
<div id="gallery-imginfo-header-right">
|
||||
<i id="gallery-imginfo-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="gallery-imginfo-content">
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup" id="splash-screen" data-version="1">
|
||||
|
@ -1058,6 +1058,9 @@ input::file-selector-button {
|
||||
.input-toggle > input:checked + label {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
.input-toggle > input:disabled + label {
|
||||
background: var(--background-color1);
|
||||
}
|
||||
.input-toggle > input:checked + label:before {
|
||||
right: calc(var(--input-border-size) + var(--input-switch-padding));
|
||||
opacity: 1;
|
||||
@ -2053,3 +2056,57 @@ div#enlarge-buttons {
|
||||
padding-top: 6pt;
|
||||
color: var(--small-label-color);
|
||||
}
|
||||
/* Gallery CSS */
|
||||
|
||||
button:disabled {
|
||||
background-color: var(--secondary-button-background);
|
||||
}
|
||||
|
||||
#gallery-search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#gallery-search>* {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#imagecontainer {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gallery-container {
|
||||
}
|
||||
|
||||
.gallery-image img {
|
||||
width: var(--gallery-width);
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
position: relative;
|
||||
width: var(--gallery-width);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.gallery-image-hover {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
opacity: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.gallery-image-hover button {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.gallery-image:hover .gallery-image-hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
#tab-content-gallery>* {
|
||||
margin: 8px;
|
||||
}
|
||||
|
42
ui/media/css/single-gallery.css
Normal file
42
ui/media/css/single-gallery.css
Normal file
@ -0,0 +1,42 @@
|
||||
@import url("/media/css/themes.css");
|
||||
@import url("/media/css/main.css");
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--background-color1);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text-color);
|
||||
cursor: default;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
/* Code for Firefox */
|
||||
color: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
color: gray;
|
||||
}
|
@ -14,6 +14,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"num_outputs_parallel",
|
||||
"stable_diffusion_model",
|
||||
"clip_skip",
|
||||
"use_gallery",
|
||||
"vae_model",
|
||||
"sampler_name",
|
||||
"scheduler_name",
|
||||
@ -58,6 +59,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"tree_toggle",
|
||||
"json_toggle",
|
||||
"extract_lora_from_prompt",
|
||||
"gallery-thumbnail-size",
|
||||
"embedding-card-size-selector",
|
||||
"lora_model",
|
||||
"enable_vae_tiling",
|
||||
|
@ -165,6 +165,15 @@ let embeddingsModeField = document.querySelector("#embeddings-mode")
|
||||
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector")
|
||||
let addEmbeddingsThumb = document.querySelector("#add-embeddings-thumb")
|
||||
let addEmbeddingsThumbInput = document.querySelector("#add-embeddings-thumb-input")
|
||||
let galleryImginfoDialog = document.querySelector("#gallery-imginfo")
|
||||
let galleryThumbnailSize = document.querySelector("#gallery-thumbnail-size")
|
||||
let galleryImginfoDialogContent = document.querySelector("#gallery-imginfo-content")
|
||||
let galleryPageField = document.querySelector("#gallery-page")
|
||||
let galleryPrevBtn = document.querySelector("#gallery-prev")
|
||||
let galleryNextBtn = document.querySelector("#gallery-next")
|
||||
let galleryPromptSearchField = document.querySelector("#gallery-prompt-search")
|
||||
let galleryModelSearchField = document.querySelector("#gallery-model-search")
|
||||
let galleryImageContainer = document.querySelector("#imagecontainer")
|
||||
|
||||
let positiveEmbeddingText = document.querySelector("#positive-embedding-text")
|
||||
let negativeEmbeddingText = document.querySelector("#negative-embedding-text")
|
||||
@ -209,6 +218,7 @@ let undoButton = document.querySelector("#undo")
|
||||
let undoBuffer = []
|
||||
const UNDO_LIMIT = 20
|
||||
const MAX_IMG_UNDO_ENTRIES = 5
|
||||
var GALLERY_NAME="default"
|
||||
|
||||
let IMAGE_STEP_SIZE = 64
|
||||
|
||||
@ -1085,14 +1095,361 @@ function makeImage() {
|
||||
newTaskRequests.forEach(setEmbeddings)
|
||||
newTaskRequests.forEach(createTask)
|
||||
|
||||
updateTitle()
|
||||
updateInitialText()
|
||||
}
|
||||
|
||||
const countBeforeBanner = localStorage.getItem("countBeforeBanner") || 1
|
||||
if (countBeforeBanner <= 0) {
|
||||
// supportBanner.classList.remove("displayNone")
|
||||
async function onIdle() {
|
||||
const serverCapacity = SD.serverCapacity
|
||||
if (pauseClient === true) {
|
||||
await resumeClient()
|
||||
}
|
||||
|
||||
for (const taskEntry of getUncompletedTaskEntries()) {
|
||||
if (SD.activeTasks.size >= serverCapacity) {
|
||||
break
|
||||
}
|
||||
const task = htmlTaskMap.get(taskEntry)
|
||||
if (!task) {
|
||||
const taskStatusLabel = taskEntry.querySelector(".taskStatusLabel")
|
||||
taskStatusLabel.style.display = "none"
|
||||
continue
|
||||
}
|
||||
await onTaskStart(task)
|
||||
}
|
||||
}
|
||||
|
||||
function getTaskUpdater(task, reqBody, outputContainer) {
|
||||
const outputMsg = task["outputMsg"]
|
||||
const progressBar = task["progressBar"]
|
||||
const progressBarInner = progressBar.querySelector("div")
|
||||
|
||||
const batchCount = task.batchCount
|
||||
let lastStatus = undefined
|
||||
return async function(event) {
|
||||
if (this.status !== lastStatus) {
|
||||
lastStatus = this.status
|
||||
switch (this.status) {
|
||||
case SD.TaskStatus.pending:
|
||||
task["taskStatusLabel"].innerText = "Pending"
|
||||
task["taskStatusLabel"].classList.add("waitingTaskLabel")
|
||||
break
|
||||
case SD.TaskStatus.waiting:
|
||||
task["taskStatusLabel"].innerText = "Waiting"
|
||||
task["taskStatusLabel"].classList.add("waitingTaskLabel")
|
||||
task["taskStatusLabel"].classList.remove("activeTaskLabel")
|
||||
break
|
||||
case SD.TaskStatus.processing:
|
||||
case SD.TaskStatus.completed:
|
||||
task["taskStatusLabel"].innerText = "Processing"
|
||||
task["taskStatusLabel"].classList.add("activeTaskLabel")
|
||||
task["taskStatusLabel"].classList.remove("waitingTaskLabel")
|
||||
break
|
||||
case SD.TaskStatus.stopped:
|
||||
break
|
||||
case SD.TaskStatus.failed:
|
||||
if (!SD.isServerAvailable()) {
|
||||
logError(
|
||||
"Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.",
|
||||
event,
|
||||
outputMsg
|
||||
)
|
||||
} else if (typeof event?.response === "object") {
|
||||
let msg = "Stable Diffusion had an error reading the response:<br/><pre>"
|
||||
if (this.exception) {
|
||||
msg += `Error: ${this.exception.message}<br/>`
|
||||
}
|
||||
try {
|
||||
// 'Response': body stream already read
|
||||
msg += "Read: " + (await event.response.text())
|
||||
} catch (e) {
|
||||
msg += "Unexpected end of stream. "
|
||||
}
|
||||
const bufferString = event.reader.bufferedString
|
||||
if (bufferString) {
|
||||
msg += "Buffered data: " + bufferString
|
||||
}
|
||||
msg += "</pre>"
|
||||
logError(msg, event, outputMsg)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if ("update" in event) {
|
||||
const stepUpdate = event.update
|
||||
if (!("step" in stepUpdate)) {
|
||||
return
|
||||
}
|
||||
// task.instances can be a mix of different tasks with uneven number of steps (Render Vs Filter Tasks)
|
||||
const overallStepCount =
|
||||
task.instances.reduce(
|
||||
(sum, instance) =>
|
||||
sum +
|
||||
(instance.isPending
|
||||
? Math.max(0, instance.step || stepUpdate.step) /
|
||||
(instance.total_steps || stepUpdate.total_steps)
|
||||
: 1),
|
||||
0 // Initial value
|
||||
) * stepUpdate.total_steps // Scale to current number of steps.
|
||||
const totalSteps = task.instances.reduce(
|
||||
(sum, instance) => sum + (instance.total_steps || stepUpdate.total_steps),
|
||||
stepUpdate.total_steps * (batchCount - task.batchesDone) // Initial value at (unstarted task count * Nbr of steps)
|
||||
)
|
||||
const percent = Math.min(100, 100 * (overallStepCount / totalSteps)).toFixed(0)
|
||||
|
||||
const timeTaken = stepUpdate.step_time // sec
|
||||
const stepsRemaining = Math.max(0, totalSteps - overallStepCount)
|
||||
const timeRemaining = timeTaken < 0 ? "" : millisecondsToStr(stepsRemaining * timeTaken * 1000)
|
||||
outputMsg.innerHTML = `Batch ${task.batchesDone} of ${batchCount}. Generating image(s): ${percent}%. Time remaining (approx): ${timeRemaining}`
|
||||
outputMsg.style.display = "block"
|
||||
progressBarInner.style.width = `${percent}%`
|
||||
|
||||
updateTitle()
|
||||
if (stepUpdate.output) {
|
||||
showImages(reqBody, stepUpdate, outputContainer, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function abortTask(task) {
|
||||
if (!task.isProcessing) {
|
||||
return false
|
||||
}
|
||||
task.isProcessing = false
|
||||
task.progressBar.classList.remove("active")
|
||||
task["taskStatusLabel"].style.display = "none"
|
||||
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
|
||||
if (!task.instances?.some((r) => r.isPending)) {
|
||||
return
|
||||
}
|
||||
task.instances.forEach((instance) => {
|
||||
try {
|
||||
instance.abort()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
if (task.batchesDone > 0) {
|
||||
document.title = "Easy Diffusion"
|
||||
}
|
||||
}
|
||||
|
||||
function onTaskErrorHandler(task, reqBody, instance, reason) {
|
||||
if (!task.isProcessing) {
|
||||
return
|
||||
}
|
||||
console.log("Render request %o, Instance: %o, Error: %s", reqBody, instance, reason)
|
||||
abortTask(task)
|
||||
const outputMsg = task["outputMsg"]
|
||||
logError(
|
||||
"Stable Diffusion had an error. Please check the logs in the command-line window. <br/><br/>" +
|
||||
reason +
|
||||
"<br/><pre>" +
|
||||
reason.stack +
|
||||
"</pre>",
|
||||
task,
|
||||
outputMsg
|
||||
)
|
||||
setStatus("request", "error", "error")
|
||||
}
|
||||
|
||||
function onTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) {
|
||||
if (typeof stepUpdate === "object") {
|
||||
if (stepUpdate.status === "succeeded") {
|
||||
showImages(reqBody, stepUpdate, outputContainer, false)
|
||||
} else {
|
||||
task.isProcessing = false
|
||||
const outputMsg = task["outputMsg"]
|
||||
let msg = ""
|
||||
if ("detail" in stepUpdate && typeof stepUpdate.detail === "string" && stepUpdate.detail.length > 0) {
|
||||
msg = stepUpdate.detail
|
||||
if (msg.toLowerCase().includes("out of memory")) {
|
||||
msg += `<br/><br/>
|
||||
<b>Suggestions</b>:
|
||||
<br/>
|
||||
1. If you have set an initial image, please try reducing its dimension to ${MAX_INIT_IMAGE_DIMENSION}x${MAX_INIT_IMAGE_DIMENSION} or smaller.<br/>
|
||||
2. Try picking a lower level in the '<em>GPU Memory Usage</em>' setting (in the '<em>Settings</em>' tab).<br/>
|
||||
3. Try generating a smaller image.<br/>`
|
||||
} else if (msg.includes("DefaultCPUAllocator: not enough memory")) {
|
||||
msg += `<br/><br/>
|
||||
Reason: Your computer is running out of system RAM!
|
||||
<br/><br/>
|
||||
<b>Suggestions</b>:
|
||||
<br/>
|
||||
1. Try closing unnecessary programs and browser tabs.<br/>
|
||||
2. If that doesn't help, please increase your computer's virtual memory by following these steps for
|
||||
<a href="https://www.ibm.com/docs/en/opw/8.2.0?topic=tuning-optional-increasing-paging-file-size-windows-computers" target="_blank">Windows</a> or
|
||||
<a href="https://linuxhint.com/increase-swap-space-linux/" target="_blank">Linux</a>.<br/>
|
||||
3. Try restarting your computer.<br/>`
|
||||
}
|
||||
} else {
|
||||
msg = `Unexpected Read Error:<br/><pre>StepUpdate: ${JSON.stringify(stepUpdate, undefined, 4)}</pre>`
|
||||
}
|
||||
logError(msg, stepUpdate, outputMsg)
|
||||
}
|
||||
}
|
||||
if (task.isProcessing && task.batchesDone < task.batchCount) {
|
||||
task["taskStatusLabel"].innerText = "Pending"
|
||||
task["taskStatusLabel"].classList.add("waitingTaskLabel")
|
||||
task["taskStatusLabel"].classList.remove("activeTaskLabel")
|
||||
return
|
||||
}
|
||||
if ("instances" in task && task.instances.some((ins) => ins != instance && ins.isPending)) {
|
||||
return
|
||||
}
|
||||
|
||||
task.isProcessing = false
|
||||
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
|
||||
task["taskStatusLabel"].style.display = "none"
|
||||
|
||||
let time = millisecondsToStr(Date.now() - task.startTime)
|
||||
|
||||
if (task.batchesDone == task.batchCount) {
|
||||
if (!task.outputMsg.innerText.toLowerCase().includes("error")) {
|
||||
task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time}`
|
||||
}
|
||||
task.progressBar.style.height = "0px"
|
||||
task.progressBar.style.border = "0px solid var(--background-color3)"
|
||||
task.progressBar.classList.remove("active")
|
||||
setStatus("request", "done", "success")
|
||||
} else {
|
||||
localStorage.setItem("countBeforeBanner", countBeforeBanner - 1)
|
||||
}
|
||||
|
||||
if (randomSeedField.checked) {
|
||||
seedField.value = task.seed
|
||||
}
|
||||
|
||||
if (SD.activeTasks.size > 0) {
|
||||
return
|
||||
}
|
||||
const uncompletedTasks = getUncompletedTaskEntries()
|
||||
if (uncompletedTasks && uncompletedTasks.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pauseClient) {
|
||||
resumeBtn.click()
|
||||
}
|
||||
renderButtons.style.display = "none"
|
||||
renameMakeImageButton()
|
||||
|
||||
if (isSoundEnabled()) {
|
||||
playSound()
|
||||
}
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
let all_tasks = [...document.querySelectorAll("div .imageTaskContainer").entries()].map(c => htmlTaskMap.get(c[1]))
|
||||
let tasks_to_be_run = all_tasks.filter(task => task.isProcessing)
|
||||
let img_remaining_per_task = tasks_to_be_run.map(task => { return task.numOutputsTotal - Math.max(0, (task.instances || []).reduce((total, value) => {
|
||||
if (value.status === 'completed') {
|
||||
return total + 1
|
||||
} else {
|
||||
return total
|
||||
}
|
||||
}, 0) * task.reqBody.num_outputs) } )
|
||||
let img_remaining = img_remaining_per_task.reduce((total, value) => total + value, 0);
|
||||
if (img_remaining > 0) {
|
||||
document.title = `${img_remaining} - Easy Diffusion`;
|
||||
} else {
|
||||
document.title = "Completed - Easy Diffusion"
|
||||
}
|
||||
}
|
||||
|
||||
async function onTaskStart(task) {
|
||||
if (!task.isProcessing || task.batchesDone >= task.batchCount) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof task.startTime !== "number") {
|
||||
task.startTime = Date.now()
|
||||
}
|
||||
if (!("instances" in task)) {
|
||||
task["instances"] = []
|
||||
}
|
||||
|
||||
task["stopTask"].innerHTML = '<i class="fa-solid fa-circle-stop"></i> Stop'
|
||||
task["taskStatusLabel"].innerText = "Starting"
|
||||
task["taskStatusLabel"].classList.add("waitingTaskLabel")
|
||||
|
||||
let newTaskReqBody = task.reqBody
|
||||
if (task.batchCount > 1) {
|
||||
// Each output render batch needs it's own task reqBody instance to avoid altering the other runs after they are completed.
|
||||
newTaskReqBody = Object.assign({}, task.reqBody)
|
||||
if (task.batchesDone == task.batchCount - 1) {
|
||||
// Last batch of the task
|
||||
// If the number of parallel jobs is no factor of the total number of images, the last batch must create less than "parallel jobs count" images
|
||||
// E.g. with numOutputsTotal = 6 and num_outputs = 5, the last batch shall only generate 1 image.
|
||||
newTaskReqBody.num_outputs = task.numOutputsTotal - task.reqBody.num_outputs * (task.batchCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const startSeed = task.seed || newTaskReqBody.seed
|
||||
const genSeeds = Boolean(
|
||||
typeof newTaskReqBody.seed !== "number" || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1)
|
||||
)
|
||||
if (genSeeds) {
|
||||
newTaskReqBody.seed = parseInt(startSeed) + task.batchesDone * task.reqBody.num_outputs
|
||||
}
|
||||
|
||||
// Update the seed *before* starting the processing so it's retained if user stops the task
|
||||
if (randomSeedField.checked) {
|
||||
seedField.value = task.seed
|
||||
}
|
||||
|
||||
const outputContainer = document.createElement("div")
|
||||
outputContainer.className = "img-batch"
|
||||
task.outputContainer.insertBefore(outputContainer, task.outputContainer.firstChild)
|
||||
|
||||
const eventInfo = { reqBody: newTaskReqBody }
|
||||
const callbacksPromises = PLUGINS["TASK_CREATE"].map((hook) => {
|
||||
if (typeof hook !== "function") {
|
||||
console.error("The provided TASK_CREATE hook is not a function. Hook: %o", hook)
|
||||
return Promise.reject(new Error("hook is not a function."))
|
||||
}
|
||||
try {
|
||||
return Promise.resolve(hook.call(task, eventInfo))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(callbacksPromises)
|
||||
let instance = eventInfo.instance
|
||||
if (!instance) {
|
||||
const factory = PLUGINS.OUTPUTS_FORMATS.get(eventInfo.reqBody?.output_format || newTaskReqBody.output_format)
|
||||
if (factory) {
|
||||
instance = await Promise.resolve(factory(eventInfo.reqBody || newTaskReqBody))
|
||||
}
|
||||
if (!instance) {
|
||||
console.error(
|
||||
`${factory ? "Factory " + String(factory) : "No factory defined"} for output format ${eventInfo.reqBody
|
||||
?.output_format || newTaskReqBody.output_format}. Instance is ${instance ||
|
||||
"undefined"}. Using default renderer.`
|
||||
)
|
||||
instance = new SD.RenderTask(eventInfo.reqBody || newTaskReqBody)
|
||||
}
|
||||
}
|
||||
|
||||
task["instances"].push(instance)
|
||||
task.batchesDone++
|
||||
|
||||
instance.enqueue(getTaskUpdater(task, newTaskReqBody, outputContainer)).then(
|
||||
(renderResult) => {
|
||||
onTaskCompleted(task, newTaskReqBody, instance, outputContainer, renderResult)
|
||||
},
|
||||
(reason) => {
|
||||
onTaskErrorHandler(task, newTaskReqBody, instance, reason)
|
||||
}
|
||||
)
|
||||
|
||||
setStatus("request", "fetching..")
|
||||
renderButtons.style.display = "flex"
|
||||
renameMakeImageButton()
|
||||
updateInitialText()
|
||||
}
|
||||
|
||||
/* Hover effect for the init image in the task list */
|
||||
@ -1352,6 +1709,7 @@ function getCurrentUserRequest() {
|
||||
output_quality: parseInt(outputQualityField.value),
|
||||
output_lossless: outputLosslessField.checked,
|
||||
metadata_output_format: metadataOutputFormatField.value,
|
||||
use_gallery: useGalleryField.checked?GALLERY_NAME:null,
|
||||
original_prompt: promptField.value,
|
||||
active_tags: activeTags.map((x) => x.name),
|
||||
inactive_tags: activeTags.filter((tag) => tag.inactive === true).map((x) => x.name),
|
||||
@ -1851,6 +2209,7 @@ function onDimensionChange() {
|
||||
|
||||
diskPathField.disabled = !saveToDiskField.checked
|
||||
metadataOutputFormatField.disabled = !saveToDiskField.checked
|
||||
useGalleryField.disabled = !saveToDiskField.checked
|
||||
|
||||
gfpganModelField.disabled = !useFaceCorrectionField.checked
|
||||
useFaceCorrectionField.addEventListener("change", function(e) {
|
||||
@ -3115,3 +3474,272 @@ document.addEventListener("on_all_tasks_complete", (e) => {
|
||||
playSound()
|
||||
}
|
||||
})
|
||||
/* Gallery JS */
|
||||
|
||||
const IMAGE_INFO = {
|
||||
"Prompt": "prompt",
|
||||
"Negative Prompt": "negative_prompt",
|
||||
"Seed": "seed",
|
||||
"Time": "time_created",
|
||||
"Model": "use_stable_diffusion_model",
|
||||
"VAE Model": "use_vae_model",
|
||||
"Hypernetwork": "use_hypernetwork_model",
|
||||
"LORA": "lora",
|
||||
"Path": "path",
|
||||
"Width": "width",
|
||||
"Height": "height",
|
||||
"Steps": "num_inference_steps",
|
||||
"Sampler": "sampler_name",
|
||||
"Guidance Scale": "guidance_scale",
|
||||
"Tiling": "tiling",
|
||||
"Upscaler": "use_upscale",
|
||||
"Face Correction": "use_face_correction",
|
||||
"Clip Skip": "clip_skip",
|
||||
}
|
||||
|
||||
const IGNORE_TOKENS = ["None", "none", "Null", "null", "false", "False", null]
|
||||
|
||||
function openImageInNewTab(img, reqData) {
|
||||
let w = window.open("/gallery-image.html")
|
||||
w.addEventListener("DOMContentLoaded", () => {
|
||||
let fimg = w.document.getElementById("focusimg")
|
||||
fimg.src = img.src
|
||||
|
||||
w.document.body.classList.add(themeField.value)
|
||||
w.document.getElementById("use_these_settings").addEventListener("click", () => {
|
||||
restoreTaskToUI({
|
||||
batchCount: 1,
|
||||
numOutputsTotal: 1,
|
||||
reqBody: reqData
|
||||
})
|
||||
})
|
||||
w.document.getElementById("use_as_input").addEventListener("click", () => {
|
||||
onUseURLasInput(img.src)
|
||||
showToast("Loaded image as EasyDiffusion input image", 5000, false, w.document)
|
||||
})
|
||||
let table = w.document.createElement("table")
|
||||
galleryDetailTable(table, reqData)
|
||||
w.document.getElementById("focusbox").replaceChildren(table)
|
||||
document.dispatchEvent(new CustomEvent("newGalleryWindow", { detail: w }))
|
||||
})
|
||||
}
|
||||
|
||||
function galleryDetailTable(table, reqData) {
|
||||
for (const [label, key] of Object.entries(IMAGE_INFO)) {
|
||||
if (IGNORE_TOKENS.findIndex( k => (k == reqData[key])) == -1) {
|
||||
let data = reqData[key]
|
||||
if (key == "path") {
|
||||
data = "…/"+data.split(/[\/\\]/).pop()
|
||||
}
|
||||
table.appendChild(htmlToElement(`<tr><th style="text-align: right;opacity:0.7;">${label}:</th><td>${data}</td></tr>`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function galleryImage(reqData) {
|
||||
|
||||
let div = document.createElement("div")
|
||||
div.classList.add("gallery-image")
|
||||
|
||||
let img = createElement("img", { style: "cursor: zoom-in;", src: "/image/" + reqData.path}, undefined, undefined)
|
||||
img.dataset["request"] = JSON.stringify(reqData)
|
||||
|
||||
img.addEventListener("click", function() {
|
||||
function previousImage(img) {
|
||||
const allImages = Array.from(galleryImageContainer.querySelectorAll(".gallery-image img"))
|
||||
const index = allImages.indexOf(img)
|
||||
return allImages.slice(0, index).reverse()[0]
|
||||
}
|
||||
|
||||
function nextImage(img) {
|
||||
const allImages = Array.from(galleryImageContainer.querySelectorAll(".gallery-image img"))
|
||||
const index = allImages.indexOf(img)
|
||||
return allImages.slice(index + 1)[0]
|
||||
}
|
||||
|
||||
function imageModalParameter(img) {
|
||||
const previousImg = previousImage(img)
|
||||
const nextImg = nextImage(img)
|
||||
|
||||
return {
|
||||
src: img.src,
|
||||
previous: previousImg ? () => imageModalParameter(previousImg) : undefined,
|
||||
next: nextImg ? () => imageModalParameter(nextImg) : undefined,
|
||||
}
|
||||
}
|
||||
imageModal(imageModalParameter(img))
|
||||
})
|
||||
|
||||
let hover = document.createElement("div")
|
||||
hover.classList.add("gallery-image-hover")
|
||||
|
||||
let infoBtn = document.createElement("button")
|
||||
infoBtn.classList.add("tertiaryButton")
|
||||
infoBtn.innerHTML = '<i class="fa-regular fa-file-lines"></i>'
|
||||
infoBtn.addEventListener("click", function() {
|
||||
let table = document.createElement("table")
|
||||
|
||||
galleryDetailTable(table, reqData)
|
||||
|
||||
galleryImginfoDialogContent.replaceChildren(table)
|
||||
galleryImginfoDialog.showModal()
|
||||
})
|
||||
hover.appendChild(infoBtn)
|
||||
|
||||
let openInNewTabBtn = createElement("button", {style: "margin-left: 0.2em;"}, ["tertiaryButton"],
|
||||
htmlToElement('<i class="fa-solid fa-arrow-up-right-from-square"></i>'))
|
||||
openInNewTabBtn.addEventListener("click", (e) => {
|
||||
openImageInNewTab(img, reqData)
|
||||
})
|
||||
hover.appendChild(openInNewTabBtn)
|
||||
|
||||
hover.appendChild(document.createElement("br"))
|
||||
|
||||
let useAsInputBtn = createElement("button", {}, ["tertiaryButton"], "Use as Input")
|
||||
useAsInputBtn.addEventListener("click", (e) => {
|
||||
onUseURLasInput(img.src)
|
||||
showToast("Loaded image as input image")
|
||||
})
|
||||
|
||||
let useSettingsBtn = createElement("button", {}, ["tertiaryButton"], "Use Settings")
|
||||
useSettingsBtn.addEventListener("click", (e) => {
|
||||
restoreTaskToUI({
|
||||
batchCount: 1,
|
||||
numOutputsTotal: 1,
|
||||
reqBody: reqData
|
||||
})
|
||||
showToast("Loaded settings")
|
||||
})
|
||||
|
||||
hover.appendChild(useAsInputBtn)
|
||||
hover.appendChild(document.createElement("br"))
|
||||
hover.appendChild(useSettingsBtn)
|
||||
|
||||
div.replaceChildren(img, hover)
|
||||
return div
|
||||
}
|
||||
|
||||
function onUseURLasInput(url) {
|
||||
toDataURL(url, blob => {
|
||||
onUseAsInputClick(null, {src:blob})
|
||||
})
|
||||
}
|
||||
|
||||
modalDialogCloseOnBackdropClick(galleryImginfoDialog)
|
||||
makeDialogDraggable(galleryImginfoDialog)
|
||||
|
||||
galleryImginfoDialog.querySelector("#gallery-imginfo-close-button").addEventListener("click", () => {
|
||||
galleryImginfoDialog.close()
|
||||
})
|
||||
|
||||
|
||||
galleryThumbnailSize.addEventListener("input", layoutGallery)
|
||||
window.addEventListener("resize", layoutGallery)
|
||||
|
||||
|
||||
function layoutGallery() {
|
||||
let thumbSize = parseFloat(galleryThumbnailSize.value)
|
||||
thumbSize = (10*thumbSize*thumbSize)>>>0
|
||||
let root = document.querySelector(':root')
|
||||
root.style.setProperty('--gallery-width', thumbSize + "px")
|
||||
let msnry = new Masonry(galleryImageContainer, {
|
||||
gutter: 10,
|
||||
itemSelector: '.gallery-image',
|
||||
columnWidth: thumbSize,
|
||||
fitWidth: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
galleryModelSearchField.addEventListener("keyup", debounce(e => refreshGallery(true), 500))
|
||||
galleryPromptSearchField.addEventListener("keyup", debounce(e => refreshGallery(true), 500))
|
||||
galleryPageField.addEventListener("keyup", e => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault()
|
||||
refreshGallery(false)
|
||||
}
|
||||
})
|
||||
|
||||
function refreshGallery(newsearch = false) {
|
||||
if (newsearch) {
|
||||
galleryPageField.value = 0
|
||||
}
|
||||
galleryImageContainer.innerHTML = ""
|
||||
let params = new URLSearchParams({
|
||||
workspace: GALLERY_NAME,
|
||||
prompt: galleryPromptSearchField.value,
|
||||
model: galleryModelSearchField.value,
|
||||
page: galleryPageField.value
|
||||
})
|
||||
|
||||
fetch('/all_images?' + params)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
if (galleryPageField.value > 0 && json.length == 0) {
|
||||
decrementGalleryPage()
|
||||
alert("No more images")
|
||||
return
|
||||
}
|
||||
json.forEach(item => {
|
||||
galleryImageContainer.appendChild(galleryImage(item))
|
||||
})
|
||||
// Wait for all images to be loaded
|
||||
Promise.all(Array.from(galleryImageContainer.querySelectorAll("img")).map(img => {
|
||||
if (img.complete)
|
||||
{
|
||||
return Promise.resolve(img.naturalHeight !== 0)
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
img.addEventListener('load', () => resolve(true))
|
||||
img.addEventListener('error', () => resolve(false))
|
||||
})
|
||||
})).then(results => {
|
||||
// then layout the images
|
||||
layoutGallery()
|
||||
})
|
||||
})
|
||||
|
||||
params.set("images_per_page", 1)
|
||||
// 50 has to be replaced if custom images per page is implemented
|
||||
params.set("page", (parseInt(galleryPageField.value) + 1) * 50)
|
||||
|
||||
fetch("/all_images?" + params)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
if (json.length == 0) {
|
||||
galleryNextBtn.disabled = true
|
||||
} else {
|
||||
galleryNextBtn.disabled = false
|
||||
}
|
||||
})
|
||||
if (galleryPageField.value == 0) {
|
||||
galleryPrevBtn.disabled = true
|
||||
} else {
|
||||
galleryPrevBtn.disabled = false
|
||||
}
|
||||
document.getElementById("gallery-refresh").innerText = "Refresh"
|
||||
}
|
||||
|
||||
|
||||
galleryPrevBtn.addEventListener("click", decrementGalleryPage)
|
||||
galleryNextBtn.addEventListener("click", incrementGalleryPage)
|
||||
|
||||
|
||||
document.addEventListener("tabClick", (e) => {
|
||||
if (e.detail.name == 'gallery') {
|
||||
refreshGallery()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function decrementGalleryPage() {
|
||||
let page = Math.max(galleryPageField.value - 1, 0)
|
||||
galleryPageField.value = page
|
||||
refreshGallery(false)
|
||||
}
|
||||
|
||||
|
||||
function incrementGalleryPage() {
|
||||
galleryPageField.value++
|
||||
refreshGallery(false)
|
||||
}
|
||||
|
2504
ui/media/js/masonry.pkgd.js
Normal file
2504
ui/media/js/masonry.pkgd.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -108,6 +108,13 @@ var PARAMETERS = [
|
||||
return `<input id="${parameter.id}" name="${parameter.id}" size="30">`
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "use_gallery",
|
||||
type: ParameterType.checkbox,
|
||||
label: "Save images to the gallery",
|
||||
note: "Stores metadata of all images into a database so that they show up on the gallery tab.",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: "block_nsfw",
|
||||
type: ParameterType.checkbox,
|
||||
@ -441,6 +448,7 @@ let testDiffusers = document.querySelector("#use_v3_engine")
|
||||
let backendEngine = document.querySelector("#backend")
|
||||
let profileNameField = document.querySelector("#profileName")
|
||||
let modelsDirField = document.querySelector("#models_dir")
|
||||
let useGalleryField = document.querySelector("#use_gallery")
|
||||
|
||||
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
|
||||
|
||||
@ -593,6 +601,7 @@ function applySettingsFromConfig(config) {
|
||||
saveToDiskField.addEventListener("change", function(e) {
|
||||
diskPathField.disabled = !this.checked
|
||||
metadataOutputFormatField.disabled = !this.checked
|
||||
useGalleryField.disabled = !this.checked
|
||||
})
|
||||
|
||||
function getCurrentRenderDeviceSelection() {
|
||||
|
@ -872,19 +872,19 @@ function createTab(request) {
|
||||
|
||||
|
||||
/* TOAST NOTIFICATIONS */
|
||||
function showToast(message, duration = 5000, error = false) {
|
||||
const toast = document.createElement("div")
|
||||
function showToast(message, duration = 5000, error = false, doc=document) {
|
||||
const toast = doc.createElement("div")
|
||||
toast.classList.add("toast-notification")
|
||||
if (error === true) {
|
||||
toast.classList.add("toast-notification-error")
|
||||
}
|
||||
toast.innerHTML = message
|
||||
document.body.appendChild(toast)
|
||||
doc.body.appendChild(toast)
|
||||
|
||||
// Set the position of the toast on the screen
|
||||
const toastCount = document.querySelectorAll(".toast-notification").length
|
||||
const toastCount = doc.querySelectorAll(".toast-notification").length
|
||||
const toastHeight = toast.offsetHeight
|
||||
const previousToastsHeight = Array.from(document.querySelectorAll(".toast-notification"))
|
||||
const previousToastsHeight = Array.from(doc.querySelectorAll(".toast-notification"))
|
||||
.slice(0, -1) // exclude current toast
|
||||
.reduce((totalHeight, toast) => totalHeight + toast.offsetHeight + 10, 0) // add 10 pixels for spacing
|
||||
toast.style.bottom = `${10 + previousToastsHeight}px`
|
||||
@ -896,7 +896,7 @@ function showToast(message, duration = 5000, error = false) {
|
||||
const removeTimeoutId = setTimeout(() => {
|
||||
toast.remove()
|
||||
// Adjust the position of remaining toasts
|
||||
const remainingToasts = document.querySelectorAll(".toast-notification")
|
||||
const remainingToasts = doc.querySelectorAll(".toast-notification")
|
||||
const removedToastBottom = toast.getBoundingClientRect().bottom
|
||||
|
||||
remainingToasts.forEach((toast) => {
|
||||
@ -908,13 +908,13 @@ function showToast(message, duration = 5000, error = false) {
|
||||
// Wait for the slide-down animation to complete
|
||||
setTimeout(() => {
|
||||
// Remove the slide-down class after the animation has completed
|
||||
const slidingToasts = document.querySelectorAll(".slide-down")
|
||||
const slidingToasts = doc.querySelectorAll(".slide-down")
|
||||
slidingToasts.forEach((toast) => {
|
||||
toast.classList.remove("slide-down")
|
||||
})
|
||||
|
||||
// Adjust the position of remaining toasts again, in case there are multiple toasts being removed at once
|
||||
const remainingToastsDown = document.querySelectorAll(".toast-notification")
|
||||
const remainingToastsDown = doc.querySelectorAll(".toast-notification")
|
||||
let heightSoFar = 0
|
||||
remainingToastsDown.forEach((toast) => {
|
||||
toast.style.bottom = `${10 + heightSoFar}px`
|
||||
@ -1232,3 +1232,18 @@ function playSound() {
|
||||
})
|
||||
}
|
||||
}
|
||||
function toDataURL(url, callback){
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('get', url)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = function(){
|
||||
var fr = new FileReader()
|
||||
|
||||
fr.onload = function(){
|
||||
callback(this.result)
|
||||
}
|
||||
|
||||
fr.readAsDataURL(xhr.response)
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user