Files
olivv-cs-ComfyUI-FunPack/app/legacy/temp_browser.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
Two pieces of the "port v4's exact UI onto v5's backend" plan:

- core/contract.py: new optional ModuleSpec.category field (closed set:
  continuity, guidance, conditioning, sampling, post, system), validated in
  core/schema.py, threaded through core/registry.py's FIELDS and
  to_manifest(). Presentation grouping, independent of STAGE (execution
  order) -- a module can be stage="sampling" and category="guidance" at
  once. Only the 4 existing modules that actually render a settings panel
  (alg, sharpen, appearance, full_control) got a category assigned; the
  other 12 have none to categorize (no SETTINGS block) and stay
  uncategorised, which is valid.

- v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied
  verbatim into app/legacy/. core/routes.py's "/" route now serves
  legacy/index.html instead of the composer entry point; a <base> tag in
  that file resolves its own relative script/link tags against
  /funpack/app/legacy/ regardless of what URL it's served at, so every
  file moved as a unit with zero per-file path edits. Verified live: all
  61 assets load 200 OK, the real v4 onboarding screen renders pixel-for-
  pixel (it's the actual DOM/CSS, not a reimplementation), and several v4
  API calls (/api/media, /api/projects, /api/git/status) already succeed
  against v5's real backend by coincidence of shared route naming -- the
  rest 404/405 as expected, since api.js's ~90 functions haven't been
  rewired to v5's actual routes yet (next piece of this plan).

The old composer entry point (app/index.html, app/composer/**) stays in
the tree, unreferenced, until a later cleanup pass once nothing imports it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:58:51 +03:00

155 lines
5.7 KiB
JavaScript

// Temp files browser: a lightweight media-bin view over ComfyUI's temp directory
// (scene previews & other transient outputs that are wiped on restart). A section of
// the unified Settings window. Each item can be opened in a new tab or saved to disk
// — no in-app player.
(function () {
const { el, clear } = window.dom;
const API = window.MovieEditorAPI;
let files = [];
// Probe <video> elements still holding a network connection. Chrome keeps stalled media
// connections open and allows only ~6 per origin — a grid of live <video> thumbnails
// starved the pool so /view tabs wouldn't load and even generate/API calls hung until a
// ComfyUI reboot. Thumbnails are snapshotted to a canvas and the probe released at once.
const _probes = new Set();
function _releaseProbe(vid) {
_probes.delete(vid);
try { vid.removeAttribute("src"); vid.load(); } catch (_) {}
}
function _releaseAllProbes() { [..._probes].forEach(_releaseProbe); }
function _fmtSize(n) {
if (!n && n !== 0) return "";
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
function _appendThumb(thumb, f) {
const url = API.tempFileUrl(f);
if (f.kind === "image") {
const img = el("img");
img.src = url; img.loading = "lazy";
thumb.append(img);
return;
}
if (f.kind === "video") {
const ph = el("span", "media-icon media-vid-ph", "▶");
thumb.append(ph);
const vid = document.createElement("video");
vid.muted = true; vid.preload = "metadata"; vid.playsInline = true;
vid.src = url;
_probes.add(vid);
vid.onerror = () => _releaseProbe(vid);
vid.onloadeddata = () => {
if (thumb.isConnected && !thumb.querySelector("canvas")) {
const canvas = document.createElement("canvas");
canvas.className = "media-vid-thumb";
canvas.width = vid.videoWidth || 320;
canvas.height = vid.videoHeight || 180;
try {
canvas.getContext("2d").drawImage(vid, 0, 0, canvas.width, canvas.height);
ph.remove();
thumb.append(canvas);
} catch (_) {}
}
_releaseProbe(vid); // frame captured — free the media connection immediately
};
return;
}
thumb.append(el("span", "media-icon media-aud-ph", "♪"));
}
function _card(f) {
const card = el("div", "media-card tmp-card");
const thumb = el("div", "media-thumb");
_appendThumb(thumb, f);
card.append(thumb);
const nameRow = el("div", "media-name-row");
const nameEl = el("div", "media-name", f.filename);
nameEl.title = f.subfolder ? `${f.subfolder}/${f.filename}` : f.filename;
nameRow.append(nameEl);
card.append(nameRow);
const meta = el("div", "pj-meta tmp-meta", _fmtSize(f.size));
card.append(meta);
const acts = el("div", "tmp-card-acts");
if (f.kind === "video" || f.kind === "image" || f.kind === "audio") {
const open = el("button", "btn ghost tiny", f.kind === "video" ? "↗ Open video" : "↗ Open");
open.title = "Open in a new browser tab";
open.onclick = () => window.open(API.tempFileUrl(f), "_blank");
acts.append(open);
}
const save = el("button", "btn ghost tiny", "⤓ Save");
save.title = "Download to your computer";
save.onclick = async () => {
save.disabled = true;
try { await API.downloadTempFile(f); }
catch (e) { alert("Save failed: " + (e.message || e)); }
finally { save.disabled = false; }
};
acts.append(save);
card.append(acts);
return card;
}
function renderBody(body) {
clear(body);
if (!files.length) {
body.append(el("div", "pj-meta media-grid-empty", "No temp media right now. Scene previews and other transient outputs appear here while ComfyUI is running."));
return;
}
const grid = el("div", "media-grid tmp-grid");
files.forEach((f) => grid.append(_card(f)));
body.append(grid);
}
async function refresh(body, countEl) {
_releaseAllProbes(); // drop probes from the previous grid before rebuilding
body.append(el("div", "pj-meta", "Loading…"));
try {
files = (await API.listTemp()).files || [];
} catch (e) {
clear(body);
body.append(el("div", "pj-meta media-grid-empty", "Could not list temp files: " + (e.message || e)));
return;
}
if (countEl) countEl.textContent = files.length ? `${files.length} file(s)` : "";
clear(body);
renderBody(body);
}
function mount(container, ctx) {
container.append(el("div", "es-hint",
"Transient ComfyUI outputs — scene previews and the like. These are wiped when ComfyUI restarts, so save anything worth keeping."));
const body = el("div", "tmp-body");
container.append(body);
const countEl = el("span", "pj-meta tmp-count");
const refreshBtn = el("button", "btn ghost tiny", "⟳ Refresh");
refreshBtn.onclick = () => refresh(body, countEl);
ctx.setActions([countEl, refreshBtn]);
refresh(body, countEl);
return () => _releaseAllProbes();
}
window.SettingsWindow.register({
id: "tempfiles", group: "System", order: 2, title: "Temp Files",
subtitle: "Transient ComfyUI outputs — wiped when the server restarts.",
keywords: "temp files previews transient media save download browser",
iconBg: "linear-gradient(180deg,#62d6cd,#2c9e95)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M1.8 4.2c0-.7.5-1.2 1.2-1.2h3l1.5 1.8h5.7c.7 0 1.2.5 1.2 1.2v6c0 .7-.5 1.2-1.2 1.2H3c-.7 0-1.2-.5-1.2-1.2V4.2z" fill="#fff"/></svg>',
mount,
});
window.TempBrowserModal = {
open: () => window.SettingsWindow.open("tempfiles"),
close: () => window.SettingsWindow.close(),
};
})();