Merge Improvements from JeLuF:gallery

This commit is contained in:
ManInDark 2023-08-14 18:01:05 +02:00 committed by GitHub
commit fb031a4c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2794 additions and 52 deletions

View File

@ -740,3 +740,30 @@ croppr.js is licensed under the MIT license:
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.
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

@ -110,19 +110,6 @@ def init():
images = images.offset(page*images_per_page).limit(images_per_page)
return images.all()
@server_api.get("/single_image")
def get_single_image(image_path: str, db: Session = Depends(get_db)):
from easydiffusion.easydb.mappings import GalleryImage
image_path = str(abspath(image_path))
try:
image: GalleryImage = db.query(GalleryImage).filter(GalleryImage.path == image_path).first()
head = "<head><link rel='stylesheet' href='/media/css/single-gallery.css'></head>"
body = f"<body><div><button id='use_these_settings' class='primaryButton' json='{image.settingsJSON()}'>Use these settings</button><button id='use_as_input' class='primaryButton' disabled>Use as Input</button></div><img src='/image/" + image.path + "'>" + image.htmlForm() + "</body>"
return Response(content="<html>" + head + body + "</head>", media_type="text/html")
except Exception as e:
print(e)
raise HTTPException(status_code=404, detail="Image not found")
def get_filename_from_url(url):
path = urlparse(url).path
name = path[path.rfind('/')+1:]

View File

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

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

@ -26,6 +26,7 @@
<script src="/media/js/FileSaver.min.js"></script>
<script src="/media/js/marked.min.js"></script>
<script src="/media/js/croppr.js"></script>
<script src="/media/js/masonry.pkgd.js"></script>
</head>
<body>
<div id="container">
@ -523,12 +524,26 @@
<input id="gallery-model-search" type="text" onkeydown="gallery_keyDown_handler(event)" 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 class="primaryButton" onclick="incrementGalleryPage()"><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>

View File

@ -1889,6 +1889,8 @@ div#enlarge-buttons {
}
/* Gallery CSS */
/*
.gallery {
display: flex;
justify-content: center;
@ -1896,6 +1898,7 @@ div#enlarge-buttons {
flex-direction: column;
font-family: sans-serif;
}
*/
#gallery-search {
display: flex;
@ -1909,29 +1912,32 @@ div#enlarge-buttons {
}
.gallery-container {
columns: 5 ;
column-gap: 1.5rem;
width: 95%;
margin: 0 0 ;
}
.gallery-container div {
margin: 0 1.5rem 1.5rem 0;
display: inline-block;
width: 100%;
padding: 5px;
transition: all .75s ease-in-out;
}
.gallery-container div img {
width: 100%;
border-radius: 5px;
transition: all .25s ease-in-out;
box-shadow: 5px 5px 5px rgba(0,0,0,0.4);
.gallery-image img {
width: var(--gallery-width);
}
.gallery-container div img:hover {
box-shadow: 1px 1px 15px rgba(32,0,128,0.8);
.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;
}

View File

@ -56,6 +56,7 @@ const SETTINGS_IDS_LIST = [
"tree_toggle",
"json_toggle",
"extract_lora_from_prompt",
"gallery-thumbnail-size",
"embedding-card-size-selector",
]

View File

@ -142,6 +142,9 @@ let embeddingsSearchBox = document.querySelector("#embeddings-search-box")
let embeddingsList = document.querySelector("#embeddings-list")
let embeddingsModeField = document.querySelector("#embeddings-mode")
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector")
let galleryImginfoDialog = document.querySelector("#gallery-imginfo")
let galleryThumbnailSize = document.querySelector("#gallery-thumbnail-size")
let galleryImginfoDialogContent = document.querySelector("#gallery-imginfo-content")
let positiveEmbeddingText = document.querySelector("#positive-embedding-text")
let negativeEmbeddingText = document.querySelector("#negative-embedding-text")
@ -3085,40 +3088,186 @@ let recentResolutionsValues = []
})()
/* 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 galleryImage(item) {
function galleryDetailTable(table) {
for (const [label, key] of Object.entries(IMAGE_INFO)) {
if (IGNORE_TOKENS.findIndex( k => (k == item[key])) == -1) {
let data = item[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>`))
}
}
}
let div = document.createElement("div")
let img = document.createElement("img")
div.classList.add("gallery-image")
let img = createElement("img", { style: "cursor: zoom-in;", src: "/image/" + item.path}, undefined, undefined)
img.dataset["request"] = JSON.stringify(item)
img.addEventListener("click", (event) => {
let w;
w = window.open("/single_image?image_path=" + item.path, "_blank")
let w = window.open("/gallery-image.html")
w.addEventListener("DOMContentLoaded", () => {
w.document.getElementsByTagName("body")[0].classList.add(themeField.value)
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(JSON.parse(w.document.getElementById("use_these_settings").getAttribute("json")))
restoreTaskToUI({
batchCount: 1,
numOutputsTotal: 1,
reqBody: item
})
})
w.document.getElementById("use_as_input").addEventListener("click", () => {
alert("use as input")
onUseURLasInput(img.src)
showToast("Loaded image as EasyDiffusion input image", 5000, false, w.document)
})
let table = w.document.createElement("table")
galleryDetailTable(table)
w.document.getElementById("focusbox").replaceChildren(table)
document.dispatchEvent(new CustomEvent("newGalleryWindow", { detail: w }))
})
})
img.src = "/image/" + item.path
img.dataset["request"] = JSON.stringify(item)
div.appendChild(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)
galleryImginfoDialogContent.replaceChildren(table)
galleryImginfoDialog.showModal()
})
hover.appendChild(infoBtn)
let imageExpandBtn=createElement("button", { style: "margin-left: 0.2em;"}, ["tertiaryButton"], undefined)
imageExpandBtn.innerHTML = '<i class="fa-solid fa-expand"></i>'
imageExpandBtn.addEventListener("click", function() {
function previousImage(img) {
const allImages = Array.from(document.getElementById("imagecontainer").querySelectorAll(".gallery-image img"))
const index = allImages.indexOf(img)
return allImages.slice(0, index).reverse()[0]
}
function nextImage(img) {
const allImages = Array.from(document.getElementById("imagecontainer").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))
})
hover.appendChild(imageExpandBtn)
let openInNewTabBtn = document.createElement("button")
openInNewTabBtn.classList.add("tertiaryButton")
openInNewTabBtn.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i>'
openInNewTabBtn.style["margin-left"] = "0.2em"
openInNewTabBtn.addEventListener("click", (e) => {
window.open(img.src)
})
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")
})
hover.appendChild(useAsInputBtn)
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 container = document.getElementById("imagecontainer")
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( container, {
gutter: 10,
itemSelector: '.gallery-image',
columnWidth: thumbSize,
fitWidth: true,
})
}
function refreshGallery(newsearch = false) {
if (newsearch) {
document.getElementById("gallery-page").value = 0
}
let container = document.getElementById("imagecontainer")
container.innerHTML = ""
let params = new URLSearchParams({
prompt: document.getElementById("gallery-prompt-search").value,
model: document.getElementById("gallery-model-search").value,
page: document.getElementById("gallery-page").value
})
container.innerHTML = ""
fetch('/all_images?' + params)
.then(response => response.json())
.then(json => {
@ -3130,10 +3279,31 @@ function refreshGallery(newsearch = false) {
json.forEach(item => {
container.appendChild(galleryImage(item))
})
// Wait for all images to be loaded
Promise.all(Array.from(container.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()
})
})
document.getElementById("gallery-refresh").innerText = "Refresh"
}
document.addEventListener("tabClick", (e) => {
if (e.detail.name == 'gallery') {
refreshGallery()
}
})
function decrementGalleryPage() {
let page = Math.max(document.getElementById("gallery-page").value - 1, 0)
document.getElementById("gallery-page").value = page

2504
ui/media/js/masonry.pkgd.js Normal file

File diff suppressed because it is too large Load Diff

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`
@ -1198,4 +1198,18 @@ function makeDialogDraggable(element) {
})() )
}
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()
}