Merge branch 'gallery' into beta

This commit is contained in:
ManInDark 2023-11-23 22:47:30 +01:00
commit 7851ba8fc4
No known key found for this signature in database
GPG Key ID: 72EC12A5B2D62779
16 changed files with 3147 additions and 13 deletions

View File

@ -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.

View File

@ -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:**

View File

@ -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,6 +94,27 @@ 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

View 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)

View File

@ -151,6 +151,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.")

View File

@ -92,6 +92,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):

View File

@ -156,8 +156,51 @@ def save_images_to_disk(
output_quality=output_format.output_quality,
output_lossless=output_format.output_lossless,
)
if save_data.metadata_output_format:
for metadata_output_format in save_data.metadata_output_format.split(","):
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.trim()
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"],
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 task_data.metadata_output_format:
for metadata_output_format in task_data.metadata_output_format.split(","):
if metadata_output_format.lower() in ["json", "txt", "embed"]:
save_dicts(
metadata_entries,

14
ui/gallery-image.html Normal file
View 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>

View File

@ -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">
@ -52,6 +53,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>
@ -561,6 +565,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">

View File

@ -1040,6 +1040,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;
@ -2039,4 +2042,57 @@ div#enlarge-buttons {
border-radius: 4pt;
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;
}

View 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;
}

View File

@ -14,6 +14,7 @@ const SETTINGS_IDS_LIST = [
"num_outputs_parallel",
"stable_diffusion_model",
"clip_skip",
"use_gallery",
"vae_model",
"sampler_name",
"width",
@ -54,6 +55,7 @@ const SETTINGS_IDS_LIST = [
"tree_toggle",
"json_toggle",
"extract_lora_from_prompt",
"gallery-thumbnail-size",
"embedding-card-size-selector",
"lora_model",
]

View File

@ -146,6 +146,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")
@ -190,6 +199,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
@ -1657,6 +1667,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),
@ -2148,6 +2159,7 @@ function onDimensionChange() {
diskPathField.disabled = !saveToDiskField.checked
metadataOutputFormatField.disabled = !saveToDiskField.checked
useGalleryField.disabled = !saveToDiskField.checked
gfpganModelField.disabled = !useFaceCorrectionField.checked
useFaceCorrectionField.addEventListener("change", function(e) {
@ -3269,3 +3281,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 = "&hellip;/"+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

File diff suppressed because it is too large Load Diff

View File

@ -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,
@ -434,6 +441,7 @@ let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_ac
let testDiffusers = document.querySelector("#use_v3_engine")
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")
@ -577,6 +585,7 @@ function applySettingsFromConfig(config) {
saveToDiskField.addEventListener("change", function(e) {
diskPathField.disabled = !this.checked
metadataOutputFormatField.disabled = !this.checked
useGalleryField.disabled = !this.checked
})
function getCurrentRenderDeviceSelection() {

View File

@ -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()
}