| import { api } from "../../../scripts/api.js"; | |
| import { app } from "../../../scripts/app.js"; | |
| import { $el } from "../../../scripts/ui.js"; | |
| import { lightbox } from "./common/lightbox.js"; | |
| $el("style", { | |
| textContent: ` | |
| .pysssss-image-feed { | |
| position: absolute; | |
| background: var(--comfy-menu-bg); | |
| color: var(--fg-color); | |
| z-index: 99; | |
| font-family: sans-serif; | |
| font-size: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .pysssss-image-feed--top, .pysssss-image-feed--bottom { | |
| width: 100vw; | |
| min-height: 30px; | |
| max-height: calc(var(--max-size, 20) * 1vh); | |
| } | |
| .pysssss-image-feed--top { | |
| top: 0; | |
| } | |
| .pysssss-image-feed--bottom { | |
| bottom: 0; | |
| flex-direction: column-reverse; | |
| padding-top: 5px; | |
| } | |
| .pysssss-image-feed--left, .pysssss-image-feed--right { | |
| top: 0; | |
| height: 100vh; | |
| min-width: 200px; | |
| max-width: calc(var(--max-size, 10) * 1vw); | |
| } | |
| .pysssss-image-feed--left { | |
| left: 0; | |
| } | |
| .pysssss-image-feed--right { | |
| right: 0; | |
| } | |
| .pysssss-image-feed--left .pysssss-image-feed-menu, .pysssss-image-feed--right .pysssss-image-feed-menu { | |
| flex-direction: column; | |
| } | |
| .pysssss-image-feed-menu { | |
| position: relative; | |
| flex: 0 1 min-content; | |
| display: flex; | |
| gap: 5px; | |
| padding: 5px; | |
| justify-content: space-between; | |
| } | |
| .pysssss-image-feed-btn-group { | |
| align-items: stretch; | |
| display: flex; | |
| gap: .5rem; | |
| flex: 0 1 fit-content; | |
| justify-content: flex-end; | |
| } | |
| .pysssss-image-feed-btn { | |
| background-color:var(--comfy-input-bg); | |
| border-radius:5px; | |
| border:2px solid var(--border-color); | |
| color: var(--fg-color); | |
| cursor:pointer; | |
| display:inline-block; | |
| flex: 0 1 fit-content; | |
| text-decoration:none; | |
| } | |
| .pysssss-image-feed-btn.sizing-btn:checked { | |
| filter: invert(); | |
| } | |
| .pysssss-image-feed-btn.clear-btn { | |
| padding: 5px 20px; | |
| } | |
| .pysssss-image-feed-btn.hide-btn { | |
| padding: 5px; | |
| aspect-ratio: 1 / 1; | |
| } | |
| .pysssss-image-feed-btn:hover { | |
| filter: brightness(1.2); | |
| } | |
| .pysssss-image-feed-btn:active { | |
| position:relative; | |
| top:1px; | |
| } | |
| .pysssss-image-feed-menu section { | |
| border-radius: 5px; | |
| background: rgba(0,0,0,0.6); | |
| padding: 0 5px; | |
| display: flex; | |
| gap: 5px; | |
| align-items: center; | |
| position: relative; | |
| } | |
| .pysssss-image-feed-menu section span { | |
| white-space: nowrap; | |
| } | |
| .pysssss-image-feed-menu section input { | |
| flex: 1 1 100%; | |
| background: rgba(0,0,0,0.6); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| z-index: 100; | |
| } | |
| .sizing-menu { | |
| position: relative; | |
| } | |
| .size-controls-flyout { | |
| position: absolute; | |
| transform: scaleX(0%); | |
| transition: 200ms ease-out; | |
| transition-delay: 500ms; | |
| z-index: 101; | |
| width: 300px; | |
| } | |
| .sizing-menu:hover .size-controls-flyout { | |
| transform: scale(1, 1); | |
| transition: 200ms linear; | |
| transition-delay: 0; | |
| } | |
| .pysssss-image-feed--bottom .size-controls-flyout { | |
| transform: scale(1,0); | |
| transform-origin: bottom; | |
| bottom: 0; | |
| left: 0; | |
| } | |
| .pysssss-image-feed--top .size-controls-flyout { | |
| transform: scale(1,0); | |
| transform-origin: top; | |
| top: 0; | |
| left: 0; | |
| } | |
| .pysssss-image-feed--left .size-controls-flyout { | |
| transform: scale(0, 1); | |
| transform-origin: left; | |
| top: 0; | |
| left: 0; | |
| } | |
| .pysssss-image-feed--right .size-controls-flyout { | |
| transform: scale(0, 1); | |
| transform-origin: right; | |
| top: 0; | |
| right: 0; | |
| } | |
| .pysssss-image-feed-menu > * { | |
| min-height: 24px; | |
| } | |
| .pysssss-image-feed-list { | |
| flex: 1 1 auto; | |
| overflow-y: auto; | |
| display: grid; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 4px; | |
| grid-auto-rows: min-content; | |
| grid-template-columns: repeat(var(--img-sz, 3), 1fr); | |
| transition: 100ms linear; | |
| scrollbar-gutter: stable both-edges; | |
| padding: 5px; | |
| background: var(--comfy-input-bg); | |
| border-radius: 5px; | |
| margin: 5px; | |
| margin-top: 0px; | |
| } | |
| .pysssss-image-feed-list:empty { | |
| display: none; | |
| } | |
| .pysssss-image-feed-list div { | |
| height: 100%; | |
| text-align: center; | |
| } | |
| .pysssss-image-feed-list::-webkit-scrollbar { | |
| background: var(--comfy-input-bg); | |
| border-radius: 5px; | |
| } | |
| .pysssss-image-feed-list::-webkit-scrollbar-thumb { | |
| background:var(--comfy-menu-bg); | |
| border: 5px solid transparent; | |
| border-radius: 8px; | |
| background-clip: content-box; | |
| } | |
| .pysssss-image-feed-list::-webkit-scrollbar-thumb:hover { | |
| background: var(--border-color); | |
| background-clip: content-box; | |
| } | |
| .pysssss-image-feed-list img { | |
| object-fit: var(--img-fit, contain); | |
| max-width: 100%; | |
| max-height: calc(var(--max-size) * 1vh); | |
| border-radius: 4px; | |
| } | |
| .pysssss-image-feed-list img:hover { | |
| filter: brightness(1.2); | |
| }`, | |
| parent: document.body, | |
| }); | |
| app.registerExtension({ | |
| name: "pysssss.ImageFeed", | |
| setup() { | |
| let visible = true; | |
| const showButton = $el("button.comfy-settings-btn", { | |
| textContent: "🖼️", | |
| style: { | |
| right: "16px", | |
| cursor: "pointer", | |
| display: "none", | |
| }, | |
| }); | |
| const getVal = (n, d) => { | |
| const v = localStorage.getItem("pysssss.ImageFeed." + n); | |
| if (v && !isNaN(+v)) { | |
| return v; | |
| } | |
| return d; | |
| }; | |
| const saveVal = (n, v) => { | |
| localStorage.setItem("pysssss.ImageFeed." + n, v); | |
| }; | |
| const imageFeed = $el("div.pysssss-image-feed", { | |
| parent: document.body, | |
| }); | |
| const imageList = $el("div.pysssss-image-feed-list"); | |
| const feedLocation = app.ui.settings.addSetting({ | |
| id: "pysssss.ImageFeed.Location", | |
| name: "🐍 Image Feed Location", | |
| defaultValue: "bottom", | |
| type: () => { | |
| return $el("tr", [ | |
| $el("td", [ | |
| $el("label", { | |
| textContent: "🐍 Image Feed Location:", | |
| }), | |
| ]), | |
| $el("td", [ | |
| $el( | |
| "select", | |
| { | |
| style: { | |
| fontSize: "14px", | |
| }, | |
| oninput: (e) => { | |
| feedLocation.value = e.target.value; | |
| imageFeed.className = `pysssss-image-feed pysssss-image-feed--${feedLocation.value}`; | |
| }, | |
| }, | |
| ["left", "top", "right", "bottom"].map((m) => | |
| $el("option", { | |
| value: m, | |
| textContent: m, | |
| selected: feedLocation.value === m, | |
| }) | |
| ) | |
| ), | |
| ]), | |
| ]); | |
| }, | |
| onChange(value) { | |
| imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`; | |
| }, | |
| }); | |
| const feedDirection = app.ui.settings.addSetting({ | |
| id: "pysssss.ImageFeed.Direction", | |
| name: "🐍 Image Feed Direction", | |
| defaultValue: "newest first", | |
| type: () => { | |
| return $el("tr", [ | |
| $el("td", [ | |
| $el("label", { | |
| textContent: "🐍 Image Feed Direction:", | |
| }), | |
| ]), | |
| $el("td", [ | |
| $el( | |
| "select", | |
| { | |
| style: { | |
| fontSize: "14px", | |
| }, | |
| oninput: (e) => { | |
| feedDirection.value = e.target.value; | |
| imageList.replaceChildren(...[...imageList.childNodes].reverse()); | |
| }, | |
| }, | |
| ["newest first", "oldest first"].map((m) => | |
| $el("option", { | |
| value: m, | |
| textContent: m, | |
| selected: feedDirection.value === m, | |
| }) | |
| ) | |
| ), | |
| ]), | |
| ]); | |
| }, | |
| }); | |
| const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", { | |
| textContent: "Clear", | |
| onclick: () => imageList.replaceChildren(), | |
| }); | |
| const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", { | |
| textContent: "❌", | |
| onclick: () => { | |
| imageFeed.style.display = "none"; | |
| showButton.style.display = "unset"; | |
| saveVal("Visible", 0); | |
| visible = false; | |
| }, | |
| }); | |
| let columnInput; | |
| function updateColumnCount(v) { | |
| columnInput.parentElement.title = `Controls the number of columns in the feed (${v} columns).\nClick label to set custom value.`; | |
| imageFeed.style.setProperty("--img-sz", v); | |
| saveVal("ImageSize", v); | |
| columnInput.max = Math.max(10, v, columnInput.max); | |
| columnInput.value = v; | |
| } | |
| imageFeed.append( | |
| $el("div.pysssss-image-feed-menu", [ | |
| $el("section.sizing-menu", {}, [ | |
| $el("label.size-control-handle", { textContent: "↹ Resize Feed" }), | |
| $el("div.size-controls-flyout", {}, [ | |
| $el("section.size-control.feed-size-control", {}, [ | |
| $el("span", { | |
| textContent: "Feed Size...", | |
| }), | |
| $el("input", { | |
| type: "range", | |
| min: 10, | |
| max: 80, | |
| oninput: (e) => { | |
| e.target.parentElement.title = `Controls the maximum size of the image feed panel (${e.target.value}vh)`; | |
| imageFeed.style.setProperty("--max-size", e.target.value); | |
| saveVal("FeedSize", e.target.value); | |
| }, | |
| $: (el) => { | |
| requestAnimationFrame(() => { | |
| el.value = getVal("FeedSize", 25); | |
| el.oninput({ target: el }); | |
| }); | |
| }, | |
| }), | |
| ]), | |
| $el("section.size-control.image-size-control", {}, [ | |
| $el("a", { | |
| textContent: "Column count...", | |
| style: { | |
| cursor: "pointer", | |
| textDecoration: "underline", | |
| }, | |
| onclick: () => { | |
| const v = +prompt("Enter custom column count", 20); | |
| if (!isNaN(v)) { | |
| updateColumnCount(v); | |
| } | |
| }, | |
| }), | |
| $el("input", { | |
| type: "range", | |
| min: 1, | |
| max: 10, | |
| step: 1, | |
| oninput: (e) => { | |
| updateColumnCount(e.target.value); | |
| }, | |
| $: (el) => { | |
| columnInput = el; | |
| requestAnimationFrame(() => { | |
| updateColumnCount(getVal("ImageSize", 4)); | |
| }); | |
| }, | |
| }), | |
| ]), | |
| ]), | |
| ]), | |
| $el("div.pysssss-image-feed-btn-group", {}, [clearButton, hideButton]), | |
| ]), | |
| imageList | |
| ); | |
| showButton.onclick = () => { | |
| imageFeed.style.display = "block"; | |
| showButton.style.display = "none"; | |
| saveVal("Visible", 1); | |
| visible = true; | |
| }; | |
| document.querySelector(".comfy-settings-btn").after(showButton); | |
| if (!+getVal("Visible", 1)) { | |
| hideButton.onclick(); | |
| } | |
| api.addEventListener("executed", ({ detail }) => { | |
| if (visible && detail?.output?.images) { | |
| for (const src of detail.output.images) { | |
| const href = `/view?filename=${encodeURIComponent(src.filename)}&type=${ | |
| src.type | |
| }&subfolder=${encodeURIComponent(src.subfolder)}&t=${+new Date()}`; | |
| const method = feedDirection.value === "newest first" ? "prepend" : "append"; | |
| imageList[method]( | |
| $el("div", [ | |
| $el( | |
| "a", | |
| { | |
| target: "_blank", | |
| href, | |
| onclick: (e) => { | |
| const imgs = [...imageList.querySelectorAll("img")].map((img) => img.getAttribute("src")); | |
| lightbox.show(imgs, imgs.indexOf(href)); | |
| e.preventDefault(); | |
| }, | |
| }, | |
| [$el("img", { src: href })] | |
| ), | |
| ]) | |
| ); | |
| } | |
| } | |
| }); | |
| }, | |
| }); | |