flatcherlee's picture
Upload 2334 files
3d5837a verified
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;
}
div > .pysssss-image-feed {
position: static;
}
.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);
}
.comfyui-body-left .pysssss-image-feed--left, .comfyui-body-right .pysssss-image-feed--right {
height: 100%;
}
.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",
async setup() {
let visible = true;
const seenImages = new Map();
const showButton = $el("button.comfy-settings-btn", {
textContent: "🖼️",
style: {
right: "16px",
cursor: "pointer",
display: "none",
},
});
let showMenuButton;
if (!app.menu?.element.style.display && app.menu?.settingsGroup) {
showMenuButton = new (await import("../../../scripts/ui/components/button.js")).ComfyButton({
icon: "image-multiple",
action: () => showButton.click(),
tooltip: "Show Image Feed 🐍",
content: "Show Image Feed 🐍",
});
showMenuButton.enabled = false;
app.menu.settingsGroup.append(showMenuButton);
}
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");
const imageList = $el("div.pysssss-image-feed-list");
function updateMenuParent(location) {
if (showMenuButton) {
const el = document.querySelector(".comfyui-body-" + location);
if (!el) return;
el.append(imageFeed);
} else {
if (!imageFeed.parent) {
document.body.append(imageFeed);
}
}
}
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}`;
updateMenuParent(feedLocation.value);
window.dispatchEvent(new Event("resize"));
},
},
["left", "top", "right", "bottom", "hidden"].map((m) =>
$el("option", {
value: m,
textContent: m,
selected: feedLocation.value === m,
})
)
),
]),
]);
},
onChange(value) {
if (value === "hidden") {
imageFeed.remove();
showButton.style.display = "none";
} else {
showButton.style.display = visible ? "none" : "unset";
imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`;
updateMenuParent(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 deduplicateFeed = app.ui.settings.addSetting({
id: "pysssss.ImageFeed.Deduplication",
name: "🐍 Image Feed Deduplication",
tooltip: `Ensures unique images in the image feed but at the cost of CPU-bound performance impact \
(from hundreds of milliseconds to seconds per image, depending on byte size). For workflows that produce duplicate images, turning this setting on may yield overall client-side performance improvements \
by reducing the number of images in the feed.
Recommended: "enabled (max performance)" uness images are erroneously deduplicated.`,
defaultValue: 0,
type: "combo",
options: (value) => {
let dedupeOptions = {"disabled": 0, "enabled (slow)": 1, "enabled (performance)": 0.5, "enabled (max performance)": 0.25};
return Object.entries(dedupeOptions).map(([k, v]) => ({
value: v,
text: k,
selected: k === value,
})
)
},
});
const maxImages = app.ui.settings.addSetting({
id: "pysssss.ImageFeed.MaxImages",
name: "🐍 Image Feed Max Images",
tooltip: `Limits the number of images in the feed to a maximum, removing the oldest images as new ones are added.`,
defaultValue: 0,
type: "number",
});
const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", {
textContent: "Clear",
onclick: () => {
imageList.replaceChildren();
window.dispatchEvent(new Event("resize"));
},
});
const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", {
textContent: "❌",
onclick: () => {
imageFeed.style.display = "none";
showButton.style.display = feedLocation.value === "hidden" ? "none" : "unset";
if (showMenuButton) showMenuButton.enabled = true;
saveVal("Visible", 0);
visible = false;
window.dispatchEvent(new Event("resize"));
},
});
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;
window.dispatchEvent(new Event("resize"));
}
function addImageToFeed(href) {
const method = feedDirection.value === "newest first" ? "prepend" : "append";
if (maxImages.value > 0 && imageList.children.length >= maxImages.value) {
imageList.children[method === "prepend" ? imageList.children.length - 1 : 0].remove();
}
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 })]
),
])
);
// If lightbox is open, update it with new image
lightbox.updateWithNewImage(href, feedDirection.value);
}
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);
window.dispatchEvent(new Event("resize"));
},
$: (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 = "flex";
showButton.style.display = "none";
if (showMenuButton) showMenuButton.enabled = false;
saveVal("Visible", 1);
visible = true;
window.dispatchEvent(new Event("resize"));
};
document.querySelector(".comfy-settings-btn").after(showButton);
window.dispatchEvent(new Event("resize"));
if (!+getVal("Visible", 1)) {
hideButton.onclick();
}
api.addEventListener("executed", ({ detail }) => {
if (visible && detail?.output?.images) {
if (detail.node?.includes?.(":")) {
// Ignore group nodes
const n = app.graph.getNodeById(detail.node.split(":")[0]);
if (n?.getInnerNodes) return;
}
for (const src of detail.output.images) {
const href = `./view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&
subfolder=${encodeURIComponent(src.subfolder)}&t=${+new Date()}`;
// deduplicateFeed.value is essentially the scaling factor used for image hashing
// but when deduplication is disabled, this value is "0"
if (deduplicateFeed.value > 0) {
// deduplicate by ignoring images with the same filename/type/subfolder
const fingerprint = JSON.stringify({ filename: src.filename, type: src.type, subfolder: src.subfolder });
if (seenImages.has(fingerprint)) {
// NOOP: image is a duplicate
} else {
seenImages.set(fingerprint, true);
let img = $el("img", { src: href })
img.onerror = () => {
// fall back to default behavior
addImageToFeed(href);
}
img.onload = () => {
// redraw the image onto a canvas to strip metadata (resize if performance mode)
let imgCanvas = document.createElement("canvas");
let imgScalar = deduplicateFeed.value;
imgCanvas.width = imgScalar * img.width;
imgCanvas.height = imgScalar * img.height;
let imgContext = imgCanvas.getContext("2d");
imgContext.drawImage(img, 0, 0, imgCanvas.width, imgCanvas.height);
const data = imgContext.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
// calculate fast hash of the image data
let hash = 0;
for (const b of data.data) {
hash = ((hash << 5) - hash) + b;
}
// add image to feed if we've never seen the hash before
if (seenImages.has(hash)) {
// NOOP: image is a duplicate
} else {
// if we got to here, then the image is unique--so add to feed
seenImages.set(hash, true);
addImageToFeed(href);
}
}
}
} else {
addImageToFeed(href);
}
}
}
});
},
});