mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2025-06-20 18:08:00 +02:00
Masonry layout
- Fix masonry layout. Fill row wise - Resizeable thumbnails - Remove /single-image endpoint. Use Javascript instead to fill the page - Dispatch an event when a single image page gets loaded, so that plugins can be used on those pages - Load a gallery when opening the tab. No initial click on the refresh button required any more - "Use as input" and "use settings" buttons work in both the gallery and the single image view - showToast takes a "doc" argument to allow toasts to be shown on the single image page
This commit is contained in:
commit
bf6e4a6a00
@ -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.
|
||||
|
@ -100,12 +100,16 @@ def init():
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
@server_api.get("/all_images")
|
||||
def get_all_images(db: Session = Depends(get_db)):
|
||||
def get_all_images(prompt: str = "", model: str = "", page: int = 0, images_per_page: int = 50, db: Session = Depends(get_db)):
|
||||
from easydiffusion.easydb.mappings import GalleryImage
|
||||
images = db.query(GalleryImage).all()
|
||||
return images
|
||||
|
||||
|
||||
images = db.query(GalleryImage).order_by(GalleryImage.time_created.desc())
|
||||
if prompt != "":
|
||||
images = images.filter(GalleryImage.path.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:]
|
||||
|
@ -25,10 +25,57 @@ class GalleryImage(Base):
|
||||
prompt = Column(String)
|
||||
negative_prompt = Column(String)
|
||||
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)
|
||||
|
@ -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">
|
||||
@ -517,7 +518,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-content-gallery" class="tab-content">
|
||||
<button class="primaryButton" onclick="refreshGallery()">Refresh</button>
|
||||
<div id="gallery-search">
|
||||
<button class="primaryButton" onclick="decrementGalleryPage()"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
<textarea id="gallery-prompt-search" onkeydown="gallery_keyDown_handler(event)" placeholder="Search for a prompt..."></textarea>
|
||||
<textarea id="gallery-model-search" onkeydown="gallery_keyDown_handler(event)" placeholder="Search for a model..."></textarea>
|
||||
<label for="gallery-page">Page:</label>
|
||||
<input id="gallery-page" name="Page" value="0" size="1" onkeypress="gallery_keyDown_handler(event)">
|
||||
<button class="primaryButton" id="gallery-refresh" onclick="refreshGallery()">Load</button>
|
||||
<input id="gallery-thumbnail-size" name="gallery-thumbnail-size" class="editor-slider" type="range" value="300" min="50" max="800">
|
||||
<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>
|
||||
|
@ -1889,6 +1889,8 @@ div#enlarge-buttons {
|
||||
}
|
||||
|
||||
/* Gallery CSS */
|
||||
|
||||
/*
|
||||
.gallery {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -1896,29 +1898,30 @@ div#enlarge-buttons {
|
||||
flex-direction: column;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
*/
|
||||
|
||||
#gallery-search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#gallery-search>* {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.gallery-container {
|
||||
columns: 6 380px ;
|
||||
column-gap: 1.5rem;
|
||||
width: 95%;
|
||||
margin: 0 0 ;
|
||||
}
|
||||
|
||||
.gallery-image img {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
transition: all .25s ease-in-out;
|
||||
box-shadow: 5px 5px 5px rgba(0,0,0,0.4);
|
||||
width: var(--gallery-width);
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
position: relative;
|
||||
margin: 0 1.5rem 1.5rem 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
|
||||
transition: all .75s ease-in-out;
|
||||
width: var(--gallery-width);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.gallery-image-hover {
|
||||
@ -1938,6 +1941,6 @@ div#enlarge-buttons {
|
||||
}
|
||||
|
||||
|
||||
#tab-content-gallery>button {
|
||||
#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;
|
||||
}
|
@ -56,6 +56,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"tree_toggle",
|
||||
"json_toggle",
|
||||
"extract_lora_from_prompt",
|
||||
"gallery-thumbnail-size",
|
||||
"embedding-card-size-selector",
|
||||
]
|
||||
|
||||
|
@ -143,6 +143,7 @@ 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")
|
||||
@ -3109,16 +3110,52 @@ const IMAGE_INFO = {
|
||||
"Clip Skip": "clip_skip",
|
||||
}
|
||||
|
||||
const IGNORE_TOKENS = ["None", "none", "Null", "null", "false", "False"]
|
||||
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")
|
||||
div.classList.add("gallery-image")
|
||||
|
||||
let img = document.createElement("img")
|
||||
img.src = "/image/" + item.path
|
||||
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 = 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: item
|
||||
})
|
||||
})
|
||||
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)
|
||||
w.document.getElementById("focusbox").replaceChildren(table)
|
||||
document.dispatchEvent(new CustomEvent("newGalleryWindow", { detail: w }))
|
||||
})
|
||||
})
|
||||
|
||||
let hover = document.createElement("div")
|
||||
hover.classList.add("gallery-image-hover")
|
||||
|
||||
@ -3127,29 +3164,17 @@ function galleryImage(item) {
|
||||
infoBtn.innerHTML = '<i class="fa-regular fa-file-lines"></i>'
|
||||
infoBtn.addEventListener("click", function() {
|
||||
let table = document.createElement("table")
|
||||
console.log(item)
|
||||
|
||||
for (const [label, key] of Object.entries(IMAGE_INFO)) {
|
||||
console.log(label, key, item[key])
|
||||
console.log(IGNORE_TOKENS.findIndex( k => (k == item[key])))
|
||||
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>`))
|
||||
}
|
||||
}
|
||||
galleryDetailTable(table)
|
||||
|
||||
galleryImginfoDialogContent.replaceChildren(table)
|
||||
galleryImginfoDialog.showModal()
|
||||
})
|
||||
hover.appendChild(infoBtn)
|
||||
|
||||
|
||||
let imageExpandBtn=document.createElement("button")
|
||||
imageExpandBtn.classList.add("tertiaryButton")
|
||||
let imageExpandBtn=createElement("button", { style: "margin-left: 0.2em;"}, ["tertiaryButton"], undefined)
|
||||
imageExpandBtn.innerHTML = '<i class="fa-solid fa-expand"></i>'
|
||||
imageExpandBtn.style["margin-left"] = "0.2em"
|
||||
imageExpandBtn.addEventListener("click", function() {
|
||||
function previousImage(img) {
|
||||
const allImages = Array.from(document.getElementById("imagecontainer").querySelectorAll(".gallery-image img"))
|
||||
@ -3189,10 +3214,11 @@ function galleryImage(item) {
|
||||
|
||||
hover.appendChild(document.createElement("br"))
|
||||
|
||||
let useAsInputBtn = document.createElement("button")
|
||||
useAsInputBtn.classList.add("tertiaryButton")
|
||||
useAsInputBtn.innerHTML = "Use as Input"
|
||||
useAsInputBtn.addEventListener("click", (e) => onUseAsInputClick(null, img))
|
||||
let useAsInputBtn = createElement("button", {}, ["tertiaryButton"], "Use as Input")
|
||||
useAsInputBtn.addEventListener("click", (e) => {
|
||||
onUseURLasInput(img.src)
|
||||
showToast("Loaded image as input image")
|
||||
})
|
||||
|
||||
hover.appendChild(useAsInputBtn)
|
||||
|
||||
@ -3200,6 +3226,12 @@ function galleryImage(item) {
|
||||
return div
|
||||
}
|
||||
|
||||
function onUseURLasInput(url) {
|
||||
toDataURL(url, blob => {
|
||||
onUseAsInputClick(null, {src:blob})
|
||||
})
|
||||
}
|
||||
|
||||
modalDialogCloseOnBackdropClick(galleryImginfoDialog)
|
||||
makeDialogDraggable(galleryImginfoDialog)
|
||||
|
||||
@ -3207,15 +3239,79 @@ galleryImginfoDialog.querySelector("#gallery-imginfo-close-button").addEventList
|
||||
galleryImginfoDialog.close()
|
||||
})
|
||||
|
||||
galleryThumbnailSize.addEventListener("input", layoutGallery)
|
||||
window.addEventListener("resize", layoutGallery)
|
||||
|
||||
function layoutGallery() {
|
||||
let container = document.getElementById("imagecontainer")
|
||||
let thumbSize = parseInt(galleryThumbnailSize.value)
|
||||
let root = document.querySelector(':root')
|
||||
root.style.setProperty('--gallery-width', thumbSize + "px")
|
||||
let msnry = new Masonry( container, {
|
||||
gutter: 10,
|
||||
itemSelector: '.gallery-image',
|
||||
columnWidth: thumbSize
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function refreshGallery() {
|
||||
let container = document.getElementById("imagecontainer")
|
||||
container.innerHTML=""
|
||||
fetch('/all_images')
|
||||
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
|
||||
})
|
||||
|
||||
fetch('/all_images?' + params)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
console.log(json)
|
||||
json.forEach( item => {
|
||||
if (document.getElementById("gallery-page").value > 0 && json.length == 0) {
|
||||
decrementGalleryPage()
|
||||
alert("No more images")
|
||||
return
|
||||
}
|
||||
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
|
||||
refreshGallery()
|
||||
}
|
||||
|
||||
function incrementGalleryPage() {
|
||||
document.getElementById("gallery-page").value++
|
||||
refreshGallery()
|
||||
}
|
||||
|
||||
function gallery_keyDown_handler(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
refreshGallery()
|
||||
}
|
||||
}
|
||||
|
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