Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1711a01929 | ||
|
|
1bf88f335f |
@@ -34,10 +34,13 @@ The usual way to run a workflow over a folder of images is to swap the file in *
|
||||
- **📁 Browse for folder** — a built-in folder picker (browses the ComfyUI server's filesystem) so you don't have to hand-type paths. Or just paste a path.
|
||||
- **Natural sort order.** `frame2.png` comes before `frame10.png`, the way you'd expect — not `frame10` before `frame2`.
|
||||
- **⏮ Reset to start** rewinds to the first image. **‹ Prev / Next ›** step the position without queuing. **↻ Rescan** re-reads the folder after you add or remove files.
|
||||
- **Scrub bar** — the status box is draggable: click anywhere to jump to that point in the folder, or click-drag left/right to scrub. A fill shows how far through you are. Perfect for big folders where stepping one-by-one would take forever.
|
||||
- **Pick your filetype** — `png`, `jpg`, `jpeg`, `webp`, `bmp`, `gif`, `tiff`, or `all` for every supported type at once.
|
||||
- **Reverse** runs the sequence last→first.
|
||||
- **Hold (pause) on a frame** — ⏸ Hold last (the image you just got) or ⏸ Hold next (the upcoming one) freezes the sequence so every Queue re-emits that same frame. Handy for locking onto one image while you tweak the rest of the graph. Click **▶ Resume** to carry on where you left off.
|
||||
- **Remembers your last folder.** A freshly-added node pre-fills with the folder you last used, so you're not re-typing paths all day.
|
||||
- **Live status line** shows exactly where you are: `Next: 3 / 240 · frame_003.png`.
|
||||
- **Thumbnail previews** of the current and next image at the bottom of the node, so you can see what's coming before you Queue.
|
||||
- **Thumbnail previews** of the next two images (**Next** and **Next + 1**) at the bottom of the node, so you can see what's coming before you Queue.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -59,7 +62,7 @@ Restart ComfyUI. No extra Python dependencies — it uses Pillow and torch, whic
|
||||
5. Wire **image** (and **mask**) into your workflow.
|
||||
6. **Queue.** It loads the first image. Queue again → the next. And so on.
|
||||
|
||||
The status line shows `Next: N / TOTAL · filename`. Hit **⏮ Reset to start** any time to begin again from the first file.
|
||||
The status line shows `Next: N / TOTAL · filename` and doubles as a **scrub bar** — click it to jump anywhere in the folder, or click-drag to scrub. Hit **⏮ Reset to start** any time to begin again from the first file.
|
||||
|
||||
At the bottom of the node, two thumbnails show what's coming: **Next** (the image that loads on your next Queue) and **Next + 1** (the one after it, looping back to the start at the end).
|
||||
|
||||
@@ -74,8 +77,13 @@ At the bottom of the node, two thumbnails show what's coming: **Next** (the imag
|
||||
| **reverse** | Run the sequence last→first instead of first→last. |
|
||||
| **⏮ Reset to start** | Rewind to the first image in the current order. |
|
||||
| **‹ Prev / Next ›** | Step the position back/forward **without** queuing. |
|
||||
| **Status / scrub bar** | Shows `Next: N / TOTAL · filename`. Click to jump anywhere in the folder, or click-drag left/right to scrub; the fill shows your position. |
|
||||
| **⏸ Hold last** | Freeze on the image you just loaded — every Queue re-emits it. Click **▶ Resume** to continue. |
|
||||
| **⏸ Hold next** | Freeze on the upcoming image — every Queue re-emits it. Click **▶ Resume** to continue. |
|
||||
| **↻ Rescan** | Re-read the folder (after adding/removing files). |
|
||||
|
||||
While a hold is active, the other buttons grey out; **▶ Resume** restores the sequence from where it was about to go.
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Notes |
|
||||
@@ -95,6 +103,7 @@ ComfyUI only re-runs a node when one of its inputs changes, so a hidden `index`
|
||||
- **Position is per-session widget state.** It's stored on the node and saved with your workflow JSON, so reopening a graph resumes where you left off — but external tools that re-run the workflow headless start from whatever index was saved.
|
||||
- **One queue = one image.** This is a deliberately manual, paced loader. If you want every image processed in a single Queue, that's a different (batch) node.
|
||||
- **Changing filetype mid-run keeps the index number.** If the new type has fewer files it wraps via modulo — hit **Reset** after switching type to start cleanly from the first file of the new set.
|
||||
- **Last-folder memory is per-browser.** The remembered folder lives in the browser's localStorage, so it's per-machine/per-browser and only pre-fills *new* nodes — a node loaded from a saved workflow keeps its own saved path.
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "sequential-image-loader"
|
||||
description = "Load a folder of images one at a time, in order — a fresh image on every Queue press. A paced, single-image folder sequencer for batch-processing frames or photos through any ComfyUI workflow."
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
dependencies = []
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 104 KiB |
+231
-29
@@ -24,6 +24,17 @@ function setWidget(widget, value) {
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// --- Remember the last folder used, so a freshly-added node pre-fills with
|
||||
// it (persists across sessions via localStorage; saved workflows still
|
||||
// keep their own per-node path).
|
||||
const LAST_DIR_KEY = "SeqLoader.lastDir";
|
||||
function saveLastDir(dir) {
|
||||
try { if (dir) localStorage.setItem(LAST_DIR_KEY, dir); } catch (e) { /* ignore */ }
|
||||
}
|
||||
function loadLastDir() {
|
||||
try { return localStorage.getItem(LAST_DIR_KEY) || ""; } catch (e) { return ""; }
|
||||
}
|
||||
|
||||
// --- Global queue hook: advance index AFTER the prompt is fully built and
|
||||
// submitted, so THIS run uses the current image and the next queue gets
|
||||
// the following one. We must await orig() first: in current ComfyUI
|
||||
@@ -41,6 +52,7 @@ function installQueueHook() {
|
||||
for (const n of nodes) {
|
||||
if (n?.type !== NODE_NAME) continue;
|
||||
if (n.mode === 2 || n.mode === 4) continue; // muted / bypassed
|
||||
if (n._seqHold) continue; // paused: keep re-emitting the held frame
|
||||
const idxW = findWidget(n, "index");
|
||||
if (!idxW) continue;
|
||||
const total = (n._seqFiles || []).length;
|
||||
@@ -92,19 +104,27 @@ async function refreshFileList(node) {
|
||||
|
||||
// --- Thumbnail previews of the current + next image. Fetched from the
|
||||
// server (the files live there) as blobs so base-path / auth are
|
||||
// handled by api.fetchApi. Skips refetching when the path is unchanged.
|
||||
// handled by api.fetchApi. A new request for a different frame ABORTS
|
||||
// the one in flight, so spamming Prev/Next can never leave a slow load
|
||||
// queued ahead of the frame you actually want.
|
||||
async function setThumb(img, path) {
|
||||
if (!img) return;
|
||||
if (!path) {
|
||||
if (img._abort) { img._abort.abort(); img._abort = null; }
|
||||
img._path = "";
|
||||
img.removeAttribute("src");
|
||||
return;
|
||||
}
|
||||
if (img._path === path) return;
|
||||
if (img._path === path) return; // already showing / fetching this one
|
||||
if (img._abort) { img._abort.abort(); img._abort = null; } // cancel stale load
|
||||
img._path = path;
|
||||
const controller = (typeof AbortController !== "undefined")
|
||||
? new AbortController() : null;
|
||||
img._abort = controller;
|
||||
try {
|
||||
const res = await api.fetchApi(
|
||||
"/seqloader/thumb?path=" + encodeURIComponent(path));
|
||||
"/seqloader/thumb?path=" + encodeURIComponent(path),
|
||||
controller ? { signal: controller.signal } : undefined);
|
||||
if (!res.ok) { img.removeAttribute("src"); return; }
|
||||
const blob = await res.blob();
|
||||
if (img._path !== path) return; // a newer request superseded this one
|
||||
@@ -112,7 +132,10 @@ async function setThumb(img, path) {
|
||||
img._url = URL.createObjectURL(blob);
|
||||
img.src = img._url;
|
||||
} catch (e) {
|
||||
img.removeAttribute("src");
|
||||
// AbortError is expected when a newer frame supersedes this load.
|
||||
if (e?.name !== "AbortError") img.removeAttribute("src");
|
||||
} finally {
|
||||
if (img._abort === controller) img._abort = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +144,7 @@ function updatePreviews(node) {
|
||||
const total = paths.length;
|
||||
const cur = node._seqCurImg, nxt = node._seqNextImg;
|
||||
if (total === 0) {
|
||||
clearTimeout(node._seqThumbTimer);
|
||||
setThumb(cur, "");
|
||||
setThumb(nxt, "");
|
||||
if (node._seqCurLab) node._seqCurLab.textContent = "Next";
|
||||
@@ -130,8 +154,8 @@ function updatePreviews(node) {
|
||||
const idx = findWidget(node, "index")?.value | 0;
|
||||
const pos = ((idx % total) + total) % total;
|
||||
const nextPos = (pos + 1) % total;
|
||||
setThumb(cur, paths[pos]);
|
||||
setThumb(nxt, paths[nextPos]);
|
||||
// Labels/titles update instantly; defer the thumbnail fetches a touch so
|
||||
// rapid Prev/Next clicks coalesce into a single load of the final frame.
|
||||
const baseName = (p) => (p || "").split(/[\\/]/).pop();
|
||||
if (node._seqCurImg) node._seqCurImg.title = baseName(paths[pos]);
|
||||
if (node._seqNextImg) node._seqNextImg.title = baseName(paths[nextPos]);
|
||||
@@ -139,6 +163,11 @@ function updatePreviews(node) {
|
||||
if (node._seqNextLab) {
|
||||
node._seqNextLab.textContent = (total === 1) ? "Next + 1 (loops)" : "Next + 1";
|
||||
}
|
||||
clearTimeout(node._seqThumbTimer);
|
||||
node._seqThumbTimer = setTimeout(() => {
|
||||
setThumb(cur, paths[pos]);
|
||||
setThumb(nxt, paths[nextPos]);
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function updateStatus(node) {
|
||||
@@ -150,6 +179,7 @@ function updateStatus(node) {
|
||||
const idx = findWidget(node, "index")?.value | 0;
|
||||
|
||||
if (total === 0) {
|
||||
el.style.background = "#1a1a1a";
|
||||
const ft = findWidget(node, "filetype")?.value || "png";
|
||||
const label = ft === "all" ? "image" : ft.toUpperCase();
|
||||
const dir = (findWidget(node, "directory")?.value || "").trim();
|
||||
@@ -161,6 +191,10 @@ function updateStatus(node) {
|
||||
}
|
||||
const pos = ((idx % total) + total) % total; // wrap, handle negatives
|
||||
const name = files[pos] || "";
|
||||
// Progress fill behind the text: left edge = first file, right = last.
|
||||
const pct = total > 1 ? (pos / (total - 1)) * 100 : 100;
|
||||
el.style.background = "linear-gradient(to right, "
|
||||
+ `rgba(60,110,180,0.55) ${pct}%, #1a1a1a ${pct}%)`;
|
||||
el.innerHTML =
|
||||
`<span style="color:#9cf;font-weight:bold">Next: ${pos + 1} / ${total}</span>`
|
||||
+ `<span style="color:#888"> · </span>`
|
||||
@@ -190,6 +224,48 @@ function attachUI(node) {
|
||||
container.appendChild(status);
|
||||
node._seqStatusEl = status;
|
||||
|
||||
// Make the status box a scrubber: click to jump anywhere in the folder,
|
||||
// or click-drag left/right to scrub. The box fills to show position.
|
||||
status.style.cursor = "ew-resize";
|
||||
status.title = "Click to jump • drag left/right to scrub";
|
||||
const scrubTo = (clientX) => {
|
||||
if (node._seqHold) return; // disabled while paused
|
||||
const total = (node._seqPaths || []).length;
|
||||
if (total <= 1) return;
|
||||
const idxW = findWidget(node, "index");
|
||||
if (!idxW) return;
|
||||
const rect = status.getBoundingClientRect();
|
||||
let frac = (clientX - rect.left) / Math.max(1, rect.width);
|
||||
frac = Math.max(0, Math.min(1, frac));
|
||||
const pos = Math.round(frac * (total - 1));
|
||||
if ((((idxW.value | 0) % total) + total) % total !== pos) {
|
||||
setWidget(idxW, pos);
|
||||
updateStatus(node);
|
||||
}
|
||||
};
|
||||
status.addEventListener("pointerdown", (e) => {
|
||||
if (node._seqHold) return;
|
||||
if ((node._seqPaths || []).length <= 1) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
status._scrubbing = true;
|
||||
try { status.setPointerCapture(e.pointerId); } catch (_) { /* ignore */ }
|
||||
scrubTo(e.clientX);
|
||||
});
|
||||
status.addEventListener("pointermove", (e) => {
|
||||
if (!status._scrubbing) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
scrubTo(e.clientX);
|
||||
});
|
||||
const endScrub = (e) => {
|
||||
if (!status._scrubbing) return;
|
||||
status._scrubbing = false;
|
||||
try { status.releasePointerCapture(e.pointerId); } catch (_) { /* ignore */ }
|
||||
};
|
||||
status.addEventListener("pointerup", endScrub);
|
||||
status.addEventListener("pointercancel", endScrub);
|
||||
|
||||
// Browse button (primary way to set the folder). Full width above the
|
||||
// navigation row.
|
||||
const browseBtn = makeButton("📁 Browse for folder…", "browse", () => {
|
||||
@@ -198,28 +274,58 @@ function attachUI(node) {
|
||||
// The column container stretches children to full width by default.
|
||||
container.appendChild(browseBtn);
|
||||
|
||||
// Button row.
|
||||
// Navigation row.
|
||||
const row = document.createElement("div");
|
||||
row.style.cssText = "display:flex; flex-direction:row; flex-wrap:wrap; gap:5px;";
|
||||
|
||||
row.appendChild(makeButton("⏮ Reset to start", "reset", () => {
|
||||
const resetBtn = makeButton("⏮ Reset to start", "reset", () => {
|
||||
setWidget(idxW, 0);
|
||||
updateStatus(node);
|
||||
}));
|
||||
row.appendChild(makeButton("‹ Prev", "neutral", () => {
|
||||
});
|
||||
const prevBtn = makeButton("‹ Prev", "neutral", () => {
|
||||
const cur = idxW?.value | 0;
|
||||
setWidget(idxW, Math.max(0, cur - 1));
|
||||
updateStatus(node);
|
||||
}));
|
||||
row.appendChild(makeButton("Next ›", "neutral", () => {
|
||||
});
|
||||
const nextBtn = makeButton("Next ›", "neutral", () => {
|
||||
setWidget(idxW, (idxW?.value | 0) + 1);
|
||||
updateStatus(node);
|
||||
}));
|
||||
row.appendChild(makeButton("↻ Rescan", "scan", () => {
|
||||
refreshFileList(node);
|
||||
}));
|
||||
});
|
||||
row.appendChild(resetBtn);
|
||||
row.appendChild(prevBtn);
|
||||
row.appendChild(nextBtn);
|
||||
container.appendChild(row);
|
||||
|
||||
// Hold row — pause on the last or next image. Below the nav buttons,
|
||||
// above Rescan.
|
||||
const holdRow = document.createElement("div");
|
||||
holdRow.style.cssText = "display:flex; flex-direction:row; gap:5px;";
|
||||
const holdLastBtn = makeButton("⏸ Hold last", "hold", () => {
|
||||
if (node._seqHold) resumeHold(node);
|
||||
else enterHold(node, "last");
|
||||
});
|
||||
const holdNextBtn = makeButton("⏸ Hold next", "hold", () => {
|
||||
if (node._seqHold) resumeHold(node);
|
||||
else enterHold(node, "next");
|
||||
});
|
||||
holdRow.appendChild(holdLastBtn);
|
||||
holdRow.appendChild(holdNextBtn);
|
||||
container.appendChild(holdRow);
|
||||
|
||||
// Rescan row.
|
||||
const rescanRow = document.createElement("div");
|
||||
rescanRow.style.cssText = "display:flex; flex-direction:row; gap:5px;";
|
||||
const rescanBtn = makeButton("↻ Rescan", "scan", () => {
|
||||
refreshFileList(node);
|
||||
});
|
||||
rescanRow.appendChild(rescanBtn);
|
||||
container.appendChild(rescanRow);
|
||||
|
||||
node._seqBtns = {
|
||||
browse: browseBtn, holdLast: holdLastBtn, holdNext: holdNextBtn,
|
||||
reset: resetBtn, prev: prevBtn, next: nextBtn, rescan: rescanBtn,
|
||||
};
|
||||
|
||||
// Image previews of the current + next file, at the bottom.
|
||||
const preview = document.createElement("div");
|
||||
preview.style.cssText = "display:flex; gap:6px; margin-top:2px;";
|
||||
@@ -251,13 +357,13 @@ function attachUI(node) {
|
||||
node._seqNextLab = nextCell.lab;
|
||||
|
||||
// Attach as a DOM widget so LiteGraph manages its layout. The panel is
|
||||
// status + browse + button row (~28px each) plus a ~115px preview row
|
||||
// (two 96px thumbnails with labels). A constant floor of 238 fits the
|
||||
// content without clipping or leaving a gap.
|
||||
// status + browse + nav row + hold row + rescan row (~28px each) plus a
|
||||
// ~115px preview row (two 96px thumbnails with labels). A constant floor
|
||||
// of 306 fits the content without clipping or leaving a gap.
|
||||
const widget = node.addDOMWidget("seqloader_ui", "seqloader_panel", container, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => 238,
|
||||
getMinHeight: () => 306,
|
||||
});
|
||||
|
||||
// Make the panel fill the node's full width: define `width` as a getter
|
||||
@@ -273,29 +379,58 @@ function attachUI(node) {
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// Re-scan whenever the folder path, subfolder toggle, filetype, or
|
||||
// direction changes.
|
||||
// direction changes. Remember the folder path for next time.
|
||||
for (const wn of ["directory", "include_subfolders", "filetype", "reverse"]) {
|
||||
const w = findWidget(node, wn);
|
||||
if (!w) continue;
|
||||
const origCb = w.callback;
|
||||
w.callback = (v, ...rest) => {
|
||||
try { origCb?.call(w, v, ...rest); } catch (e) { /* ignore */ }
|
||||
if (wn === "directory") saveLastDir((v || "").trim());
|
||||
refreshFileList(node);
|
||||
};
|
||||
}
|
||||
|
||||
updateHoldUI(node);
|
||||
|
||||
// Pre-fill a brand-new (empty) node with the last folder used. A
|
||||
// workflow-loaded node has its own saved path restored later by
|
||||
// onConfigure, so this only affects fresh nodes.
|
||||
const dirW = findWidget(node, "directory");
|
||||
if (dirW && !((dirW.value || "").trim())) {
|
||||
const last = loadLastDir();
|
||||
if (last) setWidget(dirW, last);
|
||||
}
|
||||
|
||||
// Initial populate (deferred so the widget value restore can land first).
|
||||
setTimeout(() => refreshFileList(node), 50);
|
||||
}
|
||||
|
||||
const THEMES = {
|
||||
reset: { fg: "#ffe0d0", bg: "rgba(70,50,50,0.95)", border: "rgba(220,140,100,0.9)" },
|
||||
scan: { fg: "#d2f3e2", bg: "rgba(48,66,60,0.95)", border: "rgba(110,200,160,0.9)" },
|
||||
browse: { fg: "#dde7ff", bg: "rgba(48,58,78,0.95)", border: "rgba(120,160,230,0.9)" },
|
||||
hold: { fg: "#ffe9c2", bg: "rgba(74,62,40,0.95)", border: "rgba(220,180,110,0.9)" },
|
||||
resume: { fg: "#d2f3e2", bg: "rgba(40,72,56,0.95)", border: "rgba(110,210,160,0.95)" },
|
||||
neutral: { fg: "#ccc", bg: "#2a2a2a", border: "#555" },
|
||||
};
|
||||
|
||||
function setButtonTheme(btn, kind) {
|
||||
const th = THEMES[kind] || THEMES.neutral;
|
||||
btn.style.border = `1px solid ${th.border}`;
|
||||
btn.style.background = th.bg;
|
||||
btn.style.color = th.fg;
|
||||
}
|
||||
|
||||
function setEnabled(btn, on) {
|
||||
if (!btn) return;
|
||||
btn.disabled = !on; // native disabled also blocks the click event
|
||||
btn.style.opacity = on ? "1" : "0.4";
|
||||
btn.style.cursor = on ? "pointer" : "not-allowed";
|
||||
}
|
||||
|
||||
function makeButton(label, kind, onClick) {
|
||||
const themes = {
|
||||
reset: { fg: "#ffe0d0", bg: "rgba(70,50,50,0.95)", border: "rgba(220,140,100,0.9)" },
|
||||
scan: { fg: "#d2f3e2", bg: "rgba(48,66,60,0.95)", border: "rgba(110,200,160,0.9)" },
|
||||
browse: { fg: "#dde7ff", bg: "rgba(48,58,78,0.95)", border: "rgba(120,160,230,0.9)" },
|
||||
neutral: { fg: "#ccc", bg: "#2a2a2a", border: "#555" },
|
||||
};
|
||||
const th = themes[kind] || themes.neutral;
|
||||
const th = THEMES[kind] || THEMES.neutral;
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
@@ -306,11 +441,78 @@ function makeButton(label, kind, onClick) {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (btn.disabled) return;
|
||||
onClick();
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
// --- Hold (pause) state. "Hold last" freezes on the image you just got;
|
||||
// "Hold next" freezes on the upcoming one. While held, the queue hook
|
||||
// stops advancing (so every Queue re-emits the held frame) and all the
|
||||
// other controls grey out. Resume restores the position and re-enables
|
||||
// everything. Pure frontend state — Python loads files[index] regardless.
|
||||
function enterHold(node, type) {
|
||||
const idxW = findWidget(node, "index");
|
||||
const total = (node._seqPaths || []).length;
|
||||
if (!idxW || total === 0) return;
|
||||
const k = idxW.value | 0;
|
||||
node._seqResumeIndex = k; // where the sequence continues after Resume
|
||||
node._seqHold = true;
|
||||
node._seqHoldType = type;
|
||||
if (type === "last") {
|
||||
// The frame just rendered is one behind the "next" pointer.
|
||||
setWidget(idxW, ((k - 1) % total + total) % total);
|
||||
}
|
||||
updateHoldUI(node);
|
||||
updateStatus(node);
|
||||
}
|
||||
|
||||
function resumeHold(node) {
|
||||
const idxW = findWidget(node, "index");
|
||||
node._seqHold = false;
|
||||
node._seqHoldType = null;
|
||||
if (idxW && typeof node._seqResumeIndex === "number") {
|
||||
setWidget(idxW, node._seqResumeIndex);
|
||||
}
|
||||
updateHoldUI(node);
|
||||
updateStatus(node);
|
||||
}
|
||||
|
||||
function updateHoldUI(node) {
|
||||
const b = node._seqBtns;
|
||||
if (!b) return;
|
||||
const held = !!node._seqHold;
|
||||
const type = node._seqHoldType;
|
||||
|
||||
if (node._seqStatusEl) {
|
||||
node._seqStatusEl.style.cursor = held ? "default" : "ew-resize";
|
||||
}
|
||||
|
||||
if (!held) {
|
||||
b.holdLast.textContent = "⏸ Hold last";
|
||||
b.holdNext.textContent = "⏸ Hold next";
|
||||
setButtonTheme(b.holdLast, "hold");
|
||||
setButtonTheme(b.holdNext, "hold");
|
||||
for (const key of ["browse", "reset", "prev", "next", "rescan",
|
||||
"holdLast", "holdNext"]) {
|
||||
setEnabled(b[key], true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Held: grey everything except the active button, which becomes Resume.
|
||||
for (const key of ["browse", "reset", "prev", "next", "rescan"]) {
|
||||
setEnabled(b[key], false);
|
||||
}
|
||||
const active = type === "last" ? b.holdLast : b.holdNext;
|
||||
const other = type === "last" ? b.holdNext : b.holdLast;
|
||||
active.textContent = "▶ Resume";
|
||||
setButtonTheme(active, "resume");
|
||||
setEnabled(active, true);
|
||||
setEnabled(other, false);
|
||||
}
|
||||
|
||||
// --- Server-side folder picker. Browsers can't hand back a real absolute
|
||||
// path, and the images live on the ComfyUI server anyway, so we browse
|
||||
// the server's filesystem via /seqloader/browse and write the chosen
|
||||
@@ -482,7 +684,7 @@ app.registerExtension({
|
||||
attachUI(this);
|
||||
// Enforce a sensible minimum node size once. Width ~320; height
|
||||
// accommodates the standard widgets plus the DOM panel.
|
||||
const min = [320, 500];
|
||||
const min = [320, 574];
|
||||
if (this.size[0] < min[0]) this.size[0] = min[0];
|
||||
if (this.size[1] < min[1]) this.size[1] = min[1];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user