diff --git a/server.py b/server.py index 7ca44eb5a..858826080 100644 --- a/server.py +++ b/server.py @@ -116,6 +116,15 @@ class PromptServer(): else: return web.Response(status=400) + @routes.get("/output/images") + async def get_output(request): + output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output") + + if not os.path.exists(output_dir): + return web.Response(status=404) + + images = [f for f in os.listdir(output_dir) if f.endswith('.png')] + return web.json_response({"images": images}) @routes.get("/view") async def view_image(request): @@ -142,13 +151,20 @@ class PromptServer(): @routes.post("/delete") async def delete(request): + body = await request.json() + filename = body["delete"] current_dir = os.path.abspath(os.getcwd()) output_dir = os.path.join(current_dir, "output") if not os.path.exists(output_dir): return web.json_response({"message": "Output directory does not exist."}, status=404) try: - for file_name in os.listdir(output_dir): - file_path = os.path.join(output_dir, file_name) + if (filename == "all"): + for file_name in os.listdir(output_dir): + file_path = os.path.join(output_dir, file_name) + if os.path.isfile(file_path): + os.remove(file_path) + else: + file_path = os.path.join(output_dir, filename) if os.path.isfile(file_path): os.remove(file_path) return web.json_response({"message": "All content deleted from Output folder."}, status=200) diff --git a/web/extensions/core/imageFeed.js b/web/extensions/core/imageFeed.js index 50582a1e5..6bafe2c65 100644 --- a/web/extensions/core/imageFeed.js +++ b/web/extensions/core/imageFeed.js @@ -7,39 +7,195 @@ import { app } from "/scripts/app.js"; app.registerExtension({ name: "Comfy.ImageFeed", setup() { - const imageList = document.createElement("div"); - Object.assign(imageList.style, { - minHeight: "30px", - maxHeight: "300px", - width: "100vw", - position: "absolute", - bottom: 0, - background: "#333", - overflow: "auto", - }); - document.body.append(imageList); + //CODE HERE + //create imageList element + const imageList = document.createElement("div"); + Object.assign(imageList.style, { + minHeight: "30px", + maxHeight: "1000px", + width: "100vw", + position: "absolute", + bottom: 0, + background: "#333", + overflow: "auto", + border: "2px solid #333", + zIndex: "99", + display: "flex", + flexWrap: "wrap", + userSelect: "none", + alignContent: "baseline" + }); + // add CSS rules for resize cursor + const resizeHandle = document.createElement("div"); + Object.assign(resizeHandle.style, { + position: "absolute", + top: "-5px", + right: "0", + left: "0", + height: "10px", + cursor: "row-resize", + zIndex: "1" + }); + imageList.appendChild(resizeHandle); + + // add hover style to resize handle + const hoverStyle = document.createElement("style"); + hoverStyle.innerHTML = ` + .resize-handle:hover { + background-color: #666; + } + `; + document.head.appendChild(hoverStyle); + + // set class for resize handle + resizeHandle.classList.add("resize-handle"); + + // add mousedown event listener to resize handle + let startY = 0; + let startHeight = 0; + resizeHandle.addEventListener("mousedown", (event) => { + startY = event.clientY; + startHeight = parseInt(getComputedStyle(imageList).height); + document.addEventListener("mousemove", resize); + document.addEventListener("mouseup", stopResize); + }); + + // resize function + function resize(event) { + const newHeight = startHeight + startY - event.clientY; + imageList.style.height = newHeight + "px"; + } + + function loadImages(detail) { + const allImages = detail.output.images.filter( + (img) => img.type === "output" && img.filename !== "_output_images_will_be_put_here" + ); + for (const src of allImages) { + const imgContainer = document.createElement("div"); + imgContainer.style.cssText = "height: 120px; width: 120px; position: relative;"; + + const imgDelete = document.createElement("button"); + imgDelete.innerHTML = "🗑️"; + imgDelete.style.cssText = + "position: absolute; top: 0; right: 0; width: 20px; text-indent: -4px; right: 5px; height: 20px; cursor: pointer; position: absolute; top: 5px; font-size: 12px; line-height: 12px;"; + + imgDelete.addEventListener("click", async () => { + const confirmDelete = confirm("Are you sure you want to delete this image?"); + if (confirmDelete) { + await api.deleteImage(src.filename); + imgContainer.remove(); + } + }); + + const img = document.createElement("img"); + img.style.cssText = "height: 120px; width: 120px; object-fit: cover;"; + img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent(src.subfolder)}`; + img.addEventListener("click", () => { + const popup = document.createElement("div"); + popup.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 999;"; + + const popupImg = document.createElement("img"); + popupImg.src = img.src; + popupImg.style.cssText = "max-height: 80vh; max-width: 80vw;"; + let currentIndex = allImages.indexOf(src); + + const closeButton = document.createElement("button"); + closeButton.innerHTML = "❌"; + closeButton.style.cssText = "position: absolute; top: 0; right: 0; padding: 5px; font-size: 20px; line-height: 20px; background-color: transparent; border: none; color: white; cursor: pointer;"; + + const nextButton = document.createElement("button"); + nextButton.innerHTML = "▶"; + nextButton.style.cssText = "position: absolute; top: 50%; right: 10px; padding: 5px; font-size: 20px; line-height: 20px; background-color: transparent; border: none; color: white; cursor: pointer; transform: translateY(-50%);"; + + const prevButton = document.createElement("button"); + prevButton.innerHTML = "◀"; + prevButton.style.cssText = "position: absolute; top: 50%; left: 10px; padding: 5px; font-size: 20px; line-height: 20px; background-color: transparent; border: none; color: white; cursor: pointer; transform: translateY(-50%);"; + + closeButton.addEventListener("click", () => { + popup.remove(); + }); + nextButton.addEventListener("click", () => { + currentIndex--; + if (currentIndex < 0) { + currentIndex = allImages.length - 1; + } + popupImg.src = `/view?filename=${encodeURIComponent(allImages[currentIndex].filename)}&type=${allImages[currentIndex].type}&subfolder=${encodeURIComponent(allImages[currentIndex].subfolder)}`; + }); + prevButton.addEventListener("click", () => { + currentIndex++; + if (currentIndex >= allImages.length) { + currentIndex = 0; + } + popupImg.src = `/view?filename=${encodeURIComponent(allImages[currentIndex].filename)}&type=${allImages[currentIndex].type}&subfolder=${encodeURIComponent(allImages[currentIndex].subfolder)}`; + }); + popup.addEventListener("click", (event) => { + if (event.target === popup) { + popup.remove(); + } + }); + popup.append(popupImg); + popup.append(closeButton); + popup.append(nextButton); + popup.append(prevButton); + document.body.append(popup); + }); + + + imgContainer.append(imgDelete); + imgContainer.append(img); + imageList.prepend(imgContainer); + } + } + + api.getOutput().then(data => { + try { + var images = data.filenames[0].map((filename) => { + return { filename: filename, type: 'output', subfolder: '' }; + }); + var output = {images: images} + var detail = {output: output} + loadImages(detail); + } catch(err){} + }); + + // stop resize function + function stopResize() { + document.removeEventListener("mousemove", resize); + document.removeEventListener("mouseup", stopResize); + } + + // append imageList element to document + document.body.append(imageList); + const menu = document.createElement("div"); + Object.assign(menu.style, { + height: "100%", + width: "90px", + right:"0px", + top:"0px" + }); + imageList.append(menu); function makeButton(text, style) { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = text; - Object.assign(btn.style, { - ...style, - height: "20px", - cursor: "pointer", - position: "absolute", - top: "5px", - fontSize: "12px", - lineHeight: "12px", - }); - imageList.append(btn); + Object.assign(btn.style, { + ...style, + height: "20px", + width: "80px", + cursor: "pointer", + position: "absolute", + fontSize: "12px", + lineHeight: "12px", + }); + menu.append(btn); return btn; } const showButton = document.createElement("button"); - const closeButton = makeButton("❌", { - width: "20px", + const closeButton = makeButton("❌ Close", { textIndent: "-4px", + top: "5px", right: "5px", }); closeButton.onclick = () => { @@ -47,12 +203,21 @@ app.registerExtension({ showButton.style.display = "unset"; }; - const clearButton = makeButton("Clear", { - right: "30px", + const clearButton = makeButton("✖ Clear", { + top: "30px", + right: "5px", }); clearButton.onclick = () => { imageList.replaceChildren(closeButton, clearButton); }; + const deleteAllButton = makeButton("🗑️ Delete", { + top: "55px", + right: "5px", + }); + deleteAllButton.onclick = () => { + api.deleteAllImages(); + imageList.replaceChildren(closeButton, clearButton); + }; showButton.classList.add("comfy-settings-btn"); showButton.style.right = "16px"; @@ -60,33 +225,13 @@ app.registerExtension({ showButton.style.display = "none"; showButton.textContent = "🖼️"; showButton.onclick = () => { - imageList.style.display = "block"; + imageList.style.display = "flex"; showButton.style.display = "none"; }; document.querySelector(".comfy-settings-btn").after(showButton); api.addEventListener("executed", ({ detail }) => { - if (detail?.output?.images) { - for (const src of detail.output.images) { - if (src.type == 'output') { //Only show output images, not preview images - const img = document.createElement("img"); - const a = document.createElement("a"); - a.href = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent( - src.subfolder - )}`; - a.target = "_blank"; - Object.assign(img.style, { - height: "120px", - width: "120px", - objectFit: "cover", - }); - - img.src = a.href; - a.append(img); - imageList.prepend(a); - } - } - } + loadImages(detail); }); }, }); \ No newline at end of file diff --git a/web/scripts/api.js b/web/scripts/api.js index 988909b66..5c128122b 100644 --- a/web/scripts/api.js +++ b/web/scripts/api.js @@ -123,6 +123,9 @@ class ComfyApi extends EventTarget { await this.#postItem("delete", { delete: "all" }) } } + async deleteImage(filename) { + await this.#postItem("delete", { delete: filename }) + } /** * Gets a list of embedding names @@ -223,6 +226,20 @@ class ComfyApi extends EventTarget { } } + /** + * Gets the prompt execution history + * @returns Prompt history including node outputs + */ + async getOutput() { + try { + const res = await fetch("/output/images"); + return { filenames: Object.values(await res.json()) }; + } catch (error) { + console.error(error); + return { output: [] }; + } + } + /** * Sends a POST request to the API * @param {*} type The endpoint to post to