Files
JeyzerMC-comfyui-flakes/web/pickers.js
T
Mehdi Chaid fb039296ab feat: show preset display name in Select a Preset overlay
Add list_preset_display_names to flake_io.py and return display_names
from the /flakes/presets API endpoint. Use the display name (falling
back to last path segment) in the preset picker grid thumbnails.
2026-05-14 09:36:17 -04:00

607 lines
27 KiB
JavaScript

import { openOverlay } from "./modal.js";
import { css, makeButton, makeComfyInput, familyFolder } from "./utils.js";
import { getCoverUrl, fetchBrowse } from "./api.js";
const BROWSE_TYPE_LABELS = {
checkpoints: "models/checkpoints",
loras: "models/loras",
flakes: "models/flakes",
inputs: "input",
};
export async function openFileLoadPicker({ flakes, directories, family = "", displayNames = {} }) {
// Exclude model_presets from everything
const allFlakes = flakes.filter(n => {
const norm = n.replace(/\\/g, "/");
return !norm.startsWith("model_presets/") && norm !== "model_presets";
});
const allDirs = directories.filter(d => {
const norm = d.replace(/\\/g, "/");
return !norm.startsWith("model_presets/") && norm !== "model_presets";
});
return new Promise((resolve) => {
const { content, footer, close, handlers } = openOverlay();
handlers.onClose = (v) => resolve(v ?? null);
css(content.parentElement, content.parentElement.style.cssText + "min-width:420px;max-width:640px;");
const title = document.createElement("h3");
css(title, "margin:0 0 8px;font-size:16px;color:#fff;font-weight:500;");
title.textContent = "Load existing flake";
content.appendChild(title);
// Breadcrumb / path bar
const pathBar = document.createElement("div");
css(pathBar, "font-size:11px;color:#888;margin-bottom:6px;word-break:break-all;cursor:pointer;");
content.appendChild(pathBar);
// Search bar
const searchRow = document.createElement("div");
css(searchRow, "margin-bottom:8px;");
const searchInput = makeComfyInput("", "Search flakes...");
searchRow.appendChild(searchInput);
content.appendChild(searchRow);
const grid = document.createElement("div");
css(grid, "display:grid;grid-template-columns:repeat(auto-fill, minmax(80px, 1fr));gap:4px;max-height:360px;overflow:auto;");
content.appendChild(grid);
const initialFolder = (() => {
const folder = familyFolder(family);
return folder ? `img/${folder}` : "";
})();
let currentFolder = initialFolder;
let selectedName = null;
let selectedEl = null;
function normPath(p) {
return p.replace(/\\/g, "/");
}
function getSubfolders(folder) {
const prefix = folder ? normPath(folder) + "/" : "";
const subs = new Set();
for (const d of allDirs) {
const n = normPath(d);
if (!n.startsWith(prefix)) continue;
const rest = n.slice(prefix.length);
if (!rest || rest.includes("/")) continue;
subs.add(rest);
}
return [...subs].sort();
}
function getFlakesInFolder(folder) {
const prefix = folder ? normPath(folder) + "/" : "";
return allFlakes.filter(f => {
const n = normPath(f);
if (!prefix) return !n.includes("/");
if (!n.startsWith(prefix)) return false;
return !n.slice(prefix.length).includes("/");
}).sort();
}
function renderBreadcrumb() {
const base = "models/flakes/";
pathBar.textContent = base + (currentFolder ? normPath(currentFolder) + "/" : "");
}
function renderGrid(filter = "") {
grid.replaceChildren();
selectedName = null;
selectedEl = null;
const term = filter.toLowerCase().trim();
renderBreadcrumb();
if (term) {
// Search mode: scoped to currentFolder so the breadcrumb stays meaningful
const prefix = currentFolder ? normPath(currentFolder) + "/" : "";
const scoped = prefix
? allFlakes.filter(n => normPath(n).startsWith(prefix))
: allFlakes;
const filtered = scoped.filter(n => normPath(n).slice(prefix.length).toLowerCase().includes(term));
if (filtered.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No flakes found.";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;grid-column:1 / -1;");
grid.appendChild(empty);
return;
}
for (const name of filtered) {
grid.appendChild(makeFlakeThumb(name));
}
return;
}
// Folder mode: subfolders + flakes in current folder
const subfolders = getSubfolders(currentFolder);
const folderFlakes = getFlakesInFolder(currentFolder);
if (currentFolder) {
// Up folder item
const upItem = document.createElement("div");
css(upItem, "height:80px;background:#252525;border:1px solid #444;border-radius:4px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;user-select:none;box-sizing:border-box;");
const upIcon = document.createElement("div");
upIcon.textContent = "\u2191";
css(upIcon, "font-size:24px;color:#888;");
const upLabel = document.createElement("div");
upLabel.textContent = "..";
css(upLabel, "font-size:10px;color:#aaa;");
upItem.appendChild(upIcon);
upItem.appendChild(upLabel);
upItem.addEventListener("click", () => {
const parts = normPath(currentFolder).split("/").filter(Boolean);
parts.pop();
currentFolder = parts.join("/");
renderGrid();
});
upItem.addEventListener("dblclick", (e) => e.stopPropagation());
grid.appendChild(upItem);
}
for (const sub of subfolders) {
const folderItem = document.createElement("div");
css(folderItem, "height:80px;background:#252525;border:1px solid #444;border-radius:4px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;user-select:none;box-sizing:border-box;");
const fIcon = document.createElement("div");
fIcon.textContent = "\uD83D\uDCC1";
css(fIcon, "font-size:32px;");
const fLabel = document.createElement("div");
fLabel.textContent = sub;
css(fLabel, "font-size:10px;color:#ddd;text-align:center;padding:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;");
folderItem.appendChild(fIcon);
folderItem.appendChild(fLabel);
folderItem.addEventListener("click", () => {
currentFolder = currentFolder ? normPath(currentFolder) + "/" + sub : sub;
renderGrid();
});
folderItem.addEventListener("dblclick", () => {
currentFolder = currentFolder ? normPath(currentFolder) + "/" + sub : sub;
renderGrid();
});
grid.appendChild(folderItem);
}
for (const name of folderFlakes) {
grid.appendChild(makeFlakeThumb(name));
}
if (subfolders.length === 0 && folderFlakes.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No flakes found.";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;grid-column:1 / -1;");
grid.appendChild(empty);
}
}
function makeFlakeThumb(name) {
const thumb = document.createElement("div");
css(thumb, `position:relative;height:80px;background:#2a2a2a;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:10px;color:#ddd;user-select:none;box-sizing:border-box;overflow:hidden;background-image:url(${getCoverUrl(name)});background-size:cover;background-position:center;`);
const overlay = document.createElement("div");
css(overlay, "position:absolute;inset:0;background:rgba(0,0,0,0.45);pointer-events:none;z-index:0;");
thumb.appendChild(overlay);
const displayName = displayNames[name] || "";
const label = displayName || (name.includes("/") ? name.split("/").pop() : name);
const nameEl = document.createElement("div");
nameEl.title = name;
css(nameEl, "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:500;text-align:center;line-height:1.2;text-shadow:0 1px 3px rgba(0,0,0,0.8);padding:0 4px;overflow:hidden;z-index:1;word-break:break-word;hyphens:auto;");
nameEl.textContent = label;
thumb.appendChild(nameEl);
thumb.addEventListener("click", () => {
if (selectedEl) {
selectedEl.style.borderColor = "#444";
}
selectedName = name;
selectedEl = thumb;
thumb.style.borderColor = "#2a6acf";
});
thumb.addEventListener("dblclick", () => {
close({ name });
});
return thumb;
}
pathBar.addEventListener("click", () => {
currentFolder = "";
searchInput.value = "";
renderGrid();
});
renderGrid();
searchInput.addEventListener("input", () => renderGrid(searchInput.value));
const cancelBtn = makeButton("Cancel");
cancelBtn.addEventListener("click", () => close(undefined));
footer.appendChild(cancelBtn);
const selectBtn = makeButton("Select", true);
selectBtn.addEventListener("click", () => {
if (!selectedName) {
window.alert("Select a flake first.");
return;
}
close({ name: selectedName });
});
footer.appendChild(selectBtn);
});
}
export async function openPresetPicker({ selected = "", family = "" } = {}) {
return new Promise(async (resolve) => {
let presets = [];
let displayNames = {};
try {
const query = family ? `?family=${encodeURIComponent(family)}` : "";
const r = await fetch(`/flakes/presets${query}`, { cache: "no-store" });
const d = await r.json();
presets = d.presets || [];
displayNames = d.display_names || {};
} catch (err) {
console.error("[flakes] failed to load presets for picker:", err);
}
const { content, footer, close, handlers } = openOverlay();
handlers.onClose = (v) => resolve(v ?? null);
css(content.parentElement, content.parentElement.style.cssText + "min-width:420px;max-width:640px;");
const title = document.createElement("h3");
css(title, "margin:0 0 8px;font-size:16px;color:#fff;font-weight:500;");
title.textContent = "Select a Preset";
content.appendChild(title);
const pathBar = document.createElement("div");
css(pathBar, "font-size:11px;color:#888;margin-bottom:6px;word-break:break-all;cursor:pointer;");
content.appendChild(pathBar);
const searchRow = document.createElement("div");
css(searchRow, "margin-bottom:8px;");
const searchInput = makeComfyInput("", "Search presets...");
searchRow.appendChild(searchInput);
content.appendChild(searchRow);
const grid = document.createElement("div");
css(grid, "display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:8px;max-height:400px;overflow:auto;padding:4px;");
content.appendChild(grid);
let selectedName = selected || null;
let selectedEl = null;
let currentFolder = familyFolder(family) || "";
function normPath(p) {
return p.replace(/\\/g, "/");
}
function getSubfolders(folder) {
const prefix = folder ? normPath(folder) + "/" : "";
const subs = new Set();
for (const p of presets) {
const n = normPath(p);
if (!n.startsWith(prefix)) continue;
const rest = n.slice(prefix.length);
if (!rest) continue;
const slashIdx = rest.indexOf("/");
if (slashIdx === -1) continue;
subs.add(rest.slice(0, slashIdx));
}
return [...subs].sort();
}
function getPresetsInFolder(folder) {
const prefix = folder ? normPath(folder) + "/" : "";
return presets.filter(p => {
const n = normPath(p);
if (!prefix) return !n.includes("/");
if (!n.startsWith(prefix)) return false;
return !n.slice(prefix.length).includes("/");
}).sort();
}
function renderBreadcrumb() {
const base = "model_presets/";
pathBar.textContent = base + (currentFolder ? normPath(currentFolder) + "/" : "");
}
function makePresetThumb(name) {
const thumb = document.createElement("div");
css(thumb, `position:relative;height:100px;background:#2a2a2a;border:2px solid ${name === selectedName ? "#2a6acf" : "#444"};border-radius:6px;cursor:pointer;font-size:11px;color:#ddd;user-select:none;box-sizing:border-box;overflow:hidden;background-image:url(/flakes/preset_cover?name=${encodeURIComponent(name)});background-size:cover;background-position:center;transition:border-color 0.15s ease;`);
const overlay = document.createElement("div");
css(overlay, "position:absolute;inset:0;background:rgba(0,0,0,0.4);pointer-events:none;z-index:0;transition:background 0.15s ease;");
thumb.appendChild(overlay);
const displayLabel = displayNames[name] || name.replace(/\\/g, "/").split("/").pop() || name;
const nameEl = document.createElement("div");
nameEl.title = name;
css(nameEl, "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:500;text-align:center;line-height:1.2;text-shadow:0 1px 3px rgba(0,0,0,0.9);padding:6px 4px;overflow:hidden;z-index:1;word-break:break-word;hyphens:auto;");
nameEl.textContent = displayLabel;
thumb.appendChild(nameEl);
thumb.addEventListener("mouseenter", () => {
thumb.style.borderColor = "#555";
overlay.style.background = "rgba(0,0,0,0.25)";
});
thumb.addEventListener("mouseleave", () => {
thumb.style.borderColor = name === selectedName ? "#2a6acf" : "#444";
overlay.style.background = "rgba(0,0,0,0.4)";
});
thumb.addEventListener("click", () => {
if (selectedEl) {
selectedEl.style.borderColor = "#444";
}
selectedName = name;
selectedEl = thumb;
thumb.style.borderColor = "#2a6acf";
});
thumb.addEventListener("dblclick", () => {
close({ name });
});
if (name === selectedName) {
selectedEl = thumb;
}
return thumb;
}
function renderGrid(filter = "") {
grid.replaceChildren();
selectedEl = null;
const term = String(filter || "").toLowerCase().trim();
renderBreadcrumb();
if (term) {
const prefix = currentFolder ? normPath(currentFolder) + "/" : "";
const scoped = prefix
? presets.filter(n => normPath(n).startsWith(prefix))
: presets;
const filtered = scoped.filter(n => normPath(n).slice(prefix.length).toLowerCase().includes(term));
if (filtered.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No presets found.";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;grid-column:1 / -1;");
grid.appendChild(empty);
return;
}
for (const name of filtered) {
grid.appendChild(makePresetThumb(name));
}
return;
}
const subfolders = getSubfolders(currentFolder);
const folderPresets = getPresetsInFolder(currentFolder);
if (currentFolder) {
const upItem = document.createElement("div");
css(upItem, "height:100px;background:#252525;border:1px solid #444;border-radius:6px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;user-select:none;box-sizing:border-box;");
const upIcon = document.createElement("div");
upIcon.textContent = "\u2191";
css(upIcon, "font-size:24px;color:#888;");
const upLabel = document.createElement("div");
upLabel.textContent = "..";
css(upLabel, "font-size:10px;color:#aaa;");
upItem.appendChild(upIcon);
upItem.appendChild(upLabel);
upItem.addEventListener("click", () => {
const parts = normPath(currentFolder).split("/").filter(Boolean);
parts.pop();
currentFolder = parts.join("/");
searchInput.value = "";
renderGrid();
});
upItem.addEventListener("dblclick", (e) => e.stopPropagation());
grid.appendChild(upItem);
}
for (const sub of subfolders) {
const folderItem = document.createElement("div");
css(folderItem, "height:100px;background:#252525;border:1px solid #444;border-radius:6px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;user-select:none;box-sizing:border-box;");
const fIcon = document.createElement("div");
fIcon.textContent = "\uD83D\uDCC1";
css(fIcon, "font-size:32px;");
const fLabel = document.createElement("div");
fLabel.textContent = sub;
css(fLabel, "font-size:10px;color:#ddd;text-align:center;padding:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;");
folderItem.appendChild(fIcon);
folderItem.appendChild(fLabel);
folderItem.addEventListener("click", () => {
currentFolder = currentFolder ? normPath(currentFolder) + "/" + sub : sub;
searchInput.value = "";
renderGrid();
});
folderItem.addEventListener("dblclick", () => {
currentFolder = currentFolder ? normPath(currentFolder) + "/" + sub : sub;
searchInput.value = "";
renderGrid();
});
grid.appendChild(folderItem);
}
for (const name of folderPresets) {
grid.appendChild(makePresetThumb(name));
}
if (subfolders.length === 0 && folderPresets.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No presets found.";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;grid-column:1 / -1;");
grid.appendChild(empty);
}
}
pathBar.addEventListener("click", () => {
currentFolder = "";
searchInput.value = "";
renderGrid();
});
renderGrid();
searchInput.addEventListener("input", () => renderGrid(searchInput.value));
searchInput.focus();
const cancelBtn = makeButton("Cancel");
cancelBtn.addEventListener("click", () => close(undefined));
footer.appendChild(cancelBtn);
const selectBtn = makeButton("Select", true);
selectBtn.addEventListener("click", () => {
if (!selectedName) {
window.alert("Select a preset first.");
return;
}
close({ name: selectedName });
});
footer.appendChild(selectBtn);
});
}
export function openFileBrowser({ type, defaultPath = "" }) {
return new Promise((resolve) => {
const { content, footer, close, handlers } = openOverlay();
handlers.onClose = (v) => resolve(v ?? null);
css(content.parentElement, content.parentElement.style.cssText + "min-width:420px;max-width:560px;");
const title = document.createElement("h3");
css(title, "margin:0 0 8px;font-size:16px;color:#fff;font-weight:500;");
title.textContent = type === "checkpoints" ? "Select Checkpoint" : type === "loras" ? "Select LoRA" : type === "inputs" ? "Select Input Image" : "Select Flake";
content.appendChild(title);
const pathBar = document.createElement("div");
css(pathBar, "font-size:11px;color:#888;margin-bottom:8px;word-break:break-all;");
content.appendChild(pathBar);
const searchRow = document.createElement("div");
css(searchRow, "margin-bottom:8px;");
const searchInput = makeComfyInput("", "Search...");
searchRow.appendChild(searchInput);
content.appendChild(searchRow);
const listBox = document.createElement("div");
css(listBox, "display:flex;flex-direction:column;gap:2px;max-height:320px;overflow:auto;");
content.appendChild(listBox);
let currentPath = defaultPath;
let selectedFile = null;
let dirEntries = [];
function renderEntries(filter = "") {
listBox.replaceChildren();
const term = String(filter || "").toLowerCase().trim();
const entries = term
? dirEntries.filter(e => String(e.name || "").toLowerCase().includes(term))
: dirEntries;
if (currentPath) {
const upBtn = document.createElement("button");
upBtn.textContent = "\u2191 ..";
css(upBtn, "text-align:left;padding:6px 10px;background:#252525;color:#ddd;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:12px;");
upBtn.addEventListener("mouseenter", () => { upBtn.style.background = "#333"; });
upBtn.addEventListener("mouseleave", () => { upBtn.style.background = "#252525"; });
upBtn.addEventListener("click", () => {
const parts = currentPath.replace(/\\/g, "/").split("/").filter(Boolean);
parts.pop();
loadDir(parts.join("/"));
});
listBox.appendChild(upBtn);
}
for (const entry of entries) {
const row = document.createElement("button");
row.textContent = entry.type === "dir" ? "\uD83D\uDCC1 " + entry.name : entry.name;
css(row, "text-align:left;padding:6px 10px;background:#2a2a2a;color:#ddd;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:12px;");
row.addEventListener("mouseenter", () => { row.style.background = "#333"; });
row.addEventListener("mouseleave", () => { row.style.background = "#2a2a2a"; });
if (entry.type === "dir") {
row.addEventListener("click", () => {
const next = currentPath ? currentPath.replace(/\\/g, "/") + "/" + entry.name : entry.name;
loadDir(next);
});
} else {
row.addEventListener("click", () => {
selectedFile = currentPath ? currentPath.replace(/\\/g, "/") + "/" + entry.name : entry.name;
for (const b of listBox.querySelectorAll("button")) {
b.style.borderColor = "#444";
}
row.style.borderColor = "#2a6acf";
});
row.addEventListener("dblclick", () => {
selectedFile = currentPath ? currentPath.replace(/\\/g, "/") + "/" + entry.name : entry.name;
close({ file: selectedFile });
});
}
listBox.appendChild(row);
}
if (entries.length === 0) {
const empty = document.createElement("div");
empty.textContent = term ? "No matches" : "Empty folder";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;font-size:12px;");
listBox.appendChild(empty);
}
}
const baseLabel = BROWSE_TYPE_LABELS[type] || type;
function fmtPath(rel) {
const r = (rel || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
return r ? `${baseLabel}/${r}/` : `${baseLabel}/`;
}
async function loadDir(path) {
pathBar.textContent = fmtPath(path);
selectedFile = null;
searchInput.value = "";
try {
const data = await fetchBrowse(type, path);
currentPath = data.path || "";
pathBar.textContent = fmtPath(currentPath);
dirEntries = data.entries || [];
renderEntries();
} catch (err) {
if (path) {
// Requested directory does not exist — fall back to root
return loadDir("");
}
dirEntries = [];
listBox.replaceChildren();
const errEl = document.createElement("div");
css(errEl, "color:#f88;padding:12px;text-align:center;font-size:12px;");
errEl.textContent = err.message || "failed to load";
listBox.appendChild(errEl);
}
}
searchInput.addEventListener("input", () => renderEntries(searchInput.value));
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const term = searchInput.value.toLowerCase().trim();
const matches = dirEntries.filter(e => e.type === "file" && e.name.toLowerCase().includes(term));
if (matches.length > 0) {
selectedFile = currentPath ? currentPath.replace(/\\/g, "/") + "/" + matches[0].name : matches[0].name;
close({ file: selectedFile });
}
}
});
loadDir(defaultPath);
searchInput.focus();
const cancelBtn = makeButton("Cancel");
cancelBtn.addEventListener("click", () => close(undefined));
footer.appendChild(cancelBtn);
const selectBtn = makeButton("Select", true);
selectBtn.addEventListener("click", () => {
if (!selectedFile) {
window.alert("Select a file first.");
return;
}
close({ file: selectedFile });
});
footer.appendChild(selectBtn);
});
}