Add current/next image thumbnail previews (Next / Next+1)

This commit is contained in:
shootthesound
2026-06-08 23:59:33 +01:00
parent adf25ced93
commit 82d32b157f
6 changed files with 127 additions and 4 deletions
+3
View File
@@ -37,6 +37,7 @@ The usual way to run a workflow over a folder of images is to swap the file in *
- **Pick your filetype** — `png`, `jpg`, `jpeg`, `webp`, `bmp`, `gif`, `tiff`, or `all` for every supported type at once.
- **Reverse** runs the sequence last→first.
- **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.
## Install
@@ -60,6 +61,8 @@ Restart ComfyUI. No extra Python dependencies — it uses Pillow and torch, whic
The status line shows `Next: N / TOTAL · filename`. 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).
## Controls
| Control | What it does |
+1
View File
@@ -0,0 +1 @@
- [Publishing ComfyUI nodes](publishing-comfyui-nodes.md) — two-target publish flow: Comfy Registry (comfy node publish) + legacy Manager PR (Comfy-Org/ComfyUI-Manager)
+16
View File
@@ -0,0 +1,16 @@
---
name: publishing-comfyui-nodes
description: How the user publishes ComfyUI custom nodes (Comfy Registry + legacy Manager)
metadata:
type: project
---
The user (shootthesound) publishes ComfyUI custom nodes to two places:
1. **Comfy Registry** — needs a `pyproject.toml` with `[project]` (name = lowercase-hyphen id, version, license, description) and `[tool.comfy]` (`PublisherId = "shootthesound"`, `DisplayName`). Publish with `comfy node publish --token <COMFY_API_KEY>` (comfy-cli installed, v1.10.3). Registry URL: `https://registry.comfy.org/nodes/<name>`.
2. **Legacy ComfyUI-Manager** — PR adding an entry to `custom-node-list.json` in **Comfy-Org/ComfyUI-Manager** (moved from ltdrdata). New entries go at the TOP of the `custom_nodes` array. The user already has a fork `shootthesound/ComfyUI-Manager` (default branch `main`); GitHub allows only one fork per repo, so reuse it — branch off upstream `main`, push branch to the fork, open PR against `Comfy-Org:main`. Entry shape: `author, title, reference, files[], install_type:"git-clone", description`.
**Why:** The user asked to publish to "both the legacy manager and the comfy registry" — this is a recurring two-target workflow for them.
**How to apply:** Never write the Comfy API key into any file; pass it inline to the publish command only. Advise rotating the key if it was pasted in chat.
BIN
View File
Binary file not shown.
+19
View File
@@ -8,6 +8,7 @@ resolves index → file → tensors and exposes a listing route the UI uses
to show progress.
"""
import io
import os
import numpy as np
@@ -250,8 +251,26 @@ try:
return web.json_response({
"count": len(files),
"files": [os.path.basename(p) for p in files],
"paths": files,
})
@PromptServer.instance.routes.get("/seqloader/thumb")
async def _seqloader_thumb(request):
path = (request.query.get("path", "") or "").strip()
if (not path or not os.path.isfile(path)
or not path.lower().endswith(_ALL_EXTS)):
return web.Response(status=404)
try:
img = Image.open(path)
img = ImageOps.exif_transpose(img)
img = img.convert("RGB")
img.thumbnail((256, 256))
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=85)
return web.Response(body=buf.getvalue(), content_type="image/jpeg")
except Exception as e:
return web.Response(status=500, text=str(e))
@PromptServer.instance.routes.post("/seqloader/browse")
async def _seqloader_browse(request):
try:
+88 -4
View File
@@ -82,13 +82,67 @@ async function refreshFileList(node) {
});
const data = await res.json();
node._seqFiles = Array.isArray(data.files) ? data.files : [];
node._seqPaths = Array.isArray(data.paths) ? data.paths : [];
} catch (e) {
node._seqFiles = [];
node._seqPaths = [];
}
updateStatus(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.
async function setThumb(img, path) {
if (!img) return;
if (!path) {
img._path = "";
img.removeAttribute("src");
return;
}
if (img._path === path) return;
img._path = path;
try {
const res = await api.fetchApi(
"/seqloader/thumb?path=" + encodeURIComponent(path));
if (!res.ok) { img.removeAttribute("src"); return; }
const blob = await res.blob();
if (img._path !== path) return; // a newer request superseded this one
if (img._url) URL.revokeObjectURL(img._url);
img._url = URL.createObjectURL(blob);
img.src = img._url;
} catch (e) {
img.removeAttribute("src");
}
}
function updatePreviews(node) {
const paths = node._seqPaths || [];
const total = paths.length;
const cur = node._seqCurImg, nxt = node._seqNextImg;
if (total === 0) {
setThumb(cur, "");
setThumb(nxt, "");
if (node._seqCurLab) node._seqCurLab.textContent = "Next";
if (node._seqNextLab) node._seqNextLab.textContent = "Next + 1";
return;
}
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]);
const baseName = (p) => (p || "").split(/[\\/]/).pop();
if (node._seqCurImg) node._seqCurImg.title = baseName(paths[pos]);
if (node._seqNextImg) node._seqNextImg.title = baseName(paths[nextPos]);
if (node._seqCurLab) node._seqCurLab.textContent = "Next";
if (node._seqNextLab) {
node._seqNextLab.textContent = (total === 1) ? "Next + 1 (loops)" : "Next + 1";
}
}
function updateStatus(node) {
updatePreviews(node);
const el = node._seqStatusEl;
if (!el) return;
const files = node._seqFiles || [];
@@ -166,14 +220,44 @@ function attachUI(node) {
}));
container.appendChild(row);
// 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;";
const makeCell = (labelText) => {
const cell = document.createElement("div");
cell.style.cssText =
"flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:3px;";
const lab = document.createElement("div");
lab.style.cssText =
"font-size:10px; color:#9cf; text-align:center; white-space:nowrap; "
+ "overflow:hidden; text-overflow:ellipsis;";
lab.textContent = labelText;
const img = document.createElement("img");
img.style.cssText =
"width:100%; height:96px; object-fit:contain; background:#111; "
+ "border:1px solid #3a3a3a; border-radius:4px;";
cell.appendChild(lab);
cell.appendChild(img);
return { cell, lab, img };
};
const curCell = makeCell("Next");
const nextCell = makeCell("Next + 1");
preview.appendChild(curCell.cell);
preview.appendChild(nextCell.cell);
container.appendChild(preview);
node._seqCurImg = curCell.img;
node._seqNextImg = nextCell.img;
node._seqCurLab = curCell.lab;
node._seqNextLab = nextCell.lab;
// Attach as a DOM widget so LiteGraph manages its layout. The panel is
// three fixed rows (status + browse + button row) at ~28px each with two
// 6px gaps and 6px top+bottom padding — a constant floor of 112 fits the
// 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.
const widget = node.addDOMWidget("seqloader_ui", "seqloader_panel", container, {
serialize: false,
hideOnZoom: false,
getMinHeight: () => 112,
getMinHeight: () => 238,
});
// Make the panel fill the node's full width: define `width` as a getter
@@ -398,7 +482,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, 370];
const min = [320, 500];
if (this.size[0] < min[0]) this.size[0] = min[0];
if (this.size[1] < min[1]) this.size[1] = min[1];
};