mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2025-06-21 02:18:24 +02:00
Merge Improvements from JeLuF:gallery
This commit is contained in:
commit
fb031a4c46
@ -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.
|
||||
|
@ -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:]
|
||||
|
@ -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
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>
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -56,6 +56,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"tree_toggle",
|
||||
"json_toggle",
|
||||
"extract_lora_from_prompt",
|
||||
"gallery-thumbnail-size",
|
||||
"embedding-card-size-selector",
|
||||
]
|
||||
|
||||
|
@ -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 = "…/"+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
|
||||
@ -3150,4 +3320,4 @@ function gallery_keyDown_handler(event) {
|
||||
event.preventDefault()
|
||||
refreshGallery(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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
@ -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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user