Files
KOFiblto-ComfyUI-SaturnNodes/web/js/image_loader.js
T

636 lines
25 KiB
JavaScript

import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
// Custom styles for the visual image picker
const visualStyles = document.createElement("style");
visualStyles.textContent = `
.img-visual-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: #111;
border: 1px solid #222;
border-radius: 6px;
box-sizing: border-box;
height: 100%;
min-height: 0;
max-height: 100%;
overflow-y: hidden;
}
.img-visual-container::-webkit-scrollbar {
width: 6px;
}
.img-visual-container::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1);
border-radius: 4px;
}
.img-visual-container::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.15);
border-radius: 4px;
}
.img-visual-container::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.3);
}
.img-control-bar {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
width: 100%;
box-sizing: border-box;
}
.img-search-bar {
width: 100%;
padding: 6px 10px;
background: #1c1c1c;
border: 1px solid #333;
border-radius: 4px;
color: #fff;
font-size: 11px;
box-sizing: border-box;
}
.img-search-bar:focus {
border-color: #007acc;
outline: none;
}
.img-grid-container {
flex: 1 1 0px;
min-height: 0;
max-height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
box-sizing: border-box;
padding-right: 4px;
}
/* Webkit Scrollbar */
.img-grid-container::-webkit-scrollbar {
width: 6px;
}
.img-grid-container::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1);
}
.img-grid-container::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.15);
border-radius: 4px;
}
.img-grid-container::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.3);
}
.img-folder-container {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.img-folder-header {
display: flex;
align-items: center;
padding: 5px 8px;
background: linear-gradient(90deg, #1d1d1d, #141414);
border: 1px solid #262626;
border-radius: 4px;
cursor: pointer;
user-select: none;
transition: background 0.15s, border-color 0.15s;
}
.img-folder-header:hover {
background: linear-gradient(90deg, #242424, #1a1a1a);
border-color: #383838;
}
.img-folder-toggle {
font-size: 8px;
color: #888;
margin-right: 6px;
transition: transform 0.2s ease-in-out;
}
.img-folder-header.collapsed .img-folder-toggle {
transform: rotate(-90deg);
}
.img-folder-name {
font-size: 11px;
font-weight: bold;
color: #d1d1d1;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.img-folder-count {
font-size: 10px;
color: #666;
}
.img-folder-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--img-tile-size, 80px), 1fr));
gap: 8px;
padding: 4px 2px;
}
.img-folder-container.collapsed .img-folder-grid {
display: none !important;
}
.img-tile {
aspect-ratio: 1/1;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
position: relative;
box-sizing: border-box;
transition: all 0.15s ease-in-out;
}
.img-tile:hover {
border-color: #007acc;
transform: scale(1.04);
}
.img-tile.selected {
border-color: #00ff66 !important;
border-width: 3px !important;
box-shadow: 0 0 10px rgba(0, 255, 102, 0.4);
}
.img-tile img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.img-tile img.loaded {
opacity: 1;
}
.img-tile-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #222, #111);
color: #666;
font-size: 9px;
text-align: center;
padding: 6px;
box-sizing: border-box;
word-break: break-all;
}
.img-tile-label {
position: absolute;
bottom: 0;
width: 100%;
background: rgba(0, 0, 0, 0.75);
color: #ccc;
font-size: 8px;
padding: 2px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
}
`;
document.head.appendChild(visualStyles);
app.registerExtension({
name: "Comfy.ImageLoaderCustom",
async nodeCreated(node) {
const isVisualImageLoader = [
"VisualImageLoader",
"ImageLoaderVisualPrettyV2",
"ImageLoaderCustom",
"FavoritePromptLoader"
].includes(node.comfyClass) || [
"VisualImageLoader",
"ImageLoaderVisualPrettyV2",
"ImageLoaderCustom",
"FavoritePromptLoader"
].includes(node.type);
if (isVisualImageLoader) {
node.size = [380, 480];
node.computeSize = function() {
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
if (displayModeWidget && displayModeWidget.value === "Show All") {
// Logic added below
}
return [node.size[0], Math.max(480, node.size[1])];
};
const folderWidget = node.widgets ? node.widgets.find(w => w.name === "folder_path") : null;
const getHiddenWidget = (name, defaultVal) => {
const widgets = node.widgets.filter(w => w.name === name);
if (widgets.length > 1) {
for (let i = 1; i < widgets.length; i++) {
node.widgets.splice(node.widgets.indexOf(widgets[i]), 1);
}
}
if (widgets.length === 1) {
widgets[0].type = "hidden";
widgets[0].hidden = true;
widgets[0].computeSize = () => [0, 0];
return widgets[0];
}
const w = node.addWidget("text", name, defaultVal, () => {}, { serialize: true });
w.type = "hidden";
w.hidden = true;
w.computeSize = () => [0, 0];
return w;
};
const hiddenWidget = getHiddenWidget("_selected_image", "");
// Create Visual HTML DOM Layout
const viewContainer = document.createElement("div");
viewContainer.className = "img-visual-container";
// Isolate mouse wheel scroll events from triggering canvas zooming/panning
viewContainer.addEventListener("wheel", (e) => {
e.stopPropagation();
});
const controlBar = document.createElement("div");
controlBar.className = "img-control-bar";
controlBar.style.display = "flex";
controlBar.style.flexDirection = "row";
controlBar.style.gap = "6px";
const refreshBtn = document.createElement("button");
refreshBtn.innerHTML = "↻";
refreshBtn.title = "Refresh Images";
refreshBtn.style.background = "#1c1c1c";
refreshBtn.style.border = "1px solid #333";
refreshBtn.style.borderRadius = "4px";
refreshBtn.style.color = "#fff";
refreshBtn.style.cursor = "pointer";
refreshBtn.style.padding = "4px 8px";
refreshBtn.style.fontSize = "14px";
refreshBtn.onmouseover = () => refreshBtn.style.background = "#333";
refreshBtn.onmouseout = () => refreshBtn.style.background = "#1c1c1c";
controlBar.appendChild(refreshBtn);
const searchBar = document.createElement("input");
searchBar.type = "text";
searchBar.className = "img-search-bar";
searchBar.placeholder = "Search images or positive prompts...";
searchBar.style.flex = "1";
controlBar.appendChild(searchBar);
viewContainer.appendChild(controlBar);
const gridContainer = document.createElement("div");
gridContainer.className = "img-grid-container";
viewContainer.appendChild(gridContainer);
node.computeSize = function() {
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
let userSavedHeight = 480;
const applyDisplayMode = (mode) => {
const isShowAll = mode === "Show All";
if (isShowAll) {
viewContainer.style.setProperty("height", "auto", "important");
viewContainer.style.setProperty("max-height", "none", "important");
viewContainer.style.setProperty("overflow-y", "visible", "important");
gridContainer.style.setProperty("height", "auto", "important");
gridContainer.style.setProperty("max-height", "none", "important");
gridContainer.style.setProperty("overflow-y", "visible", "important");
gridContainer.style.setProperty("flex", "none", "important");
setTimeout(() => {
const contentH = gridContainer.scrollHeight || 0;
const neededHeight = contentH + 160;
node.setSize([node.size[0], Math.max(420, neededHeight)]);
if (app.graph) app.graph.setDirtyCanvas(true, true);
}, 50);
} else {
viewContainer.style.removeProperty("height");
viewContainer.style.removeProperty("max-height");
viewContainer.style.setProperty("overflow-y", "hidden", "important");
gridContainer.style.removeProperty("height");
gridContainer.style.removeProperty("max-height");
gridContainer.style.setProperty("overflow-y", "auto", "important");
gridContainer.style.setProperty("flex", "1 1 0px", "important");
const restoredH = node.userCustomHeight || userSavedHeight || 480;
if (viewContainer.style) {
viewContainer.style.height = `${restoredH - 80}px`;
}
node.setSize([node.size[0], Math.max(420, restoredH)]);
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
};
node.computeSize = function() {
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
if (displayModeWidget && displayModeWidget.value === "Show All" && gridContainer) {
return [node.size[0], Math.max(420, (gridContainer.scrollHeight || 0) + 160)];
}
return [node.size[0], Math.max(420, node.size[1])];
};
const updateNodeSize = () => {
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
const mode = displayModeWidget ? displayModeWidget.value : "Scrollable";
applyDisplayMode(mode);
};
const initialZoom = localStorage.getItem("comfy_img_picker_zoom") || "80";
viewContainer.style.setProperty("--img-tile-size", `${initialZoom}px`);
// Embed DOM Widget inside the node container
const domWidget = node.addDOMWidget("img_visual_picker", "HTML", viewContainer, {
getValue() { return getHiddenWidget("_selected_image", "").value; },
setValue(val) { getHiddenWidget("_selected_image", "").value = val; },
serialize: false
});
const onResize = node.onResize;
node.onResize = function(size) {
if (onResize) onResize.apply(this, arguments);
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
const isShowAll = displayModeWidget && displayModeWidget.value === "Show All";
if (!isShowAll && size) {
node.userCustomHeight = size[1];
userSavedHeight = size[1];
if (viewContainer && viewContainer.style) {
viewContainer.style.height = `${size[1] - 80}px`;
}
}
};
domWidget.computeSize = function() {
return [node.size[0] - 30, node.size[1] - 140];
};
// Zoom Slider Widget
const zoomWidget = node.addWidget(
"slider",
"tile_size",
parseInt(initialZoom),
(val) => {
localStorage.setItem("comfy_img_picker_zoom", val);
viewContainer.style.setProperty("--img-tile-size", `${val}px`);
},
{ min: 50, max: 200, step: 1 }
);
zoomWidget.serialize = false;
let activeRequest = null;
let debounceTimer = null;
let imagesData = { names: [], mapping: {}, prompts: {} };
let searchQuery = "";
// Setup Intersection Observer for lazy loading image thumbnails
const lazyObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const tile = entry.target;
const img = tile.querySelector("img");
if (img && img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
lazyObserver.unobserve(tile);
}
});
}, { root: gridContainer, rootMargin: "200px 0px 200px 0px" });
const renderGrid = () => {
gridContainer.innerHTML = "";
lazyObserver.disconnect();
let folder = folderWidget ? (folderWidget.value || "") : "";
if (node.comfyClass === "FavoritePromptLoader") {
folder = app.ui.settings.getSettingValue("FlowControl.FavoritesFolder", "output/favorites");
const favWidget = node.widgets ? node.widgets.find(w => w.name === "_favorites_folder") : null;
if(favWidget) favWidget.value = folder;
}
const selectedVal = getHiddenWidget("_selected_image", "").value;
// Group items by subfolder
const foldersMap = {};
foldersMap["Root"] = [];
imagesData.names.forEach(relPath => {
const prompt = imagesData.prompts[relPath] || "";
// Filter by search query if present (checks both path name and positive prompt text)
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesPath = relPath.toLowerCase().includes(query);
const matchesPrompt = prompt.toLowerCase().includes(query);
if (!matchesPath && !matchesPrompt) {
return;
}
}
const parts = relPath.split("/");
const subFolder = parts.length > 1 ? parts.slice(0, -1).join("/") : "Root";
const filename = parts[parts.length - 1];
if (!foldersMap[subFolder]) {
foldersMap[subFolder] = [];
}
foldersMap[subFolder].push({ relPath, filename });
});
// Sort folders: Root first, then alphabetically
const sortedFolders = Object.keys(foldersMap).sort((a, b) => {
if (a === "Root") return -1;
if (b === "Root") return 1;
return a.localeCompare(b);
});
sortedFolders.forEach(subFolder => {
const items = foldersMap[subFolder];
if (items.length === 0) return;
const folderContainer = document.createElement("div");
folderContainer.className = "img-folder-container";
const isCollapsed = localStorage.getItem(`comfy_img_folder_collapsed_${subFolder}`) === "true";
if (isCollapsed) {
folderContainer.classList.add("collapsed");
}
// Folder Header
const header = document.createElement("div");
header.className = "img-folder-header";
if (isCollapsed) {
header.classList.add("collapsed");
}
const toggle = document.createElement("span");
toggle.className = "img-folder-toggle";
toggle.innerText = "▼";
header.appendChild(toggle);
const nameSpan = document.createElement("span");
nameSpan.className = "img-folder-name";
nameSpan.innerText = subFolder;
header.appendChild(nameSpan);
const countSpan = document.createElement("span");
countSpan.className = "img-folder-count";
countSpan.innerText = `(${items.length})`;
header.appendChild(countSpan);
header.addEventListener("click", () => {
const collapsed = folderContainer.classList.toggle("collapsed");
header.classList.toggle("collapsed", collapsed);
localStorage.setItem(`comfy_img_folder_collapsed_${subFolder}`, collapsed ? "true" : "false");
// Populate grid nodes dynamically if expanded to save memory
if (!collapsed) {
populateGrid();
} else {
grid.innerHTML = "";
}
});
folderContainer.appendChild(header);
const grid = document.createElement("div");
grid.className = "img-folder-grid";
folderContainer.appendChild(grid);
const populateGrid = () => {
grid.innerHTML = "";
items.forEach(item => {
const tile = document.createElement("div");
tile.className = "img-tile";
tile.dataset.imagePath = item.relPath;
if (selectedVal === item.relPath) {
tile.classList.add("selected");
}
const img = document.createElement("img");
let fetchFolder = folder;
img.dataset.src = `/image_loader/get_thumbnail?folder=${encodeURIComponent(fetchFolder)}&image=${encodeURIComponent(item.relPath)}`;
img.onload = () => img.classList.add("loaded");
img.onerror = () => {
img.remove();
const fallback = document.createElement("div");
fallback.className = "img-tile-fallback";
fallback.innerText = item.filename;
tile.prepend(fallback);
};
tile.appendChild(img);
const label = document.createElement("div");
label.className = "img-tile-label";
label.innerText = item.filename;
tile.appendChild(label);
tile.addEventListener("click", () => {
const allTiles = gridContainer.querySelectorAll(".img-tile");
allTiles.forEach(t => t.classList.remove("selected"));
tile.classList.add("selected");
const curWidget = getHiddenWidget("_selected_image", "");
curWidget.value = item.relPath;
if (curWidget.callback) curWidget.callback(item.relPath);
// Mark the graph modified to force ComfyUI to re-run on selection change
app.graph.change();
node.triggerSlotEvent?.(0);
});
grid.appendChild(tile);
lazyObserver.observe(tile);
});
};
if (!isCollapsed) {
populateGrid();
}
gridContainer.appendChild(folderContainer);
});
setTimeout(() => updateNodeSize(), 50);
};
const updateImagesList = async () => {
let folder = folderWidget ? (folderWidget.value || "") : "";
if (node.comfyClass === "FavoritePromptLoader") {
folder = app.ui.settings.getSettingValue("FlowControl.FavoritesFolder", "output/favorites");
}
const currentRequest = Symbol();
activeRequest = currentRequest;
try {
const response = await api.fetchApi(`/image_loader/get_images?folder=${encodeURIComponent(folder)}`);
if (activeRequest !== currentRequest) return;
imagesData = await response.json();
renderGrid();
} catch (e) {
console.error("Failed to load images", e);
}
};
if (folderWidget) {
folderWidget.callback = () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => updateImagesList(), 350);
};
}
refreshBtn.onclick = () => {
refreshBtn.style.transform = "rotate(180deg)";
refreshBtn.style.transition = "transform 0.3s";
updateImagesList();
setTimeout(() => {
refreshBtn.style.transform = "none";
refreshBtn.style.transition = "none";
}, 300);
};
searchBar.addEventListener("input", () => {
searchQuery = searchBar.value;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => renderGrid(), 150);
});
const originalOnConfigure = node.onConfigure;
node.onConfigure = function(config) {
if (originalOnConfigure) originalOnConfigure.apply(this, arguments);
const curWidget = getHiddenWidget("_selected_image", "");
const widgetIndex = node.widgets.indexOf(curWidget);
const savedValue = (config.widgets_values && widgetIndex !== -1) ? config.widgets_values[widgetIndex] : curWidget.value;
if (savedValue) {
curWidget.value = savedValue;
}
setTimeout(() => updateImagesList(), 100);
};
setTimeout(() => {
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
if (displayModeWidget) {
const origCallback = displayModeWidget.callback;
displayModeWidget.callback = function(val) {
if (origCallback) origCallback.apply(this, arguments);
applyDisplayMode(val || displayModeWidget.value);
};
applyDisplayMode(displayModeWidget.value || "Scrollable");
}
updateImagesList();
}, 100);
}
}
});