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, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. 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) images = images.offset(page*images_per_page).limit(images_per_page)
return images.all() 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): def get_filename_from_url(url):
path = urlparse(url).path path = urlparse(url).path
name = path[path.rfind('/')+1:] name = path[path.rfind('/')+1:]

View File

@ -143,6 +143,10 @@ def init():
def read_root(): def read_root():
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS) 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") @server_api.on_event("shutdown")
def shutdown_event(): # Signal render thread to close on shutdown def shutdown_event(): # Signal render thread to close on shutdown
task_manager.current_state_error = SystemExit("Application shutting down.") 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/FileSaver.min.js"></script>
<script src="/media/js/marked.min.js"></script> <script src="/media/js/marked.min.js"></script>
<script src="/media/js/croppr.js"></script> <script src="/media/js/croppr.js"></script>
<script src="/media/js/masonry.pkgd.js"></script>
</head> </head>
<body> <body>
<div id="container"> <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> <input id="gallery-model-search" type="text" onkeydown="gallery_keyDown_handler(event)" placeholder="Search for a model..."></input>
<label for="gallery-page">Page:</label> <label for="gallery-page">Page:</label>
<input id="gallery-page" name="Page" value="0" size="1" onkeypress="gallery_keyDown_handler(event)"> <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" id="gallery-refresh" onclick="refreshGallery(true)">Load</button>
<button class="primaryButton" onclick="incrementGalleryPage()"><i class="fa-solid fa-arrow-right"></i></button> <button class="primaryButton" onclick="incrementGalleryPage()"><i class="fa-solid fa-arrow-right"></i></button>
</div> </div>
<div class="gallery"> <div class="gallery">
<div class="gallery-container" id="imagecontainer"></div> <div class="gallery-container" id="imagecontainer"></div>
</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> </div>

View File

@ -1889,6 +1889,8 @@ div#enlarge-buttons {
} }
/* Gallery CSS */ /* Gallery CSS */
/*
.gallery { .gallery {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -1896,6 +1898,7 @@ div#enlarge-buttons {
flex-direction: column; flex-direction: column;
font-family: sans-serif; font-family: sans-serif;
} }
*/
#gallery-search { #gallery-search {
display: flex; display: flex;
@ -1909,29 +1912,32 @@ div#enlarge-buttons {
} }
.gallery-container { .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 { .gallery-image img {
width: 100%; width: var(--gallery-width);
border-radius: 5px;
transition: all .25s ease-in-out;
box-shadow: 5px 5px 5px rgba(0,0,0,0.4);
} }
.gallery-container div img:hover { .gallery-image {
box-shadow: 1px 1px 15px rgba(32,0,128,0.8); 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", "tree_toggle",
"json_toggle", "json_toggle",
"extract_lora_from_prompt", "extract_lora_from_prompt",
"gallery-thumbnail-size",
"embedding-card-size-selector", "embedding-card-size-selector",
] ]

View File

@ -142,6 +142,9 @@ let embeddingsSearchBox = document.querySelector("#embeddings-search-box")
let embeddingsList = document.querySelector("#embeddings-list") let embeddingsList = document.querySelector("#embeddings-list")
let embeddingsModeField = document.querySelector("#embeddings-mode") let embeddingsModeField = document.querySelector("#embeddings-mode")
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector") 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 positiveEmbeddingText = document.querySelector("#positive-embedding-text")
let negativeEmbeddingText = document.querySelector("#negative-embedding-text") let negativeEmbeddingText = document.querySelector("#negative-embedding-text")
@ -3085,40 +3088,186 @@ let recentResolutionsValues = []
})() })()
/* Gallery JS */ /* 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 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 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) => { img.addEventListener("click", (event) => {
let w; let w = window.open("/gallery-image.html")
w = window.open("/single_image?image_path=" + item.path, "_blank")
w.addEventListener("DOMContentLoaded", () => { 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", () => { 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", () => { 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 let hover = document.createElement("div")
img.dataset["request"] = JSON.stringify(item) hover.classList.add("gallery-image-hover")
div.appendChild(img)
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 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) { function refreshGallery(newsearch = false) {
if (newsearch) { if (newsearch) {
document.getElementById("gallery-page").value = 0 document.getElementById("gallery-page").value = 0
} }
let container = document.getElementById("imagecontainer") let container = document.getElementById("imagecontainer")
container.innerHTML = ""
let params = new URLSearchParams({ let params = new URLSearchParams({
prompt: document.getElementById("gallery-prompt-search").value, prompt: document.getElementById("gallery-prompt-search").value,
model: document.getElementById("gallery-model-search").value, model: document.getElementById("gallery-model-search").value,
page: document.getElementById("gallery-page").value page: document.getElementById("gallery-page").value
}) })
container.innerHTML = ""
fetch('/all_images?' + params) fetch('/all_images?' + params)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@ -3130,10 +3279,31 @@ function refreshGallery(newsearch = false) {
json.forEach(item => { json.forEach(item => {
container.appendChild(galleryImage(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.getElementById("gallery-refresh").innerText = "Refresh"
} }
document.addEventListener("tabClick", (e) => {
if (e.detail.name == 'gallery') {
refreshGallery()
}
})
function decrementGalleryPage() { function decrementGalleryPage() {
let page = Math.max(document.getElementById("gallery-page").value - 1, 0) let page = Math.max(document.getElementById("gallery-page").value - 1, 0)
document.getElementById("gallery-page").value = page document.getElementById("gallery-page").value = page
@ -3150,4 +3320,4 @@ function gallery_keyDown_handler(event) {
event.preventDefault() event.preventDefault()
refreshGallery(true) refreshGallery(true)
} }
} }

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 */ /* TOAST NOTIFICATIONS */
function showToast(message, duration = 5000, error = false) { function showToast(message, duration = 5000, error = false, doc=document) {
const toast = document.createElement("div") const toast = doc.createElement("div")
toast.classList.add("toast-notification") toast.classList.add("toast-notification")
if (error === true) { if (error === true) {
toast.classList.add("toast-notification-error") toast.classList.add("toast-notification-error")
} }
toast.innerHTML = message toast.innerHTML = message
document.body.appendChild(toast) doc.body.appendChild(toast)
// Set the position of the toast on the screen // 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 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 .slice(0, -1) // exclude current toast
.reduce((totalHeight, toast) => totalHeight + toast.offsetHeight + 10, 0) // add 10 pixels for spacing .reduce((totalHeight, toast) => totalHeight + toast.offsetHeight + 10, 0) // add 10 pixels for spacing
toast.style.bottom = `${10 + previousToastsHeight}px` toast.style.bottom = `${10 + previousToastsHeight}px`
@ -896,7 +896,7 @@ function showToast(message, duration = 5000, error = false) {
const removeTimeoutId = setTimeout(() => { const removeTimeoutId = setTimeout(() => {
toast.remove() toast.remove()
// Adjust the position of remaining toasts // Adjust the position of remaining toasts
const remainingToasts = document.querySelectorAll(".toast-notification") const remainingToasts = doc.querySelectorAll(".toast-notification")
const removedToastBottom = toast.getBoundingClientRect().bottom const removedToastBottom = toast.getBoundingClientRect().bottom
remainingToasts.forEach((toast) => { remainingToasts.forEach((toast) => {
@ -908,13 +908,13 @@ function showToast(message, duration = 5000, error = false) {
// Wait for the slide-down animation to complete // Wait for the slide-down animation to complete
setTimeout(() => { setTimeout(() => {
// Remove the slide-down class after the animation has completed // 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) => { slidingToasts.forEach((toast) => {
toast.classList.remove("slide-down") toast.classList.remove("slide-down")
}) })
// Adjust the position of remaining toasts again, in case there are multiple toasts being removed at once // 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 let heightSoFar = 0
remainingToastsDown.forEach((toast) => { remainingToastsDown.forEach((toast) => {
toast.style.bottom = `${10 + heightSoFar}px` 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()
}